Files
lingxi/docs/开发计划/灵犀_A5审计与日志_实施报告_V0.1.md
T
Stev_Wang 83aa04b856 docs(plan): A5 实施报告与计划回写
新增 A5 审计与日志实施报告;回写开发计划状态、§6.6 完成情况与 WBS 状态;README 文档索引增加 A5 报告。
2026-11-04 16:28:30 +08:00

158 lines
13 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.
# 灵犀 A5 审计与日志 · 实施报告 V0.1
| 项 | 内容 |
| --- | --- |
| 实施日期 | 2026-11-04 |
| 批次 | A5(审计与日志) |
| 状态 | **已完成**(开发 + 全部门禁 + 浏览器 e2e 通过;未提交 git) |
| 依据 | 开发计划 V0.1 §6.6 / §7.5;PRD V1.6 §5.4.6 / R-05;技术架构 V0.4 §5.4.5 / §9.7;工程规范 08 |
| 视觉基线 | A0「方向 A · 云白蓝」;用户选定 **操作日志列表 A / 日志详情 A / 运行日志 C** |
| 迁移 | `prisma/migrations/20261104054622_a5_audit_logs/` |
| 前端路由 | `/system/audit-logs`、`/system/audit-logs/:id`、`/system/logs` |
---
## 1 范围与完成度
| 任务 | 内容 | 状态 |
| --- | --- | --- |
| A5-BE-01 | 审计表与 append-only(月分区 / 复合主键 / DB 触发器 / `lingxi_app` 最小权限) | ✅ |
| A5-BE-02 | 审计采集(`@Audit` + 拦截器落库、成功 / 失败双写、前后值、脱敏) | ✅ |
| A5-BE-03 | 审计检索与导出(多条件游标检索、详情 diff、异步 CSV、签名下载、导出自身留痕) | ✅ |
| A5-BE-04 | 敏感访问与保留归档(查看审计留痕、保留期配置、归档占位) | ✅ |
| A5-INF-01 | 运行日志与观测入口(Pino / trace_id / 日志平台配置与外链;OTel 待接) | ✅(OTel 待确认) |
| A5-FE-01 | 操作日志列表与详情(方案 A:筛选卡 + 全宽表格 + 游标分页 / 独立详情 + diff + 原始 JSON) | ✅ |
| A5-FE-02 | 导出与只读(560 抽屉、进度轮询、历史、签名下载、权限控制) | ✅ |
| A5-QA-01 | 不可篡改与留痕用例(7 类采样 + append-only + 浏览器 e2e) | ✅ |
> 侧栏同步更名:`审计日志` → **`操作日志`**、`客户字段` → **`字段管理`**,新增 **`运行日志`**;A1–A4 既有页面与功能未改动。
---
## 2 交付物
### 2.1 文档与契约
- 技术架构 §5.4.5 / §9.7 对应实现已回写本报告;开发计划 §6.6 / §7.5 / §13 状态与 WBS 已回写。
- `e2e/README.md` 增加 A5 用例与运行命令。
- UI 原型与实现截图:`design/a5-ui-v1/`(A/B/C 三套原型、对比页、评审记录、实现截图)。
### 2.2 共享契约(`packages/shared`)
- `audit/audit-actions.ts`:10 个审计分类(含 `other` 兜底)、54 个动作(现有 36 个 `@Audit` + A5 新增 + asset/task/sensitive 预留),`auditActionMeta` 未知 code 兜底。
- `audit/audit-diff.ts`:`buildAuditDiff` 纯函数(对象一层展开、数组 index、权限点数组增删语义);前后端共用。
- `schemas/audit.ts`:列表 / 详情 / 动作目录 / 导出创建 / 导出任务(含 `download_token`)/ 导出历史的 Zod 契约。
- `config/system-configs.ts`:新增 `audit` 分组与 3 项配置:`audit.retention_months`(默认 12,min 12)、`audit.export_max_rows`(默认 50,000)、`audit.logs_platform_url`(public,供运行日志页读取)。
- 单测:契约、动作目录、diff、配置校验(shared 合计 163)。
### 2.3 数据库与种子
迁移 `20261104054622_a5_audit_logs`(expand-only,新增表,不改历史迁移):
- `audit_logs`:`RANGE (occurred_at)` 按月分区,复合主键 `(id, occurred_at)`;字段覆盖主体 / 动作 / 分类 / 对象 / 客户 / 结果 / 请求 / trace / IP / UA / 前后值 / 原因 / 错误 / 耗时;6 组索引。
- `audit_logs_ensure_partitions(months_ahead int)`:幂等创建当月 ~ +N 月分区;迁移时创建 14 个月;应用启动 + 每日定时调用。
- append-only:`BEFORE UPDATE/DELETE` 行触发器 + `BEFORE TRUNCATE` 语句触发器(`insufficient_privilege`);`REVOKE UPDATE/DELETE/TRUNCATE FROM PUBLIC`;创建 `lingxi_app` NOLOGIN 角色并仅授予审计表 `SELECT/INSERT`。
- `audit_export_jobs`:任务状态 / 进度 / `truncated` / 文件信息 / 过期 / 下载计数 / `download_count`。
- 种子:`system_configs` 27→29 项(A5 新增 3);6 条纯追加演示审计(固定 ULID,重复执行跳过);`SEED.md` 更新。
### 2.4 后端(`apps/api`)
| 模块 | 接口 |
| --- | --- |
| platform/audit | `GET /audit-logs`、`GET /audit-logs/actions`、`GET /audit-logs/{id}` |
| platform/audit | `POST /audit-logs/export`、`GET /audit-logs/exports`、`GET /audit-logs/exports/{id}` |
| platform/audit | `GET /audit-logs/exports/{id}/download?token=`(`@Public` + HMAC 短时效) |
- `AuditInterceptor`:成功 / 失败都落库;响应前 `await`,写入失败只记 Pino、不影响主请求;`AUDIT_SINK` 可选注入,未装配退化为仅 Pino;修复原 `defer(ALS.run())` 导致 handler 内 `patchAudit` 丢失上下文的隐患。
- 采集覆盖:登录 / 登出 / 登录失败、角色与权限、用户 / 部门、客户 / 行业 / 客户组 / 客户字段、字典类型 / 条目、系统配置;前后值与 targetName / customerId / customerName。
- 敏感访问:审计列表与详情本身写 `audit.log.view`;导出与下载分别写 `audit.export` / `audit.export.download`。
- 导出:异步 `setImmediate`(A6 任务协议就绪后替换)、分批 CSV(UTF-8 BOM、转义)、`audit.export_max_rows` 截断标记、本地 `apps/api/.data/audit-exports/`、启动恢复卡死任务、HMAC 签名下载并计数。
- 保留 / 归档:`AuditRetentionService` 仅返回计划(`supported:false`),不 UPDATE/DELETE 审计数据(QT-10 待确认)。
- 查询:所有过滤在 SQL `where` 完成;稳定排序 `occurred_at DESC, id DESC`;关键字覆盖对象名 / 对象 ID / 请求路径 / 原因 / 操作人姓名 / 账号 / trace_id / IP。
### 2.5 前端(`apps/web`)
| 页面 | 路由 | 实现 |
| --- | --- | --- |
| 操作日志 | `/system/audit-logs` | 方案 A:页头只读标识 + 查看留痕提示 + 两行筛选(关键字 / 分类 / 操作人 / 客户 / 结果 / 时间预设 + 自定义日期)+ 全宽表格 + 真实游标「加载更多」 |
| 日志详情 | `/system/audit-logs/:id` | 方案 A:左 diff / 完整原始 JSON,右上下文 / 只读说明 / 相关记录;loading / error / 404 三态;失败请求可打开 |
| 运行日志 | `/system/logs` | 方案 C:平台入口卡 / trace 检索卡 / 服务状态卡 / 最近错误卡;真实数据仅 `audit.logs_platform_url`,其余为诚实空态 |
- 导出:560 SideSheet,创建 → 1.5s 轮询 → 成功 / 失败 / 过期 / 历史 / 刷新 / 签名下载;创建成功失效审计列表,使 `audit.export` 记录进入列表。
- 权限:`audit:read` 守卫列表 / 详情 / 运行日志;`audit:export` 控制导出入口(无权限不渲染)。
- 侧栏:`操作日志`(紧随数据字典)、`运行日志`、`字段管理`;面包屑详情补「管理中心 / 操作日志 / 日志详情」。
- 适配:1024 宽隐藏 IP / trace 并收窄列宽,保留 时间 / 操作人 / 操作 / 对象 / 客户 / 结果 / 查看;1440 九列完整。
- 数据层:`api/audit.ts`(shared Zod parse)、`hooks/queries/use-audit.ts`(InfiniteQuery / 详情 / 动作目录 / 导出轮询 / 创建 mutation)、`api/query-keys.ts`、`endpoints.ts`、`apiBaseUrl()`。
- 样式:`styles/audit.css`、`styles/run-logs.css`,只用 `--lx-*` token。
---
## 3 验证结果
| 门禁 | 命令 | 结果 |
| --- | --- | --- |
| 格式 | `pnpm format:check` | ✅ |
| Lint | `pnpm lint` | ✅ 0 error / 0 warning |
| 类型 | `pnpm typecheck` | ✅ 10/10 tasks |
| 单测(shared) | `pnpm --filter @lingxi/shared test` | ✅ 16 files / 163 tests |
| 单测(api) | `pnpm --filter @lingxi/api test` | ✅ 24 files / 164 tests(lines 65.75%,阈值 60) |
| 单测(web) | `pnpm --filter @lingxi/web test` | ✅ 32 files / 187 tests(lines 92.72%) |
| 集成 | `pnpm --filter @lingxi/api test:int` | ✅ 8 files / 70 tests(含 audit 12) |
| 构建 | `pnpm build` | ✅ 8/8 tasks |
| OpenAPI | `pnpm openapi:diff` | ✅ 无破坏性变更 |
| 浏览器 e2e | `A5_E2E=1 pnpm exec playwright test e2e/a5-audit-logs.spec.ts` | ✅ 5/5(约 57s) |
**e2e 覆盖**:列表 / 查看留痕提示 / 分类筛选 / 重置;详情 diff / 完整原始 JSON / trace / IP / 请求 / 耗时;导出创建 → 轮询完成 → 导出历史 → API 断言 `download_token` + CSV 200 → 列表新增 `audit.export`;无 `audit:read` → 403;有 `audit:read` 无 `audit:export` 不渲染导出;运行日志未配置空态 → 配置后外链出现。
**数据库实测**:`UPDATE / DELETE / TRUNCATE audit_logs` 均被 PG 拒绝;`SET LOCAL ROLE lingxi_app` 下 INSERT / SELECT 成功、UPDATE 被权限拒绝;分区函数重复执行 `created=0`;`prisma migrate status` up to date;seed 二次执行配置不覆盖、演示审计不重复。
**浏览器人工验收**(真实 API + PG + seed):1440 列表 / 详情 / 导出抽屉 / 运行日志(已配置 / 未配置)、1024 列表;无控制台错误、无横向溢出,body 不出现纵向滚动。
---
## 4 关键口径与实现说明
1. **append-only 双保险**:DB 触发器阻止任何角色 UPDATE/DELETE/TRUNCATE;生产应用账号应使用 `lingxi_app`(仅审计表 SELECT/INSERT)。本地仍用 postgres,由触发器兜底。
2. **分区无 default 分区**:迁移建 14 个月,启动 / 每日 `ensure` 继续创建;写入范围由 ensure 保证,避免 default 分区与后续 ATTACH 冲突。
3. **审计查看留痕**:列表 / 详情访问写 `audit.log.view`(LOG-14);因此频繁轮询列表会产生较多浏览记录,是否采样 / 仅详情留痕待产品确认。
4. **导出自身留痕**:创建写 `audit.export`,下载写 `audit.export.download`;创建成功前端失效列表,保证新记录可见。
5. **权限口径**:`audit:read` / `audit:export` 为特殊权限,默认关闭;无权限不渲染导出入口,接口仍校验(40303)。
6. **运行日志与审计分离**:运行日志只提供日志平台入口与 trace 检索指引,不写入业务库;服务状态 / 错误列表无后端接口,页面用空态说明,不伪造数据。
7. **时间筛选包含当天**:预设「今天 / 近 7 天 / 近 30 天」`to` 取当天 23:59:59.999,保证列表刷新后能看到当天新产生的审计记录。
8. **不伪造统计**:后端未提供 summary 接口,列表页不展示 KPI 数字,只显示真实「已加载 N 条 / 还有更多 / 已全部加载」。
9. **导出预计条数**:无服务端 count 接口,抽屉显示「当前筛选已加载 N 条,实际以服务端筛选为准」,不编造条数。
10. **详情 diff label**:后端只返回路径(label 为 null),前端用本地路径中文名映射兜底。
---
## 5 与计划的差异与待确认
| 编号 | 项 | 现状 |
| --- | --- | --- |
| A5-D-01 | 七类留痕 | 登录 / 权限 / 客户与字典等配置 / 导出 / 敏感访问已实测;资产(B1)与任务(A6)接口未建设,已登记 action code 与预留,未伪造数据 |
| A5-D-02 | OTel 接入 | SDK 属新增运行时依赖,本轮未引入;Pino 字段、trace_id 贯通、审计与运行日志分离已就绪,运行日志页标注「OTel 待接入」 |
| A5-D-03 | 运行日志服务指标 | 无指标 / trace / 错误查询接口,页面按方案 C 布局但服务状态与最近错误为空态,不伪造 |
| A5-D-04 | 保留期 | 默认 / 下限 12 个月(min 12 / max 60),QT-10 最终值确认后改 shared 定义即可 |
| A5-D-05 | 审计查看留痕频率 | 列表每次请求都留痕,符合 LOG-14;如需采样 / 去重需产品确认 |
| A5-D-06 | 导出存储 | A5 使用本地 `apps/api/.data/audit-exports/`,B3 / A6 接入对象存储与统一任务协议后替换;过期文件未定时物理清理 |
| A5-D-07 | 导出并发 | 未做限流,待 A6 / 运维策略 |
| A5-D-08 | e2e 审计数据 | 审计表 append-only,e2e 产生的审计记录不清理(仅清理临时账号 / 角色并还原配置),属预期留痕 |
---
## 6 已知遗留
1. 资产 / 任务 / 成本 / 客户资产包接口就绪后接入 `asset.*` / `task.*` / `sensitive.*` 动作(action code 已登记)。
2. 导出文件物理清理、并发限流、对象存储签名 URL(B3 / A6)。
3. OTel SDK 与日志平台真实查询接口(待依赖评审 / 运维提供)。
4. 列表「查看」动作产生 `audit.log.view` 记录,后续可按需调整采样策略。
5. UI 原型中 A/B/C 三套为设计产物(`design/` 目录不入库);实现按用户选定 A + A + C 落地。
---
## 7 下一步
- 按计划进入 **A6 任务中心与队列骨架**:审计导出将迁移到统一任务协议(进度 / 重试 / 取消 / 幂等)。
- A7 AI 配置可直接复用审计采集与 `@Audit` 装饰器;资产 / 任务接入后补齐七类留痕。
- QT-10 / Q-08 保留期、OTel 依赖、审计查看采样策略确认后回写本报告。