160 lines
20 KiB
Markdown
160 lines
20 KiB
Markdown
# PRD:TECH 全局多视角审计与外部集成追踪
|
||
|
||
Status: ready-for-agent
|
||
|
||
---
|
||
|
||
## Problem Statement
|
||
|
||
当前系统只有账号操作日志、资产操作日志和手动轮询日志等局部实现。它们的操作者、资源、结果和查询结构不一致,账号与资产审计使用裸 goroutine 写入,进程退出或数据库短暂失败时会丢失;资金、审批、配置和跨模块业务又缺少统一审计。现有记录无法从一次请求、一个业务链路或多个受影响资源串联完整过程。
|
||
|
||
Gateway、运营商、企业微信和支付等外部交互也没有通用 Integration Log。七月迭代的状态同步、审批和资金处理如果继续各建日志,将无法解释“为什么没有请求上游”“哪次回调改变了业务状态”以及“某次资金变化对应哪个审批”。
|
||
|
||
Access Log 当前只递归脱敏请求体,响应体仍原样记录,登录 Token、个人数据和敏感配置存在泄漏风险;回调和文件路由也缺少专门的正文记录策略。
|
||
|
||
## Solution
|
||
|
||
一次停机发布切换到四类边界清晰的记录:Access Log 负责 HTTP 调试,Audit Event 负责不可变业务审计,现有钱包流水/订单/退款等 Domain Ledger 继续作为领域事实,Integration Log 负责外部交互和未实际发出的同步尝试。Audit Event 通过资源关系表关联一个操作涉及的多个资源,并用 `request_id/correlation_id/parent_event_id` 串联请求和跨任务业务链路。
|
||
|
||
本次切换覆盖全部现有敏感写操作:新旧业务统一使用 Audit Writer,旧账号、资产和手动轮询审计表停止新增,不双写;历史数据保留原表并通过 Query 只读投影到新审计中心。切换准备或验证失败则本次版本整体不放量,不能以局部模块继续写旧表作为中间态。
|
||
|
||
## User Stories
|
||
|
||
1. 作为审计人员,我希望回答谁在什么入口对哪些资源做了什么,结果、风险和前后变化是什么。
|
||
2. 作为运维人员,我希望按 `request_id` 或 `correlation_id` 查看一次请求或完整业务链路中的审计、任务、外部交互和领域流水。
|
||
3. 作为资产运营人员,我希望从卡、设备、退款、订单、钱包等资源查看跨模块时间线,而不被普通无变化轮询淹没。
|
||
4. 作为财务人员,我希望资金审计能关联审批、业务单、钱包流水和资产处理,同时明确钱包流水才是金额事实。
|
||
5. 作为安全人员,我希望集中查看失败、拒绝、高风险和严重事件,并确保敏感字段默认脱敏。
|
||
6. 作为集成运维人员,我希望看到 Gateway、运营商、企微和支付的脱敏请求结果,以及合并、限流、提前完成等未发请求原因。
|
||
7. 作为历史数据查询者,我希望旧账号、资产和手动轮询记录仍可只读检索,但发布后不再出现新旧两份不一致记录。
|
||
8. 作为普通代理或企业用户,我希望只能在原业务详情看到自己有权限资源的脱敏轨迹,不能进入平台全局审计中心。
|
||
|
||
## Implementation Decisions
|
||
|
||
### 四类记录的权威边界
|
||
|
||
- Access Log 存储在现有日志文件/日志平台,只用于 HTTP 调试、性能和 `request_id` 检索,不作为业务事实或业务审计权威。
|
||
- Audit Event 存储在 PostgreSQL,回答操作者、动作、资源、结果、风险和字段变化。事件创建后不可更新或删除;普通读操作不创建 Audit Event,敏感读取例外。
|
||
- Domain Ledger 继续由钱包流水、订单、退款、充值、企微审批实例、套餐使用记录等业务表承担。审计 Query 可以链接或投影这些记录,但 Audit Event 不替代领域事实。
|
||
- Integration Log 存储 Gateway、运营商回调、企微、微信/支付宝及其他外部交互,也记录业务同步尝试在发请求前被 `merged/rate_limited/completed/cancelled` 的解释结果。
|
||
- 普通高频轮询成功且状态未变化只写 Integration Log;状态变化、人工强制触发、连续失败或高风险异常再写 Audit Event。
|
||
- Outbox 和通用异步任务状态是可靠投递/执行事实,不塞入 Audit Event JSON。审计链路 Query 通过稳定 ID 关联它们。
|
||
|
||
### Audit Event 数据模型
|
||
|
||
- 新建 `tb_audit_event`,核心字段包括:不可变唯一 `event_id`、`occurred_at`、类别、动作编码/名称、操作者快照、入口来源、租户/店铺/企业标签、结果、风险、摘要、错误码/摘要、前后数据、元数据、请求/关联/父事件 ID、IP/User-Agent/路径/方法、`content_hash` 和创建时间。
|
||
- `result` 为 string 类型:`success/failed/denied/partial`;`risk_level` 为 string:`normal/warning/high/critical`。操作者、来源、类别和动作均使用集中常量或注册表,不允许各 Service 自由拼接 magic string。
|
||
- `actor_kind` 至少支持 `admin_user/agent_user/enterprise_user/personal_customer/open_api_account/system_task/carrier_callback/wecom_callback`。系统和回调允许 `actor_id=NULL`,但 `actor_name` 必须是可读快照。
|
||
- `source` 表示入口而不是人员,至少支持 `admin_api/personal_api/open_api/asynq/scheduled_job/gateway/carrier_callback.* /wecom_callback/wecom_polling/data_migration`。
|
||
- 新建 `tb_audit_event_resource`,每个事件可关联多个 `primary/affected/reference` 资源;字段为事件内部 ID、资源类型、可空资源 ID、资源键快照、关系和创建时间。一个事件至少有一个 `primary` 资源。
|
||
- 资源关系使用复合唯一索引,资源 ID 和资源键各有时间线索引;不建立数据库外键或 GORM 关联标签。
|
||
- `content_hash` 基于脱敏、标准化后的不可变事件内容生成,用于完整性核对,不包含数据库自增 ID。Repository 不提供 Update/Delete 方法。
|
||
- `before_data`、`after_data`、`metadata` 分别最多 16KB;超限时保存截断标志、原字节数、摘要和受控任务/制品引用,批量明细留在对应业务任务表或对象存储。
|
||
|
||
### 动作注册表与写入可靠性
|
||
|
||
- 建立 Action Registry,定义稳定动作编码、中文名称、类别、默认风险、允许的资源类型和敏感字段规则;DTO 枚举说明、筛选项和前端名称都从同一注册表生成。
|
||
- 至少覆盖账号/角色/权限、店铺、资产、套餐、钱包/资金、订单/退款/充值、企微、支付与系统配置、数据同步、导入导出及登录安全的本期动作。未经注册的动作不得写入生产审计。
|
||
- 钱包余额、人工退款结果、代理钱包回退、线下充值入账、账号角色/权限、支付/企微/关键系统配置、人工卡状态、敏感店铺业务员归属及手工绑定企微审批号等成功事件必须与业务变更同事务 `AppendWithTx`;审计失败则业务事务回滚。
|
||
- 旧 MVC Service 未迁移为 DDD 时通过统一 Audit Writer Adapter 接入新模型,不要求为审计一次性重构全部业务;但当前触碰的复杂资金、审批和卡状态用例仍按各自 Spec 迁入 Application/Domain。
|
||
- 业务已经回滚的 `failed/denied` 事件使用独立短事务写入,禁止裸 goroutine。该审计再失败时保留原业务错误,同时写 `critical` 应用日志和监控指标。
|
||
- 异步系统事件与状态变化通过原业务事务或 Outbox 可靠关联,不使用 `go func()`。Asynq/Outbox 载荷必须传递 `event_id/request_id/correlation_id/parent_event_id`。
|
||
- `request_id` 由现有中间件生成并贯穿同一 HTTP 请求;`correlation_id` 在退款、充值、审批、钱包、卡状态等跨请求业务起点生成并贯穿 Outbox/Asynq/Integration Log;`parent_event_id` 表示直接因果,不用于替代 correlation。
|
||
- 对全部旧审计调用建立切换清单和自动检查。发布产物中禁止继续调用旧 `account_audit/asset_audit` 写服务或直接 Create 旧日志模型;启动装配不再注入旧 Writer。
|
||
|
||
### Integration Log
|
||
|
||
- 新建 `tb_integration_log`,至少保存唯一 `integration_id`、provider、方向、operation、外部单号、资源、触发来源/场景/序列/尝试、计划/开始时间、结果、HTTP/渠道码及摘要、脱敏请求响应摘要、耗时、是否改变状态、元数据、请求/关联 ID、可空 Audit Event ID 和创建时间。
|
||
- `direction` 为 `inbound/outbound`。`result` 至少支持 `success/failed/not_found/invalid_payload/ignored/merged/rate_limited/completed/cancelled`;实施可增加内部 `pending` 执行态,但公开 DTO 必须返回中文结果名称并保持终态语义明确。
|
||
- 一次外部尝试使用稳定 `integration_id`。实际调用前先持久化可恢复的尝试事实,完成后条件更新终态;若发出请求后响应未知,必须记录“结果未知”,不得伪装为普通失败并盲目重发具有副作用的外部请求。
|
||
- 数据同步按 UR#94 记录 `trigger_source/scene/series/attempt`;同序列详情一次返回 0/3/5 全部尝试。合并、互斥、限频和已达预期即使没有 HTTP 状态也必须可解释。
|
||
- 运营商/支付/企微入站回调先保存脱敏摘要和幂等标识,再进入业务处理;原始加密报文、完整回调正文、签名和附件不进入普通审计详情。
|
||
- Integration Log 只保存外部交互事实,不存业务审批/支付/卡状态的权威状态;业务改变时通过 `audit_event_id/correlation_id` 关联。
|
||
|
||
### 脱敏与 Access Log 修正
|
||
|
||
- 永不进入 Audit/Integration/Access 正文的数据包括密码、操作密码、验证码、Access/Refresh Token、Secret、回调 Token/EncodingAESKey、支付私钥/公钥原文、完整身份证、对象存储签名 URL、企微 `media_id` 和 Authorization/Cookie。Sanitizer 直接删除或只保留字段存在/长度,不把原值保存成可逆掩码。
|
||
- 手机号、IP、ICCID、钱包金额和第三方交易号允许在受控审计存储中作为业务快照,但普通 Query 默认脱敏;完整查看需要 `audit:sensitive:view`,导出需要独立 `audit:export` 和字段授权。
|
||
- 查看完整敏感值本身写高风险 Audit Event,携带被查看事件/资源和操作者;不能因为已有全局查看权限跳过二次审计。
|
||
- Access Log 的响应体必须和请求体使用同一递归 Sanitizer,再执行 50KB 限制;当前 `truncateBody(c.Response().Body())` 原样记录行为必须移除。
|
||
- 路由级策略:登录/Token/支付或企微配置只记字段名和长度;支付、企微、运营商回调只记摘要与哈希;文件上传下载不记文件内容和签名 URL;普通 JSON 脱敏后最多 50KB。
|
||
- 非 JSON 解析失败不能直接原样记录敏感回调或文件;应先应用路由策略和安全文本截断。Query 参数和 Header 同样覆盖 token、secret、sign、nonce、authorization、cookie 等字段。
|
||
|
||
### 一次性切换与历史投影
|
||
|
||
- 本需求是经用户确认的全局例外:在同一次停机发布中完成新表/索引、Audit Writer、Integration Writer、现有敏感写入口、Query API 和必要前端切换。不得按模块长期双轨运行。
|
||
- 旧 `tb_account_operation_log`、`tb_asset_operation_log` 和 `tb_polling_manual_trigger_log` 发布后停止新写入,不删除、不回填新表。数据库权限或运行时写入护栏应使意外旧写尽快暴露,不能静默继续。
|
||
- 旧手动轮询日志当前还承担进度状态。切换时其用户可见的手动触发、进度和监控接口保持原契约,但运行状态由七月公共异步任务状态承接,外部尝试由 Integration Log 承接;不得因停写旧表使运维功能消失。
|
||
- 历史 Query 使用 `UNION ALL` 把旧账号、资产和手动轮询记录规范化为只读投影,返回 `record_source=legacy_account/legacy_asset/legacy_polling` 和确定性历史事件键;旧记录不伪造不存在的 correlation、风险或多资源关系。
|
||
- 现有 `GET /api/admin/assets/{identifier}/operation-logs` 在过渡期保留兼容响应,但读取新 Audit Event 与旧资产投影,不再直接绑定旧表;新前端以全局资源时间线为准。
|
||
- 发布门禁要求旧写入口清单为零、新旧 Query 样本对账通过、关键事务审计失败回滚通过、Access Log 脱敏通过。任一失败则在开放流量前整体停止发布。
|
||
- 数据迁移全部为增量且可回滚。新系统一旦接收生产写入,已生成 Audit/Integration/Outbox 记录不得删除回滚;应用采用前向修复,不能回到旧 Writer 形成新的分裂历史。
|
||
|
||
### 查询 API 与权限
|
||
|
||
- 全局审计中心提供:
|
||
- `GET /api/admin/audit/events` 及 `/{event_id}`
|
||
- `GET /api/admin/audit/actors`、`/{kind}/{id}/summary`、`/{kind}/{id}/events`
|
||
- `GET /api/admin/audit/resources/search`、`/{type}/{id}/timeline`
|
||
- `GET /api/admin/audit/requests/{request_id}/timeline`
|
||
- `GET /api/admin/audit/correlations/{correlation_id}/timeline`
|
||
- `GET /api/admin/audit/risks/overview`、`/events`
|
||
- `GET /api/admin/audit/integrations` 及 `/{id}`
|
||
- `GET /api/admin/audit/finance/timeline`
|
||
- `POST /api/admin/audit/exports`
|
||
- 所有列表服务端分页,默认 20、最大 100,稳定时间+ID排序。公共关键词只匹配已有索引支持的摘要、资源键、操作者和请求 ID,不对 JSONB 做无索引模糊扫描。
|
||
- 资源搜索先查询业务读模型返回候选 `resource_type/id/key/display_name`,再打开时间线;静态 `/resources/search` 必须先于动态资源路由。`include_related=true` 只展开事件已直接关联资源,不递归遍历图。
|
||
- Request Timeline 组合数据库中可关联的 Audit Event、Outbox/任务摘要和 Integration Log;Access Log 仍在文件/日志平台,API 只返回事件内已快照的 HTTP 摘要和 `request_id` 日志检索标识,不在请求时扫描本地日志文件。
|
||
- Finance Timeline 组合 Audit Event 与钱包流水、订单、退款、充值和企微实例,每条明确 `record_source`;金额结论以 Domain Ledger 为准。
|
||
- 权限码至少拆分 `audit:global:view/actor:view/resource:view/request:view/risk:view/integration:view/finance:view/sensitive:view/export`。超级管理员拥有全量;普通平台角色按授权和原数据范围取交集。
|
||
- 代理和企业账号不能进入全局、人员、风险、资金全局或外部集成中心;在业务详情查看资源轨迹时,Query 必须重新执行店铺/企业权限和字段脱敏。前端隐藏 Tab 不是授权边界。
|
||
- 审计导出复用统一 Export DataSource,创建时快照过滤条件、数据范围、字段授权和脱敏级别;敏感查看权限不自动授予敏感导出权限,权限解析失败时拒绝而非回退全字段。
|
||
|
||
### 前端审计中心
|
||
|
||
- `/operations/audit` 使用工作台式 Tab:全局事件、人员行为、资源轨迹、请求/业务链路、资金审计、风险事件、外部集成;Tab 和字段以后端权限为准。
|
||
- 全局事件表展示时间、风险、操作者、操作、主要资源、来源、结果和 request ID;行详情抽屉分为事件摘要、操作者/入口、关联资源、结构化字段差异、请求/业务链路和错误/外部交互。
|
||
- 资源轨迹先搜索候选再选择,普通无变化轮询不进入业务时间线;外部集成 Tab 可按 provider、operation、方向、结果、资源、触发来源/场景/序列筛选,并连续展示 0/3/5 尝试。
|
||
- 人员、风险、请求和业务链路使用服务端汇总/时间线,不由前端下载全量事件再聚合。第一版不做自动封禁、风险处置工单或自由拖拽关系图。
|
||
- 敏感字段默认掩码;有权限用户点击“显示敏感数据”后重新请求受控接口并产生敏感读取审计。无权限、历史字段不存在和数据已按策略删除均使用明确但不泄密的状态。
|
||
|
||
### 保留、清理与可观测性
|
||
|
||
- 资金、权限、审批和关键配置 Audit Event 保留 5 年;普通资产/业务 Audit Event 保留 2 年;Audit Event 默认不由在线应用删除。
|
||
- Integration Log 默认 180 天;Gateway 无变化成功记录 30 天,异常或状态变化记录 180 天;Access Log 保留 30 天。清理按时间/主键分批执行并记录结果。
|
||
- 达到单表维护阈值后按月分区并归档超期分区,本期不为尚未达到阈值预建复杂分区管理,但表和 Query 必须支持后续演进。
|
||
- 应用运行账号不提供 Audit Event Update/Delete 能力;归档/清理由独立受控维护身份执行。Integration Log 只允许执行态到终态的受控条件更新,不允许事后改写请求结果。
|
||
- 监控 Audit/Integration 写入失败、失败短事务失败、旧表意外新增、Outbox 积压、Integration Log 增长/清理、敏感读取和导出次数。
|
||
|
||
## Testing Decisions
|
||
|
||
- 领域/Application 测试覆盖事件不可变、多资源至少一个 primary、动作注册、风险默认值、内容哈希稳定、16KB 截断和禁止字段删除。
|
||
- PostgreSQL 集成测试验证唯一/查询索引、关键业务与 Audit Event 同事务、审计写入失败回滚、失败/拒绝短事务、重复事件幂等和资源时间线。
|
||
- 对账号、角色权限、资产、套餐、钱包、退款、充值、配置、导入导出、登录安全和手动同步建立切换清单;自动测试或静态检查证明生产装配不再调用旧 Writer,旧三表发布后无新增。
|
||
- 历史投影测试使用旧账号/资产/手动轮询样本,验证 `UNION ALL` 的字段映射、确定性历史键、分页排序、`record_source` 和新旧交界时间无重复/漏项。
|
||
- Integration 测试覆盖 outbound 成功/失败/响应未知、inbound 回调、未发送的 merged/rate_limited/completed、同序列 0/3/5、状态变化关联 Audit Event 和重复回调。
|
||
- Access Log 测试覆盖嵌套 JSON、数组、非 JSON、登录/Token、支付/企微/运营商回调、上传下载和响应体,证明 Token、Secret、操作密码、签名 URL、Authorization、Cookie 等不落盘且 50KB 生效。
|
||
- 权限测试覆盖超级管理员、不同平台角色、代理和企业;验证 Tab、API、字段、资源范围、敏感查看与导出权限相互独立,越权资源不泄露存在性。
|
||
- Query 性能测试使用代表性事件/资源/Integration 数据,验证分页和常用过滤使用索引、无 JSONB 全表模糊扫描、无资源/操作者 N+1,满足项目 P95/P99 目标。
|
||
- HTTP 集成测试穿过真实 Fiber 认证、Handler、Query/GORM 和统一错误响应;新 Handler 同步注册两个 OpenAPI 文档生成器并验证静态/动态路由顺序。
|
||
- 停机发布演练覆盖暂停 Worker、迁移、旧写护栏、新 Writer 切换、样本对账、恢复 Worker、放量前失败退出和放量后的前向修复;已生成审计数据不得通过清表回滚。
|
||
- 前端验收覆盖七个视角、权限空态、历史投影、资源候选、request/correlation 跳转、0/3/5 序列、敏感二次查看、导出和错误状态。
|
||
|
||
## Out of Scope
|
||
|
||
- 不把 Access Log、Audit Event、Domain Ledger 和 Integration Log 合并成一张万能日志表。
|
||
- 不把普通列表、详情和未读数查询全部写成业务审计;只审计敏感读取。
|
||
- 不在线回填旧日志到新表,不长期双写新旧审计,不删除旧历史表。
|
||
- 不为数据同步另建 `tb_card_sync_execution` 或独立同步审计页面。
|
||
- 不把整个旧业务仓库一次性迁成 DDD;只统一其审计 Adapter,复杂用例按各自需求迁移。
|
||
- 不建设自动风控封禁、风险处置工单、自由关系图或实时行为分析平台。
|
||
- 不在 API 请求中扫描本地 Access Log 文件,也不把日志文件升级为业务权威存储。
|
||
- 不允许应用用户修改/删除 Audit Event,不在普通审计详情暴露完整外部报文和密钥。
|
||
|
||
## Further Notes
|
||
|
||
- 用户已明确确认一次性全局切换,覆盖标准稿中的“旧写停止、不双写、历史只读投影”口径;这不是可由实现阶段改回渐进双写的建议项。
|
||
- 当前已核实旧账号/资产审计使用裸 goroutine,Access Log 响应体未脱敏;这两项是发布前必须消除的现存缺陷。
|
||
- 当前手动轮询日志兼做进度存储,切断旧表时必须先由公共异步任务状态承接,不得违反 UR#94“轮询管理外部行为保持现状”的确认结论。
|
||
- 本需求较大,进入实现前应依据本 Spec 拆成可独立验证的纵向切片,但不得按“先建表、再 Service、再 Handler”的水平层级拆分,也不得改变一次停机切换这一最终发布门禁。
|