Files
shanxia2.0/docs/superpowers/specs/2026-06-29-operation-audit-design.md
T
2026-06-29 11:15:39 +08:00

317 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 操作审计与业务语义日志 — 设计文档
> **日期**: 2026-06-29
> **状态**: 已批准(待实现)
> **OpenSpec**: `openspec/changes/operation-audit-enhancement/`
> **前置**: 现有 `ApiRequestLogMiddleware`、`sys_api_request_log`、`operlog/list` API
> **界面模式**: P01 标准列表(操作日志管理页)
## 目标
满足客户对「用户操作可追溯」的要求,在**不重建**审计体系的前提下:
1. **继续全量 API 采集** — 接口地址、入参、出参、时间、操作人
2. **补齐请求人姓名与租户** — `create_name`(nick_name)、`tenant_no`
3. **增加业务语义字段** — 模块、操作类型、业务主键、可读摘要
4. **提供全局管理页** — 筛选、分页、详情
5. **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 = 导出客户列表
```
---
## 架构与数据流
```mermaid
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` 扩展**:
```python
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`
1. `ALTER TABLE sys_api_request_log` 增加五列 + 索引
2. 同步 `ALTER` `sys_api_request_log_his`
3. 插入菜单与 `super_admin` 角色菜单(确定性 id)
4. 同步 `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 上传不产生文件二进制日志
---
## 待客户确认
1. **「所有操作」范围** — 当前默认全 API;若仅写操作需配置排除规则
2. **日志保留天数** — 建议 90 天热数据 + 归档
3. **响应体 PII** — 是否扩展脱敏手机号/身份证
---
## 参考
- `backend/app/core/api_request_log_middleware.py`
- `backend/app/crud/monitor/oper_log.py`
- `backend/app/api/monitor.py`
- `docs/sql/sx.sql` → `sys_oper_log`(RuoYi 对照)
- `openspec/changes/operation-audit-enhancement/`