dy-dashboard/services/audit_schema.py
2026-10-28 11:14:17 +08:00

68 lines
5.1 KiB
Python

"""结构化操作日志公共模型。"""
import re
from services import db as _db
_READY=False
HIGH_ACTIONS={'权限变更','产品彻底删除','删除订单','下单回退','回退订单','修改成本价','价格审核','批量删除','数据修复','批量数据修复'}
MEDIUM_ACTIONS={'产品批量编辑','产品导入','修改下单数量','订单字段编辑','到货批次及数量','品牌分类','新增供应商'}
PAGE_PATHS=[
('/api/planning','计划预测'),('/api/purchase','采购管理'),('/api/domestic','国内订单管理'),('/api/overseas','海外订单管理'),('/api/packaging','包材订单管理'),('/api/products','产品信息管理'),('/api/product','产品信息管理'),('/api/bom','物料清单管理'),('/api/supplier','供应商管理'),('/api/permissions','权限配置'),('/api/users','用户管理'),('/api/config','参数配置'),('/api/warehouse','仓库管理'),('/api/inventory','库存管理'),('/api/feedback','问题反馈'),('/api/knowledge','知识库')]
def ensure_audit_schema():
global _READY
if _READY:return
column_rows=_db.query("SELECT COLUMN_NAME FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='dashboard_logs'")
if not isinstance(column_rows, (list, tuple)):
_READY=True
return
cols={r['COLUMN_NAME'] for r in column_rows}
defs={
'event_type':"VARCHAR(40) NOT NULL DEFAULT 'data_change'",'result_status':"VARCHAR(20) NOT NULL DEFAULT 'success'",'risk_level':"VARCHAR(20) NOT NULL DEFAULT 'normal'",'object_type':"VARCHAR(100) NULL",'object_id':"VARCHAR(200) NULL",'old_values':"LONGTEXT NULL",'new_values':"LONGTEXT NULL",'request_method':"VARCHAR(10) NULL",'request_path':"VARCHAR(255) NULL",'request_id':"VARCHAR(64) NULL",'user_agent':"VARCHAR(500) NULL",'role_snapshot':"VARCHAR(50) NULL",'position_snapshot':"VARCHAR(100) NULL",'http_status':"INT NULL"}
for name,definition in defs.items():
if name not in cols: _db.query(f"ALTER TABLE dashboard_logs ADD COLUMN `{name}` {definition}")
indexes={r['INDEX_NAME'] for r in _db.query("SELECT INDEX_NAME FROM information_schema.statistics WHERE table_schema=DATABASE() AND table_name='dashboard_logs'")}
for name,sql in {'idx_logs_created':'created_at','idx_logs_user_created':'user_id,created_at','idx_logs_page_created':'page,created_at','idx_logs_action_created':'action,created_at','idx_logs_risk_created':'risk_level,created_at','idx_logs_result_created':'result_status,created_at','idx_logs_request':'request_id'}.items():
if name not in indexes: _db.query(f"CREATE INDEX {name} ON dashboard_logs ({sql})")
high = ','.join(['%s'] * len(HIGH_ACTIONS)); medium = ','.join(['%s'] * len(MEDIUM_ACTIONS))
_db.query(f"UPDATE dashboard_logs SET risk_level='high' WHERE action IN ({high}) OR COALESCE(field,'') REGEXP '成本价|单价|权限|删除|回退'", tuple(HIGH_ACTIONS))
_db.query(f"UPDATE dashboard_logs SET risk_level='medium' WHERE risk_level<>'high' AND action IN ({medium})", tuple(MEDIUM_ACTIONS))
_READY=True
def infer_page(path,action=''):
for prefix,name in PAGE_PATHS:
if str(path or '').startswith(prefix):return name
return action or ''
ACTION_PATHS = [
('/api/domestic/orders/update','编辑国内订单'),('/api/packaging/orders/update','编辑包材订单'),('/api/overseas/orders/update','编辑海外订单'),('/api/purchase/add_pending','加入待下单'),('/api/purchase/confirm_order','确认下单'),('/api/new_products/bom/save','保存新品BOM'),('/api/bom/create_with_materials','保存物料清单'),('/api/bom/delete','删除物料清单'),('/api/users/update','编辑用户'),('/api/users/create','新增用户'),('/api/product/remark/save','保存产品批注'),('/api/planning/scheme/save','保存计划预测方案'),('/api/planning/scheme/delete','删除计划预测方案'),('/api/supplier/update','编辑供应商'),('/api/supplier/delete','删除供应商'),('/api/feedback/submit','提交问题反馈')]
def infer_action(path, method=''):
for prefix,name in ACTION_PATHS:
if str(path or '').startswith(prefix):return name
page=infer_page(path,'系统操作')
return ('删除' if method=='DELETE' else '更新') + page
def classify_event(action,path='',method=''):
text=str(action or '')
if '登录' in text:return 'login'
if '权限' in text:return 'security'
if '导入' in text:return 'import'
if '导出' in text:return 'export'
if '删除' in text or method=='DELETE':return 'delete'
if any(x in text for x in ('新增','创建')):return 'create'
if method in ('POST','PUT','PATCH'):return 'update'
return 'data_change'
def classify_risk(action='',field='',path=''):
if action in HIGH_ACTIONS or re.search(r'成本价|单价|权限|删除|回退',str(field or '')) or re.search(r'permission|delete|rollback|update_price',str(path or ''),re.I):return 'high'
if action in MEDIUM_ACTIONS:return 'medium'
return 'normal'
def infer_object(data,path=''):
if not isinstance(data,dict):return ('','')
for key in ('product_code','material_code','supplier_code','record_id','order_id','subject_id','target_id','id','user_id'):
value=data.get(key)
if value not in (None,''):
return (key.replace('_code','').replace('_id',''),str(value))
return ('','')