## Context 已有 `ApiRequestLogMiddleware` 全量采集 Web API 至 `sys_api_request_log`(path、query、request_body、response_body、user_id、username、create_at 等)。`operlog/list` 将上述数据适配为 RuoYi `OperLogVO`,权限为 admin/super_admin,CRM 详情 Tab 已调用但未按业务实体过滤。 约束:MySQL 5.7、租户隔离、数据权限、Fresh Mint UI(P01 列表)、中间件与 `ApiCryptoMiddleware` 顺序不变(Crypto 外层、RequestLog 内层,记录明文 JSON)。 ## Goals / Non-Goals **Goals:** - 客户可见:姓名、时间、地址、入参、出参 - 业务语义可筛、可按 `business_id` 关联单据 - 写操作通过 `@oper_log` 声明;未声明接口保留路径推断兜底 - 全局操作日志管理页 **Non-Goals:** - 前端 UI 行为埋点(不发 API 的操作) - 独立审计子系统(MQ/ES) - 与 CRM Java 仓 `sys_oper_log` 数据合并 - 本期全量 `@oper_log` 覆盖所有写接口 - multipart 文件内容入库 ## Decisions ### D1 方案 A + 语义字段(不新建 sys_oper_log) 在 `sys_api_request_log` 扩展列,中间件继续全量采集;语义由 `@oper_log` + ContextVar 注入。理由:基础设施已存在,改动面可控。 ### D2 语义字段集 | 列 | 说明 | |----|------| | `tenant_no` | 租户隔离 | | `business_title` | 模块,如 `CRM客户管理` | | `business_type` | tinyint,对齐 `sys_oper_type`(0其他 1新增 2修改 3删除 5导出 6导入) | | `business_id` | 业务主键,可空 | | `oper_summary` | 人读摘要,如 `修改客户【杭州某某化工】` | 展示优先级:`oper_summary` 用于列表摘要;`title`/`businessType` 映射 operlog API 字段。 ### D3 写入链路 1. 请求进入 → 路由 `@oper_log` 在 handler 执行前/包装器内设置 `ContextVar` 2. `call_next` 完成 → 中间件 `finally` 读 ContextVar,与 JWT 字段一并 `_persist_log` 3. ContextVar 为空时:`business_title`/`business_type` 由 `oper_log.py` 现有路径规则在**读时**兜底(落库可为 NULL,或中间件落库时同步写推断值——实现取**落库时写推断值**以减少读时计算) ### D4 请求人姓名 JWT payload 已有 `real_name`(nick_name)。中间件 `_claims_from_auth` 扩展解析并写入 `create_name`;`username` 仍为 `user_no`。 ### D5 oper_summary 模板 `{动词}{对象}【{显示名或编号}】`,动词与 `business_type` 一致。装饰器可传 `summary` 或模板参数;Service 层可在执行业务后更新摘要(二期可选)。 ### D6 查询与权限 - `operlog/list`:DB 层筛选 `tenant_no`、语义字段;数据权限仍按 `user_id` - 管理页菜单:`system:operlog:list`;仅 admin/super_admin(与现 monitor API 一致) - CRM Tab:`business_id` + 可选 `business_title` 精确过滤 ### D7 记录范围(默认) - **记录**:经 FastAPI 的业务 API(含 GET 列表) - **跳过**:OPTIONS、/health、/docs、/openapi.json(与现中间件一致) - **不完整**:multipart、超大 body、敏感字段脱敏(保持现逻辑) 客户若后续要求「仅写操作」,通过 `API_REQUEST_LOG_EXCLUDE_PATHS` 配置扩展(本期仅预留 settings 字段,默认空)。 ### D8 归档 表 `sys_api_request_log_his` 已存在;本期迁移脚本扩展同名列。定时归档任务记入 tasks 后续项,不阻塞主功能。 ## Risks / Trade-offs | 风险 | 缓解 | |------|------| | 日志量膨胀 | 后续归档 + 可选排除只读路径 | | 未注解接口语义弱 | 路径推断兜底 + 逐步补注解 | | 响应体含 PII | 保持脱敏;详情仅授权角色可见 | | ContextVar 在子线程丢失 | 落库用 `asyncio.to_thread` 时传入已收集的 dict,不依赖子线程读 ContextVar | ## Migration Plan 1. 执行 `docs/sql/migrations/2026-06-29-operation-audit.sql` 2. 部署后端(中间件 + 装饰器 + API) 3. 部署前端(管理页 + CRM Tab) 4. 确认生产 `API_REQUEST_LOG_ENABLED=true` 5. 为核心 CRM 写接口补 `@oper_log` ## Open Questions - 日志在线保留天数(默认 90 天归档,待客户确认) - 响应体是否额外脱敏手机号/身份证(默认保持现状,仅 password/token 类)