317 lines
10 KiB
Markdown
317 lines
10 KiB
Markdown
# 操作审计与业务语义日志 — 设计文档
|
||
|
||
> **日期**: 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/`
|