95 lines
6.2 KiB
Python
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 ('','')
|