10 KiB
操作审计与业务语义日志 — 设计文档
日期: 2026-06-29
状态: 已批准(待实现)
OpenSpec:openspec/changes/operation-audit-enhancement/
前置: 现有ApiRequestLogMiddleware、sys_api_request_log、operlog/listAPI
界面模式: P01 标准列表(操作日志管理页)
目标
满足客户对「用户操作可追溯」的要求,在不重建审计体系的前提下:
- 继续全量 API 采集 — 接口地址、入参、出参、时间、操作人
- 补齐请求人姓名与租户 —
create_name(nick_name)、tenant_no - 增加业务语义字段 — 模块、操作类型、业务主键、可读摘要
- 提供全局管理页 — 筛选、分页、详情
- CRM 详情 Tab — 按
business_id精确查日志
已确认业务规则
| # | 规则 | 决策 |
|---|---|---|
| D1 | 总体方案 | 方案 A + 语义字段;不新建 sys_oper_log 表 |
| D2 | 采集范围 | 默认全 API(含 GET 列表);跳过 health/docs/OPTIONS |
| D3 | 写操作语义 | @oper_log 装饰器声明;只读走路径推断 |
| D4 | 请求人展示 | 列表/详情 姓名优先(create_name),账号为 username |
| D5 | 入参出参 | 已在中间件记录;详情完整展示(受截断限制) |
| D6 | 文件上传 | multipart 不记文件体 |
| D7 | 权限 | operlog API 与菜单:admin / super_admin + 数据权限 |
| D8 | 归档 | 本期只做表结构扩展;定时归档为后续项 |
| D9 | business_type | 对齐 sys_oper_type:0/1/2/3/5/6 |
| D10 | oper_summary 模板 | {动词}{对象}【{名或编号}】 |
非目标(本期不做)
- 前端 UI 埋点(不发 API 的行为)
- MQ/ES 独立审计平台
- 与 CRM Java 仓
sys_oper_log数据合并 - 全模块写接口
@oper_log一次性覆盖 - multipart 文件内容入库
- 响应体手机号/身份证扩展脱敏(待客户确认)
现状与差距
已实现
| 能力 | 实现位置 |
|---|---|
| 中间件采集 | backend/app/core/api_request_log_middleware.py |
| 落库表 | sys_api_request_log、sys_api_request_log_his |
| 查询 API | POST /api/web/monitor/operlog/list、/detail |
| CRM 嵌入 | CustomerDetailDialog、contract/detail Tab |
| 默认开关 | API_REQUEST_LOG_ENABLED=true |
字段映射(客户需求 → 现状)
| 客户项 | 表字段 | OperLog 字段 | 现状 |
|---|---|---|---|
| 接口地址 | path |
operUrl |
✅ |
| 入参 | query_string + request_body |
operParam |
✅(有例外) |
| 出参 | response_body |
jsonResult |
✅(有截断) |
| 时间 | create_at |
operTime |
✅ |
| 账号 | username |
operName 兜底 |
✅ |
| 姓名 | create_name |
operName 优先 |
⚠️ 未写入 |
| 模块/类型/单据 | — | title、businessType |
⚠️ 仅读时推断 |
| 可读摘要 | — | — | ❌ |
| 租户 | — | — | ❌ |
采集限制(保持)
| 场景 | 处理 |
|---|---|
| body > 10MB | [body omitted: N bytes] |
| 文本截断 | API_REQUEST_LOG_MAX_BODY 65536 字符 |
| 原始读取 | API_REQUEST_LOG_MAX_BODY_BYTES 131072 |
| 敏感键 | password/token 等 → *** |
| multipart | [multipart/form-data body omitted] |
数据模型扩展
sys_api_request_log / sys_api_request_log_his 新增列
| 字段 | 类型 | 说明 |
|---|---|---|
tenant_no |
varchar(64) | 租户编号,JWT 写入 |
business_title |
varchar(100) | 模块标题,如 CRM客户管理 |
business_type |
tinyint | 0其他 1新增 2修改 3删除 5导出 6导入 |
business_id |
varchar(64) NULL | 业务主键 |
oper_summary |
varchar(500) NULL | 人读摘要 |
建议索引:
idx_arl_tenant_createat(tenant_no,create_at)idx_arl_business(business_id,business_title,create_at)
business_type 枚举
| 值 | 含义 | 典型场景 |
|---|---|---|
| 0 | 其他 | 登录、只读查询 |
| 1 | 新增 | create/add |
| 2 | 修改 | update/edit/审批/转移 |
| 3 | 删除 | delete/remove |
| 5 | 导出 | export |
| 6 | 导入 | import |
存储样式示例
修改客户(有 @oper_log)
tenant_no = shanxia2.0
business_title = CRM客户管理
business_type = 2
business_id = SXCU0000000000000123
oper_summary = 修改客户【杭州某某化工有限公司】
path = /api/web/crm/customer/update
http_method = POST
request_body = {"id":"SXCU...","customerName":"杭州某某化工有限公司",...}
response_body = {"code":200,"message":"success",...}
user_id = d3000000000000000000000000000001
username = liming
create_name = 李明
create_at = 2026-06-29 14:32:05
ip_address = 192.168.1.10
duration_ms = 45
status_code = 200
客户列表(无装饰器,推断兜底)
business_title = CRM客户管理
business_type = 0
business_id = NULL
oper_summary = NULL
path = /api/web/crm/customer/list
request_body = {"page":1,"pageSize":10}
response_body = {"code":200,"data":{...}}
导出(business_id 可空)
business_title = CRM客户管理
business_type = 5
business_id = NULL
oper_summary = 导出客户列表
架构与数据流
flowchart LR
Client[Web / 小程序] --> Crypto[ApiCryptoMiddleware]
Crypto --> LogMW[ApiRequestLogMiddleware]
LogMW --> Route[FastAPI Route]
Route --> Decorator["@oper_log → ContextVar"]
Decorator --> Service[Service / CRUD]
Service --> Route
Route --> LogMW
LogMW --> DB[(sys_api_request_log)]
DB --> OperAPI[operlog/list detail]
OperAPI --> AdminPage[操作日志管理页]
OperAPI --> CRMTab[CRM 详情 Tab]
中间件与加解密顺序
main.py 注册顺序:先 ApiRequestLogMiddleware,后 ApiCryptoMiddleware → Crypto 为外层。RequestLog 在解密后的明文上记录 JSON。
ContextVar 与装饰器
app/core/context.py 扩展:
oper_log_meta: ContextVar[dict] = ContextVar("oper_log_meta", default=None)
@oper_log 参数(建议):
| 参数 | 说明 |
|---|---|
title |
business_title |
business_type |
int |
business_id_param |
从 path/query/body 取 id 的键名 |
summary |
固定摘要或带占位符 |
summary_from |
从 body 字段组装摘要的可选映射 |
装饰器在 handler 入口解析参数并 oper_log_meta.set(...);中间件 finally 读取后落库。
子线程注意: _persist_log 经 asyncio.to_thread 执行时,须在主线程先把 ContextVar 值拷贝进 kwargs,避免线程内丢失。
路径推断兜底(无装饰器)
沿用 oper_log.py 中 _MODULE_TITLE_RULES、_ACTION_TYPE_MAP;中间件落库时若 ContextVar 为空则写入推断结果。
API
现有(增强)
| 方法 | 路径 | 变更 |
|---|---|---|
| POST | /api/web/monitor/operlog/list |
增加 businessId、businessTitle、tenant_no DB 筛选;title/businessType 读落库列 |
| POST | /api/web/monitor/operlog/detail |
operName = create_name 优先 |
OperLogListBody 扩展字段
| 字段 | 类型 | 说明 |
|---|---|---|
businessId |
string? | 业务主键 |
businessTitle |
string? | 模块模糊 |
beginTime / endTime |
string? | 时间范围(可选本期) |
前端
操作日志管理页
| 项 | 值 |
|---|---|
| 路径建议 | web-admin/src/views/system/operlog/index.vue |
| Hash | /#/system/operlog(以菜单 SQL 为准) |
| 模式 | P01,参考 views/system/unit/index.vue |
| 权限 | system:operlog:list、system:operlog:query |
列表列: 模块、操作类型(Tag)、操作人、操作时间、摘要、状态、耗时、操作(详情)
筛选: 模块、操作类型、操作人、状态、时间范围
详情弹窗: 接口地址、IP、入参(operParam)、出参(jsonResult)、错误信息
CRM Tab 改造
| 页面 | 变更 |
|---|---|
CustomerDetailDialog |
listOperLog({ businessId: customerId, pageSize: ... }) |
contract/detail |
listOperLog({ businessId: contractId, ... }) |
移除「拉全量 + 前端 filter path/body」逻辑。
菜单 SQL
挂于「系统管理」下,order_num 在字典/岗位等之后;menuCode 示例 system:operlog:list。
配置
| 环境变量 | 默认 | 说明 |
|---|---|---|
API_REQUEST_LOG_ENABLED |
true |
生产必须为 true |
API_REQUEST_LOG_MAX_BODY |
65536 |
文本字符截断 |
API_REQUEST_LOG_MAX_BODY_BYTES |
131072 |
读取字节上限 |
API_REQUEST_LOG_EXCLUDE_PATHS |
空(本期新增) | 逗号分隔 path 前缀排除 |
迁移
脚本: docs/sql/migrations/2026-06-29-operation-audit.sql
ALTER TABLE sys_api_request_log增加五列 + 索引- 同步
ALTERsys_api_request_log_his - 插入菜单与
super_admin角色菜单(确定性 id) - 同步
docs/sql/shanxia2.0.sql
实施阶段
| 阶段 | 内容 | 估时 |
|---|---|---|
| 1 | SQL + ORM + 中间件姓名/租户/语义 | 1 天 |
| 2 | ContextVar + @oper_log + CRM 写接口样例 |
1 天 |
| 3 | operlog API + 管理页 + CRM Tab | 1~2 天 |
| 4 | 验收 + graphify | 0.5 天 |
验收清单
- 修改客户后
sys_api_request_log含create_name、path、request_body、response_body - 有
@oper_log的记录含business_title、business_type、business_id、oper_summary - 管理页可筛选、详情可看入参出参
- 客户详情 Tab 仅显示该
business_id日志 - 非 admin 无法访问 operlog API
- 租户 A 看不到租户 B 日志
- multipart 上传不产生文件二进制日志
待客户确认
- 「所有操作」范围 — 当前默认全 API;若仅写操作需配置排除规则
- 日志保留天数 — 建议 90 天热数据 + 归档
- 响应体 PII — 是否扩展脱敏手机号/身份证
参考
backend/app/core/api_request_log_middleware.pybackend/app/crud/monitor/oper_log.pybackend/app/api/monitor.pydocs/sql/sx.sql→sys_oper_log(RuoYi 对照)openspec/changes/operation-audit-enhancement/