Constraint: 切换 main 前必须保存当前七月分支全部项目进展,套餐生效提案仅属于 Iteration/7-11。 Rejected: 将七月套餐修复直接移植到 main | 两个分支的可靠投递架构不同。 Confidence: medium Scope-risk: broad Directive: 不得将本提交整体 cherry-pick 到 main;main 套餐热修必须基于其纯 Asynq 代码独立实施。 Tested: git diff --check;openspec validate fix-package-activation-starvation --strict。 Not-tested: 按用户要求未运行自动化测试;go build ./... 因当前审计改造中的 Enterprise 模型字面量和 role.recordFailure 参数类型错误未通过。
365 lines
49 KiB
Markdown
365 lines
49 KiB
Markdown
## Context
|
||
|
||
当前仓库同时存在四类不能互相替代的事实:Access Log 面向开发人员记录 HTTP 调试信息;`tb_integration_log` 记录外部请求、回调、未发送尝试和结果未知;订单、支付、退款、钱包流水、审批、套餐权益等业务表是 Domain Ledger;账号和资产 operation log 则是结构不一致、覆盖有限且使用裸 goroutine 的旧内部审计。现状没有统一回答操作者、动作、多资源、结果、风险和跨请求因果关系的 Audit Event,也没有 Integration Log 的平台查询接口。
|
||
|
||
本 Change 是新的独立规划,不恢复七月旧 Change。实施前以当前代码重新盘点 HTTP、Application、旧 Service、Worker、Scheduler 和 Callback 写入口;旧 490 项覆盖清单只作遗漏参考,不能直接作为现行契约。当前盘点已确认账号权限、店铺企业、个人客户、卡、设备、设备卡槽、资产流转、换货、套餐、订单、支付、退款、充值、钱包、佣金、审批、配置、导入导出、通知、轮询、外部集成和可靠事件等领域。
|
||
|
||
架构遵循触碰式迁移:复杂写在既有 Domain/Application 完整事务边界接入审计;简单写使用 Application 事务脚本;尚未迁移的旧用例由旧 Service 通过统一 Adapter 接入;所有调查读取使用 `Handler → Query → GORM/DTO`,不经过聚合根。新增表不建立外键,不使用 GORM 关联标签。
|
||
|
||
## Goals / Non-Goals
|
||
|
||
**Goals:**
|
||
|
||
- 建立统一、不可变、多资源、可串联的内部 Audit Event,覆盖全部非查询业务操作及有调查价值的失败、拒绝和自动状态变化。
|
||
- 让同一事实可以从全局、操作者、资源、请求、业务关联、资金、风险和外部集成等视角查询。
|
||
- 为卡、设备及其多卡槽关系、换货旧新资产、资金和批量操作保存足以独立理解历史的业务标识快照。
|
||
- 保持 Audit Event、Integration Log、Domain Ledger、Outbox/任务状态和 Access Log 的权威边界。
|
||
- 让平台看到完整业务审计数据,让代理/企业只看到其有权资源的安全业务结论。
|
||
- 停止旧账号/资产 operation log 新增;旧表原样保留且不进入新审计中心,新中心从切换后的 Audit Event 开始形成完整视角。
|
||
- 将高频 Audit Event 和 Integration Log 按日压缩到对象存储,并在可证明归档完整后删除 PostgreSQL 上月数据,避免日志无限占用数据库;对象存储备份长期保留。
|
||
|
||
**Non-Goals:**
|
||
|
||
- 第一阶段不提供面向用户的审计导出、对象存储历史查询/恢复、敏感二次查看、细粒度平台权限码、平台数据行限制、风险处置或自动恢复操作。
|
||
- 不把 Access Log 文件导入 PostgreSQL,不在调查接口中扫描本地日志文件。
|
||
- 不保存密码、验证码、Token、Secret、私钥、回调凭据、Authorization、Cookie、签名 URL、支付密钥或完整第三方原始正文。
|
||
- 不用 Audit Event 替代钱包流水、订单、支付、退款、审批、套餐权益等 Domain Ledger。
|
||
- 不为每个领域新建审计表,不在线回填或删除旧 operation log,不建立长期新旧双写。
|
||
- 不建设任意关系图、任意 JSONB 搜索、审计数据修改/删除 API、归档下载 API 或 Integration Log 恢复 API。
|
||
- 不借审计接入一次性重构未触碰业务模块;迁移单位是完整用例。
|
||
|
||
## Decisions
|
||
|
||
### 1. 四类事实保持分离,以稳定标识关联
|
||
|
||
```text
|
||
HTTP 请求 ──────────────── Access Log(开发调试)
|
||
业务写事务 ─┬──────────── Domain Ledger(业务权威)
|
||
├──────────── Audit Event(谁对什么做了什么)
|
||
└──────────── Outbox(可靠副作用)
|
||
外部请求/回调 ─────────── Integration Log(外部交互事实)
|
||
```
|
||
|
||
`request_id` 关联一次 HTTP 请求;`correlation_id` 关联跨请求、Outbox、Asynq、回调和业务后续步骤;`parent_event_id` 只表示直接因果。Integration Log、Outbox 和任务通过稳定 ID 进入关联时间线,但查询结果必须标明 `record_source`,不得把不同事实伪装成同一种记录。
|
||
|
||
**否决方案:** 把所有内容合并为万能日志表。它会混淆业务事实、技术尝试和审计责任,并让保留期、更新规则与查询索引互相冲突。
|
||
|
||
### 2. Audit Event 使用主事件和资源关系两张表
|
||
|
||
`tb_audit_event` 保存操作级事实:
|
||
|
||
| 字段组 | 主要字段 | 含义 |
|
||
|---|---|---|
|
||
| 身份 | `id`、`event_id`、`occurred_at` | 数据库内部 ID、稳定公开 ID和真实发生时间 |
|
||
| 动作 | `category`、`action_code`、`action_name`、`summary` | 稳定动作编码、中文快照和内部摘要 |
|
||
| 操作者 | `actor_kind`、`actor_id`、`actor_name`、`actor_shop_id/name`、`actor_enterprise_id/name` | 人工、OpenAPI、系统任务或外部系统真实身份快照 |
|
||
| 入口 | `source`、`request_path`、`request_method`、`ip_address`、`user_agent` | API、Worker、Scheduler、Callback 等入口及 HTTP 摘要 |
|
||
| 目标范围 | `scope_type`、`scope_id`、`scope_name` | 本次操作主要涉及的店铺、企业、平台或个人范围快照 |
|
||
| 结果 | `result`、`risk_level`、`error_code`、`error_summary` | `success/failed/denied/partial/unknown` 与稳定风险 |
|
||
| 链路 | `request_id`、`correlation_id`、`parent_event_id` | 请求、业务链路和直接因果 |
|
||
| 批量 | `batch_total`、`success_count`、`fail_count` | 批次根事件统计,普通事件为零 |
|
||
| 补充 | `metadata`、`content_hash`、`created_at` | 有界业务参数、不可变内容哈希和落库时间 |
|
||
|
||
`tb_audit_event_resource` 保存每个资源自己的身份与变化:
|
||
|
||
| 字段 | 含义 |
|
||
|---|---|
|
||
| `audit_event_id` | 主事件内部 ID;普通索引,不建外键 |
|
||
| `resource_type`、`resource_id` | 注册资源类型与可空内部 ID,统一按字符串投影 |
|
||
| `resource_key`、`display_name` | 事件发生时的稳定业务 Key 和可读名称 |
|
||
| `relation` | `primary/affected/reference` |
|
||
| `role` | `old_card/new_card/bound_card/entry_device/shop/order` 等稳定业务角色 |
|
||
| `identity_snapshot` | 事件发生时的业务标识快照,不依赖当前业务表 |
|
||
| `before_data`、`after_data` | 只保存该资源本次实际涉及的业务字段 |
|
||
| `subject_visibility` | `internal_only/subject_result/subject_detail` |
|
||
| `subject_summary` | 代理/企业可见的安全结论;不从内部摘要临时删字段生成 |
|
||
| `subject_data` | 写入时按 Action Registry 白名单生成的主体可见结构化业务字段 |
|
||
| `sort_order`、`created_at` | 稳定展示顺序和写入时间 |
|
||
|
||
每个事件至少有一个 `primary` 资源。资源关系使用事件、资源、关系和角色复合唯一约束;资源 ID、Key、事件时间建立时间线索引。业务 Repository 不提供 Update/Delete;只有内部 Retention Worker 可在归档完整性门禁通过后按已归档月份执行受控删除。
|
||
|
||
**否决方案:** 继续按账号、资产、订单各建 operation log。它无法低成本支持跨领域资源时间线,并重复制造不一致字段。
|
||
|
||
### 3. Action Registry 与 Resource Registry 是写入和展示契约
|
||
|
||
所有常量位于 `pkg/constants/`。Action Registry 至少定义动作编码、中文名称、类别、默认风险、允许的操作者来源、资源类型与角色、是否必须同事务、默认主体可见性和安全字段规则。Resource Registry 定义资源类型、中文名、稳定 Key、展示名称和允许的快照字段。
|
||
|
||
未经注册的 action、缺少主要资源、资源角色不合法、系统安全凭据未被清理或高风险动作未使用要求的事务策略时,统一 Writer 拒绝写入。Action 中文名保存快照,历史不因后续改名而改变;Query 同时返回编码和中文名。
|
||
|
||
覆盖清单按完整业务用例登记:入口、action、actor、资源、事务、失败策略、Audit Event、Domain Ledger、Integration Log、Outbox 和 N/A 理由。自动扫描只发现候选,业务语义必须由实现任务确认;Setter、装配方法和纯查询不得因命名被误判为业务动作。
|
||
|
||
### 4. 操作者上下文由入口提供,业务语义由 Application/Service 显式写入
|
||
|
||
HTTP 中间件只构造 Audit Context:操作者、来源、IP、User-Agent、路径、方法和 request ID。Worker、Scheduler、OpenAPI 与 Callback 构造对应系统或外部操作者上下文。Application 或旧 Service 在完整用例边界显式提供 action、资源、前后值、结果和关联 ID。
|
||
|
||
全局 Fiber 中间件不得自动生成 Audit Event,因为它不知道业务是否落地、影响哪些资源、修改前后值或真实结果。Handler 也不得在业务完成前自行写成功事件。
|
||
|
||
依赖通过结构体字段或构造器显式注入小接口:复杂写 Application 使用 `AuditWriter` Port 与 GORM 事务 Adapter;旧 Service 可暂时使用接受 `*gorm.DB`/事务上下文的兼容 Adapter,调用迁完后删除旧门面。失败/拒绝短事务使用同一 Writer 的独立入口,禁止 goroutine。
|
||
|
||
### 5. 不同结果采用不同可靠性策略
|
||
|
||
| 场景 | 记录策略 |
|
||
|---|---|
|
||
| 成功或部分成功且改变关键业务事实 | 与 Domain Ledger 和必要 Outbox 同一 GORM 事务;审计失败则业务回滚 |
|
||
| 权限或业务规则拒绝且可确定主要业务资源 | 业务未落地后使用独立短事务记录 `denied`;资源 ID 可空,但必须有稳定 Key/快照 |
|
||
| 已识别主要业务资源后的执行失败 | 保留原业务错误,独立短事务记录 `failed`;二次失败写 critical 日志和指标 |
|
||
| 参数解析失败、只有 actor 或无法确定主要业务资源 | 不写 Audit Event,仅保留 Access/Security Log |
|
||
| 外部请求未改变内部事实 | 只写 Integration Log |
|
||
| 外部回调或系统任务改变内部事实 | 保留 Integration/任务事实,并写 `external_system/system_task` Audit Event |
|
||
| 普通查询 | 不写 Audit Event |
|
||
| Action Registry 标记的敏感读取 | 返回敏感结果前写 Audit Event;写入失败则不返回该结果 |
|
||
|
||
第一阶段“所有非查询操作”包含通知已读等低风险写操作;它们仍进入 Action Registry,但可标记低风险并在默认调查列表中降低优先级,不能擅自 N/A。普通列表和详情保持 N/A;查看审计中的受控敏感详情、明文业务凭证或其他被 Registry 明确标记的敏感读取属于例外,必须记录读取者、目标资源和读取字段类别,但仍不得返回系统安全凭据。
|
||
|
||
### 6. 多资源、设备多卡和批量操作按实际业务关系记录
|
||
|
||
一个同步业务操作写一个根事件并关联全部资源。设备某张卡操作必须把设备、卡和必要的 `device_sim_binding` 都作为一等资源:通过设备入口操作卡时,卡为 `primary/affected`,设备为 `reference`;修改绑定关系时设备、旧卡、新卡及相关绑定均为 `affected`,并保存 `slot_position/is_current`。
|
||
|
||
换货事件必须区分旧卡、旧设备、新卡、新设备及其绑定卡,不得只保存通用 `asset_identifier`。订单、退款、审批、钱包、流水、套餐权益、个人客户绑定和店铺按实际变化关联为 `affected/reference`。
|
||
|
||
批量操作写一条根事件保存条件与统计;每个实际变化资源写可进入自身时间线的子事件,子事件共享 correlation 并以根事件为 parent。未处理或未命中资源不写子事件;失败项若已识别资源则写失败子事件。
|
||
|
||
### 7. 资源快照按当前真实领域盘点,不复制完整 Model
|
||
|
||
| 资源 | 至少保存的身份快照 |
|
||
|---|---|
|
||
| 账号 | ID、用户名、手机号、用户类型、所属店铺/企业、企微 userid/name |
|
||
| 角色/权限 | ID、角色名称/类型、权限 code/name、目标账号 |
|
||
| 店铺/企业 | ID、编码、名称、上级/归属店铺、层级 |
|
||
| 个人客户 | ID、昵称、当前手机号、微信主体标识 |
|
||
| IoT 卡 | ID、ICCID、VirtualNo、MSISDN、运营商、店铺、系列、generation |
|
||
| 设备 | ID、VirtualNo、IMEI、SN、名称/型号、店铺、系列、generation |
|
||
| 设备卡槽 | binding ID、设备标识、slot、卡 ICCID/VirtualNo、是否当前卡 |
|
||
| 资产分配 | 分配单号、资产类型/ID/标识、来源/目标主体、关联设备/卡 |
|
||
| 换货 | 换货单号、流程类型、旧/新资产完整标识、店铺、状态 |
|
||
| 套餐/系列 | ID、编码、名称、类型、期限、价格、上下架状态 |
|
||
| 套餐权益 | usage ID、订单号、套餐、资产完整标识、generation、状态、激活/到期时间 |
|
||
| 订单 | ID、订单号、买家、操作者、资产、套餐、金额、支付方式/状态、购买角色 |
|
||
| 支付 | 支付 ID/单号、业务单号、渠道、金额、状态、第三方交易号、配置 ID |
|
||
| 退款/充值 | 业务单号、订单/资产/店铺、申请和批准金额、审批、支付方式、状态 |
|
||
| 钱包/流水 | 钱包 ID/类型、店铺或资产、币种、流水 ID、reference、amount、balance before/after |
|
||
| 佣金 | 记录/申请 ID、店铺、订单、系列、金额、状态、结算周期 |
|
||
| 审批 | 实例 ID、业务类型/ID、提交人、provider、external ref、correlation、状态 |
|
||
| 配置 | 配置 Key/模块或配置 ID/name/provider;凭据只记是否已配置 |
|
||
| 导入/批量/导出任务 | 任务 ID/单号、文件名、目标、操作者、总数与结果数;审计中心自身不导出 |
|
||
| 通知 | event ID、接收人、类别、ref type/id/key |
|
||
| 轮询 | 配置/规则/触发 ID、资产、任务类型、触发人;运行进度仍由任务表承担 |
|
||
| Integration/Outbox | integration ID/provider/operation/resource/external ID;event ID/type/aggregate |
|
||
|
||
实现任务必须继续扫描新增领域并更新该矩阵。手机号、IP、ICCID、VirtualNo、金额和交易号可供平台完整展示;安全凭据无论用户权限如何都不进入快照、前后值或 metadata。
|
||
|
||
### 8. 平台调查 API 采用少量稳定视角,不返回 GORM Model
|
||
|
||
| 视角 | API | 主要内容 |
|
||
|---|---|---|
|
||
| 全局事件 | `GET /api/admin/audit/events`、`/{event_id}` | 组合筛选、事件详情、多资源及各自变化 |
|
||
| 操作者 | `GET /api/admin/audit/actors/{kind}/{id}/events` | 指定人员/系统的行为时间线 |
|
||
| 资源搜索/时间线 | `GET /api/admin/audit/resources/search`、`/{type}/{id}/timeline` | 注册资源候选与通用资源轨迹 |
|
||
| 请求链路 | `GET /api/admin/audit/requests/{request_id}/timeline` | Audit、Integration、Outbox/任务摘要及 Domain Ledger 链接 |
|
||
| 业务链路 | `GET /api/admin/audit/correlations/{correlation_id}/timeline` | 跨请求、异步、外部和后续业务步骤 |
|
||
| 资金 | `GET /api/admin/audit/finance/timeline` | Audit Event + 钱包流水/订单/支付/退款/充值/佣金/审批 |
|
||
| 风险 | `GET /api/admin/audit/risks/overview`、`/events` | 高风险、失败、拒绝、部分成功和结果未知 |
|
||
| 外部集成 | `GET /api/admin/audit/integrations/overview`、`/integrations`、`/integrations/{integration_id}` | 总览、组合筛选、详情和尝试序列 |
|
||
|
||
查询入参按来源分为四类,Handler/DTO 和前端契约必须明确来源,不能只列字段名:
|
||
|
||
| API | 关键入参 | 入参来源 |
|
||
|---|---|---|
|
||
| `GET /api/admin/audit/events` | `created_from`、`created_to`、`action`、`category`、`actor_kind`、`actor_id`、`source`、`result`、`risk`、`scope_type`、`scope_id`、`resource_type`、`resource_id`、`resource_key`、`request_id`、`correlation_id`、`page`、`page_size` | 调查人员在全局事件筛选区输入或从其他视角“查看相关事件”跳转带入;除分页默认值外不由后端猜测 |
|
||
| `GET /api/admin/audit/events/{event_id}` | `event_id` | 来自事件列表、资源/actor/风险/链路时间线节点的稳定跳转 ID;允许调查人员粘贴稳定 ID |
|
||
| `GET /api/admin/audit/actors/{kind}/{id}/events` | `kind`、`id`、`action`、`result`、`risk`、`resource_type`、`resource_id`、`created_from`、`created_to`、`page`、`page_size` | `kind/id` 来自事件详情中的 actor 引用或平台账号选择器,其他字段来自当前视角筛选区 |
|
||
| `GET /api/admin/audit/resources/search` | `resource_type`、`keyword`、page/page_size | 调查人员选择资源类型并输入业务标识;keyword 对卡使用 ICCID/VirtualNo、设备使用 VirtualNo/IMEI/SN,其他类型使用 Registry 声明的 Key |
|
||
| `GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline` | `resource_type`、`resource_id`、`created_from`、`created_to`、`action`、`result`、`page`、`page_size` | 类型和 ID 必须来自下方业务页面映射、资源搜索结果或调查节点 `resource_refs[]`;后端不从模糊 keyword 猜 ID |
|
||
| `GET /api/admin/audit/requests/{request_id}/timeline` | `request_id` | 来自事件/Integration 详情的 request ID 或开发人员从 Access Log 粘贴;不是前端自行生成 |
|
||
| `GET /api/admin/audit/correlations/{correlation_id}/timeline` | `correlation_id` | 来自事件、Integration、Outbox/任务或业务详情中的稳定关联 ID;不是按时间或资源推断 |
|
||
| `GET /api/admin/audit/finance/timeline` | `shop_id`、`wallet_id`、`order_id`、`order_no`、`payment_id`、`payment_no`、`refund_id`、`refund_no`、`recharge_id`、`recharge_no`、`approval_instance_id`、`third_party_trade_no`、`actor_kind`、`actor_id`、`correlation_id`、`created_from`、`created_to`、`page`、`page_size` | 调查人员在资金筛选区输入业务标识,或按下方业务页面映射和调查节点跳转带入;服务端按已提供的稳定字段解析 Domain Ledger,不要求前端补齐同一业务的其他 ID |
|
||
| `GET /api/admin/audit/risks/overview`、`/events` | `created_from`、`created_to`、`risk`、`result`、`action`、`source`、`page`、`page_size` | 调查人员选择时间窗口和风险筛选;从 overview 分桶跳转时前端带入相同筛选条件 |
|
||
| Integration overview/list | `created_from`、`created_to`、`bucket`、`integration_id`、`provider`、`direction`、`operation`、`result`、`result_category`、`external_id`、`resource_type`、`resource_id`、`resource_key`、`trigger_source`、`trigger_scene`、`trigger_series`、`state_changed`、`http_status`、`provider_code`、`request_id`、`correlation_id`、`page`、`page_size` | 调查人员筛选、通知目标跳转或其他时间线节点带入;枚举选项来自后端稳定常量,不由前端自由拼接 |
|
||
| `GET /api/admin/audit/integrations/{integration_id}` | `integration_id` | 来自 Integration 列表、通知目标或关联时间线节点;使用稳定 Integration ID,不使用数据库自增 ID |
|
||
|
||
认证用户类型、平台/代理/企业身份、当前账号 ID、代理店铺范围和企业 ID 永远来自认证上下文,不允许由 query/path/body 传入。中文名称、派生结果类别、资源展示名称、关联资源、Domain Ledger 节点和 attempts 由 Query 根据稳定 ID 批量派生,不要求前端提供。
|
||
|
||
所有列表默认 20、最大 100,使用稳定时间+ID倒序;时间线统一返回 `record_source`、节点 ID、发生时间、标题、结果、资源和可跳转引用。专业资金视角以 Domain Ledger 为金额权威;通用资源时间线只陈述事件,不重新计算业务结论。
|
||
|
||
平台审计路由第一阶段只校验用户类型为超级管理员或平台账号,不引入权限码,不做店铺/企业数据行过滤。平台返回存储中的完整业务字段,但不会返回从未入库的系统安全凭据。
|
||
|
||
所有新 Handler 使用 RouteSpec,统一响应 `{code,msg,data,timestamp}`,错误定义在 `pkg/errors/`;同步生产组合根、共享文档 Handler、`cmd/api/docs.go` 和 `cmd/gendocs/main.go`。
|
||
|
||
| 场景 | 错误码 | 对外语义 |
|
||
|---|---|---|
|
||
| 参数、枚举、分页或时间范围非法 | `CodeInvalidParam` | 参数错误,不返回底层校验细节 |
|
||
| 非平台访问内部调查或主体越权 | `CodeForbidden` | 无权限操作该资源或资源不存在 |
|
||
| 在线窗口内稳定 ID 不存在 | 复用项目资源不存在错误码 | 资源不存在,不猜测归档内容 |
|
||
| 显式时间范围早于或跨越在线边界 | 新增 `CodeAuditDataArchived` | 数据已归档,第一阶段不支持在线查询;响应携带当前在线边界 |
|
||
|
||
#### 8.1 现有业务页面到资源时间线的参数映射
|
||
|
||
资源时间线不能要求前端重新解析 ICCID、虚拟号或中文描述。第一阶段以当前业务接口已经返回的稳定字段作为跳转来源,并冻结以下映射。表中的字段路径均位于统一响应的 `response.data` 下;分页接口使用 `items[]`。
|
||
|
||
平台在列表行操作和详情页签显示“审计记录”,代理/企业显示“活动记录”;点击后再加载对应时间线,不改变原业务详情响应。只有下表所需字段齐全且当前身份允许目标接口时才显示入口。
|
||
|
||
**资产详情完整调用样例**:页面先调用 `GET /api/admin/assets/resolve/8986...`。若返回 `data.asset_type="card"`、`data.asset_id=321`、`data.iccid="8986..."`,平台“审计记录”页签调用 `GET /api/admin/audit/resources/iot_card/321/timeline?page=1&page_size=20`,代理“活动记录”页签调用 `GET /api/admin/agent/resource-activities/iot_card/8986...?page=1&page_size=20`。若返回 `asset_type="device"`,平台使用 `data.asset_id` 调 `.../resources/device/{asset_id}/timeline`,代理使用 `data.virtual_no` 调 `.../agent/resource-activities/device/{virtual_no}`。企业身份不能调用当前 resolve 接口,必须从企业卡/设备列表按下表进入企业活动接口。
|
||
|
||
| 业务页面/前置接口 | 当前响应中的稳定字段 | 平台内部时间线 | 代理安全活动 | 企业安全活动 |
|
||
|---|---|---|---|---|
|
||
| 卡列表 `GET /api/admin/iot-cards/standalone` | `items[].id`、`items[].iccid`、`items[].virtual_no`、`items[].shop_id`、`items[].device_virtual_no`、`items[].authorized_enterprise_id` | `iot_card/{id}` | `iot_card/{iccid}`;后端按卡当前 `shop_id` 校验自己及下级店铺 | 不以通用列表中的企业 ID 作为授权证明;企业从企业卡列表进入 |
|
||
| 设备列表 `GET /api/admin/devices` | `items[].id`、`items[].virtual_no`、`items[].imei`、`items[].sn`、`items[].shop_id`、`items[].bound_card_count`、`items[].authorized_enterprise_id` | `device/{id}` | `device/{virtual_no}`;后端按设备当前店铺归属校验 | 企业从企业设备列表进入并复核当前有效授权 |
|
||
| 设备卡槽 `GET /api/admin/devices/{virtual_no}/cards` | `bindings[].id`、`bindings[].iot_card_id`、`bindings[].iccid`、`bindings[].slot_position`、`bindings[].is_current` | 卡使用 `iot_card/{iot_card_id}`;需要查看绑定关系时使用 `device_sim_binding/{binding.id}`;设备 ID 由上层设备列表或详情提供 | 点击某张卡使用 `iot_card/{iccid}`;设备轨迹继续使用上层设备 `virtual_no` | 同代理参数,但必须再次验证设备及卡均处于当前企业有效授权范围;绑定关系不作为越权捷径 |
|
||
| 统一资产详情 `GET /api/admin/assets/resolve/{identifier}` | `asset_type`、`asset_id`、`identifier`、`virtual_no`、`iccid`、`bound_device_id`、`cards[].card_id`、换货轨迹中的 `asset_id/can_view` | `asset_type=card` 映射为 `iot_card/{asset_id}`,`device` 映射为 `device/{asset_id}`;绑定资产按其 ID 跳转 | 卡用 `iot_card/{iccid}`,设备用 `device/{virtual_no}`;换货前后代仅在 `can_view=true` 时开放跳转 | 不展示入口:当前接口明确禁止企业调用;企业只能从企业卡/设备列表进入 |
|
||
| 资产分配列表/详情 `GET /api/admin/asset-allocation-records[/{id}]` | `items[].id/allocation_no/asset_type/asset_id/asset_identifier/from_owner_type/from_owner_id/to_owner_type/to_owner_id/related_device_id`;详情另有 `related_card_ids[]` | 分配记录 `asset_allocation_record/{id}`;主资产使用 `{asset_type}/{asset_id}`;关联设备、卡和店铺分别使用对应资源 ID | 分配记录使用 `asset_allocation_record/{allocation_no}`;仅当记录关联的当前资产或店铺仍在代理范围内时返回安全活动 | 第一阶段不提供独立分配记录入口;企业从已授权卡/设备时间线查看允许感知的分配结论 |
|
||
| 换货列表/详情 `GET /api/admin/exchanges[/{id}]` | `items[].id/exchange_no/old_asset_type/old_asset_id/new_asset_type/new_asset_id/shop_id/submitter_id` | 换货单 `exchange_order/{id}`;旧、新资产分别使用响应中的类型和 ID | 换货单使用 `exchange_order/{exchange_no}`;后端按 `shop_id` 校验;旧、新资产仅在各自当前可管理时允许独立跳转 | 第一阶段不提供独立换货单入口;企业从有效授权资产时间线查看允许感知的换货结论 |
|
||
| 店铺列表/详情 `GET /api/admin/shops[/{id}]` | `items[].id/shop_name/shop_code/parent_id/business_owner_account_id` 或详情同名字段 | `shop/{id}` | `shop/{shop_code}`,仅自己及下级店铺 | 不提供店铺活动入口 |
|
||
| 企业列表 `GET /api/admin/enterprises` | `items[].id/enterprise_name/enterprise_code/owner_shop_id` | `enterprise/{id}` | `enterprise/{enterprise_code}`,仅 `owner_shop_id` 在代理范围内 | 第一阶段不提供企业自身活动入口;认证上下文 `enterprise_id` 仅用于卡/设备授权范围,不作为前端路径参数 |
|
||
| 企业卡列表 `GET /api/admin/enterprises/{id}/cards` | `items[].id`、`items[].iccid`、`items[].virtual_no`、`items[].device_id` | `iot_card/{id}` | 不作为代理授权来源 | `iot_card/{iccid}`;路由中的企业 ID 不能作为信任依据,必须使用认证上下文 `enterprise_id` 复核有效授权 |
|
||
| 企业设备列表 `GET /api/admin/enterprises/{id}/devices` | `items[].device_id`、`items[].virtual_no`;若后续注册详情路由,可使用 `device.device_id` 和 `cards[].card_id` | `device/{device_id}` | 不作为代理授权来源 | `device/{virtual_no}`;必须使用认证上下文 `enterprise_id` 复核有效授权 |
|
||
|
||
平台内部接口固定使用 Resource Registry 类型与内部稳定 ID:`GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline`。代理和企业固定使用各自安全接口与 Registry 声明的业务标识:`GET /api/admin/{agent|enterprise}/resource-activities/{resource_type}/{identifier}`。前端只负责透传上表字段,资源解析、身份范围、当前归属和有效授权全部由后端判断。
|
||
|
||
第一阶段首批 Resource Registry 类型至少包括 `account`、`shop`、`enterprise`、`iot_card`、`device`、`device_sim_binding`、`asset_allocation_record`、`exchange_order`、`order`、`refund`、`agent_recharge`、`asset_wallet` 和 `approval_instance`。其中统一资产接口返回的 `card` 只在跳转层转换为 `iot_card`;分配记录和换货接口已经返回 `iot_card/device`,不得再次错误转换。
|
||
|
||
当前资产分配列表已按关联店铺过滤,但详情 `GetByID` 仍是直接按主键读取。新的平台时间线和代理安全活动 Query 不得把“能够打开现有详情”当作归属证明,必须独立查询分配记录关联店铺、资产当前归属及调用方范围;越权与不存在继续使用统一错误,不泄露记录是否存在。
|
||
|
||
#### 8.2 账号、组织、交易和资金页面的导航映射
|
||
|
||
下表冻结平台页面的第一跳。资源页签统一使用 `GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline`;资金页签统一使用 `GET /api/admin/audit/finance/timeline`。同一页面可以同时展示“审计记录”和“资金链路”,但只传当前业务接口实际返回的字段,其他关联由 Query 解析。
|
||
|
||
| 页面/前置接口 | `response.data` 字段 | 页面入口与目标调用 |
|
||
|---|---|---|
|
||
| 账号列表/详情 `GET /api/admin/accounts[/{id}]` | 列表 `items[].id`;详情 `id` | “审计记录”→资源时间线,`resource_type=account`、`resource_id=id` |
|
||
| 店铺列表/详情 `GET /api/admin/shops[/{id}]` | 列表 `items[].id`;详情 `id` | “审计记录”→`shop/{id}`;“资金链路”→`finance/timeline?shop_id={id}` |
|
||
| 企业列表 `GET /api/admin/enterprises` | `items[].id` | “审计记录”→`enterprise/{id}`;当前没有企业 GET 详情接口,前端不得假设存在 |
|
||
| 订单列表/详情 `GET /api/admin/orders[/{id}]` | `items[].id/order_no` 或详情 `id/order_no` | “审计记录”→`order/{id}`;“资金链路”→`finance/timeline?order_id={id}`,无需再传 payment/refund ID |
|
||
| 退款列表/详情 `GET /api/admin/refunds[/{id}]` | `items[].id/refund_no/order_id/approval_instance_id` 或详情同名字段 | “审计记录”→`refund/{id}`;“资金链路”→`finance/timeline?refund_id={id}`;审批实例非空时可跳 `approval_instance/{approval_instance_id}` |
|
||
| 代理充值列表/详情 `GET /api/admin/agent-recharges[/{id}]` | `items[].id/recharge_no/shop_id/agent_wallet_id/approval_instance_id` 或详情同名字段 | “审计记录”→`agent_recharge/{id}`;“资金链路”→`finance/timeline?recharge_id={id}`;详情没有 `payment_no` 时由服务端关联支付,不要求前端猜测 |
|
||
| 代理在线充值创建 `POST /api/admin/agent-recharges` | `recharge_id`、`recharge_no`、`payment_no` | 结果页“资金链路”→`finance/timeline?recharge_id={recharge_id}`;`payment_no` 仅作为额外精确筛选,不直接假设存在 Integration 详情 |
|
||
| 资产钱包 `GET /api/admin/assets/{identifier}/wallet` | `wallet_id`、`resource_type`、`resource_id` | “资金链路”→`finance/timeline?wallet_id={wallet_id}`;“资产审计”→资源时间线 `{resource_type}/{resource_id}`,其中现有 `iot_card/device` 可直接透传 |
|
||
| 店铺资金概况 `GET /api/admin/shops/fund-summary` | `items[].shop_id` | 行内“资金链路”→`finance/timeline?shop_id={shop_id}`;不得要求该响应没有提供的 agent wallet ID |
|
||
| 店铺主钱包流水 `GET /api/admin/shops/{shop_id}/main-wallet/transactions` | 上层 path `shop_id`;`items[].id/asset_type/asset_id/asset_identifier` | 默认→`finance/timeline?shop_id={shop_id}`;存在资产 ID 时可跳资源时间线 `{asset_type}/{asset_id}` |
|
||
| 资产钱包流水 `GET /api/admin/assets/{identifier}/wallet/transactions` | 上层钱包接口的 `wallet_id`;`items[].id/reference_type/reference_no` | 默认→`finance/timeline?wallet_id={wallet_id}`;业务引用仅在 Query/节点返回明确资源引用后继续跳转,不由前端解析编号前缀 |
|
||
|
||
第一阶段资金 Query 必须允许只凭订单、退款、充值、钱包或店铺中的任一稳定条件进入,不得要求前端先取得所有关联 ID。当前微信订单支付响应、代理充值详情/支付状态和店铺资金概况均缺少部分支付或钱包标识,关联补全属于服务端 Query 职责。
|
||
|
||
#### 8.3 调查结果节点的统一跳转引用
|
||
|
||
平台事件列表、资源/actor/request/correlation/finance/risk 时间线及 Integration 关联节点必须返回同一 `investigation_refs` 结构;字段没有事实依据时为 `null` 或空数组,不得猜测:
|
||
|
||
```json
|
||
{
|
||
"event_id": "evt_xxx",
|
||
"actor_ref": {"kind": "account", "id": "123"},
|
||
"resource_refs": [
|
||
{"resource_type": "iot_card", "resource_id": "321", "resource_key": "8986...", "display_name": "8986..."}
|
||
],
|
||
"request_id": "req_xxx",
|
||
"correlation_id": "corr_xxx",
|
||
"integration_refs": [{"integration_id": "int_xxx"}]
|
||
}
|
||
```
|
||
|
||
前端映射固定为:`event_id`→事件详情;`actor_ref.kind/id`→操作者视角;`resource_refs[].resource_type/resource_id`→平台资源时间线;`request_id`→请求时间线;`correlation_id`→业务链路;`integration_refs[].integration_id`→Integration 详情。代理/企业活动 DTO 不得返回该内部结构;允许继续查看的关联资源只返回安全的 `resource_type/identifier/display_name`。
|
||
|
||
`actor_ref.kind` 第一阶段冻结为 `account`、`openapi`、`system_task`、`scheduled_job`、`external_system`;人工平台、代理和企业后台账号统一使用 `account`,账号类型由事件快照展示,不把可变用户类型拼进路径。
|
||
|
||
通知必须先调用既有 `GET /api/admin/notifications/{id}/target`。仅当 `data.available=true` 时展示跳转;`target_type=integration_log` 时将 `data.target_key` 原样传给 `GET /api/admin/audit/integrations/{integration_id}`,其他 `target_type` 先进入对应业务详情页,再由该页面按本节矩阵进入审计,禁止直接解析通知 `ref_type/ref_id/ref_key` 拼审计 URL。
|
||
|
||
#### 8.4 导航降级规则
|
||
|
||
- 业务响应缺少目标接口必需的稳定 ID/identifier 时隐藏该审计或活动入口,不按名称、时间、中文描述或编号前缀猜测。
|
||
- 平台只有 Resource Registry 稳定 Key 而没有内部 ID 时,先调用 `GET /api/admin/audit/resources/search?resource_type=...&keyword=...`;唯一命中后使用结果的稳定 ID,零命中或多命中停留搜索结果,不自动选择。
|
||
- 已删除资源只要调查节点仍有稳定 `resource_type/resource_id`,平台仍可打开历史时间线;普通业务详情已不存在不影响审计快照。
|
||
- 代理/企业遇到资源不存在、授权撤销或越权时统一显示活动不可用,不回退到平台调查、资源搜索或旧 operation log。
|
||
- 旧 operation log 只由平台既有独立旧历史入口访问;任何旧记录都不跳转或拼接到新 `/api/admin/audit/*`。
|
||
|
||
### 9. 代理/企业使用独立资源活动投影
|
||
|
||
代理/企业不得访问 `/api/admin/audit/*`,也不得复用内部 DTO。第一阶段冻结两个 GET 契约:代理使用 `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}`,企业使用 `GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}`;`resource_type/identifier` 来自代理或企业当前卡/设备等业务详情页,或其已有受权资源列表的“活动记录”跳转,不能由前端传入 shop/enterprise 身份冒充范围;`page/page_size` 来自活动列表分页控件。调用方身份、代理店铺范围和企业 ID 由认证上下文注入。响应资源摘要包含 `resource_type/resource_id/resource_key/display_name`,活动项只包含外部动作编码/中文名、`subject_summary`、Registry 白名单约束的 `subject_data`、结果、发生时间和允许公开的关联资源摘要。
|
||
|
||
资源活动 API 先验证资源归属,再读取 Event Resource 中的主体投影:代理按自身及下级店铺范围;企业按当前有效卡/设备授权关系,不能因通用店铺过滤缺少上下文而退化为全量。
|
||
|
||
响应只包含动作的外部名称、发生时间、结果、`subject_summary` 和写入时生成的 `subject_data`。平台操作者、内部原因/备注、IP、内部 before/after、Audit Event ID、Integration Log、风险规则和内部因果不可见;`internal_only` 事件不泄露其存在。自身操作或业务允许公开的变化可使用 `subject_detail`,平台内部处理通常使用 `subject_result`。Query 不得读取内部 before/after 后临时删字段生成 `subject_data`。
|
||
|
||
### 10. Integration Log 复用现表并补查询契约
|
||
|
||
现有 `tb_integration_log` 和 Repository 保留,不新建第二套 Integration 表。第一阶段不增加多资源子表:一次外呼保存直接主资源,其他资源经关联 Audit Event 展开;只有未来出现无法通过 Audit Event 关联的真实多资源外呼需求才重新评审。
|
||
|
||
查询返回原始 result、中文名和派生类别:`pending→processing`、`success→succeeded`、`unknown→indeterminate`、`failed/not_found/invalid_payload/conflict→failed`、`ignored/merged/rate_limited/completed/cancelled→not_sent`。`completed` 是未请求上游且业务已达预期,不能伪装为 success。
|
||
|
||
有稳定 `trigger_series` 时,详情按 attempt 和时间返回完整尝试序列;没有 series 时只展示单次交互,不按同资源或 correlation 猜测重试。`correlation_id` 表示业务链路,不表示技术重试。新可重试/分阶段外呼必须传播稳定 series 和单调 attempt;HTTP→Application→Outbox→Worker→Integration 必须传播 correlation。
|
||
|
||
现有字段增量修正:correlation 长度与 Outbox/审批统一至少 100;成功解析资源后补真实 `resource_id`;企微审批回调不再把 application ID 伪装为审批资源;允许调用方提供经筛选、有长度上限的可读业务错误摘要,原始第三方错误正文仍不入库。历史 Integration JSON 在返回前使用字段白名单和同一凭据删除器重新清理,禁止直接透传旧 `request_summary/response_summary/metadata`;这属于安全删除,不是业务字段展示脱敏。
|
||
|
||
组合查询补最小 B-tree 索引:`(result, created_at, id)`、`(provider, created_at, id)`、非空 `external_id`、`audit_event_id`、`trigger_series+attempt`、`correlation_id+created_at`。第一阶段不给 JSONB 建 GIN,不支持任意摘要全文搜索;overview 强制时间范围,防止高频轮询历史全表聚合。
|
||
|
||
### 11. 每日冷归档与月初受控清理
|
||
|
||
归档复用现有 S3 兼容对象存储、Worker 和 Asynq Scheduler,不增加依赖。时间边界统一使用 `Asia/Shanghai`:每日任务处理前一完整自然日 `[00:00:00, 次日 00:00:00)`,月初任务先完成上月最后一天归档,再处理上一个完整自然月。业务写入不等待对象存储;对象存储故障只让归档重试并阻止清理,不影响新的 Audit Event 或 Integration Log 正常落库。
|
||
|
||
| 数据源 | 每日归档内容 | 月初清理 | 清理后在线查询 |
|
||
|---|---|---|---|
|
||
| Audit Event + Event Resource | 按 `created_at` 输出完整事件及其全部资源关系、快照和前后数据 | 两表作为一个事实集合从 PostgreSQL 物理删除,禁止软删除、只删主表或只删资源表 | 不支持,仅保留对象存储冷归档 |
|
||
| Integration Log | 按 `created_at` 输出结构化持久化事实;月初对上月可变记录做最终快照复核 | 归档最终版本校验成功后从 PostgreSQL 物理删除 | 不支持,仅保留对象存储冷归档 |
|
||
| Access Log | N/A;继续写应用服务器本地文件并使用现有 Lumberjack 轮转与保留策略,不上传对象存储 | N/A;不涉及 PostgreSQL | 继续供开发在应用服务器排障,不接入审计 API |
|
||
| Domain Ledger | N/A,订单、支付、退款、钱包流水等继续按各领域留存 | 不清理 | 继续作为业务权威事实 |
|
||
| Outbox/Asynq/手动轮询 | N/A,沿用各自生命周期 | 不随审计归档清理 | 继续按现有查询能力使用 |
|
||
| 旧 operation log | N/A,不迁移、不纳入新归档 | 不清理 | 继续使用独立旧历史入口 |
|
||
|
||
归档格式固定为 UTF-8 `JSON Lines + gzip`。Audit 每行包含一个事件及其完整 `resources[]`,避免恢复或人工检查时主从分片错配;Integration 每行对应一条结构化持久化记录。对象 Key 使用版本化稳定前缀,例如 `audit-archive/v1/2026/07/28/audit-events-2026-07-28.jsonl.gz` 和 `integration-logs-2026-07-28-r{revision}.jsonl.gz`,不得静默覆盖内容不同的对象。本 Change 不配置删除这些归档对象的生命周期规则,所有月份备份长期保留。
|
||
|
||
每个对象同时保存 manifest,至少包含 `schema_version`、`source`、`archive_date`、`timezone`、`range_start/range_end`、`instance_id`、事件/资源或记录数量、未压缩与压缩字节数、对象 Key、SHA-256、revision、生成时间和最终状态。PostgreSQL 新增轻量 `tb_log_archive_run` 作为任务 ledger,按 `source + archive_date + instance_id + schema_version` 唯一,记录 manifest/object Key、数量、hash、状态、尝试次数、完成与清理时间;它不保存日志正文,也不作为 Audit Event 或 Domain Ledger。
|
||
|
||
每日任务通过该唯一键和 Asynq 唯一任务保持幂等。相同输入重复执行时复用已通过校验的对象;数据库计数、内容 hash 或对象 metadata 不一致时生成新 revision 并保留旧对象,不允许覆盖后宣称成功。Integration Log 允许完成状态等字段在创建日后更新,因此月初清理门禁必须基于数据库当前最终内容重新计算;存在变更时先生成新的最终 revision,再更新 manifest。仍处于可变状态且无法形成最终快照的记录会阻止该月清理,不得先删后补。
|
||
|
||
月初清理必须同时满足:上月每个自然日的 Audit 和 Integration 均有成功 manifest;Audit Event 数量与归档事件数一致、Event Resource 数量与归档资源总数一致;Integration 最终 revision 与数据库当前内容一致;对象存在且大小、SHA-256、时间范围和记录数一致;没有 `pending/failed` 归档任务。任一条件失败时整月不清理,记录 critical 日志和指标并由后续调度重试。门禁通过后,Retention Worker 使用 GORM 和 `created_at` 索引,以有界批次按 Event Resource、Audit Event、Integration Log 顺序执行数据库物理 `DELETE`;任务中断后从 ledger 继续,不重复删除窗口外数据。Audit Event、Event Resource 和 Integration Log 均不得通过 `deleted_at`、状态字段或归档标记模拟删除,也不得把上月数据迁移到另一张 PostgreSQL 历史表。第一阶段不改造现有 Integration 表为分区表;只有批量物理删除、VACUUM 或锁等待指标证明不能满足窗口后再评审月分区。
|
||
|
||
清理属于内部数据留存策略,不是业务删除、更正事件或用户审计导出。清理任务以 `retention_worker` 系统身份写入当月 Audit Event,记录清理月份、各数据源数量、manifest Key、结果和失败摘要,禁止写回被清理月份。业务 Repository、Handler 和调查 Query 仍无 Update/Delete 能力,也不存在归档下载、跨对象存储查询或恢复接口。
|
||
|
||
在线调查 DTO 必须返回 `retention{online_from, archived_before, timezone}`。默认查询仅覆盖 PostgreSQL 在线窗口;请求的显式时间范围全部早于 `online_from` 时返回稳定“数据已归档、第一阶段不支持在线查询”错误,跨越边界时拒绝并要求缩小到在线窗口,不能返回看似完整的空列表或部分时间线。ID-only 详情在在线库不存在时仍按资源不存在处理,同时页面固定展示当前 `online_from`,不得尝试扫描对象存储。
|
||
|
||
### 12. 旧日志只停写,不迁移到新审计中心
|
||
|
||
1. **Expand**:先创建新表、Registry、Writer、统一 Query 和主体活动接口;旧 Writer 继续服务未迁移用例。
|
||
2. **Migrate**:按完整纵向用例迁移调用方,每个切片同时完成资源快照、成功/失败/拒绝和查询可见性核对;同一用例不得长期双写。
|
||
3. **Contract**:生产装配和静态清单证明旧账号/资产 Writer 调用归零后停止旧表新增;旧表原样保留,不删除、不在线回填、不转换、不进入统一 Query。
|
||
|
||
新审计中心的时间范围和完整性从切换点开始。旧账号/资产历史仍由原表和既有独立查询能力承担;不为兼容新 DTO 解析中文描述、补造资源、结果、风险或 correlation。现有旧资产 operation-log 接口可收缩为平台历史入口,代理/企业改用新的安全资源活动接口,但该旧接口不会并入 `/api/admin/audit/*`。
|
||
|
||
现有资产 operation-log API 先限制为平台身份并标记兼容,代理/企业改用安全 activities;统一资源时间线稳定后再收缩旧入口。手动轮询表继续作为可变任务运行事实,手动触发/取消另写 Audit Event,不为停旧 operation log 而删除轮询进度能力。
|
||
|
||
### 13. 不可变性、性能与验证策略
|
||
|
||
本 Change 明确禁止新增、修改或运行任何自动化测试,也不创建测试任务。后续实现不得以补充单元、集成、HTTP 或端到端测试为由扩大改动范围。
|
||
|
||
Audit 业务 Repository 只有 Append/Read;应用运行账号不获得审计 Update/Delete 路径,Retention Worker 使用独立最小权限删除入口。后续业务修正通过新的业务动作产生新事件并以 correlation/parent 关联原链路,第一阶段不提供专用 `audit_event.corrected` 写接口。`content_hash` 基于清理、标准化后的主事件和资源内容计算,不包含数据库自增 ID。单个 JSON 字段设置有界大小,超限保存截断标志、原字节数和摘要;批量明细进入子事件或原业务任务表。
|
||
|
||
主要索引覆盖事件时间、actor、action/result/risk、request、correlation、parent、scope,以及资源 type+id/key+event time。Query 先分页事件 ID 再批量加载资源和必要业务投影,禁止逐事件 N+1;公共关键词只查有索引的编码、名称和 Key,不对 JSONB 全表模糊搜索。目标仍为数据库查询 <50ms、API P95<200ms、P99<500ms。
|
||
|
||
按用户确认,本 Change 的完成证据仅使用代码格式化、LSP/静态扫描、Registry 与覆盖基线比对、数据库结构和抽样数据核对、迁移检查、EXPLAIN/性能观测、接口人工调用、业务页面跳转核对、构建以及 OpenAPI 两条生成路径。
|
||
|
||
## Risks / Trade-offs
|
||
|
||
- **[第一阶段所有平台账号均可查看完整业务审计数据]** → 后端至少限制为平台身份,安全凭据不入库;细粒度权限码、敏感查看和导出待权限体系稳定后另开 Change。
|
||
- **[“所有非查询操作”产生较大数据量]** → 低风险事件降低默认展示优先级,批量使用根+子事件,JSON 有界;每日压缩冷归档、月初校验后批量清理上月,分区只在批量清理指标证明必要时再评审。
|
||
- **[关键成功审计同事务增加延迟和可用性耦合]** → 只做同库顺序 Append 和必要索引,禁止同步外部调用;这是资金与高风险操作可追责性的必要代价。
|
||
- **[新审计中心无法查询切换前历史]** → 这是明确边界:旧表原样保留并使用既有独立能力查阅,不回填、不转换;新查询从切换点起提供完整事实。
|
||
- **[correlation/series 历史传播不完整]** → 历史按已有字段展示,新用例强制传播;不得用相似资源或时间邻近猜测链路。
|
||
- **[代理/企业活动投影可能泄露内部事件存在性]** → 使用独立 Query/DTO 和资源关系上的主体快照;`internal_only` 不返回占位或计数。
|
||
- **[资源当前归属与事件时归属不同]** → 身份快照用于历史解释,访问授权以查询时有效资源归属为准;越权时不泄露资源或事件存在性。
|
||
- **[提案领域盘点仍可能随并行开发变化]** → 实施第一任务重新生成当前候选清单并逐入口评审,新增写入口未登记时门禁失败。
|
||
- **[对象存储损坏或少归档一日会造成不可恢复删除]** → manifest、行数、资源数、对象存在性和 SHA-256 是清理硬门禁;任何一天、实例或数据源失败都阻止整月清理并告警。
|
||
- **[月初清理后无法通过接口调查上月]** → 这是已确认的产品边界:接口固定展示在线窗口并拒绝归档范围查询;对象存储只用于冷留存,第一阶段不建设在线读取或恢复能力。
|
||
|
||
## Migration Plan
|
||
|
||
1. 冻结当前写入口与领域/资源清单,业务、研发、安全确认 Action/Resource Registry、N/A 理由和平台/主体可见性。
|
||
2. 增量创建 Audit Event/Resource、归档运行记录表与索引,保留现有 Integration/旧 operation 表;迁移 down 只允许在尚未产生事实的环境使用。
|
||
3. 交付 Writer、Registry、安全清理、平台基础事件/资源 Query 和主体活动 Query,以代表性配置、Outbox 人工操作、多卡设备和资金用例证明接缝;不创建旧日志历史 Adapter。
|
||
4. 按纵向切片迁移账号权限、店铺企业、资产/换货、套餐、订单支付退款充值、钱包佣金、审批配置、批量任务、通知轮询、Worker/Scheduler/Callback;每片独立验证后停止该用例旧写。
|
||
5. 交付 Integration 调查及 request/correlation/finance/risk 组合 Query,完成 OpenAPI/中文文档与前端契约;旧 operation log 不接入这些 Query。
|
||
6. 先以只归档不清理模式运行完整自然月,验证每日对象、manifest、重复任务和月度最终复核;门禁稳定后再单独启用月初清理开关。
|
||
7. 停机前核对旧 Writer 调用归零、Action 覆盖无空白、同事务失败回滚、主体隔离、凭据不落库、查询性能、归档完整性和历史样本;失败则不切换旧写或清理开关。
|
||
8. 切换生产组合根并启用旧写护栏。产生新 Audit Event 后,业务回滚只允许暂停异常生产者并前向修复;正常保留期清理由 Retention Worker 按已归档月份执行,不得恢复旧 Writer 制造分裂历史。
|
||
|
||
## Open Questions
|
||
|
||
无阻塞性产品问题。已确认月初清理后上月 Audit Event 与 Integration Log 只保留在对象存储,审计接口不再在线查询;细粒度平台权限、用户审计导出、对象存储历史查询/恢复、风险处置和自动恢复操作均不属于第一阶段。数据库月分区仅在有界批量清理无法满足指标时另行评审。
|