refactor(audit): 审计中文映射收敛为后端唯一来源

问题:同一套「字段名 → 中文」映射存了三份手工同步的副本 ——
前端 AuditLog.vue 的 fieldMap(约 95 个字段)、后端 api/v1/audit.py 的
ACTION_ALIASES,另有若干内联的 status 映射散落在 service 层。
改一边漏一边就会漂移,页面上冒出英文列名或对不上的中文。

改动:
  · 新增 app/utils/audit_labels.py 作为**唯一来源**,统一提供
    操作类型归一化(canon_action)、字段名中文化(field_label)、
    码值中文化(enum_label / bool_label / person_name_label)、
    ID 指向登记(id_ref_of)。
  · api/v1/audit.py 改为从该模块 import,删掉本地副本。
  · 新增 GET /audit/labels 下发映射(静态标签,免权限码;审计页本身
    已由 system_audit 把关)。
  · 前端 AuditLog.vue 删除本地 95 行 fieldMap,改从接口拉取;
    拉取失败时未命中项原样显示,是可控降级。

★ 码值必须**按模块**翻译:同名 status 在出库/借还/报废里含义完全不同
  (出库的 3 是「已出库」,报废的 3 是「已执行(已报废)」),
  不区分模块会把报废单显示成已出库。借还模块下还混着字符串状态
  (borrowed/returned),因其两张表共用 status 列名。
  故映射按 (模块, 字段) 匹配,未登记再退回字段名。

★ ID 字段同理:parent_id 在「系统管理」里是菜单的上级菜单,在「BOM管理」
  里是物料节点 —— 指向完全不同的表。

验证:前端 vue-tsc 改前改后均为 565 个错误、去除行号后**报错集合完全相同**,
未引入新错误;GET /audit/labels 实测返回 200。
This commit is contained in:
yueli
2026-09-23 17:16:20 +08:00
parent 69d9f010a9
commit ad2d27cd72
4 changed files with 498 additions and 158 deletions

View File

@ -12,35 +12,20 @@ audit_bp = Blueprint('audit', __name__)
# =============================================================================
# 操作类型归一化
# 操作类型归一化 / 中文化标签
#
# 问题背景:历史数据里 action 有两套写法 —— 早期装饰器(已废弃)写入中文
# (新增/修改/删除/批量删除…),现行监听器写入大写英文(CREATE/UPDATE/DELETE)。
# 前端下拉框直接取 DISTINCT action,于是同时出现「CREATE」和「新增」两个选项,
# 而表格里二者又都显示为「新增」(actionMap 做了映射),用户无法分辨。
# ★ 映射表已统一收敛到 app/utils/audit_labels.py(**唯一来源**)。
# 本模块与日报服务共用同一份,前端经 GET /audit/labels 拉取同一份 ——
# 改一处即可,不会再出现"两边手工同步、改一边漏一边"的漂移。
#
# 后果:用户选了看得懂的中文项,只能搜到 3-4 月的历史数据,误以为"没有最近的内容"。
#
# 处理:对外只暴露规范值(CREATE/UPDATE/DELETE),筛选时自动展开到全部别名,
# 历史数据无需迁移即可被正确检索。
# 历史问题(迁移前):前端 AuditLog.vue 自带一份 fieldMap,后端这里自带一份
# ACTION_ALIASES,两份副本各自演化。
# =============================================================================
ACTION_ALIASES = {
'CREATE': ('CREATE', 'create', 'INSERT', 'insert', '新增', '批量生成'),
'UPDATE': ('UPDATE', 'update', '修改', '分配', '归还'),
'DELETE': ('DELETE', 'delete', '删除', '批量删除'),
}
# 反向索引:任意别名 → 规范值
_ALIAS_TO_CANON = {
alias: canon
for canon, aliases in ACTION_ALIASES.items()
for alias in aliases
}
def canon_action(action):
"""把任意写法的 action 归一化为规范值;无法识别时原样返回"""
return _ALIAS_TO_CANON.get((action or '').strip(), (action or '').strip())
from app.utils.audit_labels import ( # noqa: E402
ACTION_ALIASES,
canon_action,
labels_payload,
)
@audit_bp.route('/logs', methods=['GET'])
@ -193,3 +178,19 @@ def get_modules():
except Exception as e:
current_app.logger.error(f"获取模块列表失败: {str(e)}")
return jsonify({'code': 500, 'msg': str(e)}), 500
@audit_bp.route('/labels', methods=['GET'])
@jwt_required()
def get_labels():
"""
下发操作类型与字段名的中文映射。
★ 后端是**唯一来源**,前端不再自带副本 —— 否则两份手工同步的映射必然漂移。
前端拉到之前(或拉取失败时)对未命中字段原样显示字段名,
是可控的降级,不会让页面崩掉。
免权限码:纯静态标签,不含任何业务数据;审计页本身已由
@permission_required('system_audit') 把关(见 /logs)。
"""
return jsonify({'code': 200, 'data': labels_payload()}), 200