dy-dashboard/services/audit_schema.py
2026-10-29 10:36:31 +08:00

95 lines
6.2 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 isinstance(data,list):
for item in data:
nested_type,nested_id=infer_object(item,path)
if nested_id:return nested_type,nested_id
return ('','')
if not isinstance(data,dict):return ('','')
# 中文字段名用于兼容前端表单及嵌套业务对象的常见编码字段。
for key in ('product_code','货品编号','产品编码','货品编码','material_code','物料编号','supplier_code','供应商编码',
'order_id','订单编号','record_id','subject_id','target_id'):
value=data.get(key)
if isinstance(value,dict):
nested_type,nested_id=infer_object(value,path)
if nested_id:return nested_type,nested_id
if isinstance(value,list):
for item in value:
nested_type,nested_id=infer_object(item,path)
if nested_id:return nested_type,nested_id
if value not in (None,''):
object_type=key.replace('_code','').replace('_id','')
if key in ('货品编号','产品编码','货品编码'):object_type='product'
elif key=='物料编号':object_type='material'
elif key=='供应商编码':object_type='supplier'
elif key=='订单编号':object_type='order'
return (object_type,str(value))
for value in data.values():
if isinstance(value,(dict,list)):
nested_type,nested_id=infer_object(value,path)
if nested_id:return nested_type,nested_id
for key in ('id','user_id'):
value=data.get(key)
if value not in (None,''):
return (key.replace('_id',''),str(value))
return ('','')