2 Commits

Author SHA1 Message Date
5ba227eff5 111
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 1m8s
2026-07-29 14:58:28 +08:00
eea19f2a5b 准备提案 2026-07-29 12:20:12 +08:00
20 changed files with 3173 additions and 0 deletions

View File

@@ -945,6 +945,7 @@ rdb.Set(ctx, key, status, time.Hour)
- **[数据库验证规范](AGENTS.md#数据库验证规范)**:使用 PostgreSQL MCP 验证接口逻辑和业务数据的正确性
- **[开发规范总览](AGENTS.md)**:完整的项目开发规范(必读)
- **[七月迭代 AI 实施与验收操作手册](docs/7月迭代/七月迭代-AI实施与验收操作手册.md)**PRD 拆票、Issues 实现、测试、双轴评审与验收流程
- **[七月迭代测试与业务交付手册](docs/7月迭代/七月迭代测试与业务交付手册.md)**:面向测试和业务人员的小白版逐项操作、预期结果、已知边界与问题反馈模板
- **[七月迭代实现与接口对接说明](docs/7月迭代/七月迭代实现与接口对接说明.md)**:按需求说明实现关键节点、口径检查、接口变化和前端应补齐的调用
- **[七月迭代联调交付说明](docs/7月迭代/七月迭代联调交付说明.md)**企业微信、Gateway、Redis/Asynq、对象存储、前端契约、限制和回滚步骤
- **[七月迭代人工验收清单](docs/7月迭代/七月迭代人工验收清单.md)**:逐项业务验收、全链路检查及临期通知手动扫描方法

View File

@@ -9,6 +9,7 @@
- [7月迭代禅道研发需求拆分表](./7月迭代禅道研发需求拆分表.md)
- [7月迭代禅道研发需求逐条录入稿](./7月迭代禅道研发需求逐条录入稿.md)
- [七月迭代 AI 实施与验收操作手册](./七月迭代-AI实施与验收操作手册.md)
- [七月迭代测试与业务交付手册](./七月迭代测试与业务交付手册.md)
- [七月迭代人工验收清单](./七月迭代人工验收清单.md)
评审、开发和验收均以标准评审稿为准。独立稿用于解释方案来源;与标准稿冲突时,标准稿优先。

View File

@@ -0,0 +1,543 @@
# 七月迭代测试与业务交付手册
> 适用人员:业务验收人员、产品人员、测试人员、实施人员。
> 文档版本2026-07-28。
> 使用方式:先阅读“开始前准备”,再按业务专题逐项操作。每完成一项,在对应复选框中打勾并保存证据。
## 一、这次交付了什么
本次七月迭代主要解决以下业务问题:
- 店铺、代理、平台账号之间的数据权限和业务员归属。
- 套餐授权、生效、续费、预计到期和临期提醒。
- 卡和设备的实名流程、支付方式、状态同步及卡片限速。
- 代理充值、信用额度、退款、企业微信审批和站内通知。
- 换货、批量订购、设备批量分配和统一导出。
- 订单、退款、充值、换货列表缺失的提交人、资产标识和审批状态。
### 1.1 本次可以验收的业务专题
| 专题 | 需求编号 | 一句话说明 |
| --- | --- | --- |
| 店铺管理 | #41#60#96 | 店铺可限制 C 端新登录、按联系电话查询并绑定平台业务员 |
| 套餐管理 | #40#43#46#55 | 批量授权套餐、设置生效条件、历史续费并展示预计最终到期时间 |
| 临期提醒 | #33 | 查询 015 天临期资产,并在 15/7/3 天产生站内通知 |
| 实名与支付 | #48#53#62 | 按卡/设备筛选实名状态,配置实名顺序和允许的支付方式 |
| 充值与钱包 | #34#38#97 | 代理在线充值、平台线下代充、信用额度和低余额提醒 |
| 退款与企微 | #35#37#57#189 | 企微审批驱动退款,退款中禁止换货,换货后的套餐权益可正确失效 |
| 换货 | #45#86#98#188 | 新旧资产独立查询、换货链、继承店铺和 C 端通知 |
| 批量业务 | #36#49 | CSV 批量订购套餐、设备批量分配店铺或套餐系列 |
| 导出 | #42 | 六类业务数据进入统一异步导出任务 |
| 卡业务 | #47#94 | 卡片固定档位限速、状态轮询及运营商回调 |
| 列表字段 | #44#181#182 | 补齐提交人、审批状态、订单渠道和资产标识 |
### 1.2 验收前必须知道的已知边界
以下内容不是测试人员操作错误:
1. **角色级导出字段权限尚未实现。** 当前统一导出可以按账号的数据范围过滤,但不能为不同角色配置不同导出列。本项应登记为“已知未交付”,不能判定为通过。
2. 退款和充值列表稳定展示提交人及审批状态,但暂不直接展示企微每一个审批节点的具体审批人。
3. 店铺关闭 C 端登录后,只阻止新的登录,不会强制踢出已经登录的用户。
4. 卡片限速不保存本地当前档位;出现“结果未知”时,需要向 Gateway 运维人员核对。
5. CSV 批量任务允许部分成功,任务详情中的逐行结果是最终依据。
6. 下架套餐续费没有单独的“续费接口”,仍使用原有创建订单流程。
### 1.3 本次明确不验收的内容
- 原路退款。
- 聚水潭对接。
- 卡与设备跨品类换货及补差价。
- 代理分销码、佣金提现。
- H5 首页隐藏设备下 ICCID。
- 停机阈值显示、资产详情敏感字段调整、授权列表样式调整。
- 新建一套本地审批系统;审批节点和审批人规则仍在企业微信后台配置。
## 二、先认识测试账号
| 账号类型 | 可以做什么 | 本手册中的简称 |
| --- | --- | --- |
| 超级管理员 | 配置系统、企微、支付方式并查看全平台数据 | 超管 |
| 平台账号 | 处理平台业务,可查看平台授权范围内的数据 | 平台账号 |
| 平台业务员 | 平台账号的一种,可绑定为店铺业务员并接收相关通知 | 业务员 |
| 代理账号 | 只能查看自己店铺及下级店铺数据 | 代理账号 |
| 店铺账号 | 属于某个店铺的账号,同一店铺可以有多个有效账号 | 店铺账号 |
| 企业账号 | 只能访问企业业务范围,不得访问代理充值等后台功能 | 企业账号 |
| 个人账号 | C 端使用卡或设备的个人客户 | 个人账号 |
> “店铺账号”不是“主账号”。通知要求写“全部有效店铺账号”时,同一店铺所有启用账号都必须收到。
## 三、开始前准备
### 3.1 环境准备
由实施或开发人员确认:
- [ ] API 服务可以正常访问。
- [ ] Worker 已启动,并与 API 使用同一个 Redis/Asynq。
- [ ] 数据库迁移已经执行到当前发布版本。
- [ ] 对象存储可上传和下载 CSV、凭证及导出文件。
- [ ] 企业微信测试应用、可信 IP、回调地址和模板已经配置。
- [ ] Gateway 测试环境可以接收卡片限速请求。
- [ ] 运营商回调开关只在对应测试环境联通后开启。
### 3.2 账号和数据准备
建议准备以下最小数据,不要直接使用生产数据:
- [ ] 1 个超管账号、1 个普通平台账号。
- [ ] 2 个启用的平台业务员,其中 1 个稍后用于换绑测试。
- [ ] 代理店铺 A、A 的下级店铺 A1、与 A 无关的店铺 B。
- [ ] A、A1、B 各至少 2 个启用店铺账号,再准备 1 个禁用店铺账号。
- [ ] 代理店铺 A 的代理账号,以及店铺 B 的代理账号。
- [ ] 至少 2 张卡和 2 台设备,分别归属 A、A1、B并准备个人账号绑定。
- [ ] 可购买套餐、下架套餐、购买即生效套餐、实名后生效套餐各 1 个。
- [ ] 预计最终到期日分别为 15、7、3、1 天的资产,以及 1 个已过期资产。
- [ ] 一笔可退款订单、一笔正在退款的订单、一笔已换货订单。
- [ ] 代理主钱包余额高于 100 元,并可通过测试消费降到 100 元以下。
### 3.3 每条用例怎样算通过
一条用例只有同时满足以下条件才算通过:
1. 页面提示正确。
2. 刷新页面后数据仍然正确。
3. 换一个无权限账号不能看到或操作该数据。
4. 重复点击、重复回调或重复触发不会重复扣款、退款、入账或通知。
5. 异步任务最终进入成功、部分成功或明确失败状态,不能长期卡在处理中。
发现问题时至少保存:账号、时间、操作页面、输入内容、预期结果、实际结果、完整截图和请求 ID。
## 四、业务验收步骤
## 4.1 店铺查询、业务员和 C 端登录限制
### 用例 A联系电话精确查询
1. 使用平台账号进入店铺列表。
2. 输入一个已存在店铺的完整 11 位联系电话并查询。
3. 再输入少一位、多一位、包含字母和不存在的号码。
预期结果:
- [ ] 完整号码只返回联系电话完全相同的店铺。
- [ ] 非法号码给出明确提示,不进行模糊查询。
- [ ] 代理账号只能查到自己及下级店铺,不能查到无关店铺 B。
### 用例 B绑定和换绑店铺业务员
1. 新建或编辑店铺 A选择启用的平台业务员甲。
2. 在店铺列表和详情查看业务员名称及状态。
3. 将业务员改为乙,再清空业务员。
4. 尝试选择禁用账号、代理账号或企业账号。
预期结果:
- [ ] 创建、编辑、换绑和清空后页面立即正确回显。
- [ ] 只能选择启用的平台账号。
- [ ] 代理账号不能给店铺指定其他业务员。
- [ ] 换绑后,后续新通知只发送给当前有效业务员。
### 用例 C禁止店铺资产新登录 C 端
1. 打开店铺 A 的“禁止 C 端登录”开关。
2. 使用 A 名下卡和设备重新发起 C 端登录。
3. 使用 B 名下资产登录。
4. 关闭开关后再次使用 A 名下资产登录。
预期结果:
- [ ] A 名下资产不能获得新的 C 端登录令牌。
- [ ] B 名下资产不受影响。
- [ ] 关闭开关后 A 名下资产可重新登录。
- [ ] 开关不会主动踢出已经登录的用户。
## 4.2 套餐授权、生效、续费和预计到期
### 用例 A系列套餐批量授权
1. 给店铺 A 首次授权一个套餐系列,并一次选择多个套餐。
2. 再次进入授权页面,新增套餐、修改价格、移除一个套餐。
3. 尝试重复选择已经授权的套餐。
预期结果:
- [ ] 首次授权必须至少包含一个套餐。
- [ ] 单次可以提交多个套餐,价格正确保存。
- [ ] 已授权套餐不能重复选择。
- [ ] 移除后只影响该授权关系,不删除套餐本身。
### 用例 B套餐生效条件
1. 分别创建“购买即生效”和“实名后生效”的套餐。
2. 分配给店铺时,分别测试跟随默认值和覆盖默认值。
3. 用资产购买套餐,再修改套餐或店铺分配设置。
预期结果:
- [ ] 页面正确显示套餐默认值、店铺覆盖值和最终生效值。
- [ ] 新购买记录保存购买时的生效规则快照。
- [ ] 后续修改配置不会改变历史订单和已购买套餐的规则。
### 用例 C下架套餐的历史续费
1. 让资产先购买套餐 P再将 P 下架。
2. 使用从未购买过 P 的新资产尝试购买。
3. 使用历史购买过 P 且仍具备续费资格的资产续费。
预期结果:
- [ ] 下架套餐不出现在普通新购列表中。
- [ ] 新资产不能绕过页面直接购买下架套餐。
- [ ] 符合历史使用资格的资产仍能通过原创建订单流程续费。
### 用例 D预计最终到期时间
分别查看无套餐、单套餐、多个排队套餐和等待实名激活的资产。
预期结果:
- [ ] 单套餐显示该套餐预计结束时间。
- [ ] 多个排队套餐显示全部套餐使用完后的预计最终到期时间。
- [ ] 无法确定激活时间时不伪造一个确定日期。
- [ ] 后台列表、后台详情和 C 端展示口径一致。
## 4.3 套餐临期列表和通知
### 用例 A实时临期列表
进入后台临期资产页面,检查准备好的 15、7、3、1 天及已过期资产。
预期结果:
- [ ] 只展示剩余 015 个上海自然日且能够精确计算的资产。
- [ ] 03 天为最高优先级并排在前面47 天和 815 天显示不同等级。
- [ ] 已过期、无套餐和无法预计到期的资产不进入临期列表。
- [ ] 卡、设备数量汇总和列表筛选结果一致。
- [ ] 代理账号只能看到自己及下级店铺的资产。
### 用例 B15/7/3 天临期通知
由超管触发一次临期扫描,等待 Worker 和通知任务处理完成。
预期结果:
- [ ] 只有当天恰好剩余 15、7、3 天的资产产生通知。
- [ ] 所属店铺的全部启用店铺账号都收到通知,不只发送给主账号。
- [ ] 当前有效绑定的平台业务员收到通知。
- [ ] 资产绑定的启用个人账号收到 C 端通知。
- [ ] 禁用账号、已换绑业务员和无关账号不收到通知。
- [ ] 通知正文明确是哪张卡或哪台设备、到期日期以及“即将过期”状态。
- [ ] 对同一批数据再次触发扫描,不产生重复通知。
手动触发接口见[附录](#六附录给实施和接口测试人员)。临期列表本身是实时查询;通知扫描不是实时触发,而是定时或手动触发。
## 4.4 实名状态和 H5 流程
### 用例 A卡和设备实名状态筛选
1. 在卡列表分别选择全部、已实名、未实名。
2. 在设备列表执行相同操作。
3. 给设备绑定已实名卡、解绑或更换成未实名卡后重新筛选。
预期结果:
- [ ] 卡按自身实名状态进入正确结果。
- [ ] 设备绑定的任意一张有效卡已实名时,设备视为已实名。
- [ ] 解绑和换卡后设备实名状态随有效绑定关系变化。
- [ ] 翻页和刷新后筛选条件仍然有效。
### 用例 B三种购买与实名顺序
分别配置并验证:无需实名、先实名后购买、先购买后实名。
预期结果:
- [ ] 无需实名:用户可以直接购买和使用业务允许的功能。
- [ ] 先实名后购买:未实名时不能进入购买,实名后可以购买。
- [ ] 先购买后实名:可以先完成购买,再引导实名激活。
- [ ] 卡和设备支持批量配置,单批最多 500 条且失败时整批不生效。
- [ ] 设备与下属卡规则冲突时,以设备返回的最终规则为准。
## 4.5 卡和设备支付方式
1. 超管分别给卡和设备配置钱包、微信、支付宝的允许组合。
2. 使用卡和设备进入 C 端购买及充值页面。
3. 创建订单后再修改系统配置,然后尝试支付旧订单。
4. 直接伪造一个当前不允许的支付方式请求。
预期结果:
- [ ] 卡和设备展示各自允许的支付方式,至少保留一种。
- [ ] 强充和普通充值场景不会错误展示钱包支付。
- [ ] 订单保存创建时选择的支付方式。
- [ ] 支付时后端再次校验;配置已变化的旧订单提示取消并重建。
- [ ] 直接请求不能绕过后端限制。
## 4.6 代理充值、信用额度和余额提醒
### 用例 A代理在线充值与平台线下代充值
1. 代理账号为自己的主钱包创建在线充值单并完成支付。
2. 平台账号为目标店铺创建线下代充值,上传凭证并进入企微审批。
3. 企微通过、拒绝各测试一次,并重复推送相同最终状态。
预期结果:
- [ ] 代理在线充值不能替其他无关店铺充值。
- [ ] 平台线下代充必须等待企微通过后入账。
- [ ] 拒绝后不入账;重复回调不重复入账。
- [ ] 到账通知发送给该店铺全部有效店铺账号和当前有效平台业务员。
- [ ] 通知正文明确“哪个店铺充值了多少钱”,金额精确到分。
### 用例 B代理充值数据权限
1. 准备店铺 A、A1、B 的充值订单。
2. 使用 A 的代理账号打开充值列表。
3. 搜索 B、直接打开 B 的订单详情,并查询 B 的支付状态。
4. 使用平台账号查看同一批订单。
预期结果:
- [ ] A 的代理账号只能看到 A 和下级 A1 的订单。
- [ ] 筛选 B 时返回空结果,不能通过参数扩大权限。
- [ ] 直接访问 B 的详情和支付状态时返回无权限或资源不存在。
- [ ] 平台账号可以按业务权限查看全量订单。
- [ ] 企业账号不能访问代理充值功能。
### 用例 C信用额度
1. 给某角色设置新建店铺默认信用额度。
2. 用该角色创建新店铺,检查主钱包额度。
3. 修改角色默认额度,再检查已有店铺。
4. 单独修改已有店铺的信用开关和额度,并测试扣款、冻结、退款和充值。
预期结果:
- [ ] 角色默认额度只影响之后新建的店铺。
- [ ] 已有店铺必须单独调整,不会被角色配置追溯覆盖。
- [ ] 可用余额允许在信用边界内显示负数。
- [ ] 超过信用边界时扣款失败,充值和退款回充后金额正确。
### 用例 D余额首次跌破 100 元提醒
1. 将代理主钱包现金可用余额从 100 元以上消费到 100 元以下。
2. 在低余额状态继续消费。
3. 充值到 100 元以上,再次消费到 100 元以下。
预期结果:
- [ ] 第一次跌破 100 元时,当前有效业务员收到一条站内通知。
- [ ] 持续低于 100 元时不重复通知。
- [ ] 回升后再次跌破,可以再次通知。
- [ ] 禁用或已换绑业务员不收到后续新通知。
## 4.7 企业微信审批和退款
### 用例 A企业微信基础配置
按以下顺序操作:保存应用 → 测试连接 → 同步成员 → 选择默认发起人 → 绑定系统账号 → 配置退款模板 → 配置线下代充值模板。
预期结果:
- [ ] 只能从应用可见成员中选择默认发起人和绑定成员。
- [ ] 模板控件 ID、类型、选项和必填项不匹配时不能保存。
- [ ] 代理提交时,企微可由默认成员代发起,但本地提交人仍是实际代理账号。
- [ ] 本系统不提供审批节点和审批人规则编辑功能。
线下代充值允许映射的业务字段为:`recharge_no``shop_id``shop_name``amount``amount_cent``payment_voucher_key``remark``submitter_id``submitter_name`。出现“当前业务不支持的字段”时,先核对是否填写了该列表之外的字段。
### 用例 B退款审批和最终处理
1. 为可退款订单提交退款申请。
2. 分别在企微完成通过、拒绝、撤销。
3. 对同一审批结果重复回调或主动同步。
4. 检查钱包回充、套餐失效、佣金和退款状态。
预期结果:
- [ ] 页面展示真实提交人、审批来源和中文审批状态。
- [ ] 企微记录不显示本地“通过/驳回”按钮。
- [ ] 通过后退款只执行一次,整单状态、钱包、套餐和佣金处理一致。
- [ ] 拒绝或撤销后不会退款。
- [ ] 退款完成通知发送给该店铺全部有效店铺账号和当前有效平台业务员。
- [ ] 重复回调、轮询或同步不重复退款和通知。
### 用例 C退款与换货交叉场景
1. 给资产创建未终结退款,再尝试创建换货。
2. 将退款拒绝、撤销或处理完成,再次创建换货。
3. 对已换货且套餐已迁移的订单退款。
预期结果:
- [ ] 存在活跃退款时直接拒绝换货,不产生半成品换货单。
- [ ] 退款终结后允许重新发起换货。
- [ ] 换货后退款能找到新资产上的对应套餐权益并正确失效。
- [ ] 只失效该退款订单产生的权益,不影响其他套餐。
## 4.8 换货查询、继承和通知
1. 在换货列表分别用旧资产和新资产的 ICCID、接入号或虚拟号搜索。
2. 同时填写新、旧资产条件。
3. 完成一次同品类换货,检查新资产店铺归属。
4. 查看旧资产、新资产和中间资产的换货链。
5. 创建物流换货单并使用个人账号查看通知。
预期结果:
- [ ] 新、旧资产搜索框互不混淆,同时填写时按两个条件共同过滤。
- [ ] 换货完成后新资产自动继承旧资产所属店铺,前端不能另选目标店铺。
- [ ] 重复完成不会重复迁移,不能越权换入其他店铺资产。
- [ ] 详情正确展示前代、后代;无权限节点不可跳转。
- [ ] 创建物流换货单后C 端收到换货弹窗或站内通知。
## 4.9 批量订购套餐
1. 下载或制作只有一列“资产标识”的 UTF-8 CSV。
2. 上传 CSV在页面统一选择一个套餐和一种支付方式。
3. 文件中同时放入成功数据、重复数据、无权限数据和不存在的数据。
4. 提交任务,刷新列表并查看详情。
5. Worker 处理中重启一次,再查看任务恢复结果。
预期结果:
- [ ] CSV 中不填写代理、套餐和支付方式,最多 1000 行、10MB。
- [ ] 每一行都有成功、失败或跳过结果和明确原因。
- [ ] 一行失败不影响其他合法行。
- [ ] 重复执行或任务恢复不会重复下单、扣款或发放套餐。
## 4.10 设备批量分配
1. 制作只有一列“设备标识”的 UTF-8 CSV可填写 VirtualNo、IMEI 或 SN。
2. 分别创建“分配店铺”和“设置套餐系列”任务。
3. 查看任务列表和详情。
4. 放入无权限设备、重复设备、不存在设备和已正确分配设备。
预期结果:
- [ ] 创建时正确保存操作类型和目标 ID。
- [ ] 任务列表返回操作名称、目标 ID、状态名称和进度。
- [ ] 逐行结果能区分成功、失败和跳过。
- [ ] 代理只能处理自己权限范围内的设备,不能越权分配 B 的设备。
- [ ] 重试不会重复分配或产生错误的套餐系列关系。
## 4.11 统一导出
分别对 IoT 卡、套餐、代理主钱包流水、代理充值、退款和换货创建 CSV、XLSX 导出任务。
预期结果:
- [ ] 导出任务异步执行,可以查看进度、失败原因和下载地址。
- [ ] 平台账号和代理账号导出的数据行符合各自数据权限。
- [ ] 下载链接受保护且过期后不能继续使用。
- [ ] Worker 重启后任务可以恢复,不重复生成错误数据。
- [ ] 角色级导出字段权限当前不得勾选“通过”,应记录为已知未交付。
## 4.12 卡片限速、复机和状态同步
### 用例 A卡片固定档位限速
1. 进入 IoT 卡详情,依次选择恢复不限速及固定档位。
2. 使用无权限账号操作其他店铺卡。
3. 模拟 Gateway 明确失败和超时。
预期结果:
- [ ] 只有 IoT 卡有固定档位入口,设备页面没有限速入口。
- [ ] 请求只使用卡 ICCID不使用设备号代替。
- [ ] 越权操作被拒绝。
- [ ] 明确失败显示失败;超时显示“结果未知”,不盲目自动重试。
### 用例 B行业卡复机和状态同步
1. 使用未实名行业卡执行复机。
2. 通过轮询、手动刷新、业务操作和已开启的运营商回调改变卡状态。
3. 检查卡、设备和详情页面状态。
预期结果:
- [ ] 本轮口径下,行业卡不因未实名而被阻止复机。
- [ ] 各入口写入相同的公共状态,不出现列表与详情互相矛盾。
- [ ] 要求 ICCID 的 Gateway 调用不得传设备号。
- [ ] 未完成真实联调的运营商回调开关保持关闭。
## 4.13 订单、退款、充值和换货列表字段
抽查卡订单、设备订单、退款、充值和换货记录。
预期结果:
- [ ] 卡的资产标识显示 ICCID。
- [ ] 设备优先显示虚拟号,没有虚拟号时显示 IMEI不使用 SN 冒充。
- [ ] C 端订单正确显示订单渠道或购买角色。
- [ ] 退款、充值、换货显示真实提交人 ID 和名称。
- [ ] 退款和线下充值显示审批来源、审批状态和中文状态名称。
- [ ] 历史本来没有快照的数据保持为空,不伪造错误值。
## 五、业务人员最终签字清单
业务负责人不需要检查数据库,只需确认以下结果:
- [ ] 不同账号看到的数据范围正确,无跨店铺越权。
- [ ] 充值、退款、换货、套餐和钱包金额正确,无重复处理。
- [ ] 企微通过、拒绝、撤销后的系统结果符合业务预期。
- [ ] 套餐生效、下架续费、预计到期和临期口径符合业务约定。
- [ ] 临期、充值、退款通知发送给正确的人,正文能看懂具体业务对象。
- [ ] 批量任务允许部分成功,失败原因足够业务人员自行处理。
- [ ] 本期不做项和已知缺口已经确认,不作为上线阻塞项或已另行排期。
建议使用以下结论之一:
- **通过**:全部必测项通过,无阻塞问题。
- **有条件通过**:只有已知缺口或已有明确排期的非阻塞问题。
- **不通过**:存在越权、错账、重复扣款/退款/入账、审批终态错误、通知错人或核心流程不可用。
## 六、附录:给实施和接口测试人员
### 6.1 手动触发临期通知扫描
```http
POST /api/admin/expiring-assets/reminder-scan
Authorization: Bearer <Token>
```
接口只负责立即提交与每日定时任务相同的扫描任务。接口返回成功不等于通知已经生成,必须继续等待 Worker 和 Outbox 处理。
### 6.2 常用排查顺序
1. 页面是否收到统一响应:`{code,msg,data,timestamp}`
2. 请求账号的类型、店铺 ID 是否正确。
3. API 和 Worker 是否连接同一个 Redis/Asynq。
4. 异步任务是否进入成功、部分成功或失败终态。
5. 企业微信、Gateway、对象存储是否有明确外部错误。
6. 通知接收人的账号是否启用、业务员是否仍绑定、个人账号是否仍绑定资产。
### 6.3 问题反馈模板
```markdown
### 问题标题
[七月迭代][功能名称] 简要说明问题
- 测试环境:
- 发生时间:
- 测试账号及账号类型:
- 店铺/资产/订单/任务编号:
- 前置条件:
- 操作步骤:
1.
2.
3.
- 预期结果:
- 实际结果:
- 是否可以稳定复现:
- 请求 ID
- 截图或录屏:
```
## 七、参考文档
- [七月迭代人工验收清单](./七月迭代人工验收清单.md)
- [七月迭代实现与接口对接说明](./七月迭代实现与接口对接说明.md)
- [七月迭代联调交付说明](./七月迭代联调交付说明.md)
- [七月迭代技术方案(标准评审稿)](./7月迭代技术方案-标准评审稿.md)
- [后台 OpenAPI 文档](../admin-openapi.yaml)

View File

@@ -0,0 +1,176 @@
# C 端 H5 页面接口矩阵
> C 端是独立 H5使用个人客户体系和 `/api/c/v1`,不复用管理 Web/H5 的账号密码登录、菜单或角色。
## 一、C 端产品定位
当前 C 端是“围绕资产提供查询、充值、购包和设备控制”的服务 H5不是完整电商商城。
客户通常从资产二维码、设备标签或外部链接进入,通过资产标识完成验证和微信身份登录。登录后可查看本人绑定的卡或设备,并围绕该资产操作。
首期不应设计购物车、商品分类、收货地址簿等页面,因为当前没有对应后端能力。
## 二、进入与登录流程
```mermaid
sequenceDiagram
participant U as 客户
participant H as C端H5
participant A as 后端
participant W as 微信
U->>H: 扫码或输入资产标识
H->>A: POST /auth/verify-asset
A-->>H: asset_token、资产类型、微信配置
H->>W: 公众号OAuth或小程序登录
W-->>H: code
H->>A: POST /auth/wechat-login 或 miniapp-login
A-->>H: JWT、客户信息、need_bind_phone
alt 需要绑定手机号
H->>A: POST /auth/send-code
H->>A: POST /auth/bind-phone
end
H->>A: GET /asset/info
A-->>H: 资产首页数据
```
关键约束:
- `verify-asset` 支持 ICCID、虚拟号、MSISDN、IMEI、SN。
- 设备内绑定卡不能以独立卡身份登录,应提示客户使用设备标识。
- `asset_token` 是短期登录凭证,不等同于登录后的 JWT。
- C 端 JWT 由签名和 Redis 当前 Token 双重校验;同一客户后登录可能使旧会话失效。
- C 端没有 Refresh Token 接口,认证失效后重新走资产验证和微信登录。
## 三、页面矩阵
### 3.1 启动、登录和账户
| 页面 | 输入 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 启动初始化 | 当前完整 URL | `app_id`、JSSDK 配置、环境错误 | 初始化微信能力 | `GET /wechat/appid``GET /wechat/jssdk-config` |
| 资产验证 | 资产标识 | 资产类型、脱敏资产摘要、登录提示 | 验证并获取 `asset_token` | `POST /auth/verify-asset` |
| 微信登录 | `asset_token`、OAuth `code` | 客户、是否新用户、是否需绑手机、Token | 公众号/小程序登录 | `POST /auth/wechat-login``POST /auth/miniapp-login` |
| 手机号绑定 | 手机号、验证码 | 绑定结果 | 发送验证码、绑定、更换手机号 | `/auth/send-code``/auth/bind-phone``/auth/change-phone` |
| 个人资料 | 无或昵称、头像 | 客户 ID、昵称、头像、手机号、状态 | 查看、更新 | `GET|PUT /profile` |
| 退出登录 | 无 | 无 | 使当前 Token 失效 | `POST /auth/logout` |
公开接口必须在没有 Bearer Token 时也能访问:微信配置、资产验证、微信/小程序登录、发送验证码。
### 3.2 资产首页
| 区块 | 关键展示字段 | 交互 | 接口 |
| --- | --- | --- | --- |
| 资产身份 | `asset_type`、主标识、ICCID/虚拟号/IMEI/SN、设备名 | 复制标识 | `GET /asset/info` |
| 状态 | 业务状态、网络/在线状态、实名状态及名称、最后同步时间 | 刷新 | `GET /asset/info``POST /asset/refresh` |
| 流量 | 总量、已用、剩余、使用比例、周期 | 查看套餐历史 | `GET /asset/info``GET /asset/package-history` |
| 当前套餐 | 套餐名、类型、生效/到期、预计最终到期、排队信息 | 查看可购套餐 | `GET /asset/info` |
| 钱包 | 余额、冻结金额、资产类型 | 充值、查看流水 | `GET /wallet/detail` |
| 实名 | `effective_realname_policy``realname_required``realname_status` | 去实名 | `GET /asset/info``GET /realname/link` |
| 支付能力 | `allowed_payment_methods` | 控制购买和充值按钮 | `GET /asset/info` |
前端不得根据“卡还是设备”自行推断实名策略和支付方式。
### 3.3 套餐购买
| 页面 | 主要输入/筛选 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 可购套餐 | 资产标识、分页/类型(以 OpenAPI 为准) | 套餐 ID、名称、系列、类型、流量、周期、售价、上下架/可购状态、有效期说明 | 选择套餐 | `GET /asset/packages` |
| 下单确认 | 资产标识、套餐 ID、支付方式、应用类型 | 资产、套餐、价格、实名要求、强充提示、允许支付方式 | 创建订单 | `POST /orders/create` |
| 支付 | 订单 ID | 应付金额、支付方式、订单状态 | 钱包支付、微信支付、支付宝支付 | `POST /orders/:id/pay` |
| 订单列表 | 状态、分页 | 订单号、资产、套餐、金额、支付方式、状态名称、创建时间 | 查看详情 | `GET /orders` |
| 订单详情 | 订单 ID | 订单、套餐、资产、金额、支付、状态时间、权益结果 | 重新支付 | `GET /orders/:id` |
调用顺序:
1. 先查询 `/asset/info`,取得实名策略和允许支付方式。
2. 查询 `/asset/packages`
3. 提交 `/orders/create`
4. 如果响应已包含强充支付参数,按响应拉起支付。
5. 普通待支付订单调用 `/orders/:id/pay`
6. 钱包支付成功后直接刷新详情;第三方支付返回页面后重新查询订单详情。
当前没有独立 C 端支付状态接口,也没有客户端主动取消订单接口。页面通过订单详情确认后端最终状态。
### 3.4 钱包与充值
| 页面 | 输入/筛选 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 钱包详情 | 资产标识 | 资产类型/ID、余额、冻结金额、可用余额 | 进入充值 | `GET /wallet/detail` |
| 钱包流水 | 类型、时间、分页 | 流水号、变动金额、前后余额、业务类型、关联订单、状态、时间 | 查看关联业务 | `GET /wallet/transactions` |
| 充值校验 | 资产标识、金额/场景 | 最低/最高金额、允许支付方式、实名/强充规则 | 进入充值 | `GET /wallet/recharge-check` |
| 创建充值 | 资产、金额、支付方式、应用类型、幂等请求标识 | 充值单号、支付参数或支付宝链接、状态 | 拉起支付 | `POST /wallet/recharge` |
| 充值记录 | 状态、分页 | 充值单号、金额、方式、支付状态、到账状态、时间 | 查看详情 | `GET /wallet/recharges``GET /recharge-orders` |
| 充值详情 | 充值订单 ID | 金额、资产、渠道、第三方单号、支付/到账时间、状态名称 | 刷新状态 | `GET /recharge-orders/:id` |
充值只使用接口返回的支付方式。钱包不能给自己钱包充值,因此强充和普通充值通常只提供微信、支付宝等第三方方式。
### 3.5 实名
| 页面 | 输入 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 实名引导 | 资产标识 | 是否需要实名、实名状态、模式、运营商、提示 | 获取实名链接 | `GET /realname/link` |
可能的策略:
- `none`:无需实名。
- `before_order`:未实名时禁止充值或购买。
- `after_order`:可先购买,支付后继续引导实名。
页面必须使用 `effective_realname_policy`,不能只读资产原始策略。
### 3.6 设备控制
仅设备资产显示该模块。
| 页面/动作 | 输入 | 展示字段 | 接口 |
| --- | --- | --- | --- |
| 卡槽列表 | 设备标识 | 卡槽号、ICCID、运营商、网络/实名/激活状态、当前卡 | `GET /device/cards` |
| 切卡 | 设备标识、目标卡/卡槽 | 操作结果、当前卡 | `POST /device/switch-card` |
| 配置 WiFi | SSID、密码等 DTO 字段 | 操作结果 | `POST /device/wifi` |
| 重启 | 设备标识 | 操作结果 | `POST /device/reboot` |
| 恢复出厂 | 设备标识、确认 | 操作结果 | `POST /device/factory-reset` |
重启、切卡、WiFi、恢复出厂都应防止重复点击恢复出厂必须二次确认。
### 3.7 换货
| 页面 | 输入 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 待处理换货提醒 | 当前登录资产 | 换货单号、旧资产、状态、提交期限 | 进入收货信息 | `GET /exchange/pending` |
| 收货信息 | 收件人、手机号、地区、详细地址 | 换货摘要 | 提交 | `POST /exchange/:id/shipping-info` |
C 端不能创建、发货、完成或取消换货,只负责查询待补资料换货单并提交收货信息。
### 3.8 通知
| 页面 | 筛选/输入 | 展示字段 | 操作 | 接口 |
| --- | --- | --- | --- | --- |
| 未读提示 | 无 | 未读数 | 打开通知中心 | `GET /notifications/unread-count` |
| 通知列表 | 类型、状态、分页 | 类型、标题、内容、风险、关联资源、时间、已读状态 | 标记已读 | `GET /notifications``PUT /notifications/:id/read` |
| 全部已读 | 无 | 无 | 全部标记已读 | `PUT /notifications/read-all` |
## 四、全局交互状态
C 端至少统一处理:
- 首次加载、局部刷新、空数据和网络异常。
- Token 失效后回到资产验证入口,并保留当前进入链接。
- 403 表示当前客户未绑定该资产或无权操作,不显示“资产不存在”的差异提示。
- 支付取消、支付失败、支付结果未知和支付成功。
- Gateway 操作已提交但结果未知,禁止无限自动重试。
- 订单、充值、换货使用后端 `status_name` 展示中文状态。
## 五、当前缺口
以下页面没有现成接口支撑,不纳入首期:
- 独立商品详情和商品分类。
- 购物车。
- 收货地址簿。
- 客户主动取消订单。
- 客户主动申请退款。
- 客户资产列表和主动切换当前资产。
- 独立支付状态查询。
若产品要求其中任一能力,应先作为独立后端用例设计,不能只在前端模拟。

146
docs/前端建设/README.md Normal file
View File

@@ -0,0 +1,146 @@
# 前端建设总览
> 状态前端尚未创建本目录用于统一产品、UI、前端和后端对系统业务、页面范围与接口契约的理解。
>
> 接口字段的唯一权威来源是 [`docs/admin-openapi.yaml`](../admin-openapi.yaml)。本目录只说明业务语义、页面需要什么、接口按什么顺序调用,不复制完整 DTO。
## 一、已经确认的产品形态
系统包含三个前端使用场景,不存在独立的“代理端”或“企业端”后端:
| 使用场景 | 形态 | 登录用户 | 后端接口 |
| --- | --- | --- | --- |
| 管理 Web | 桌面浏览器管理系统 | 超级管理员、平台账号、代理账号、企业账号 | `/api/auth``/api/admin` |
| 管理 H5 | 移动浏览器管理系统 | 超级管理员、平台账号、代理账号、企业账号 | `/api/auth``/api/admin` |
| C 端 H5 | 面向个人客户的资产服务 H5 | 个人客户 | `/api/c/v1` |
管理 Web 和管理 H5 的菜单不是按“平台端、代理端、企业端”写死,而由登录账号的角色权限决定。账号身份只决定组织关系和数据范围:
- 超级管理员:不分配角色,获取当前终端全部启用权限。
- 平台账号:可分配多个平台角色,数据范围为全平台。
- 代理账号:归属一个店铺,可分配一个客户角色,数据范围为本店及下级店铺。
- 企业账号:归属一个企业,可分配一个客户角色,数据范围应限制为本企业。
- 个人客户:独立用户体系,不参与管理端 RBAC通过客户与资产绑定控制数据范围。
登录请求中的 `device` 决定返回哪一套菜单:
- 管理 Web 传 `web`
- 管理 H5 传 `h5`
- 权限记录的 `platform=all` 同时适用于两端。
## 二、系统业务全景
系统管理两类核心资产IoT 卡和设备。设备可以绑定多张 IoT 卡;资产由平台导入后,沿店铺层级进行分配,并可进一步授权给企业使用。
```mermaid
flowchart LR
A[平台配置运营商、套餐和规则] --> B[导入 IoT 卡和设备]
B --> C[分配给代理店铺]
C --> D[代理向直属下级继续分配]
C --> E[授权给企业客户使用]
C --> F[个人客户绑定资产]
D --> F
E --> G[企业查看被授权资产]
F --> H[C 端查询流量、实名和套餐]
H --> I[充值或购买套餐]
I --> J[支付回调和套餐生效]
J --> K[轮询流量、状态和实名]
J --> L[生成钱包流水、差价和佣金]
J --> M[退款、换货和通知]
L --> N[代理提现或继续采购]
```
### 2.1 身份与组织
`Account` 是管理端账号;`Shop` 是代理组织;`Enterprise` 是企业组织;`PersonalCustomer` 是独立 C 端客户。
- 店铺最多形成 7 级上下级关系。
- 代理账号必须关联一个店铺。
- 企业账号必须关联一个企业。
- 企业可以归属于平台,也可以归属于某个店铺。
- 个人客户通过微信身份、手机号和资产绑定建立使用关系。
### 2.2 资产
- IoT 卡可独立使用,也可绑定在设备卡槽中。
- 卡和设备均有平台/店铺归属、业务状态、实名策略、套餐系列等信息。
- 设备分配时,其绑定卡需要同步处理归属。
- 企业授权是使用权,不改变资产的店铺归属。
- C 端客户绑定资产后,只能操作本人已绑定的资产。
### 2.3 套餐与分销
平台先创建套餐系列和套餐,再将系列、套餐和价格逐级下发给店铺。
- 上级只能将自己有权销售的套餐分配给下级。
- 代理通常只能向直属下级分配。
- 下级成本价不能低于上级成本价。
- 主套餐在已有生效主套餐时进入排队。
- 加油包要求已有主套餐,并立即生效,其有效期跟随主套餐。
- 下架套餐默认不再进入新客户可购列表;符合历史续费条件的资产可继续按已有套餐 ID 下单。
### 2.4 订单与支付
订单来源包括管理端代购、C 端自助购买和批量购买。
- 管理端先调用购买校验,再创建订单。
- C 端先查询资产信息和可购套餐,再创建订单。
- 支付方式由后端按资产类型、业务场景和配置计算,前端使用 `allowed_payment_methods`
- 钱包支付同步完成扣款;微信、支付宝等第三方支付依赖服务端回调更新状态。
- 支付成功后,后端负责套餐激活、排队、佣金和通知,前端不能自行修改业务状态。
### 2.5 钱包、佣金与提现
系统存在资产钱包和代理主钱包两套资金语义:
- 资产钱包:归属于卡或设备,供 C 端充值和购买套餐。
- 代理主钱包:归属于店铺,用于代理采购、代购和经营结算。
- 佣金钱包/记录:记录差价收益和一次性佣金。
- 提现:代理提交申请,系统冻结金额,审批完成后结算或解冻。
- 所有金额接口以“分”的整数为准,前端只在展示层转换为元。
### 2.6 售后
- 退款:围绕原订单、实付金额、套餐权益和审批状态处理。
- 换货后台建单C 端填写收货信息,后台发货并选择是否迁移旧资产权益。
- 套餐失效:退款或人工任务只失效目标订单产生的权益。
- 通知:管理账号和个人客户使用两套通知读取接口。
### 2.7 异步任务
导入、导出、批量购买、批量分配和部分扫描任务不在单次 HTTP 请求内完成:
1. 前端获取对象存储预签名地址。
2. 文件直传对象存储。
3.`file_key` 和业务参数提交给任务接口。
4. 获得任务 ID。
5. 轮询列表或详情。
6. 展示总数、成功数、失败数和逐行失败原因。
## 三、前端建设产物
- [前端选型与代码组织](前端选型与代码组织.md)
- [管理 Web 与管理 H5 页面接口矩阵](管理端页面接口矩阵.md)
- [C 端 H5 页面接口矩阵](C端H5页面接口矩阵.md)
- [权限矩阵与接口联调契约](权限矩阵与接口联调契约.md)
- [完整 OpenAPI](../admin-openapi.yaml)
- [现有系统流程图](../系统流程图/)
## 四、契约优先级
出现冲突时按以下顺序判断:
1. 当前代码中的路由、DTO 和权限实现。
2. `docs/admin-openapi.yaml` 生成契约。
3. 本目录的业务和页面说明。
4. 历史功能总结和历史流程图。
历史文档仍可能出现 `/api/admin/login``/api/h5/login` 等旧路径。当前统一认证入口是 `/api/auth/*`,新前端不得继续使用旧路径。
## 五、当前启动前阻塞项
1. 管理端逐接口权限门禁尚未形成统一闭环,前端隐藏菜单不能替代后端鉴权。
2. 企业账号的数据范围需要使用真实 Token 对企业、资产和授权接口做越权验证。
3. 管理 H5 的首期页面范围尚未最终确认;本文给出任务型 MVP 建议,不默认复制全部 Web 页面。
4. C 端目前是“围绕当前资产直接购买”的服务 H5没有购物车、地址簿、独立商品详情和主动退款等完整商城能力。
5. 前端技术栈尚未落地,先完成选型决策再创建脚手架。

View File

@@ -0,0 +1,213 @@
# 前端选型与代码组织
> 状态:推荐方案,尚未创建脚手架或安装依赖。
## 一、推荐结论
推荐使用 Vue 3 + TypeScript + Vite并采用 pnpm workspace 单仓库、三个独立应用:
```text
frontend/
├── apps/
│ ├── admin-web/ # 管理 Web
│ ├── admin-h5/ # 管理 H5
│ └── client-h5/ # 独立 C 端 H5
└── packages/
└── api/ # OpenAPI 生成类型和请求客户端
```
推荐组合:
| 领域 | 选择 | 理由 |
| --- | --- | --- |
| 框架 | Vue 3 + TypeScript | 管理后台和 H5 生态成熟,单文件组件适合页面型业务 |
| 构建 | Vite | 配置少,三应用可独立构建和发布 |
| 工作区 | pnpm workspace | 只解决多应用依赖和共享 API 包,不引入额外编排平台 |
| 管理 Web UI | Element Plus | 表格、表单、树、分页、弹窗等后台能力完整 |
| 管理 H5/C 端 UI | Vant | 移动表单、列表、弹层、支付结果页等 H5 交互成熟 |
| 路由 | Vue Router | 三个应用分别维护静态路由表 |
| 客户端状态 | Pinia | 只保存登录态、当前用户、菜单和按钮权限 |
| 接口类型 | openapi-typescript | 从现有 OpenAPI 生成 TypeScript 类型 |
| 请求客户端 | openapi-fetch + 原生 fetch | 类型直接复用,减少手写接口和额外封装 |
不在首期引入Nx、Turborepo、大型后台模板、服务端状态缓存库、自研组件库、微前端。
## 二、为什么建议三个应用
管理 Web 和管理 H5 使用同一套后端业务与权限,但它们是两个真实的交互界面:
- Web 以高密度表格、批量操作、复杂配置为主。
- 管理 H5 以资产查询、现场操作、审批、资金和通知为主。
- 两端登录分别传 `device=web``device=h5`,后端会返回不同菜单。
- Element Plus 和 Vant 的布局与交互模型不同,强行在一个应用混用会增加条件渲染和包体积。
- C 端 H5 使用完全不同的个人客户认证、微信 OAuth、支付回跳和资产绑定流程必须独立。
三个应用放在同一仓库,可以统一 TypeScript、Lint、构建和 OpenAPI 类型,又不会把三种页面体验绑在一个发布包中。
## 三、何时可以改成两个应用
如果管理 H5 最终只保留以下少量页面,可以使用“自适应管理端 + C 端 H5”两应用方案
- 登录和个人中心。
- 通知。
- 统一资产搜索和只读详情。
- 少量订单、退款、换货审批。
一旦管理 H5 需要独立导航、设备控制、企业授权、资金、上传或大量移动表单,就应使用独立 `admin-h5`。当前页面矩阵已经超过纯只读壳层,因此默认采用三个应用。
## 四、共享边界
首期只共享 `packages/api`
```text
Go 路由和 DTO
docs/admin-openapi.yaml
openapi-typescript
packages/api 类型与客户端
admin-web / admin-h5 / client-h5
```
暂不共享:
- 页面组件。
- 登录页面。
- 业务状态管理。
- Web/H5 表单组件。
- 业务流程组合函数。
等两个应用出现真实、稳定、完全相同的重复后再提取。不要先创建 `shared-business``shared-components` 等空泛包。
## 五、推荐仓库结构
```text
frontend/
├── apps/
│ ├── admin-web/
│ │ └── src/
│ │ ├── app/
│ │ ├── pages/
│ │ ├── routes/
│ │ └── stores/
│ ├── admin-h5/
│ │ └── src/
│ │ ├── app/
│ │ ├── pages/
│ │ ├── routes/
│ │ └── stores/
│ └── client-h5/
│ └── src/
│ ├── app/
│ ├── pages/
│ ├── routes/
│ └── stores/
├── packages/
│ └── api/
│ ├── generated/
│ ├── client.ts
│ └── index.ts
├── package.json
├── pnpm-workspace.yaml
└── tsconfig.base.json
```
目录保持扁平。页面内部先就近放置组件,不按 `api/service/model/controller` 重建一套前端分层。
## 六、请求模块的唯一职责
`packages/api` 只负责:
1. OpenAPI 生成类型。
2. Base URL。
3. Bearer Token 注入。
4. 管理端 Refresh Token 单次刷新队列。
5. `{code,msg,data,timestamp}` 解包。
6. 401、403、429、5xx 统一错误事件。
7. Request ID 提取。
它不负责:
- 页面跳转。
- Toast 文案拼接。
- 将订单状态改成本地状态机。
- 缓存所有列表和详情。
- 替页面吞掉错误。
这使请求模块保持为一个小接口、深实现的公共模块,三应用只需要学习同一套调用方式。
## 七、动态菜单实现
后端菜单只决定“哪些本地路由可见和可进入”,不能直接决定加载哪个源代码文件。
正确方式:
1. 每个管理应用维护静态路由表:`routeKey/url → 页面组件`
2. 登录后读取 `menus`
3. 将后端菜单与本地静态路由表求交集。
4. 生成导航和路由白名单。
5. 未匹配到本地页面时记录告警并隐藏,不允许字符串动态 import 任意组件。
6. 使用 `buttons` 控制操作按钮。
## 八、状态管理边界
Pinia 首期只保存:
- Access Token、Refresh Token。
- 当前用户。
- 菜单、按钮和权限码。
- 当前终端类型。
- 少量跨页面 UI 状态。
订单列表、资产详情、套餐列表等服务端数据由页面直接请求并局部刷新。等出现跨页面共享缓存、复杂失效和后台自动刷新需求后,再评估服务端状态库。
## 九、OpenAPI 工作流
建议建立固定流水线:
```text
go run ./cmd/gendocs
更新 docs/admin-openapi.yaml
生成 packages/api/generated
TypeScript 类型检查
构建三个应用
```
生成文件不手改。字段不正确时修改 Go DTO、RouteSpec 或文档生成器,然后重新生成。
当前 `admin-openapi.yaml` 同时包含管理端、C 端、开放接口和回调,文件名虽然偏旧,但首期无需为前端拆分多份 OpenAPI。
## 十、方案对比
| 方案 | 优点 | 风险 | 结论 |
| --- | --- | --- | --- |
| Vue 三应用单仓库 | Web/H5 体验清晰;共享接口类型;独立发布 | 有少量工程重复 | 推荐 |
| Vue 自适应管理端 + C端H5 | 少一个应用,首期代码更少 | Web/H5 组件混用、条件页面和包体增长 | 仅管理 H5 很小时使用 |
| React 三应用 | 也能满足需求,团队成熟时可选 | 当前没有团队技术偏好,初始约定和库选择更多 | 团队明显更熟 React 时替换推荐方案 |
如果实施团队已有稳定 React 经验,应优先服从团队能力,使用 React + TypeScript + Vite、Ant Design、Ant Design Mobile仍保持“三应用单仓库只共享 OpenAPI 包”的结构,不必为追求 Vue 统一而增加学习成本。
## 十一、创建脚手架前需要确认
1. 前端团队更熟 Vue 还是 React。
2. 管理 H5 是否作为独立正式产品发布。
3. 三个应用的域名、Base URL 和环境配置。
4. 微信公众号 OAuth 回调域名和支付回跳地址。
5. 首期页面范围及权限种子数据。
6. 后端企业数据范围和逐接口权限门禁是否完成验证。
## 十二、参考资料
- [Vue TypeScript](https://vuejs.org/guide/typescript/overview.html)
- [Pinia](https://pinia.vuejs.org/)
- [Element Plus](https://element-plus.org/)
- [Vant](https://vant-ui.github.io/vant/)
- [openapi-typescript](https://openapi-ts.dev/introduction)
- [openapi-fetch](https://openapi-ts.dev/openapi-fetch/)
- [pnpm Workspace](https://pnpm.io/workspaces)

View File

@@ -0,0 +1,306 @@
# 权限矩阵与接口联调契约
## 一、管理端权限计算模型
管理页面和操作的最终可用性不是只看 `user_type`,而是四个条件的交集:
```text
终端适用范围 web/h5/all
账号所绑定角色的菜单和按钮权限
账号身份的硬性业务限制
当前组织的数据范围
```
### 1.1 终端
权限记录包含 `platform`
- `web`:只在管理 Web 登录时返回。
- `h5`:只在管理 H5 登录时返回。
- `all`:两端都返回。
### 1.2 角色
- 超级管理员不分配角色。
- 平台账号使用平台角色,可绑定多个。
- 代理和企业账号使用客户角色,最多绑定一个。
- 登录返回 `menus``buttons` 和兼容字段 `permissions`
因此,“代理可用什么”应由给该代理账号绑定的客户角色决定,而不是维护一套固定代理菜单。
### 1.3 身份硬限制
角色权限不能突破组织身份的业务限制。例如:
- 企业账号即使错误分配了店铺菜单,也不应管理店铺。
- 企业账号不应访问代理资金、佣金、提现和代理充值。
- 代理账号不能越过本店及下级店铺的数据范围。
- 代理账号不能管理平台直属企业。
- 只有超管或平台管理身份可以维护支付、企微和全局系统配置。
### 1.4 数据范围
| 用户身份 | 组织字段 | 正常数据范围 |
| --- | --- | --- |
| 超级管理员 | 无 | 全平台 |
| 平台账号 | 无 | 全平台,但功能仍受角色权限控制 |
| 代理账号 | `shop_id` | 本店及全部下级店铺 |
| 企业账号 | `enterprise_id` | 本企业有效授权资产和本企业业务记录 |
| 个人客户 | `customer_id` | 本人绑定资产及本人订单、充值、通知 |
## 二、目标权限矩阵
下表是产品和安全目标,不代表当前每条后端接口都已完整实现。
图例:
- `权限控制`:角色有相应菜单/按钮且数据范围满足时可用。
- `范围受限`:可用,但只能处理当前组织范围。
- `只读建议`:首期只开放查询。
- `禁止`:角色配置也不得突破。
| 业务模块 | 超管 | 平台账号 | 代理账号 | 企业账号 | 个人客户 |
| --- | --- | --- | --- | --- | --- |
| 账号管理 | 全部 | 权限控制 | 范围受限 | 禁止 | 不适用 |
| 角色/权限配置 | 全部 | 权限控制 | 建议禁止 | 禁止 | 不适用 |
| 店铺管理 | 全部 | 权限控制 | 本店及下级、权限控制 | 禁止 | 不适用 |
| 企业管理 | 全部 | 权限控制 | 本店及下级企业、权限控制 | 只读本企业资料 | 不适用 |
| 企业资产授权 | 全部 | 权限控制 | 本店及下级企业、权限控制 | 只读本企业授权 | 不适用 |
| 卡/设备库存 | 全部 | 权限控制 | 本店及下级、权限控制 | 仅本企业授权资产 | 仅本人绑定资产 |
| 资产分配 | 全部 | 权限控制 | 通常仅直属下级、权限控制 | 禁止 | 禁止 |
| 资产状态与设备控制 | 全部 | 权限控制 | 范围受限、权限控制 | 本企业授权范围、权限控制 | 本人绑定范围 |
| 运营商配置 | 全部 | 权限控制 | 禁止 | 禁止 | 不适用 |
| 套餐系列/套餐设计 | 全部 | 权限控制 | 建议只读 | 禁止 | 只看可购投影 |
| 套餐授权/分配/定价 | 全部 | 权限控制 | 直属下级、权限控制 | 禁止 | 禁止 |
| 管理端订单 | 全部 | 权限控制 | 范围受限、权限控制 | 视业务角色决定 | 仅本人 C 端订单 |
| 退款/换货 | 全部 | 权限控制 | 范围受限、权限控制 | 只读建议 | C 端只提交换货收货信息 |
| 代理主钱包/佣金/提现 | 全部 | 权限控制 | 本店及下级、权限控制 | 禁止 | 不适用 |
| 代理充值 | 全部 | 权限控制 | 本店在线充值、权限控制 | 禁止 | 不适用 |
| 资产钱包 | 全部 | 权限控制 | 范围受限、权限控制 | 授权资产只读建议 | 本人绑定资产 |
| 导入/批量处理 | 全部 | 权限控制 | 范围受限、权限控制 | 禁止 | 禁止 |
| 导出 | 全部 | 权限控制 | 按数据范围导出 | 只导本企业且需明确开放 | 禁止 |
| 微信/支付宝/企微配置 | 全部 | 权限控制 | 禁止 | 禁止 | 禁止 |
| 轮询、告警、清理 | 全部 | 权限控制 | 只读建议 | 禁止 | 禁止 |
| 通知 | 全部 | 当前账号 | 当前账号 | 当前账号 | 当前客户 |
## 三、统一认证契约
### 3.1 管理 Web/H5 登录
```http
POST /api/auth/login
Content-Type: application/json
```
```json
{
"username": "账号或手机号",
"password": "密码",
"device": "web"
}
```
管理 H5 将 `device` 改为 `h5`
成功后保存:
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 86400,
"user": {
"id": 1,
"username": "admin",
"user_type": 2,
"user_type_name": "平台用户",
"shop_id": 0,
"enterprise_id": 0
},
"menus": [],
"buttons": [],
"permissions": []
}
```
所有受保护请求携带:
```http
Authorization: Bearer <access_token>
```
### 3.2 Token 刷新
```http
POST /api/auth/refresh-token
```
```json
{
"refresh_token": "..."
}
```
前端请求模块需要实现单次刷新队列:多个并发请求同时收到 401 时,只允许一个请求刷新 Token其余等待结果刷新失败统一清除会话并跳转登录。
### 3.3 C 端认证
C 端使用 `/api/c/v1/auth/*`,不使用管理端登录或刷新 Token
1. `verify-asset`
2. `wechat-login``miniapp-login`
3. 必要时 `bind-phone`
4. 后续请求携带 C 端 Bearer Token。
5. 认证失效重新走资产验证和微信登录。
## 四、响应与错误契约
### 4.1 统一响应
```json
{
"code": 0,
"msg": "success",
"data": {},
"timestamp": "2026-07-29T12:00:00+08:00"
}
```
前端成功判断必须同时尊重 HTTP 状态和业务 `code`,不能只判断 HTTP 200。
### 4.2 分页
常见请求参数为 `page``page_size`,常见返回为:
```json
{
"items": [],
"total": 0,
"page": 1,
"size": 20
}
```
不同历史模块的列表 DTO 可能有差异,类型生成和页面开发必须以对应 OpenAPI Operation 为准,不能强制假设所有列表结构完全相同。
### 4.3 HTTP 错误处理
| HTTP 状态 | 前端行为 |
| --- | --- |
| 400 | 定位请求字段,展示后端安全中文提示 |
| 401 | 管理端尝试刷新C 端重新登录 |
| 403 | 显示无权限,不重试,不猜测资源是否存在 |
| 404 | 显示资源不存在或已失效 |
| 409 | 提示状态已变化,重新拉取详情 |
| 429 | 提示操作频繁,按 `Retry-After` 或短时退避 |
| 5xx | 展示通用错误和 Request ID允许用户手动重试 |
## 五、数据展示契约
- 金额:接口整数单位通常为分,展示时格式化为元,提交时转换回分;禁止浮点数直接参与计算。
- 状态:优先使用响应中的 `status_name``*_name`,不要在多个前端重复维护中文枚举。
- 时间:按 ISO 8601 解析,统一展示为当前业务时区;提交日期范围按接口约定处理起止边界。
- ID前端将 ID 当作不透明标识,不参与金额或业务计算。
- 空值:区分 `null`、空字符串、0 和空数组,不能用统一假值替换。
- 资产标识:按字符串保存和上传,避免 ICCID 被转为科学计数法或丢失前导零。
- 支付方式:使用 `allowed_payment_methods` 或支付方式查询接口,不写死微信/支付宝/钱包组合。
- 状态流转:按钮是否出现先看权限和当前状态;提交后仍以后端状态机结果为准。
## 六、关键接口调用顺序
### 6.1 管理端创建订单
1. 解析或选择资产。
2. 查询资产可用套餐。
3. `POST /api/admin/orders/purchase-check`
4. 展示后端计算的购买条件、金额和支付方式。
5. `POST /api/admin/orders`
6. 查询订单详情确认最终状态。
### 6.2 代理在线充值
1. `GET /api/admin/agent-recharges/payment-methods`
2. `POST /api/admin/agent-recharges`
3. 按响应拉起支付。
4. 轮询 `GET /api/admin/agent-recharges/:id/payment-status`
5. 查询详情和主钱包流水确认到账。
### 6.3 企业资产授权
1. 查询企业详情。
2. 查询候选卡或设备。
3. 提交 `allocate-cards``allocate-devices`
4. 重新查询企业卡/设备列表。
5. 查询授权记录确认结果。
设备授权由后端同步处理绑定卡,前端不得自行拆分请求。
### 6.4 文件上传和异步任务
1. `POST /api/admin/storage/upload-url`
2. 直接向响应的对象存储 URL 上传。
3.`file_key` 提交业务任务接口。
4. 轮询任务详情。
5. 完成后展示结果或下载文件。
### 6.5 第三方支付
1. 创建业务订单或充值单。
2. 使用后端返回的支付参数/链接拉起支付。
3. 支付平台回调只由服务端接收,前端不得调用 `/api/callback/*`
4. 用户返回页面后重新查询业务单状态。
5. 结果未知时保持“处理中”,不在前端直接改为成功。
## 七、管理 Web/H5 的路由和权限约定
1. 登录后以 `menus[].url` 建立可访问路由白名单。
2. 路由守卫同时检查登录状态和菜单权限。
3. 按钮组件检查 `buttons`,但按钮隐藏只负责体验,不是安全门禁。
4. Web/H5 使用同一权限码,不创建 `web_xxx``h5_xxx` 两套业务权限码;终端差异使用权限记录的 `platform` 字段。
5. 不给当前终端返回的菜单不得通过手输 URL 进入。
6. 页面加载收到 403 后应移除当前不可用操作并提示重新登录获取最新权限。
## 八、联调验收矩阵
每个管理接口至少使用以下账号验证:
| 用例 | 超管 | 有权限平台 | 无权限平台 | 有权限代理 | 越权代理 | 企业账号 |
| --- | --- | --- | --- | --- | --- | --- |
| 菜单是否返回 | 验证 | 验证 | 不返回 | 验证 | 验证 | 验证 |
| 页面能否进入 | 验证 | 验证 | 禁止 | 验证 | 视权限 | 视权限 |
| API 能否调用 | 验证 | 验证 | 403 | 验证 | 403/空范围 | 仅目标能力 |
| 列表数据范围 | 全部 | 全部 | 无调用 | 本店及下级 | 不含平级/上级 | 仅本企业 |
| 详情越权 | 可见 | 可见 | 403 | 非范围资源 403 | 403 | 非本企业 403 |
| 写操作越权 | 可操作 | 按权限 | 403 | 仅允许范围 | 403 | 仅明确开放能力 |
C 端接口至少覆盖本人绑定资产、本人未绑定资产、其他客户订单、Token 被新登录替换、资产解绑、店铺禁止新 C 端登录。
## 九、当前后端安全缺口
以下问题是上线门禁,不应交给前端规避:
1. `/api/admin` 当前统一挂载认证中间件,但没有发现逐路由普遍挂载 `RequirePermission`
2. 当前 `menus/buttons` 主要用于前端显示,不能证明每个接口已做相同权限校验。
3. 部分接口明确说明按钮权限不在后端校验。
4. 企业账号没有店铺下级范围,部分使用宽松店铺过滤的查询可能需要专项验证。
5. 企业、企业授权、卡和设备列表需要使用真实企业 Token 做越权测试。
前端可以按权限正确隐藏界面,但生产上线前必须由后端补齐或验证:
- 功能权限门禁。
- 资源级权限校验。
- 企业数据范围。
- 写操作状态机和幂等。
- 敏感配置和高风险动作的身份限制。
## 十、联调权威来源
1. 路由和 DTO`internal/routes``internal/model/dto`
2. 机器可读契约:`docs/admin-openapi.yaml`
3. 业务和页面组合:`docs/前端建设`
4. 七月迭代新增字段:`docs/7月迭代/七月迭代实现与接口对接说明.md`
发现接口与 OpenAPI 不一致时,先修正文档生成器和 DTO不在前端编写长期兼容分支掩盖契约问题。

View File

@@ -0,0 +1,186 @@
# 管理 Web 与管理 H5 页面接口矩阵
> 本文描述页面需要承载的业务信息和接口组合。完整请求、响应字段、校验规则和枚举以 [`docs/admin-openapi.yaml`](../admin-openapi.yaml) 为准。
## 一、页面生成原则
管理 Web 和管理 H5 都允许超级管理员、平台账号、代理账号、企业账号登录,但不按账号类型写死整套路由。
登录后按以下顺序生成可用界面:
1. 使用 `user.user_type` 确定组织身份和数据范围提示。
2. 使用后端返回的 `menus` 生成路由和导航。
3. 使用 `buttons` 控制创建、编辑、删除、审核等操作入口。
4. 服务端返回 403 时仍必须阻止操作,不能因为前端存在按钮就认为有权限。
5. Web 登录传 `device=web`,管理 H5 登录传 `device=h5`
管理 H5 不建议机械复制全部 Web 表格。首期应覆盖查询、现场操作、审批和通知;复杂配置、批量导入和大报表优先保留在 Web。
## 二、公共页面
| 页面 | Web | 管理 H5 | 核心字段 | 主要操作 | 接口 |
| --- | --- | --- | --- | --- | --- |
| 登录 | 必须 | 必须 | 用户名/手机号、密码、终端 | 登录、记住账号 | `POST /api/auth/login` |
| 当前账号 | 必须 | 必须 | 用户名、手机号、用户类型、店铺、企业 | 查看当前身份 | `GET /api/auth/me` |
| 修改密码 | 必须 | 必须 | 旧密码、新密码、确认密码 | 修改并重新登录 | `PUT /api/auth/password` |
| 通知中心 | 必须 | 必须 | 类型、标题、内容、风险、关联资源、时间、已读状态 | 查看目标、单条已读、全部已读 | `/api/admin/notifications/*` |
| 退出登录 | 必须 | 必须 | 无 | 注销 Token | `POST /api/auth/logout` |
登录响应需持久化:`access_token``refresh_token``expires_in``user``menus``buttons``permissions`。权限变更后菜单不会自动刷新,应重新登录或提供“刷新会话”动作。
## 三、工作台建议
当前后端没有统一 Dashboard 接口。首期不要为了首页一次性改造后端,可使用现有轻量接口拼装:
| 卡片 | 数据来源 | 适用说明 |
| --- | --- | --- |
| 未读通知 | `GET /api/admin/notifications/unread-summary` | 所有具备通知菜单的账号 |
| 代理资金概况 | `GET /api/admin/shops/fund-summary` | 具备代理资金菜单且非企业账号 |
| 临期资产 | `GET /api/admin/expiring-assets` | 具备资产/套餐运营权限的账号 |
| 轮询状态 | `GET /api/admin/polling-stats` | 运维角色,优先 Web |
| 待处理退款 | `GET /api/admin/refunds` | 使用状态筛选,具体权限由菜单和服务端决定 |
| 待处理换货 | `GET /api/admin/exchanges` | 使用状态筛选 |
页面加载时各卡片独立失败、独立重试,不能因一个无权限卡片导致整个工作台失败。
## 四、组织与权限
### 4.1 账号管理
| 页面 | 主要查询字段 | 列表/详情字段 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 账号列表 | 用户名、手机号、用户类型、状态、店铺、企业、分页 | 用户名、手机号、用户类型名称、所属店铺/企业、状态名称、企微绑定状态、创建时间 | 新建、编辑、启停、重置密码、删除、分配角色、绑定企微 | `/api/admin/accounts*` | 查询和启停可做;复杂角色分配优先 Web |
表单关键规则:
- 平台账号不关联店铺或企业。
- 代理账号关联 `shop_id`
- 企业账号关联 `enterprise_id`
- 超级管理员不分配角色;平台账号可多角色;代理和企业账号最多一个客户角色。
### 4.2 角色与权限
| 页面 | 核心字段 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- |
| 角色列表/编辑 | 角色名、角色类型、状态、默认信用额度 | 新建、编辑、启停、删除 | `/api/admin/roles*` | 只读或不做 |
| 角色权限配置 | 权限树、终端 `web/h5/all`、菜单/按钮类型 | 分配、单项移除、批量移除 | `/api/admin/roles/:id/permissions*` | 不做 |
| 权限管理 | 权限名、权限码、菜单/按钮类型、适用终端、父级、路由、排序、状态 | 新建、编辑、删除 | `/api/admin/permissions*` | 不做 |
权限记录的 `available_for_role_types` 区分平台角色和客户角色;代理、企业均使用客户角色,不应按“代理端菜单”硬编码。
### 4.3 店铺
| 页面 | 主要查询字段 | 列表/详情字段 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 店铺列表 | 关键词、店铺编号、联系人手机号、状态、层级、父店铺、分页 | 店铺名、编号、层级、上级、联系人、地址、业务员、状态、C端登录限制 | 创建、编辑、删除、查看详情 | `GET|POST /api/admin/shops``GET|PUT|DELETE /api/admin/shops/:id` | 列表、详情可做;复杂建档优先 Web |
| 店铺级联选择 | 关键词、父节点 | 店铺 ID、名称、层级、子节点 | 选择目标店铺 | `GET /api/admin/shops/cascade` | 必须复用 |
| 店铺默认角色 | 店铺 ID | 已分配客户角色 | 分配、移除 | `/api/admin/shops/:shop_id/roles*` | 不做或只读 |
| 店铺授信 | 店铺 ID | 现金余额、信用额度、总可用、欠款 | 调整信用额度 | `PUT /api/admin/shops/:id/credit-limit` | 可做但必须二次确认 |
企业账号不得进入店铺管理。代理账号只允许管理本店及下级店铺,能否创建和修改仍取决于菜单、按钮和服务端校验。
## 五、企业与授权
| 页面 | 主要查询字段 | 列表/详情字段 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 企业列表 | 名称、编号、联系人、状态、归属店铺、分页 | 企业名、编号、归属店铺、联系人、地址、状态、企业账号 | 创建、编辑、启停、重置企业密码 | `/api/admin/enterprises*` | 查询和详情可做 |
| 企业卡授权 | 企业 ID、卡关键词、状态、分页 | ICCID/虚拟号、运营商、归属店铺、授权状态、授权时间 | 分配、回收 | `/api/admin/enterprises/:id/allocate-cards``recall-cards``cards` | 现场授权可做 |
| 企业设备授权 | 企业 ID、设备关键词、状态、分页 | 虚拟号、IMEI、SN、型号、归属店铺、授权状态、绑定卡 | 分配、回收 | `/api/admin/enterprises/:id/allocate-devices``recall-devices``devices` | 现场授权可做 |
| 授权记录 | 企业、资产类型、资产标识、授权状态、时间、分页 | 企业、资产摘要、授权人、授权时间、回收人、回收时间、备注 | 详情、修改备注 | `/api/admin/authorizations*` | 列表和详情可做 |
企业授权不改变卡或设备的店铺归属。设备授权时,后端会同步处理其绑定卡;前端只提交设备选择结果,不自行拆成多次卡授权。
## 六、资产
### 6.1 统一资产工作台
| 页面 | 输入/筛选 | 需要展示 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 资产快速查询 | ICCID、虚拟号、MSISDN、IMEI、SN | 资产类型、主标识、归属店铺、企业授权、业务状态、网络状态、实名、当前套餐、余额、换货链 | 进入详情 | `GET /api/admin/assets/resolve/:identifier` | 必须 |
| 资产详情 | 资产标识 | 实时状态、套餐、当前套餐、钱包、订单、操作日志 | 刷新、停机、复机、停用、修改实名/轮询、调整套餐用量/到期时间 | `/api/admin/assets/:identifier/*` | 必须,但高风险动作二次确认 |
| 分配记录 | 单号、资产类型、来源、目标、状态、时间、分页 | 分配单号、来源/目标、总数、成功数、失败数、操作人 | 查看详情 | `/api/admin/asset-allocation-records*` | 查询可做 |
| 临期资产 | 资产类型、关键词、店铺、套餐、剩余天数、日期、分页 | 资产、套餐、预计最终到期、剩余天数、临期级别、优先标记 | 跳转资产、手动扫描 | `/api/admin/expiring-assets*` | 列表可做,扫描优先 Web |
### 6.2 IoT 卡
| 页面 | 主要查询字段 | 需要展示 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 独立卡列表 | ICCID/虚拟号/MSISDN、运营商、店铺、状态、实名、系列、分页 | 标识、运营商、店铺、流量、网络/实名/业务状态、系列、轮询状态 | 分配、回收、批量实名策略、批量系列绑定、实名链接、固定档位限速 | `/api/admin/iot-cards/*` | 查询、分配、实名链接可做;导入和批量配置优先 Web |
| 卡导入任务 | 任务号、状态、分页 | 文件、总数、成功/失败数、状态、失败原因、时间 | 上传、创建任务、查看详情 | `/api/admin/iot-cards/import*` | 不做 |
固定档位限速只适用于 IoT 卡,设备页面不得出现限速按钮。
### 6.3 设备
| 页面 | 主要查询字段 | 需要展示 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 设备列表 | 虚拟号、IMEI、SN、店铺、状态、实名、在线状态、系列、分页 | 设备标识、型号、店铺、在线状态、业务状态、实名策略、系列、卡槽摘要 | 分配、回收、绑定/解绑卡、实名策略、系列绑定、删除 | `/api/admin/devices*` | 查询、分配、卡槽操作可做 |
| 设备控制 | 设备标识 | 网关卡槽、WiFi、切卡模式、在线状态、最后同步 | WiFi、切卡、切换模式、重启、恢复出厂 | `/api/admin/devices/by-identifier/:identifier/*` | 必须,危险操作二次确认 |
| 设备导入/批量分配任务 | 任务类型、状态、分页 | 文件、操作类型、目标、总数、成功/失败数、错误明细 | 上传、导入、批量分配、查看任务 | `/api/admin/devices/import*` | 不做 |
## 七、运营商、套餐与分销
| 页面 | 主要字段 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- |
| 运营商 | 名称、类型、接口配置、状态 | CRUD、启停 | `/api/admin/carriers*` | 不做 |
| 套餐系列 | 名称、编码、佣金触发规则、状态 | CRUD、启停 | `/api/admin/package-series*` | 只读 |
| 套餐 | 系列、名称、类型、周期、流量、成本价、零售价、上下架、有效期基准 | CRUD、启停、上下架、调价 | `/api/admin/packages*` | 查询可做 |
| 套餐用量明细 | 套餐使用 ID、日期范围 | 每日使用量、剩余量 | 查询 | `GET /api/admin/package-usage/:id/daily-records` | 可做 |
| 系列授权 | 目标店铺、系列、允许套餐、状态 | 创建、编辑、删除、批量管理套餐 | `/api/admin/shop-series-grants*` | 查询可做,配置优先 Web |
| 套餐批量分配 | 目标店铺、套餐列表、成本价、零售价、上下架、有效期覆盖 | 批量分配 | `POST /api/admin/shop-package-batch-allocations` | 不做 |
| 套餐批量定价 | 目标店铺、套餐和价格 | 批量调价 | `POST /api/admin/shop-package-batch-pricing` | 不做 |
## 八、订单与售后
| 页面 | 主要查询字段 | 需要展示 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 订单列表/详情 | 订单号、资产、店铺、状态、支付方式、购买角色、时间、分页 | 订单号、资产、套餐、买卖方、金额、实付、支付/订单状态、来源、审批/退款摘要 | 购买校验、创建、取消 | `/api/admin/orders*` | 列表、详情、现场代购可做 |
| 退款 | 退款号、订单号、资产、店铺、状态、审批状态、提交人、时间、分页 | 申请金额、实付、原因、凭证、审批渠道/状态、套餐权益结果 | 创建、通过、驳回、退回、重新提交 | `/api/admin/refunds*` | 查询、提交、审批可做 |
| 换货 | 旧资产、新资产、店铺、状态、提交人、时间、分页 | 新旧资产、收货信息、物流、状态时间线、迁移结果 | 创建、发货、完成、取消、续期 | `/api/admin/exchanges*` | 查询、发货、完成可做 |
| 批量购买套餐 | 文件、套餐、支付方式、凭证 | 任务状态、逐行资产、订单结果、失败原因 | 上传、创建任务、查询详情 | `/api/admin/asset-package-batch-orders*` | 不做 |
| 套餐失效任务 | 订单、任务状态、时间、分页 | 任务状态、处理结果和失败原因 | 创建、查询 | `/api/admin/order-package-invalidate-tasks*` | 不做 |
企微审批启用后,前端只读展示 `approval_provider``approval_status``approval_status_name`;存在企微审批实例时,不再显示旧人工通过、驳回或线下确认按钮。
## 九、代理资金业务
“代理资金”是管理系统中的业务模块,不是独立前端。账号是否可见由权限控制,企业账号应禁止访问。
| 页面 | 主要查询字段 | 需要展示 | 操作 | 接口 | H5建议 |
| --- | --- | --- | --- | --- | --- |
| 店铺资金概况 | 店铺、欠款、状态、分页 | 现金余额、冻结金额、信用额度、总可用、欠款、佣金 | 查看店铺明细、调额 | `GET /api/admin/shops/fund-summary` | 必须 |
| 主钱包流水 | 店铺、类型、时间、分页 | 变动金额、前后余额、关联业务、资产、操作人 | 查看关联业务 | `GET /api/admin/shops/:shop_id/main-wallet/transactions` | 必须 |
| 佣金明细/统计 | 店铺、类型、状态、时间、分页 | 订单、来源、佣金类型、金额、状态、每日趋势 | 查看、修正待审记录 | `/api/admin/shops/:shop_id/commission-*` | 必须 |
| 提现申请 | 店铺、状态、时间、分页 | 申请金额、手续费、实付、收款信息、冻结状态、审批结果 | 代理提交、平台审批/驳回 | `/shops/:shop_id/withdrawal-requests``/commission/withdrawal-requests*` | 必须 |
| 提现配置 | 最低金额、每日次数、手续费率、生效状态 | 新建配置、查看当前和历史 | `/api/admin/commission/withdrawal-settings*` | 不做 |
| 代理充值 | 店铺、充值号、方式、状态、审批状态、时间、分页 | 金额、支付方式、支付状态、凭证、提交人、审批状态 | 在线充值、线下代充值、查询支付、线下确认、驳回 | `/api/admin/agent-recharges*` | 必须 |
在线充值创建后使用 `GET /agent-recharges/:id/payment-status` 轮询本地支付和到账状态。
## 十、系统配置、集成与运维
| 页面 | 主要内容 | 接口 | H5建议 |
| --- | --- | --- | --- |
| 微信支付配置 | 配置列表、生效配置、启停 | `/api/admin/wechat-configs*` | 不做 |
| 系统配置 | 注册状态、Key、值、说明 | `/api/admin/system-configs*` | 不做 |
| 企业微信审批配置 | 应用、连接测试、成员同步、默认发起人、模板解析、场景映射 | `/api/admin/wecom*` | 不做 |
| 超管操作密码 | 是否已设置、重新设置 | `/api/admin/super-admin/operation-password*` | 可做但仅超管 |
| 导出任务 | 场景、筛选快照、状态、文件、失败原因 | `/api/admin/export-tasks*` | 查询和下载可做 |
| 对象存储 | 上传用途、文件名、类型、大小;批量下载对象 Key | `/api/admin/storage*` | 上传凭证可做 |
| 轮询配置/监控 | 配置、并发、队列、任务、初始化进度、手动触发 | `/api/admin/polling-*` | 监控可做,配置优先 Web |
| 告警与清理 | 告警规则/历史、清理配置/预览/进度/日志 | `/api/admin/polling-alert-*``/api/admin/data-cleanup*` | 告警查看可做,配置不做 |
## 十一、管理 H5 首期建议
管理 H5 首期建议只实现以下任务型页面:
1. 登录、当前账号、修改密码、通知。
2. 工作台待办。
3. 统一资产搜索和详情。
4. 卡/设备列表、状态查看和必要现场操作。
5. 订单、退款、换货列表和详情。
6. 代理资金概况、充值、佣金和提现。
7. 企业卡/设备授权。
8. 导出文件下载和异步任务结果查看。
账号、角色、权限、运营商、套餐复杂配置、企微配置、轮询配置、数据清理、批量导入优先放在 Web。若后续业务确认移动端必须配置再按权限增加不提前复制整套页面。

View File

@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-07-29

View File

@@ -0,0 +1,362 @@
## 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 IDevent 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 和单调 attemptHTTP→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 均有成功 manifestAudit 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. 不可变性、性能与验证策略
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。
验证按风险分层Writer 单元测试覆盖 Registry、资源约束、标准化、凭据删除、哈希和截断真实 PostgreSQL 集成测试覆盖同事务回滚、失败短事务、幂等和索引HTTP 集成测试覆盖统一响应、平台身份、代理/企业归属、字段隔离、入参来源和所有查询视角;跨链路场景覆盖换货、多卡设备、支付回调、退款资金、批量资产和系统任务。实施结束前执行格式化、静态覆盖门禁、目标测试、构建和 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 只保留在对象存储,审计接口不再在线查询;细粒度平台权限、用户审计导出、对象存储历史查询/恢复、风险处置和自动恢复操作均不属于第一阶段。数据库月分区仅在有界批量清理无法满足指标时另行评审。

View File

@@ -0,0 +1,43 @@
## Why
功能 ID`feature-504-multi-view-audit-center`
现有账号、资产 operation log、Access Log、Integration Log 与资金/订单等业务事实彼此割裂,无法稳定回答“谁在何时从哪个入口对哪些资源做了什么、为何产生资金或状态变化、外部系统经历了哪些尝试”。现在需要建立统一、不可变且可关联多资源的内部 Audit Event并在不混淆四类事实边界的前提下提供操作者、资源、请求、业务链路、资金、风险和外部集成等多视角调查接口。由于 Audit Event 和 Integration Log 每日数据量可能很大,还需要把两类数据库日志按日压缩归档到对象存储,并在归档校验通过后按月删除 PostgreSQL 上月数据。
## What Changes
- 新建统一 `Audit Event + Event Resource` 写入能力:所有非查询业务操作,以及已识别主要业务资源后的成功、失败、拒绝、部分成功、系统自动状态变化和外部回调引发的内部变化,均按明确事务策略记录;普通查询和参数解析前失败不产生 Audit EventAction Registry 明确登记的敏感读取除外。
- 建立动作、资源类型、资源角色、结果、风险、来源和外部可见性注册契约;每个事件至少关联一个主要资源,多资源操作为各资源保存事件发生时的业务标识快照及各自的前后变化。
- 建立平台多视角审计调查接口,覆盖全局事件、操作者、资源时间线、请求链路、业务关联链路、资金审计和风险事件;平台账号第一阶段均可读取完整业务审计数据,不实施细粒度权限码或平台数据范围过滤。
- 冻结前端审计导航契约:逐项列明业务列表/详情接口、`response.data` 字段路径、平台/代理/企业目标接口和参数映射;平台调查节点统一返回可跳转引用,缺少稳定标识时隐藏入口而不猜测。
- 为现有 Integration Log 建立只读调查中心,提供组合筛选、详情、尝试序列、资源外部交互轨迹和跨 Audit/Integration/Outbox/任务/Domain Ledger 的关联时间线;不提供重试、补偿、绑定或恢复写操作。
- 为代理和企业提供独立的安全资源活动投影只允许查询自身数据范围内的资源不暴露平台内部操作者、原因、备注、Audit Event ID 或外部交互细节,仅展示允许其感知的业务结论。
- 业务敏感字段对平台完整展示密码、验证码、Token、Secret、私钥、回调凭据、Authorization、Cookie、签名 URL 和支付密钥等系统安全凭据在写入前删除,任何接口均不返回。
- 批量操作写一条批次根事件并为每个实际变化的资源写子事件跨请求、Outbox、Asynq、外部回调和后续业务步骤通过 `request_id``correlation_id``parent_event_id` 串联。
- 新增内部日志留存任务:按 `Asia/Shanghai` 自然日将 Audit Event/Event Resource 和 Integration Log 生成 `JSON Lines + gzip` 对象及 manifest每月初仅在上月每日分片全部存在、数量和 SHA-256 校验一致后,受控删除 PostgreSQL 上月 Audit/Integration 数据。对象存储中的每日/月度备份长期保留且不被本任务删除数据库删除后的历史不再由审计接口在线查询。Access Log 继续由应用服务器上的 Lumberjack 轮转和保留,不上传对象存储。
- **BREAKING**:统一 Audit Event 切换完成后,旧账号和资产 operation log 停止新增;旧表原样保留,不回填、不转换、不接入新审计中心,既有历史查询保持独立。新审计中心只展示切换后产生的 Audit Event。手动轮询日志继续承担任务运行事实不被误当作统一业务审计表。
- 第一阶段不提供面向用户的审计导出、对象存储历史查询/恢复、敏感二次查看、风险处置、恢复操作、任意关系图或 Audit Event 修改/删除能力;内部自动归档不属于审计导出 API。
## Capabilities
### New Capabilities
- `audit-event-recording`: 定义不可变 Audit Event、多资源关联、操作者与入口快照、资源级前后变化、批次父子事件、事务可靠性和安全凭据删除规则。
- `audit-investigation-queries`: 定义平台全局、操作者、资源、请求、业务链路、资金和风险等多视角只读调查接口及通用资源时间线。
- `integration-log-investigation`: 定义 Integration Log 的组合筛选、详情、尝试序列、资源轨迹、异常总览和跨事实关联查询。
- `scoped-resource-activity`: 定义代理和企业按自身数据范围查询资源活动、安全结论投影和平台内部信息不可见规则。
- `audit-governance-cutover`: 定义全仓领域/资源/写入口盘点、Action Registry 覆盖、旧 Writer 停写、旧历史隔离、发布切换、监控与前向修复门禁。
- `audit-log-retention`: 定义 Audit Event 和 Integration Log 的每日压缩归档、manifest 校验、月初数据库物理删除、失败闭环及在线查询边界。
### Modified Capabilities
- `account-operation-audit`: 将账号操作从裸 goroutine 写旧表改为统一 Audit Event 同事务/失败短事务记录,并由统一多视角查询替代旧表新增。
- `asset-audit-readable-content`: 将资产可读字段从旧日志 JSON 补丁升级为统一资源快照与资源角色契约,使卡、设备、绑定、换货旧新资产和店铺可在多个资源时间线中交叉查询。
## Impact
- 新增 PostgreSQL 审计事件、事件资源及归档运行记录表、GORM Model、Repository、Action/Resource Registry、写入 Port/Adapter、审计上下文和 Query 投影;不建立外键或 GORM 关联标签,不新增依赖。
- 触及后台、代理、企业、个人客户、Open API、Application、旧 Service、Worker、定时任务和回调的非查询写入口复杂写继续收口 Domain/Application简单写使用 Application 事务脚本,读取统一进入 `internal/query`
- 新增平台审计、Integration Log 调查和代理/企业资源活动 Handler、DTO 与 RouteSpec并同步 `internal/bootstrap``cmd/api/docs.go``cmd/gendocs/main.go` 和中文 API/功能文档。
- 复用 Access Log、`pkg/sanitizer`、现有 Integration Log、现有 S3 兼容对象存储、公共 Asynq Scheduler、Domain Ledger、Outbox、数据范围中间件和统一错误响应Access Log 继续面向开发排障,不进入数据库审计查询。
- 实施前必须以当前代码重新盘点全部领域、资源和写入口,不能把七月旧 490 项清单或旧 22 张 Ticket 直接视为现行契约;规划与验证覆盖真实 PostgreSQL 事务、分页索引、无 N+1、P95/P99、身份隔离、凭据不落库和旧写归零。

View File

@@ -0,0 +1,120 @@
## MODIFIED Requirements
### Requirement: 记录所有账号管理操作
系统 SHALL 通过统一 Audit Event Writer 记录所有账号管理操作,包括创建、更新、删除、企业微信绑定、角色分配和角色移除;成功事件 MUST 与对应业务事实采用该用例约定的可靠事务策略,失败或拒绝事件 MUST 在业务回滚后使用独立短事务写入,不得继续向 `tb_account_operation_log` 新增记录。
#### Scenario: 创建账号时记录统一审计事件
- **WHEN** 用户创建账号成功
- **THEN** 系统写入 `account.created` Audit Event
- **AND** 事件包含操作人、目标账号资源、账号业务标识快照和创建后的关键业务字段
#### Scenario: 更新账号时记录资源级变更
- **WHEN** 用户更新账号信息(用户名、手机号、状态等)
- **THEN** 系统写入账号资源的 `before_data``after_data`
- **AND** 仅包含本次操作相关的业务字段及其变更
#### Scenario: 删除账号时保留目标快照
- **WHEN** 用户软删除账号成功
- **THEN** 系统记录删除事件及账号删除前的关键业务字段
- **AND** 账号后续无法从业务表查询时仍可通过资源标识快照识别目标账号
#### Scenario: 分配角色时记录账号与角色资源
- **WHEN** 用户为账号分配角色
- **THEN** 系统记录账号为主要资源、实际分配角色为关联资源的 Audit Event
- **AND** 事件包含实际生效的角色 ID 与角色名称快照
#### Scenario: 移除角色时记录账号与角色资源
- **WHEN** 用户移除账号的角色
- **THEN** 系统记录账号为主要资源、被移除角色为关联资源的 Audit Event
- **AND** 事件包含被移除角色 ID 与角色名称快照
### Requirement: 审计日志包含完整的操作上下文
系统 SHALL 在统一 Audit Event 中记录操作者快照、来源、结果、目标资源、资源级变更及请求关联上下文,并 MUST 支持一次操作关联多个资源。
#### Scenario: 记录操作人信息
- **WHEN** 记录账号管理 Audit Event
- **THEN** 事件包含稳定的操作者类型、操作者 ID、操作者名称快照和来源
#### Scenario: 记录目标账号信息
- **WHEN** 记录账号管理 Audit Event
- **THEN** 事件至少关联一个账号主要资源
- **AND** 资源快照包含账号 ID、用户名和账号类型
#### Scenario: 记录变更数据
- **WHEN** 账号管理操作改变业务字段
- **THEN** 账号资源的 `before_data``after_data` 使用 JSONB 保存本次操作相关字段
- **AND** 密码、验证码、Token、Secret、私钥、Authorization、Cookie 及其他系统安全凭据在写入前被删除
#### Scenario: 记录请求上下文
- **WHEN** 账号管理操作来自 HTTP 请求
- **THEN** 事件包含 `request_id`、IP、User-Agent、请求方法和请求路径
- **AND** 可通过 `request_id` 关联 Access Log
#### Scenario: 记录执行结果
- **WHEN** 已识别操作人或目标账号的操作成功、失败或被拒绝
- **THEN** 事件记录对应的 `success``failed``denied` 结果及稳定错误码
### Requirement: 操作描述使用中文
系统 SHALL 为账号审计动作维护稳定英文动作编码和中文显示名称,查询接口 MUST 返回动作编码与中文显示名称,不得把可变中文描述作为动作身份或资源关联依据。
#### Scenario: 创建操作描述
- **WHEN** 查询账号创建事件
- **THEN** 响应返回稳定动作编码 `account.created` 和中文显示名称“创建账号”
#### Scenario: 更新操作描述
- **WHEN** 查询账号更新事件
- **THEN** 响应返回稳定动作编码 `account.updated` 和中文显示名称“更新账号”
#### Scenario: 删除操作描述
- **WHEN** 查询账号删除事件
- **THEN** 响应返回稳定动作编码 `account.deleted` 和中文显示名称“删除账号”
#### Scenario: 分配角色操作描述
- **WHEN** 查询账号角色分配事件
- **THEN** 响应返回稳定动作编码和中文显示名称“分配账号角色”
### Requirement: 支持按多维度查询审计日志
系统 SHALL 通过统一审计 Query 支持按操作者、账号资源、动作、结果、时间、`request_id``correlation_id` 组合筛选,并 SHALL 使用适配分页倒序查询的索引满足数据库查询低于 50ms 的性能目标。
#### Scenario: 按操作人查询日志
- **WHEN** 平台用户查询特定操作者的账号相关事件
- **THEN** 系统按操作者及时间索引返回倒序分页结果
#### Scenario: 按目标账号查询日志
- **WHEN** 平台用户查询特定账号的资源时间线
- **THEN** 系统返回该账号作为主要、受影响或引用资源关联的事件
#### Scenario: 按时间范围查询日志
- **WHEN** 平台用户查询最近 7 天的账号相关事件
- **THEN** 系统使用时间及事件 ID 进行稳定倒序分页
### Requirement: 关联访问日志追溯完整请求链路
系统 SHALL 通过 `request_id` 关联 Audit Event 与 Access Log并 SHALL 通过 `correlation_id``parent_event_id` 关联同一业务操作的后续任务、回调和子事件。
#### Scenario: 通过request_id关联日志
- **WHEN** Audit Event 记录 `request_id="req-12345"`
- **THEN** 平台调查人员可以使用同一 `request_id` 定位对应的 HTTP Access Log
#### Scenario: 追溯完整请求链路
- **WHEN** 平台调查人员查询某个账号管理操作
- **THEN** 系统返回该请求关联的 Audit Event 及可用的后续关联事件
- **AND** Access Log 继续负责完整 HTTP 请求响应排障,不由 Audit Event 复制完整请求正文
## REMOVED Requirements
### Requirement: 异步写入不阻塞业务流程
**Reason**: 旧实现通过裸 Goroutine、部分调用方双重 Goroutine 写入账号操作日志,无法保证事务一致性、进程退出前落库、顺序和失败闭环,不满足资金与后台追责要求。
**Migration**: 所有新账号及借用账号日志的写入口迁移到统一 Audit Event Writer高风险成功操作与业务事实同一 GORM 事务,其他成功操作遵循用例明确的事务策略,失败和拒绝使用独立短事务。旧表原样保留,不回填、不转换且不进入新审计中心。
#### Scenario: 异步写入审计日志
- **WHEN** AccountService.Create 创建账号成功
- **THEN** 主流程立即返回,审计日志在独立 Goroutine 中异步写入
#### Scenario: 写入失败只记录错误日志
- **WHEN** 审计日志写入数据库失败
- **THEN** 记录 Error 级别日志,包含完整审计信息,但不影响业务操作结果
#### Scenario: 业务响应时间不受影响
- **WHEN** 执行账号创建操作
- **THEN** API 响应时间不应因审计日志写入而增加(< 1ms

View File

@@ -0,0 +1,114 @@
## MODIFIED Requirements
### Requirement: 资产操作审计日志必须补充业务可读字段
系统 SHALL 将卡、设备、绑定关系、换货单和相关店铺作为独立 Audit Event Resource 记录,并 MUST 为每个资源保存事件发生时的业务标识快照及该资源自己的 `before_data``after_data`,确保业务侧无需查询当前业务表即可理解历史操作。
#### Scenario: 卡相关事件保存完整关键标识
- **WHEN** 系统记录针对单卡或多卡的分配、回收、删除、实名、停复机、绑定或换货事件
- **THEN** 每张卡资源快照除内部卡 ID 外还包含当时可用的 ICCID、虚拟号及其他已登记关键业务标识
#### Scenario: 设备相关事件保存完整关键标识
- **WHEN** 系统记录设备绑定、解绑、切卡、远程控制、删除或换货事件
- **THEN** 每台设备资源快照除内部设备 ID 外还包含当时可用的虚拟号、IMEI、SN 及其他已登记关键业务标识
#### Scenario: 店铺相关事件保存名称快照
- **WHEN** 系统记录资产分配、资产回收、归属变更或其他涉及店铺的事件
- **THEN** 店铺资源快照在店铺 ID 之外还包含事件发生时的店铺名称
#### Scenario: 设备中的卡作为独立资源
- **WHEN** 一次操作通过设备入口影响设备中的某张卡
- **THEN** 事件分别关联设备资源与卡资源
- **AND** 使用资源角色表明入口设备、受影响卡或引用关系,不得只把卡嵌入设备 JSON
#### Scenario: 换货同时记录旧卡和新卡
- **WHEN** 换货操作将旧卡替换为新卡
- **THEN** 同一事件分别关联 `old_card``new_card` 资源角色
- **AND** 两张卡资源快照分别保存自身当时的虚拟号和 ICCID
### Requirement: 审计日志可读字段必须遵循兼容新增原则
系统 SHALL 在统一 Audit Event 切换后保留旧 `tb_asset_operation_log` 原始数据,但 MUST NOT 回填、转换或投影到新审计中心;新事件不得为兼容旧单资产结构而丢失多资源关系。
#### Scenario: 旧历史保持独立
- **WHEN** 平台需要查询切换前的资产操作历史
- **THEN** 系统仅通过既有旧资产日志入口读取原表内容
- **AND** `/api/admin/audit/*`、通用资源时间线和代理/企业资源活动接口不返回旧表记录
#### Scenario: 新增结构不修改旧历史
- **WHEN** 统一 Audit Event 上线
- **THEN** 系统不在线回填、重写或删除旧资产日志
- **AND** 新操作只写统一 Audit Event不再向旧资产日志表新增记录
#### Scenario: 不为旧记录补造新结构
- **WHEN** 旧资产日志仅保存单资产字段或不完整 JSON
- **THEN** 系统不为其创建 Audit Event、Event Resource、结果、风险或链路字段
- **AND** 不得从中文描述猜测多资源关系
### Requirement: 同类审计场景必须使用统一的可读字段命名
系统 SHALL 通过 Resource Registry 为同类资产资源定义稳定的类型、角色与标识快照字段,平台查询和安全资源活动投影 MUST 使用一致语义。
#### Scenario: 单卡与批量卡标识命名一致
- **WHEN** 系统分别记录单卡和批量卡事件
- **THEN** 每张卡均使用同一资源类型及 `iccid``virtual_no` 快照字段
- **AND** 批量场景通过多个资源关联表达,不另造语义不同的聚合字段
#### Scenario: 设备标识命名稳定
- **WHEN** 系统记录多个设备相关事件
- **THEN** 设备资源统一使用 `virtual_no``imei``sn` 等已登记字段名
#### Scenario: 资源角色表达换货与绑定语义
- **WHEN** 事件涉及入口设备、绑定卡、旧卡、新卡或目标店铺
- **THEN** 系统使用注册表中的稳定资源角色编码表达关系
- **AND** 查询接口返回对应中文角色名称
### Requirement: 审计日志补充可读字段不得引入额外破坏性变更
系统 SHALL 仅替换资产审计写入与查询投影,不得借本次变更调整资产、店铺、账号、企业授权或换货的既有业务规则。
#### Scenario: 店铺删除规则保持不变
- **WHEN** 本次变更上线
- **THEN** 店铺删除相关业务规则不因审计切换而发生变化
#### Scenario: 企业账号列表行为保持不变
- **WHEN** 本次变更上线
- **THEN** 账号列表接口的企业账号展示逻辑不因审计切换而发生变化
#### Scenario: 企业资产授权规则保持不变
- **WHEN** 企业查询资源安全活动
- **THEN** 系统复用现有有效卡和设备授权关系判断归属
- **AND** 不改变授权、撤销或资产归属业务事实
## ADDED Requirements
### Requirement: 旧资产操作日志接口必须收缩为平台历史入口
系统 SHALL 在安全资源活动接口可用后,将旧 `GET /api/admin/assets/:identifier/operation-logs` 收缩为仅超级管理员和平台用户可访问的过渡历史入口,并 MUST 停止向代理和企业返回内部资产审计 DTO。
#### Scenario: 平台查询旧资产日志
- **WHEN** 超级管理员或平台用户在过渡期调用旧资产操作日志接口
- **THEN** 系统允许读取对应资产的历史旧日志
- **AND** 不对旧表执行修改或删除
#### Scenario: 代理改用安全活动接口
- **WHEN** 代理调用旧资产操作日志接口
- **THEN** 系统返回无权限错误
- **AND** 代理通过独立安全资源活动接口查询自身范围内的业务结论
#### Scenario: 企业改用安全活动接口
- **WHEN** 企业调用旧资产操作日志接口
- **THEN** 系统返回无权限错误
- **AND** 企业通过独立安全资源活动接口查询当前企业有效授权资产的业务结论
### Requirement: 旧资产日志裸 Goroutine 写入必须停止
系统 MUST 使用统一 Audit Event Writer 替代资产审计服务的裸 Goroutine 写入;高风险成功操作 MUST 与业务事实同一 GORM 事务,失败或拒绝 MUST 在业务回滚后使用独立短事务写入。
#### Scenario: 新资产操作不再写旧表
- **WHEN** 某资产写用例完成统一 Audit Event 接入并切换上线
- **THEN** 该用例不再向 `tb_asset_operation_log` 新增记录
#### Scenario: 统一审计写入失败
- **WHEN** 高风险资产操作的 Audit Event 在业务事务内写入失败
- **THEN** 业务事务回滚并返回统一错误
- **AND** 不得通过后台 Goroutine 补写后假装与业务事实原子一致
#### Scenario: 失败事件独立落库
- **WHEN** 资产业务操作已回滚且需要记录 `failed``denied` 事件
- **THEN** 系统使用独立短事务写入失败或拒绝 Audit Event
- **AND** 二次写入失败时保留原业务错误并记录 critical 日志和指标

View File

@@ -0,0 +1,153 @@
## ADDED Requirements
### Requirement: 统一且不可变的内部审计事件
系统 SHALL 以统一 Audit Event 记录内部业务操作事实,并 SHALL 将 Audit Event 与 Access Log、Integration Log、Domain Ledger、Outbox 的职责保持分离。Audit Event MUST 至少保存稳定动作编码、动作中文名、操作者快照、入口来源、结果、风险等级、发生时间以及可用的 `request_id``correlation_id``parent_event_id`。已创建的 Audit Event 和资源关联在 PostgreSQL 保留期间 MUST 不可修改,业务接口和业务 Repository MUST 不可删除;后续业务修正 MUST 产生新的业务事件并关联原业务链路。唯一删除例外是内部 Retention Worker 在对象存储归档完整性门禁通过后按完整月份物理删除 PostgreSQL 上月 Audit Event 与 Event Resource禁止使用软删除或数据库历史表替代第一阶段 MUST NOT 提供专用审计纠错、删除或恢复写接口。
#### Scenario: 平台账号修改店铺状态
- **WHEN** 已识别的平台账号将某店铺从启用修改为禁用
- **THEN** 系统写入包含操作者账号 ID、名称、账号类型、动作编码、来源、成功结果、店铺资源及状态前后值的不可变 Audit Event
#### Scenario: 后续业务操作修正原业务结果
- **WHEN** 新的受控业务操作修正此前业务结果
- **THEN** 系统为新业务操作写入独立 Audit Event 并通过 correlation 或 parent 关联原链路
- **AND** 不更新或删除原 Audit Event
#### Scenario: 归档成功后删除数据库上月数据
- **WHEN** 上一完整自然月的 Audit Event、Event Resource 和关联归档 manifest 已通过数量、对象存在性与 SHA-256 校验
- **THEN** Retention Worker 可以按留存策略删除 PostgreSQL 中该月 Audit Event 与 Event Resource
- **AND** 对象存储中的该月备份继续长期保留
- **AND** 该清理不构成业务更正、业务删除接口或审计事实丢弃
### Requirement: 审计记录边界
系统 SHALL 记录所有会改变业务事实、权限、配置、安全状态或产生外部副作用的非查询操作,并 SHALL 记录已确定主要业务资源后的 `success``failed``denied``partial``unknown` 结果。系统任务和外部回调引起内部状态变化时 MUST 记录 Audit Event。普通查询、参数解析前失败、只有操作者但无法确定主要业务资源的非法请求 MUST NOT 生成 Audit Event只进入 Access Log 或对应安全日志Action Registry 明确标记的敏感读取除外。仅发生外部交互但未改变内部业务事实时 MUST 只写 Integration Log。
#### Scenario: 业务规则拒绝已识别资源的操作
- **WHEN** 操作者对已识别订单发起取消但订单状态不允许取消
- **THEN** 系统写入 `denied` Audit Event并关联该订单和操作者
#### Scenario: 参数解析前失败
- **WHEN** 请求因 JSON 格式错误而无法识别操作者意图对应的业务资源
- **THEN** 系统不写 Audit Event且由 Access Log 记录请求失败
#### Scenario: 只有操作者但无法定位业务资源
- **WHEN** 已认证操作者发起非法操作但系统无法确定任何主要业务资源 Key 或快照
- **THEN** 系统不写无资源 Audit Event并由 Access Log 或安全日志记录
#### Scenario: Registry 标记的敏感读取
- **WHEN** 平台账号读取被 Action Registry 标记为敏感的审计详情或明文业务凭证
- **THEN** 系统在返回结果前写入关联读取者、目标资源和字段类别的 Audit Event
- **AND** 审计写入失败时不返回敏感结果
#### Scenario: 外部回调改变内部状态
- **WHEN** 支付或实名回调已写 Integration Log 且随后改变订单、支付、充值或卡的内部状态
- **THEN** 系统写入关联该 Integration Log 和内部资源的 Audit Event
### Requirement: 操作者与入口来源快照
每个 Audit Event MUST 保存事件发生时的真实操作者类型、操作者 ID、可读名称、账号类型以及适用的店铺或企业上下文。操作者类型 SHALL 覆盖平台账号、代理账号、企业账号、个人客户、Open API 调用方、外部系统和系统任务。系统自动步骤 MUST 使用 `system``external_system`MUST NOT 伪造为最初发起人;最初发起人 SHALL 通过父事件或关联链路查询获得。
#### Scenario: Asynq 后续任务完成业务状态变化
- **WHEN** 一个由用户操作触发的 Asynq 任务稍后改变套餐或资金状态
- **THEN** 子事件的操作者类型为系统任务,并通过 `parent_event_id``correlation_id` 关联最初用户事件
#### Scenario: 企业微信审批终态回调
- **WHEN** 企业微信回调推动退款或线下充值进入终态
- **THEN** 回调事件记录外部系统为来源,并保留业务申请的真实提交人作为关联资源或链路事实,而非把本地账号伪造为外部审批人
### Requirement: 一等资源关联与资源级变化
每个 Audit Event MUST 至少关联一个 `primary` 资源,并 SHALL 支持任意数量的 `affected``reference` 资源。每个资源关联 MUST 独立保存资源类型、资源 ID、资源角色、事件发生时的稳定业务标识快照以及该资源自身的 `before_data``after_data`。删除、重新绑定、换号或修改当前业务表后,历史资源快照 MUST 保持不变。系统 MUST NOT 把多资源变化压缩为事件级单一前后 JSON。
#### Scenario: 店铺合同或状态变更可追责
- **WHEN** 操作者修改某店铺的合同相关配置或状态
- **THEN** 事件同时包含操作者快照和店铺 ID、店铺编号、店铺名称快照并将变更字段保存于该店铺资源的前后数据
#### Scenario: 已删除资源仍可识别
- **WHEN** 某卡、设备、账号或店铺在事件发生后被删除或更名
- **THEN** 历史事件仍使用事件发生时保存的业务标识快照展示该资源
### Requirement: 当前真实领域的资源注册契约
Action Registry 和 Resource Registry MUST 以当前代码盘点结果覆盖账号与权限、店铺与企业、个人客户、卡与设备、设备卡槽绑定、资产分配与授权、换货、套餐系列与套餐权益、订单与支付、退款与充值、代理和资产钱包、佣金提现、审批、系统及外部连接配置、导入批量、通知、轮询监控、Integration Log、Outbox 和异步任务。每种资源类型 MUST 定义稳定类型编码、中文名称、最小业务标识快照字段和禁止记录字段;未注册资源 MUST 不得静默降级为无语义 JSON。
#### Scenario: 注册 IoT 卡资源
- **WHEN** 动作关联 IoT 卡
- **THEN** 资源快照至少包含卡 ID、ICCID、VirtualNo并按业务需要包含 MSISDN、运营商、所属店铺、套餐系列和资产世代
#### Scenario: 注册设备资源
- **WHEN** 动作关联设备
- **THEN** 资源快照至少包含设备 ID、VirtualNo、IMEI、SN并按业务需要包含设备名称、型号、所属店铺、套餐系列和资产世代
#### Scenario: 注册资金资源
- **WHEN** 动作改变代理钱包或资产钱包
- **THEN** 事件关联钱包、店铺或卡/设备、业务单据和唯一交易流水,并保存金额、变更前余额、变更后余额、币种与业务引用
### Requirement: 设备与卡槽关系可独立追踪
设备、IoT 卡和设备卡槽绑定 SHALL 均作为一等资源。对“某设备的某张卡”执行的操作 MUST 同时关联入口设备、目标卡和绑定关系,并 MUST 保存卡槽位置及是否当前卡。设备当前卡切换 MUST 分别关联旧当前卡、新当前卡及对应绑定关系。
#### Scenario: 操作设备第二卡槽中的卡
- **WHEN** 操作者通过设备入口对第二卡槽中的 IoT 卡执行停复机、实名、限速或其他卡操作
- **THEN** 事件以该卡作为主要或受影响资源,同时保存设备标识、绑定关系、`slot_position=2` 和当时的 `is_current`
#### Scenario: 切换设备当前卡
- **WHEN** 设备从卡 A 切换到卡 B
- **THEN** 同一业务事件关联设备、卡 A、卡 B 及两个卡槽绑定,并分别保存其前后当前卡状态
### Requirement: 换货完整业务边界审计
换货事件 SHALL 关联换货单、旧资产、新资产、所属店铺以及本次操作实际影响的卡槽绑定、个人客户绑定、资产钱包、钱包流水、套餐权益和资产状态。卡换货快照 MUST 同时保存旧卡和新卡各自的 ICCID 与 VirtualNo设备换货 MUST 保存旧设备和新设备各自的 VirtualNo、IMEI、SN并 SHALL 对实际涉及的绑定卡逐张建立资源关联。
#### Scenario: 卡换货并迁移数据
- **WHEN** 卡换货完成且迁移钱包、套餐权益和个人客户绑定
- **THEN** 事件关联换货单、旧卡、新卡、旧新钱包、资金流水、被迁移套餐权益和客户绑定,并在旧新卡快照中分别保存 ICCID 与 VirtualNo
#### Scenario: 设备换货涉及多张绑定卡
- **WHEN** 设备换货影响旧设备或新设备的多张绑定卡
- **THEN** 事件除旧新设备外还逐张关联实际受影响的卡和卡槽绑定,使设备时间线与每张卡时间线均可定位该换货
### Requirement: 资金操作的同事务审计
钱包扣款、入账、退款、预占、释放、提现、佣金、信用额度和其他资金操作的成功 Audit Event MUST 与对应钱包、交易流水、订单、退款、充值或提现业务事实处于同一 GORM 事务。Audit Event 写入失败 MUST 使业务事务回滚。资金事件 MUST 关联唯一 Domain Ledger 事实Audit Event MUST NOT 替代钱包流水、订单或支付记录。
#### Scenario: 钱包扣款成功
- **WHEN** 订单使用代理主钱包完成扣款
- **THEN** 钱包余额、唯一交易流水、订单支付事实、Audit Event 以及需要的 Outbox 在同一事务提交,任一关键写入失败均回滚
#### Scenario: 退款回充成功
- **WHEN** 已批准退款向原支付钱包回充
- **THEN** 退款事件关联退款单、原订单、付款钱包、原扣款流水和退款流水,并保存退款金额及余额前后值
### Requirement: 失败与拒绝使用独立短事务
当业务事务已回滚后,系统 SHALL 使用独立短事务写入 `failed``denied` Audit Event。失败审计二次写入失败 MUST 保留原业务错误,并 MUST 记录 critical 日志和可监控指标;系统 MUST NOT 使用裸 goroutine 执行审计写入。
#### Scenario: 资金业务回滚后的失败审计
- **WHEN** 钱包扣款因余额不足或并发条件失败而回滚
- **THEN** 系统在独立短事务中记录失败或拒绝事件,且不会创建成功资金事实
#### Scenario: 失败审计自身不可用
- **WHEN** 原业务失败且独立短事务也无法写入审计事件
- **THEN** 接口仍返回原业务错误,同时输出包含 request/correlation 标识的 critical 日志和指标
### Requirement: 批量根事件与资源子事件
批量操作 SHALL 写入一条批次根事件,记录操作者、输入条件或任务、总数、成功数、失败数和总体结果;每个实际发生变化的资源 MUST 写入可独立查询的子事件。未处理或未命中的资源 MUST NOT 伪造成功子事件。部分成功时根事件 MUST 使用 `partial`
#### Scenario: 批量分配设备并连带绑定卡
- **WHEN** 批量任务成功分配部分设备且每台设备包含多张绑定卡
- **THEN** 根事件记录批量统计,每台实际变化的设备产生子事件,子事件同时关联该设备实际连带变化的卡、来源店铺和目标店铺
#### Scenario: CSV 批量购包部分成功
- **WHEN** CSV 资产套餐订购中部分行创建订单成功、部分行失败
- **THEN** 根事件结果为 `partial`,成功资产分别生成关联订单、钱包流水和套餐权益的子事件,失败行只保存必要失败事实且不伪造业务成功资源
### Requirement: 跨请求和异步链路关联
系统 SHALL 使用 `request_id` 关联同一 HTTP 请求内的事件,使用 `correlation_id` 关联跨请求、审批、Integration Log、Outbox、Asynq 和 Domain Ledger 的业务链路,使用 `parent_event_id` 表达批次父子或后续步骤因果关系。异步载荷 MUST 传递已有链路标识或建立可回查的稳定业务关联。
#### Scenario: 支付链路跨越外部回调
- **WHEN** 用户创建支付后,支付渠道稍后回调并触发钱包或订单终态
- **THEN** 创建支付、Integration Log、回调处理、订单终态和资金事件可通过 correlation 或稳定业务单号组成同一链路
### Requirement: 安全凭据写入前删除
平台审计数据 SHALL 保留完整业务字段而不做展示脱敏但密码、操作密码、验证码、Access/Refresh Token、Secret、私钥、支付密钥、回调 Token、EncodingAESKey、Authorization、Cookie 和完整签名 URL 等系统安全凭据 MUST 在进入持久化 Writer 前删除。Action Registry 和 Resource Registry MUST 为涉及配置、请求摘要和前后数据的动作声明禁止字段;禁止字段 MUST 不得出现在事件、资源快照、前后数据或 metadata 中。
#### Scenario: 更新支付配置
- **WHEN** 平台账号更新微信、富友或支付宝配置
- **THEN** Audit Event 可以保存配置 ID、名称、渠道、状态和 `credentials_configured` 等业务事实,但不保存密钥、私钥、证书正文或回调凭据
#### Scenario: 修改账号密码
- **WHEN** 账号密码修改成功或失败
- **THEN** 事件记录目标账号、动作和结果,但 before、after 与 metadata 均不包含原密码、新密码或密码散列

View File

@@ -0,0 +1,122 @@
## ADDED Requirements
### Requirement: 以当前代码重新建立审计覆盖基线
实施前系统团队 MUST 重新扫描当前仓库的 HTTP RouteSpec、Handler、Application、旧 Service、Asynq Worker、定时任务、外部回调、GORM Model、Outbox 消费者、Integration Log 接入点和旧审计 Writer并 SHALL 形成可复核的逐入口覆盖清单。清单 MUST 覆盖平台、代理、企业、个人客户、Open API 和系统自动入口,并 MUST 为每项记录代码入口、业务领域、动作、操作者来源、资源、事务边界、结果策略以及 Audit Event、Domain Ledger、Integration Log、Outbox 的使用决定或明确 N/A 理由。
#### Scenario: 发现七月后新增的定时任务
- **WHEN** 当前代码包含在线充值恢复、订单过期、告警、企微审批恢复、数据清理、通知清理、套餐临期提醒或流量落盘等计划任务
- **THEN** 新覆盖清单按当前代码逐项登记,而不是沿用旧清单中的定时任务数量
#### Scenario: 普通查询入口分类
- **WHEN** 扫描发现列表、详情、统计或其他纯读取入口
- **THEN** 清单将普通读取的 Audit Event 标记为 N/A 并写明理由,且不将其伪装为业务动作
- **AND** 对 Action Registry 标记的敏感读取另行登记读取审计、目标资源和失败关闭策略
### Requirement: 历史清单仅作遗漏参考
七月旧 490 项覆盖清单、旧 22 张 Ticket 和历史自动分类 MUST NOT 被直接视为本 Change 的现行实施契约。系统团队 SHALL 将其作为遗漏比对材料,并 MUST 对当前代码重新进行业务语义分类。自动扫描结果 MUST 经过业务和研发复核MUST 排除依赖注入 Setter、纯查询、装配方法和没有业务副作用的技术入口等误报。
#### Scenario: 自动扫描把 Setter 识别为写操作
- **WHEN** 候选清单包含 `SetXxx` 依赖注入方法或类似装配入口
- **THEN** 评审将其标记为非生产业务动作并排除,且不会为其生成 Action Registry 项
#### Scenario: 旧清单缺少新入口
- **WHEN** 当前路由、Worker 或 Scheduler 中存在旧 490 项清单没有的入口
- **THEN** 新清单补充该入口并按当前业务事实分类,旧统计数字不得覆盖当前扫描结果
### Requirement: 领域与资源盘点完整性
覆盖清单和 Resource Registry MUST 至少核验账号权限、店铺企业、个人客户、卡设备与卡槽绑定、资产分配与企业授权、换货、套餐与套餐权益、订单支付退款充值、代理及资产钱包、佣金提现、审批与企微、配置与运营商、导入批量、导出、敏感读取、通知、轮询监控、Integration Log、Outbox 和异步任务。每个领域 MUST 明确其一等资源、稳定标识快照、多资源关系、资金或状态 Domain Ledger 以及明确不迁移的旧代码范围。
#### Scenario: 盘点设备领域
- **WHEN** 团队评审设备写入口
- **THEN** 盘点同时覆盖设备、绑定卡、卡槽关系、分配记录、企业授权、套餐权益、钱包和个人客户绑定,而不是只登记设备主表
#### Scenario: 盘点资金领域
- **WHEN** 团队评审订单、退款、充值、佣金或提现入口
- **THEN** 清单明确钱包、唯一交易流水、业务单据和 Outbox 的权威事实边界Audit Event 不替代 Domain Ledger
### Requirement: 受评审的 Action Registry
每个需要审计的完整业务用例 MUST 对应稳定 Action Registry 条目。条目 MUST 声明动作编码、中文名、领域、风险等级、允许来源、主要及受影响资源、资源角色、必须快照字段、前后数据策略、禁止字段、成功事务策略、失败策略和外部可见性。新增或修改非查询业务入口时 MUST 同步更新 Registry 与覆盖清单;未登记动作 MUST 在验证门禁中失败,不得运行时静默使用任意字符串。
#### Scenario: 新增设备卡槽切换动作
- **WHEN** 新增或迁移设备当前卡切换用例
- **THEN** Registry 明确设备、旧卡、新卡和两个绑定关系的资源角色与快照要求,并由覆盖门禁校验该入口已登记
#### Scenario: 动作决定不记录 Audit Event
- **WHEN** 业务评审确认某入口是普通查询或不产生审计事实
- **THEN** 覆盖清单记录明确且可复核的 N/A 理由,而不是留空或删除该入口
### Requirement: 按完整纵向用例渐进切换
审计接入 SHALL 按可独立验证的纵向业务用例执行 `expand → migrate → contract`MUST NOT 按“先全仓建表、再全仓改 Service、最后统一切换”的水平分层方式迁移。每个迁移单元 MUST 标明其主架构通道为复杂写、简单写、Query、Infrastructure 或 Application + Port/AdapterMUST 收口该用例的完整事务、不变量、资源关联、成功/失败审计和查询可见性,并 MUST 明确本单元不迁移的旧范围。
#### Scenario: 迁移钱包扣款用例
- **WHEN** 钱包扣款纵向切片进入迁移
- **THEN** 同一切片完成 Domain/Application 资金规则、钱包和流水持久化、同事务 Audit Event、必要 Outbox、失败短事务及可查询验证不把审计留给后续水平任务
#### Scenario: 迁移简单配置写入
- **WHEN** 单表配置更新没有复杂状态机或金额不变量
- **THEN** 使用 Application 事务脚本接入 Audit Writer不为审计形式强行创建聚合或多余接口
#### Scenario: 迁移只读调查视角
- **WHEN** 实现平台审计或资源时间线查询
- **THEN** 使用 `Handler → Query → GORM/DTO`,不让查询经过聚合根或修改状态
### Requirement: 单个纵向切片的切换门禁
每个纵向用例只有在以下条件全部满足后 SHALL 停止旧 Writer真实业务成功事件可查、失败或拒绝事件可查、主要和受影响资源时间线均可定位该操作、事务策略符合风险等级、安全凭据不落库、Action Registry 与覆盖清单已更新、目标用例的旧 Writer 调用归零。未满足任一条件时 MUST 不得宣称该切片切换完成。
#### Scenario: 换货切片准备停写旧资产日志
- **WHEN** 换货用例计划停止旧资产 operation log
- **THEN** 门禁验证换货单、旧新卡或设备、设备绑定卡、钱包、套餐权益和客户绑定均按实际变化可追踪,并确认该用例不再调用旧 Writer
#### Scenario: 批量任务仅记录根事件
- **WHEN** 批量分配或购包任务只有批次根事件而单资源时间线没有子事件
- **THEN** 切换门禁失败,旧 Writer 不得在该用例中停写
### Requirement: 旧 Operation Log 的前向停写与历史保留
统一 Audit Event 切换完成的用例 MUST 停止向账号和资产 operation log 新增记录。旧表 SHALL 原样保留MUST NOT 在线回填、转换或接入新审计中心新审计中心的完整时间范围从切换点开始。手动轮询日志继续承担任务运行状态和历史事实MUST NOT 因统一审计切换而提前停写。
#### Scenario: 查询切换前账号历史
- **WHEN** 平台查询统一审计上线前的账号操作
- **THEN** 新审计中心不返回旧账号 operation log 记录
- **AND** 旧表继续原样保留,不为其创建 Audit Event 或 Event Resource
#### Scenario: 手动轮询任务仍在运行
- **WHEN** 统一 Audit Event 已记录人工触发动作
- **THEN** 手动轮询日志仍保存任务运行状态和结果Audit Event 只表达谁触发了任务及业务关联
### Requirement: 旧 Writer 归零验证
项目 MUST 维护旧账号审计 Writer、旧资产审计 Writer、裸 goroutine 审计调用和直接旧表写入的显式清单。最终 contract 阶段 MUST 通过静态扫描与真实业务验证证明已迁移用例的旧调用归零;既有旧资产历史查询和必要的手动轮询运行写入 MUST 被明确白名单化,且旧历史查询不得进入新审计路由或统一 Query。
#### Scenario: 发现旧账号 Writer 的异步调用
- **WHEN** 迁移用例仍通过裸 goroutine 或旧 account audit service 写入账号 operation log
- **THEN** 最终切换门禁失败并定位该调用点
#### Scenario: 仅保留独立旧表查询
- **WHEN** 代码只通过既有独立历史入口读取旧 operation log
- **THEN** 静态门禁允许该只读依赖,但禁止新审计 Query 依赖旧表,并禁止 Create、Update 或直接表写入
### Requirement: 发布总门禁
正式发布前 MUST 满足当前入口清单无未分类项所有资金、权限、关键配置、敏感读取和人工状态变更均有事务与失败策略Action Registry 和 Resource Registry 与代码一致;多资源、批量、设备卡槽、换货和外部回调场景通过验收;平台、代理和企业身份边界符合契约;安全凭据不落库;旧 Writer 按已迁移范围归零;查询分页、索引和无 N+1 证据满足项目性能目标;每日归档、月度最终复核、清理阻断和在线窗口语义通过验收;第一阶段没有面向用户的审计导出、归档查询/恢复或业务删除接口。
#### Scenario: 存在未分类生产写入口
- **WHEN** 覆盖清单与当前代码比对发现一个未分类的生产非查询入口
- **THEN** 发布总门禁失败,直到该入口完成业务评审并登记 Audit Event 或 N/A 决定
#### Scenario: Integration Log 调查接口包含恢复操作
- **WHEN** OpenAPI 或路由检查发现 Integration Log 查询中心提供重试、补偿、绑定或恢复写接口
- **THEN** 第一阶段发布门禁失败
### Requirement: 切换监控与前向修复
切换期间系统 MUST 监控 Audit Event 写入成功率和延迟、失败短事务二次失败、未知动作或资源、凭据删除命中、批量根子事件数量差异、归档/清理状态以及旧 Writer 调用。已提交的 Audit Event MUST 不因回滚部署、人工数据修复或业务纠错而删除;错误审计事实 MUST 通过更正事件或前向迁移修复。只有 Retention Worker 可在归档完整性门禁通过后删除 PostgreSQL 中已归档的上月 Audit/Integration 数据。发布回滚 MUST 只影响后续流量路由MUST 保留数据库当前月 Audit Event、Integration Log 和全部 Domain Ledger不得删除或回滚已验证完成的对象存储备份。
#### Scenario: 审计失败率超过发布阈值
- **WHEN** 切换后关键成功审计写入失败率或延迟超过发布阈值
- **THEN** 发布流程停止扩大迁移范围,并按失败用例前向修复;已经提交的审计和业务事实不被删除
#### Scenario: 归档校验未完成时到达月初
- **WHEN** 上月任一自然日的 Audit 或 Integration 缺少成功 manifest或对象数量、大小、SHA-256 与数据库最终内容不一致
- **THEN** 系统阻止整月在线清理并告警
- **AND** 新业务审计继续正常写入,不因对象存储故障回滚
#### Scenario: 批量根子事件数量异常
- **WHEN** 批量根事件的成功计数与可查询子事件数量不一致
- **THEN** 监控告警并阻止该纵向切片进入 contract 阶段

View File

@@ -0,0 +1,193 @@
## ADDED Requirements
### Requirement: 平台审计调查接口必须使用平台身份边界
系统 MUST 仅允许超级管理员和平台账号访问 `/api/admin/audit` 下的内部审计调查接口。第一阶段 MUST NOT 要求细粒度审计权限码,也 MUST NOT 对平台账号增加店铺或企业数据范围过滤;代理、企业和个人客户 MUST 被后端拒绝,不能仅依赖前端隐藏页面。
#### Scenario: 平台账号查看全局审计
- **WHEN** 已认证平台账号请求内部审计接口
- **THEN** 系统返回全部数据范围内符合筛选条件的审计数据
- **AND** 不要求尚未稳定的细粒度审计权限码
#### Scenario: 非平台身份直接调用内部接口
- **WHEN** 代理、企业或个人客户绕过前端直接请求 `/api/admin/audit/events`
- **THEN** 系统返回统一的禁止访问错误
- **AND** 不泄露是否存在匹配的审计事件
### Requirement: 系统必须提供全局事件列表和事件详情
系统 SHALL 提供 `GET /api/admin/audit/events``GET /api/admin/audit/events/{event_id}`。列表 MUST 支持 `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` 组合筛选;详情 MUST 返回事件上下文、操作者快照、结果、链路以及全部资源关系、身份快照和各资源前后变化。
#### Scenario: 组合筛选全局事件
- **WHEN** 平台账号按操作者、失败结果、资金类别和时间范围查询事件
- **THEN** 系统只返回同时满足条件的事件
- **AND** 每项包含稳定编码、中文名称、主要资源、结果和发生时间
#### Scenario: 查看多资源事件详情
- **WHEN** 平台账号查看一次换货审计事件
- **THEN** 系统返回换货单、旧卡/设备、新卡/设备、绑定、店铺及实际涉及的订单、钱包和套餐资源
- **AND** 每个资源分别返回 role、身份快照及自身 before/after
### Requirement: 系统必须提供操作者行为视角
系统 SHALL 提供 `GET /api/admin/audit/actors/{kind}/{id}/events`按时间倒序展示指定人工账号、OpenAPI 账号、系统任务或外部系统的操作。查询 MUST 支持 action、result、risk、resource 和时间过滤,并使用事件中的操作者快照解释历史。
#### Scenario: 调查平台账号行为
- **WHEN** 调查人员查询某平台账号最近七天的操作
- **THEN** 系统返回该操作者的成功、失败、拒绝和部分成功事件
- **AND** 账号后来改名或删除不改变历史操作者名称快照
#### Scenario: 调查系统自动状态变化
- **WHEN** 调查人员查询 `system_task` 操作者类型
- **THEN** 系统返回 Worker 和 Scheduler 引发的内部状态变化
- **AND** 不将系统操作伪造成平台人工操作
### Requirement: 系统必须提供通用资源搜索和资源时间线
系统 SHALL 提供 `GET /api/admin/audit/resources/search``GET /api/admin/audit/resources/{type}/{id}/timeline`。资源搜索 MUST 只使用 Resource Registry 已注册的精确标识或有索引关键词;资源时间线 MUST 通过 Event Resource 通用生成,不要求为每个领域新建审计表或时间线接口。
#### Scenario: 通过卡标识打开时间线
- **WHEN** 平台账号使用 ICCID 或 VirtualNo 搜索 IoT 卡并选择结果
- **THEN** 系统返回资源候选及稳定资源 ID
- **AND** 时间线展示该卡作为 primary、affected 或 reference 参与的事件
#### Scenario: 查询设备某张绑定卡的轨迹
- **WHEN** 设备入口对其第二卡槽的卡执行操作
- **THEN** 同一事件出现在设备、卡和必要绑定关系的时间线
- **AND** 时间线能区分 `entry_device``bound_card` 和卡槽角色
#### Scenario: 历史资源已经删除或标识改变
- **WHEN** 当前业务表无法再返回事件发生时的资源名称或标识
- **THEN** 系统使用 Event Resource 保存的身份快照解释历史
### Requirement: 系统必须提供请求和业务关联时间线
系统 SHALL 提供 `GET /api/admin/audit/requests/{request_id}/timeline``GET /api/admin/audit/correlations/{correlation_id}/timeline`。时间线 MUST 组合可关联的 Audit Event、Integration Log、Outbox/任务摘要及 Domain Ledger 引用,并为每个节点返回明确 `record_source`;系统 MUST NOT 把 Access Log 文件正文或不同事实复制成 Audit Event。
#### Scenario: 调查一次 HTTP 请求
- **WHEN** 平台账号通过 request ID 查询链路
- **THEN** 系统按发生时间返回该请求产生的内部操作、外部交互和可靠事件摘要
- **AND** 返回用于开发人员检索 Access Log 的 request ID 而不扫描日志文件
#### Scenario: 调查跨请求退款链路
- **WHEN** 平台账号通过退款 correlation ID 查询业务链路
- **THEN** 系统展示申请、审批、外部回调、退款处理、钱包回充、佣金和套餐后处理节点
- **AND** 每个金额或状态结论标明其 Domain Ledger 来源
#### Scenario: correlation 不能证明技术重试关系
- **WHEN** 多条 Integration Log 只有相同 correlation ID 而没有稳定 trigger series
- **THEN** 业务时间线可展示它们属于同一业务链路
- **AND** 系统不得将它们标记为同一次外呼的重试序列
### Requirement: 系统必须提供资金调查视角
系统 SHALL 提供 `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` 组合筛选。该视角 MUST 组合 Audit Event 与钱包流水、订单、支付、退款、充值、佣金和审批等 Domain Ledger并明确金额权威来自业务流水而非 Audit Event调用方只提供任一可用稳定业务条件时Query MUST 在服务端解析关联事实,不得要求前端补齐同一链路全部 ID。
#### Scenario: 解释钱包余额变化
- **WHEN** 平台账号按钱包或交易流水查询资金时间线
- **THEN** 系统返回触发人、业务动作、订单/退款/充值、审批、金额和余额前后值
- **AND** 金额结论以钱包流水及对应业务表为准
#### Scenario: 资金事实与审计内容不一致
- **WHEN** Audit Event 摘要与 Domain Ledger 的金额事实出现差异
- **THEN** 接口明确标注数据来源并以 Domain Ledger 作为资金权威
- **AND** 不通过修改历史 Audit Event 掩盖差异
### Requirement: 系统必须提供风险和异常视角
系统 SHALL 提供 `GET /api/admin/audit/risks/overview``GET /api/admin/audit/risks/events`,展示高风险、资金、安全、失败、拒绝、部分成功和结果未知事件的数量、趋势与明细。总览 MUST 限定时间范围,明细 MUST 可跳转到事件、资源、操作者和 correlation 视角。
#### Scenario: 查看失败和拒绝趋势
- **WHEN** 平台账号查询最近二十四小时风险总览
- **THEN** 系统按风险、结果、action 和来源返回聚合数量与时间趋势
- **AND** 不把普通低风险成功事件计入异常数量
#### Scenario: 从风险事件继续调查
- **WHEN** 平台账号选择一条高风险资金拒绝事件
- **THEN** 系统返回稳定事件 ID及其操作者、资源和业务链路跳转信息
### Requirement: 查询响应必须稳定、分页且面向前端投影
所有审计查询 SHALL 使用专用 DTO 和统一响应 `{code,msg,data,timestamp}`,不得直接返回 GORM Model。列表 MUST 默认每页 20、最大 100使用时间与稳定 ID 排序Query MUST 批量加载资源与业务引用,避免 N+1并满足 API P95 < 200ms、P99 < 500ms、数据库查询 < 50ms 的目标。
#### Scenario: 稳定翻页
- **WHEN** 多条事件具有相同发生时间且调用方连续翻页
- **THEN** 系统使用发生时间和稳定 ID 作为排序游标或等价稳定排序
- **AND** 不重复或遗漏事件
#### Scenario: 查询多资源列表
- **WHEN** 一页包含多个动作和资源类型
- **THEN** Query 批量加载 Event Resource 与必要展示信息
- **AND** 不为每条事件逐一查询操作者、资源或 Domain Ledger
### Requirement: 平台调查接口必须明确查询入参来源
平台调查接口的筛选字段 SHALL 来自调查人员输入或上一个视角携带的稳定跳转值;认证用户类型和当前账号 ID MUST 来自认证上下文,不得由 query、path 或 body 指定。事件 ID、资源 ID、actor ID、request ID、correlation ID 和资金业务标识 MUST 使用列表、详情、业务页面或已记录节点提供的稳定值Query MAY 根据稳定 ID 批量派生中文名称、资源展示信息、关联节点和 Domain Ledger 内容,但 MUST NOT 按时间接近、中文描述或模糊关键词猜测关联。
#### Scenario: 从事件节点跳转到其他视角
- **WHEN** 调查人员从事件、资源、操作者、风险或链路节点继续调查
- **THEN** 前端使用该节点返回的稳定 event、resource、actor、request 或 correlation 标识构造目标接口入参
- **AND** 后端只校验和查询该稳定标识,不猜测缺失关联
#### Scenario: 调查人员直接筛选
- **WHEN** 调查人员在全局、资源搜索、资金或风险视角填写时间、动作、结果、风险或业务标识
- **THEN** Handler 从受控 query/path 参数读取筛选条件并执行格式、枚举、分页及时间范围校验
- **AND** 未提供的筛选条件不由后端补猜
#### Scenario: 身份范围来自认证上下文
- **WHEN** 任一调用方请求平台调查接口
- **THEN** Handler 仅从认证上下文取得用户类型和当前账号 ID
- **AND** 忽略或拒绝调用方伪造的店铺、企业或身份范围参数
### Requirement: 业务页面必须具备可执行的审计导航契约
系统 SHALL 为已纳入第一阶段的业务列表和详情冻结“前置接口、`response.data` 字段、入口可见条件、目标审计接口、参数映射和降级行为”。账号、店铺、企业、卡、设备、设备卡槽、资产详情、分配、换货、订单、退款、代理充值、资产钱包及店铺资金页面 MUST 使用其现有响应中的稳定 ID 或 Registry Key前端 MUST NOT 解析中文名称、备注或编号前缀推断资源。
#### Scenario: 平台从卡资产详情查看审计
- **WHEN** `GET /api/admin/assets/resolve/{identifier}` 返回 `asset_type=card``asset_id``iccid`
- **THEN** 平台页面使用 `resource_type=iot_card``resource_id=asset_id` 调用 `GET /api/admin/audit/resources/{resource_type}/{resource_id}/timeline`
- **AND** 不使用 ICCID 再搜索一次或把 `card` 直接当成未注册资源类型
#### Scenario: 平台从订单详情查看资金链路
- **WHEN** `GET /api/admin/orders/{id}` 返回订单 `id`,但没有返回全部 Payment、Refund 或 Integration 标识
- **THEN** 页面调用 `GET /api/admin/audit/finance/timeline?order_id={id}`
- **AND** Query 在服务端解析关联事实,不要求前端补猜缺失 ID
#### Scenario: 前置响应缺少稳定引用
- **WHEN** 业务响应没有目标审计接口需要的稳定 ID 或 Registry Key
- **THEN** 前端不展示对应入口,后端也不按名称、时间、描述或编号前缀推断
### Requirement: 平台调查节点必须返回统一跳转引用
平台事件、资源、操作者、request、correlation、资金、风险及 Integration 关联节点 SHALL 返回统一的 `investigation_refs`,包含可空 `event_id`、可空 `actor_ref{kind,id}``resource_refs[]{resource_type,resource_id,resource_key,display_name}`、可空 `request_id`、可空 `correlation_id``integration_refs[]{integration_id}`。前端 MUST 仅使用存在的稳定引用导航;代理和企业响应 MUST NOT 复用或暴露该内部结构。
#### Scenario: 从风险节点继续调查
- **WHEN** 风险事件节点同时包含 event、actor、resource 和 correlation 引用
- **THEN** 前端可分别调用事件详情、操作者事件、资源时间线和 correlation 时间线
- **AND** 每个目标参数直接来自 `investigation_refs` 对应字段
#### Scenario: 节点缺少关联标识
- **WHEN** 节点没有 request、correlation 或 Integration 稳定标识
- **THEN** 前端隐藏对应跳转,不按相近时间或相同资源猜测链路
#### Scenario: 旧日志不能跳新审计
- **WHEN** 平台查看独立旧 operation log 入口中的切换前记录
- **THEN** 页面不为该记录构造新 Audit Event、资源时间线或 correlation 跳转
### Requirement: 调查接口必须只读并完整展示已存业务字段
第一阶段所有平台调查接口 MUST 只读MUST NOT 提供 Audit Event 修改、删除、导出、风险处置、重试、补偿、绑定或恢复操作。平台接口 SHALL 返回审计库中已保存的手机号、IP、ICCID、VirtualNo、金额、交易号和 before/after 等完整业务字段;系统安全凭据及原始第三方敏感正文 MUST 在写入前删除,因此任何查询均不得返回。
#### Scenario: 平台查看业务敏感字段
- **WHEN** 平台账号查看资金或资产事件详情
- **THEN** 系统返回已保存的完整业务金额、资产标识、操作者 IP 和业务前后值
- **AND** 不执行展示层掩码
#### Scenario: 请求审计导出或修改
- **WHEN** 调用方尝试通过审计中心导出、修改或删除审计记录
- **THEN** 系统不存在对应写接口
#### Scenario: 系统安全凭据不存在于详情
- **WHEN** 平台账号查看支付、企微或系统配置相关事件
- **THEN** 响应不包含密码、Token、Secret、私钥、回调凭据、Authorization、Cookie、签名 URL 或支付密钥
### Requirement: 调查接口必须公开在线留存边界
全局、操作者、资源、request、correlation、资金和风险查询 MUST 在 DTO 中返回 `retention{online_from, archived_before, timezone}`。系统 MUST 只查询 PostgreSQL 在线窗口;显式时间范围早于或跨越 `online_from` 时 MUST 返回稳定的已归档错误及当前在线边界,不得返回误导性空结果或不完整时间线。第一阶段 MUST NOT 从对象存储补查历史。
#### Scenario: 业务页面打开资源审计
- **WHEN** 平台从卡、设备、订单或其他业务详情打开资源时间线且未指定历史时间
- **THEN** 接口返回当前在线窗口内的事件及 retention 元数据
- **AND** 页面明确提示早于 `online_from` 的记录已转冷归档、当前不可在线查询
#### Scenario: 请求上月已归档时间线
- **WHEN** 调查人员显式指定的时间范围全部早于 `online_from`
- **THEN** 接口返回稳定的已归档错误和当前在线边界
- **AND** 不返回成功空列表、不扫描对象存储

View File

@@ -0,0 +1,88 @@
## ADDED Requirements
### Requirement: 按完整自然日压缩归档审计相关日志
系统 SHALL 复用现有 S3 兼容对象存储、Worker 和 Asynq Scheduler`Asia/Shanghai` 将前一完整自然日的 Audit Event 及全部 Event Resource、Integration Log 分别生成 UTF-8 `JSON Lines + gzip` 冷归档。归档 MUST 使用半开时间区间MUST NOT 扫描或混入 Access Log、Domain Ledger、Outbox、Asynq 任务、手动轮询或旧 operation log。Access Log SHALL 继续使用应用服务器本地文件及现有 Lumberjack 轮转与保留策略MUST NOT 被本能力上传到对象存储。对象存储失败 MUST 只使归档任务重试和阻止后续清理MUST NOT 阻止新的业务审计正常落库。
#### Scenario: 每日归档前一天日志
- **WHEN** `Asia/Shanghai` 新自然日的归档任务执行
- **THEN** 系统只归档前一天 `[00:00:00, 次日 00:00:00)` 创建的目标日志
- **AND** Audit 每行包含一个事件及其完整资源数组Integration 每行包含一条结构化持久化记录
#### Scenario: 对象存储暂时不可用
- **WHEN** 上传归档对象或读取对象 metadata 失败
- **THEN** 任务按既有 Asynq 重试策略重试并记录失败状态、日志和指标
- **AND** Audit Event 与 Integration Log Writer 不调用对象存储且继续服务业务请求
### Requirement: 归档对象必须具有可验证 manifest
每个归档对象 MUST 具有 manifest至少记录 `schema_version`、数据源、归档日期、时区、时间范围、实例 ID、事件/资源或记录数量、未压缩与压缩字节数、对象 Key、SHA-256、revision、生成时间和状态。系统 SHALL 使用 `tb_log_archive_run` 保存轻量运行 ledger并 MUST 以 `source + archive_date + instance_id + schema_version` 保证调度幂等;该表不得保存日志正文。
#### Scenario: 重复执行相同日归档
- **WHEN** 同一数据源、日期、实例和 schema version 的任务被重复投递
- **THEN** 系统校验并复用内容一致且已经成功的对象
- **AND** 不重复创建相同事实或把重复执行计为新归档日
#### Scenario: 同一对象 Key 对应不同内容
- **WHEN** 当前数据库数量或 SHA-256 与已成功 manifest 不一致
- **THEN** 系统创建不可变的新 revision 并保留旧对象
- **AND** 不静默覆盖原对象或直接宣称本次归档成功
### Requirement: Integration Log 月度清理前必须形成最终快照
由于 Integration Log 可在创建后从 pending 更新为终态,系统 MUST 在月初清理前使用数据库当前内容复核上月每日归档。内容发生变化时 MUST 先创建新的最终 revision 并通过完整性校验;仍处于可变状态或无法形成最终快照的记录 MUST 阻止该月清理。
#### Scenario: 上月 pending 记录在本月完成
- **WHEN** 某 Integration Log 的每日归档版本为 pending但月初复核时数据库记录已经完成
- **THEN** 系统生成包含最终状态的新 revision 并更新成功 manifest
- **AND** 只有最终 revision 验证通过后才允许清理数据库记录
#### Scenario: 上月记录仍无法最终确认
- **WHEN** 月初复核发现上月 Integration Log 仍处于可变状态或归档内容无法与数据库一致
- **THEN** 系统阻止整月清理并告警
- **AND** 不删除该记录或其他 PostgreSQL 上月审计数据
### Requirement: 月初清理必须以整月归档完整性为硬门禁
每月初系统 SHALL 在上月最后一天归档成功后检查上月全部自然日的 Audit 和 Integration 归档。只有 Audit Event 数量、Event Resource 数量、Integration 最终内容、对象存在性、对象大小、SHA-256、时间范围和 manifest 状态全部一致,且没有 pending 或 failed 任务时Retention Worker 才可物理删除 PostgreSQL 上月 Audit Event、Event Resource 和 Integration Log 数据。对象存储中的归档对象 MUST 长期保留且不得被该任务删除。Access Log 不参与归档或数据库删除门禁。任一检查失败 MUST 阻止整月数据库删除并产生 critical 日志和指标。
#### Scenario: 上月缺少一天 Audit 归档
- **WHEN** 月度门禁发现某日 Audit manifest 缺失或 Event Resource 数量不一致
- **THEN** 系统不删除上月任何 Audit Event、Event Resource 或 Integration Log
- **AND** 后续调度继续补档并重新执行门禁
#### Scenario: 月度门禁全部通过
- **WHEN** 上月全部 Audit/Integration 归档对象和 manifest 与数据库一致
- **THEN** Retention Worker 使用 GORM、`created_at` 索引和有界批次对该完整自然月执行数据库物理 `DELETE`
- **AND** Event Resource 与 Event 作为同一事实集合清理,任务中断后可从 ledger 安全继续
#### Scenario: 禁止用软删除代替物理删除
- **WHEN** Retention Worker 清理 PostgreSQL 上月 Audit/Integration 数据
- **THEN** 对应表记录从数据库中真实移除
- **AND** 不新增或更新 `deleted_at`、archive status、隐藏标记也不迁移到另一张 PostgreSQL 历史表
### Requirement: 清理权限和清理事实必须隔离
业务 Handler、Query、Writer 和 Repository MUST NOT 获得审计删除能力。只有 Retention Worker 的独立最小权限入口可按已通过门禁的月份物理删除 PostgreSQL Audit/Integration 数据。Audit Event 和 Event Resource Model MUST NOT 包含 `gorm.DeletedAt`Integration Log 现有无软删除字段的模型保持不变。清理任务 MUST 以 `retention_worker` 系统身份在当前月写 Audit Event记录目标月份、数据源数量、manifest Key 和结果,不得把清理事件写回被清理月份。
#### Scenario: 调用方尝试删除审计记录
- **WHEN** 平台账号或其他业务调用方尝试删除 Audit Event、Event Resource、Integration Log 或归档对象
- **THEN** 系统不存在对应业务 API 或 Repository 方法
#### Scenario: 月度数据库清理完成
- **WHEN** Retention Worker 完成 PostgreSQL 上月 Audit/Integration 数据删除
- **THEN** 当前月产生一条可调查的系统 Audit Event
- **AND** 该事件不属于本次已清理时间窗口
### Requirement: 归档历史不再支持在线调查
清理后的 Audit Event 与 Integration Log SHALL 只保留在对象存储。第一阶段 MUST NOT 提供归档下载、对象存储扫描、跨冷热存储查询或恢复接口。所有审计与 Integration 列表、时间线和总览 DTO MUST 返回 `retention{online_from, archived_before, timezone}`;显式时间范围全部位于归档区间或跨越在线边界时 MUST 返回稳定的已归档错误,不得返回看似完整的空结果或部分结果。
#### Scenario: 查询已清理月份的资源时间线
- **WHEN** 调用方显式查询早于 `online_from` 的资源活动或 Audit 时间线
- **THEN** 系统返回“数据已归档且第一阶段不支持在线查询”的稳定错误
- **AND** 不扫描对象存储或返回成功空列表
#### Scenario: 查询范围跨越冷热边界
- **WHEN** 调用方提交的开始和结束时间同时覆盖已归档区间与在线区间
- **THEN** 系统拒绝部分查询并返回当前在线窗口
- **AND** 调用方可缩小到 `online_from` 之后重新查询
#### Scenario: 查询当前在线月份
- **WHEN** 查询条件完全位于当前在线窗口
- **THEN** 系统按既有调查契约返回 PostgreSQL 在线结果和 retention 元数据
- **AND** 不访问对象存储

View File

@@ -0,0 +1,159 @@
## ADDED Requirements
### Requirement: 平台提供只读外部集成调查中心
系统 MUST 为已认证的平台账号提供只读 Integration Log 调查能力,平台账号不应用店铺或企业数据范围过滤,第一阶段不设置细粒度权限码;代理、企业和个人客户 MUST NOT 访问该调查能力。所有成功响应 MUST 使用统一 `{code, msg, data, timestamp}` 格式。
#### Scenario: 平台账号访问调查中心
- **WHEN** 已认证的平台账号查询 Integration Log 总览、列表或详情
- **THEN** 系统返回平台范围内符合查询条件的完整业务摘要,且不应用店铺或企业数据范围过滤
#### Scenario: 非平台主体尝试访问
- **WHEN** 代理、企业或个人客户直接请求平台 Integration Log 调查接口
- **THEN** 系统统一拒绝访问,且不泄露目标记录是否存在
### Requirement: 外部交互异常总览
系统 MUST 提供受时间范围约束的 Integration Log 总览至少返回交互总数、结果分布、提供方分布、入站与出站分布、异常数量、结果未知数量、陈旧待处理数量、本地状态变化数量、平均耗时、P95 耗时和按时间分桶的趋势。总览 MUST 区分实际成功、结果未知、明确失败、处理中和未发送终态,不得把 `completed``ignored``merged``rate_limited``cancelled` 计为外部请求成功。
#### Scenario: 查询指定时间范围总览
- **WHEN** 平台账号提交合法的开始时间、结束时间和时间粒度
- **THEN** 系统在该时间范围内返回固定结构的汇总、分类计数和趋势数据,并为每个稳定结果编码返回中文名称及派生类别
#### Scenario: 区分未发送终态与成功
- **WHEN** 时间范围内同时存在 `success``completed``merged``rate_limited` 记录
- **THEN** 系统仅把 `success` 计入实际成功,并把其余记录计入对应的未发送或提前完成分类
### Requirement: Integration Log 组合筛选列表
系统 MUST 提供按创建时间和主键稳定倒序的分页列表,支持组合筛选 `integration_id`、provider、direction、operation、原始 result 或派生结果类别、external_id、resource_type 与 resource_id、resource_type 与 resource_key、trigger_source、trigger_scene、trigger_series、state_changed、http_status、provider_code、request_id、correlation_id 和创建时间范围。筛选条件 MUST 使用精确匹配或受控枚举,不得提供任意 SQL、任意 JSONPath 或任意 JSONB 字段搜索。
#### Scenario: 组合查询外部失败记录
- **WHEN** 平台账号同时指定 provider、operation、失败派生类别、资源和时间范围
- **THEN** 系统仅返回同时满足全部条件的记录,并为列表项返回稳定编码、中文名称、主要资源、结果、耗时、状态变化、请求标识、关联标识和发生时间
#### Scenario: 查询结果为空
- **WHEN** 合法筛选条件没有命中任何记录
- **THEN** 系统返回成功的空分页结果,而不是资源不存在错误
#### Scenario: 拒绝无界或非法筛选
- **WHEN** 查询时间范围、页码、每页数量或枚举值超过接口约束
- **THEN** 系统返回统一参数错误,且不执行无界全表查询
### Requirement: 使用稳定 integration_id 查询结构化详情
系统 MUST 使用业务稳定的 `integration_id` 而非数据库自增 ID 定位单条详情,并按 identity、resource、trigger、result、content、linkage、timestamps 和 attempts 分区返回结构化 DTO。详情 MUST 包含可用的请求摘要、响应摘要、metadata、正文安全摘要、HTTP 状态、渠道结果码、可读安全结果摘要、耗时、本地状态变化、人工核对说明、request_id、correlation_id 和关联 Audit Event ID。
#### Scenario: 查询单条外部交互详情
- **WHEN** 平台账号使用存在的 `integration_id` 查询详情
- **THEN** 系统返回该记录的结构化详情及 provider、direction、operation、result 的稳定编码和中文名称,不要求前端解析数据库 Model 或任意 JSON 才能识别基础语义
#### Scenario: 稳定标识不存在
- **WHEN** 平台账号使用不存在的 `integration_id` 查询详情
- **THEN** 系统返回统一资源不存在错误,且不回退使用数据库自增 ID 猜测记录
### Requirement: Integration Log 查询入参必须有明确来源
总览的时间范围、粒度及聚合筛选和列表的组合筛选 SHALL 来自调查人员输入、通知目标或其他调查视角携带的稳定值provider、direction、operation、result 等枚举选项 MUST 来自后端稳定常量。详情 `integration_id` MUST 来自 Integration 列表、通知目标或事件/request/correlation 时间线节点,也 MAY 由调查人员粘贴稳定 Integration ID系统 MUST NOT 使用数据库自增 ID、相似资源或相近时间猜测目标记录。
#### Scenario: 从列表打开详情
- **WHEN** 调查人员选择 Integration 列表中的一条记录
- **THEN** 前端使用列表返回的稳定 `integration_id` 请求详情
- **AND** 后端不要求前端提供数据库主键或重复提供资源条件
#### Scenario: 从其他调查视角跳转
- **WHEN** 事件、request 或 correlation 时间线节点关联一条外部交互
- **THEN** 节点返回可跳转的稳定 `integration_id`,前端据此打开详情
#### Scenario: 查询身份来自认证上下文
- **WHEN** 调用方请求 Integration Log 总览、列表或详情
- **THEN** Handler 从认证上下文判断其是否为平台身份
- **AND** 不接受 query、path 或 body 传入平台、店铺或企业范围
#### Scenario: 从通知目标打开 Integration 详情
- **WHEN** `GET /api/admin/notifications/{id}/target` 返回 `available=true``target_type=integration_log` 和非空 `target_key`
- **THEN** 前端将 `target_key` 原样作为 `integration_id` 调用 Integration 详情
- **AND** 不直接解析通知列表的 `ref_type/ref_id/ref_key` 或按资源与时间重新搜索
#### Scenario: 通知目标不可用
- **WHEN** 通知目标解析返回 `available=false` 或缺少 `target_key`
- **THEN** 前端只展示通知正文,不显示 Integration 详情跳转
### Requirement: 尝试序列只使用显式技术序列
系统 MUST 仅在记录具有相同非空 `trigger_series` 时将其组织为同一技术尝试序列,并按 attempt、发生时间和主键稳定排序。可重试或分阶段外呼的新接入 MUST 写入稳定 `trigger_series` 和单调递增的 attempt缺少 `trigger_series` 的历史记录 MUST 作为单次交互展示,系统不得仅因 provider、operation、资源或时间接近而猜测其属于同一重试序列。
#### Scenario: 展示显式尝试序列
- **WHEN** 用户查询的记录带有 `trigger_series`,且存在同序列的多个尝试
- **THEN** 详情按 attempt 和时间返回完整尝试序列,并逐条保留是否发送、结果、耗时和状态变化
#### Scenario: 历史记录缺少序列标识
- **WHEN** 用户查询的历史记录没有 `trigger_series`
- **THEN** 系统只返回当前单次交互,不把相同资源或相同 operation 的相邻记录拼成重试序列
### Requirement: 业务关联链路与技术重试保持不同语义
系统 MUST 将 `correlation_id` 解释为跨请求、异步任务、外部交互和业务事实的业务链路标识,将 `trigger_series + attempt` 解释为一次外部操作的技术尝试序列。按 correlation_id 查询时 MUST 返回相关外部交互节点,但 MUST NOT 将这些节点统一标记为重试;跨 Audit Event、Outbox、Asynq 和 Domain Ledger 的完整时间线由统一审计调查 Query 组合Integration Log Query 只提供外部交互节点。
#### Scenario: 同一业务链路包含不同外部操作
- **WHEN** 同一 correlation_id 下存在预下单、回调和查单等不同 operation
- **THEN** 系统按业务发生时间展示相关交互并保留各自 operation不把回调或查单描述为预下单重试
#### Scenario: 同时存在业务链路和尝试序列
- **WHEN** 一条业务链路中的某个 operation 具有多个显式 trigger_series 尝试
- **THEN** 系统既保留 correlation_id 的业务链路关系,也在该 operation 内单独展示技术尝试序列
### Requirement: 调查中心不承担恢复和导出
Integration Log 调查接口 MUST 只有读取能力。第一阶段 MUST NOT 提供重试、补偿、结果确认、外部单号绑定、人工恢复、状态修改、记录删除或导出接口。`recovery_strategy` 仅作为人工核对说明展示,不得被解释为可执行命令。
#### Scenario: 查看结果未知记录
- **WHEN** 平台账号查看 result 为 `unknown` 的详情
- **THEN** 系统展示已记录的人工核对说明,但响应中不包含恢复动作地址、可执行命令或写操作按钮契约
#### Scenario: 尝试调用恢复或导出能力
- **WHEN** 调用方尝试通过审计中心执行 Integration Log 恢复、修改、删除或导出
- **THEN** 系统不存在对应业务路由或统一拒绝请求,且原 Integration Log 不发生变化
### Requirement: 安全凭据不得进入或离开 Integration Log
平台可以查看 Integration Log 中已经安全筛选的完整业务摘要但密码、操作密码、验证码、Access Token、Refresh Token、Secret、私钥、回调 Token、EncodingAESKey、Authorization、Cookie、签名 URL、支付密钥、完整加密回调正文和其他系统安全凭据 MUST 在写入前删除,查询接口 MUST NOT 返回这些内容。原始第三方错误正文 MUST NOT 直接进入 provider_message调用方必须提供有界、可读且不含凭据的业务结果摘要。
#### Scenario: 请求摘要包含业务字段和安全凭据
- **WHEN** 外部调用摘要同时包含普通业务字段和系统安全凭据
- **THEN** 系统保留普通业务字段并在持久化前删除安全凭据,详情接口只返回持久化后的安全摘要
#### Scenario: 入站回调包含完整正文
- **WHEN** 系统接收运营商、支付或企业微信回调
- **THEN** Integration Log 只保存受控业务摘要、正文大小和安全 hash不通过调查接口返回完整原始或解密正文
### Requirement: 历史 Integration Log 缺口必须显式兼容
系统 MUST 保留并查询现有 Integration Log不在线伪造回填缺失的 trigger_series、correlation_id、资源 ID 或可读 provider_message。查询 DTO MUST 对无法可靠解析的历史字段返回明确的数据完整性标识;对使用现有确定性 ICCID hash 保存 resource_key 的运营商历史记录,资源查询 MUST 支持按相同受控算法定位,但 MUST NOT 向调用方暴露 hash 作为业务标识。已保存为不可逆摘要的 provider_message MUST 原样标识为历史摘要,不得生成虚假的可读原文。历史 `request_summary/response_summary/metadata` MUST 在响应前按字段白名单和统一凭据删除规则再次清理MUST NOT 直接透传旧 JSON。
#### Scenario: 查询只有 ICCID hash 的历史运营商记录
- **WHEN** 平台账号使用合法 ICCID 查询资源外部交互轨迹,且历史记录只保存确定性 ICCID hash
- **THEN** 系统使用受控兼容规则命中该记录,并把资源解析状态标记为历史兼容,不把 hash 返回为 ICCID
#### Scenario: 历史链路字段缺失
- **WHEN** 历史记录缺少 correlation_id、trigger_series 或可靠资源 ID
- **THEN** 系统仍返回现有事实并标明对应关联能力受限,不猜测链路、重试次数或资源关系
#### Scenario: 历史 JSON 含有未覆盖的安全凭据
- **WHEN** 历史 Integration Log 摘要含有 Token、Secret、Authorization、Cookie、签名 URL 或其他禁止字段
- **THEN** Query 在返回前删除禁止字段并保留其余完整业务摘要
- **AND** 不直接序列化历史 JSON 到响应
### Requirement: Integration Log 查询必须分页并命中受控索引
列表 MUST 默认每页 20 条、最大 100 条,并要求受控时间范围;总览 MUST 限定时间范围和固定聚合维度。实现 MUST 使用创建时间、结果、provider、external_id、资源、trigger_series、request_id、correlation_id 和 audit_event_id 的受控索引完成主要查询,不得逐条补查产生 N+1不得为第一阶段的摘要展示增加任意 JSONB GIN 搜索。
#### Scenario: 查询高频轮询历史
- **WHEN** 平台账号查询包含大量 Gateway 高频记录的合法时间窗口
- **THEN** 系统使用分页和匹配的时间或筛选索引返回结果,单页查询不加载窗口外全部记录,也不逐条查询关联基础信息
#### Scenario: 深分页或超大时间窗口
- **WHEN** 调用方请求超过允许范围的页码、每页数量或时间窗口
- **THEN** 系统返回统一参数错误,引导调用方缩小时间窗口,而不是执行高成本扫描
### Requirement: Integration 调查只覆盖在线留存窗口
Integration overview、列表、详情和关联时间线 MUST 仅查询 PostgreSQL 在线数据,列表和总览 DTO MUST 返回 `retention{online_from, archived_before, timezone}`。月初清理后的上月记录只保留在对象存储,第一阶段 MUST NOT 提供归档下载、对象存储扫描、冷热联合查询或恢复能力。显式时间范围早于或跨越 `online_from` 时 MUST 返回稳定的已归档错误,不能返回成功空结果或部分结果。
#### Scenario: 查询已归档的外部交互月份
- **WHEN** 平台账号查询的 Integration 时间范围全部早于 `online_from`
- **THEN** 系统返回数据已归档和当前在线窗口
- **AND** 不执行 PostgreSQL 无效扫描或对象存储查询
#### Scenario: 当前月份外部交互调查
- **WHEN** 查询范围完全位于在线窗口
- **THEN** 系统返回符合条件的 Integration Log 与 retention 元数据
- **AND** 详情和尝试序列仍遵循现有稳定 ID 与显式 `trigger_series` 契约

View File

@@ -0,0 +1,125 @@
## ADDED Requirements
### Requirement: 代理和企业必须使用独立的安全资源活动接口
系统 SHALL 为代理和企业提供独立于平台内部审计中心的只读资源活动接口:代理使用 `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}`,企业使用 `GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}`。两者 MUST 使用 `page/page_size` 和专用安全投影 DTO响应资源摘要 MUST 包含 `resource_type/resource_id/resource_key/display_name`,活动项 MUST 仅包含外部动作编码/中文名、`subject_summary`、Registry 白名单约束的 `subject_data`、结果、发生时间及允许公开的关联资源摘要。接口响应 SHALL 使用统一的 `{code, msg, data, timestamp}` 格式,且不得通过响应字段、路径命名或错误差异暴露平台内部审计实现。
#### Scenario: 代理查询有权管理的资源活动
- **WHEN** 代理账号查询自己店铺或下级店铺范围内的卡、设备或其他已支持资源
- **THEN** 系统返回该资源允许代理感知的分页活动时间线
- **AND** 默认每页 20 条且每页最多 100 条
#### Scenario: 企业查询已授权资源活动
- **WHEN** 企业账号查询当前企业仍然有效授权的卡或设备
- **THEN** 系统返回该资源允许企业感知的分页活动时间线
#### Scenario: 外部主体无法访问平台审计中心
- **WHEN** 代理或企业账号直接调用平台内部审计接口
- **THEN** 系统统一返回无权限错误
- **AND** 不返回任何平台审计数据
### Requirement: 资源归属必须在后端按真实业务关系校验
系统 MUST 在查询活动事件之前验证目标资源属于当前调用方的数据范围,不得仅依赖前端页面可见性,也不得把缺失的店铺范围解释为不受限制。
#### Scenario: 资源标识来自受权业务入口
- **WHEN** 代理或企业从当前卡、设备等业务列表或详情页打开“活动记录”
- **THEN** 前端使用该页面已有的 `resource_type/identifier` 请求资源活动接口
- **AND** `identifier` 使用 Resource Registry 为该类型声明的业务稳定标识,不由前端猜测内部资源 ID
#### Scenario: 代理从卡资产详情打开活动记录
- **WHEN** 代理调用 `GET /api/admin/assets/resolve/{identifier}` 并得到 `asset_type=card` 与非空 `iccid`
- **THEN** 页面调用 `GET /api/admin/agent/resource-activities/iot_card/{iccid}`
- **AND** 店铺范围由认证上下文提供,不把响应中的 `shop_id` 作为授权凭证传入
#### Scenario: 代理从设备资产详情打开活动记录
- **WHEN** 代理调用资产详情并得到 `asset_type=device` 与非空 `virtual_no`
- **THEN** 页面调用 `GET /api/admin/agent/resource-activities/device/{virtual_no}`
#### Scenario: 企业从受权资产列表打开活动记录
- **WHEN** 企业从 `GET /api/admin/enterprises/{id}/cards``items[].iccid``GET /api/admin/enterprises/{id}/devices``items[].virtual_no` 打开活动记录
- **THEN** 页面分别调用 `GET /api/admin/enterprise/resource-activities/iot_card/{iccid}``GET /api/admin/enterprise/resource-activities/device/{virtual_no}`
- **AND** 后端忽略路由来源中的企业 ID 作为授权证明,始终以认证上下文企业 ID 复核当前有效授权
#### Scenario: 企业不能从平台资产详情进入
- **WHEN** 当前统一资产 resolve 接口禁止企业账号调用
- **THEN** 企业前端不展示依赖该接口的活动入口,也不尝试使用平台资源时间线替代
#### Scenario: 身份范围来自认证上下文
- **WHEN** 代理或企业请求资源活动接口
- **THEN** Handler 从认证上下文取得当前账号、代理店铺范围或企业 ID
- **AND** 不接受 query、path 或 body 传入 shop ID、enterprise ID 或其他主体范围
#### Scenario: 代理资源范围按店铺层级校验
- **WHEN** 代理账号查询资源活动
- **THEN** 系统验证资源当前归属店铺位于该代理自己或下级店铺范围内
#### Scenario: 企业卡归属按有效授权校验
- **WHEN** 企业账号查询某张卡的资源活动
- **THEN** 系统验证企业卡授权记录的 `enterprise_id` 等于当前企业且授权未撤销
#### Scenario: 企业设备归属按有效授权校验
- **WHEN** 企业账号查询某台设备的资源活动
- **THEN** 系统验证企业设备授权记录的 `enterprise_id` 等于当前企业且授权未撤销
#### Scenario: 无权资源不泄露存在性
- **WHEN** 代理或企业查询不存在、不支持或不属于自身范围的资源
- **THEN** 系统返回统一的“无权限操作该资源或资源不存在”错误
- **AND** 响应不得表明资源是否真实存在、是否曾被授权或是否存在内部审计事件
#### Scenario: 活动入口缺少业务标识
- **WHEN** 卡没有 ICCID、设备没有 VirtualNo 或响应没有 Registry 声明的稳定 identifier
- **THEN** 页面隐藏活动入口,不使用内部 ID、名称或其他字段猜测 identifier
### Requirement: 外部活动投影必须按事件可见级别过滤
系统 SHALL 根据 Audit Event 写入时确定的 `internal_only``subject_result``subject_detail` 可见级别生成外部活动投影,不得在查询时把完整内部事件直接序列化后临时删除部分字段。
#### Scenario: 内部事件完全隐藏
- **WHEN** 资源关联事件的可见级别为 `internal_only`
- **THEN** 代理和企业的活动时间线不返回该事件
- **AND** 不以占位记录或数量差异提示该事件存在
#### Scenario: 平台操作仅显示安全结论
- **WHEN** 平台内部操作影响代理或企业资源且可见级别为 `subject_result`
- **THEN** 活动时间线只返回稳定动作名称、安全业务结论、结果和发生时间
- **AND** 不表明该结论是否由平台人工操作、系统任务或内部补偿产生
#### Scenario: 主体自身操作显示允许的业务详情
- **WHEN** 代理或企业自身操作产生 `subject_detail` 事件
- **THEN** 活动时间线返回写入时按 Registry 白名单生成的 `subject_data`、允许公开的关联资源标识和处理结果
- **AND** 仍不返回平台内部字段
#### Scenario: 查询不得从内部字段临时生成主体详情
- **WHEN** 事件没有持久化 `subject_data`
- **THEN** 资源活动 Query 只返回已有安全结论,不读取内部 before/after 后删字段生成详情
### Requirement: 安全资源活动不得泄露内部调查数据
系统 MUST 从代理和企业活动响应中排除平台内部操作者、内部原因、内部备注、风险判断、内部前后数据、Audit Event ID、Integration Log 请求响应及系统安全凭据。
#### Scenario: 平台退款处理对外只展示结果
- **WHEN** 平台完成一笔影响代理资金的退款处理
- **THEN** 代理活动时间线可显示“退款资金已处理”等安全结论及最终结果
- **AND** 不返回平台操作人、人工处理方式、内部备注、风控原因或外部支付响应
#### Scenario: 外部系统交互不直接展示
- **WHEN** 某资源经历外部请求、重试、回调或查询确认
- **THEN** 代理和企业活动时间线只展示允许感知的最终业务结论
- **AND** 不返回第三方错误码、请求摘要、响应摘要、调用次数或 Integration Log 标识
### Requirement: 多资源事件必须进入每个有权资源的活动时间线
系统 SHALL 根据 Audit Event Resource 关联从不同资源入口投影同一业务操作,并 MUST 对每个入口分别执行资源归属和可见性校验。
#### Scenario: 设备换卡在相关资源时间线出现
- **WHEN** 一次换卡事件关联设备、旧卡和新卡且这些资源均属于当前主体范围
- **THEN** 系统在设备、旧卡和新卡的活动时间线中展示相应的安全业务结论
#### Scenario: 部分关联资源不属于当前主体
- **WHEN** 一个事件同时关联当前主体资源和其他主体或平台资源
- **THEN** 系统只返回当前主体有权查看资源对应的安全投影
- **AND** 不返回其他关联资源的内部标识、数量或详情
### Requirement: 主体资源活动只展示在线留存窗口
代理和企业资源活动 DTO MUST 返回 `retention{online_from, archived_before, timezone}`,并 MUST 只查询 PostgreSQL 在线 Audit Event。月初清理后的历史不从对象存储返回显式时间范围早于或跨越在线边界时 MUST 返回稳定的已归档错误,同时仍不得泄露平台内部事件或归档对象信息。
#### Scenario: 代理查看已归档月份的资源活动
- **WHEN** 代理对自身资源提交的显式时间范围早于 `online_from`
- **THEN** 系统返回统一的已归档提示和当前在线边界
- **AND** 不返回冷归档对象 Key、平台事件数量或任何内部调查字段

View File

@@ -0,0 +1,120 @@
## 0. 测试与现行契约准备
- [ ] 0.1 重新扫描当前 RouteSpec、Application、旧 Service、Worker、Scheduler、Callback、Outbox、Integration Log 和旧 Writer生成逐入口领域/动作/资源/事务/可见性矩阵,并逐项标明 Audit Event、Domain Ledger、Integration Log、Outbox 或 N/A 理由;旧 490 项仅用于遗漏比对,并增量维护 `.scratch/tech-global-audit/审计覆盖基线.md`。【主治理边界当前生产写入口及敏感读取不迁移业务实现验证清单无空白、敏感读取已单列、Setter/普通查询/装配误报已剔除并经业务/研发/安全复核】
- [ ] 0.2 为不可变事件、多资源约束、Action/Resource Registry、`subject_data` 白名单、安全凭据删除、内容哈希、JSON 大小限制、成功同事务、敏感读取失败关闭和失败短事务生成单元及真实 PostgreSQL 集成测试暂不实现生产代码。【主Infrastructure + Application边界统一写入闭环不迁移领域调用方验证运行目标测试并确认因能力缺失全部 FAIL】
- [ ] 0.3 为平台全局/操作者/资源/request/correlation/资金/风险查询、Integration overview/list/detail/attempts及代理/企业安全活动生成 Fiber + GORM 集成测试并覆盖筛选输入、视角跳转、认证上下文和服务端派生四类入参来源。【主Query/API边界第一阶段只读接口不迁移旧 operation log、前端页面与导出验证运行目标测试并确认路由或查询缺失导致 FAIL】
- [ ] 0.4 为账号权限、设备多卡槽、换货旧新资产、钱包扣款退款、支付回调、批量部分成功、系统任务和企业资源授权生成端到端业务流程测试,断言 HTTP 响应、Audit Event、Event Resource、Domain Ledger、Integration/Outbox 关联及外部安全投影。【主:验收|边界:代表性跨领域流程|不迁移:未触碰业务规则|验证:运行流程测试并确认审计断言 FAIL、既有业务断言保持现状】
- [ ] 0.5 固化第一阶段测试基线:记录所有新增测试的预期失败原因,确认没有因编译错误、环境误配或既有回归造成的伪失败,并冻结后续逐切片转绿顺序。【主:测试门禁|边界:新增审计测试|不迁移:生产代码|验证:测试报告逐项对应 specs Requirement/Scenario】
- [ ] 0.6 为 Audit/Integration 每日归档、manifest/hash、重复任务、Integration 月度最终 revision、缺日、对象损坏、月度物理删除阻断、断点续删和在线窗口错误生成单元及真实 PostgreSQL/对象存储集成测试暂不实现生产代码。【主Infrastructure + Application边界冷归档与留存闭环不迁移Access Log、对象存储历史查询/恢复|验证:运行目标测试并确认因能力缺失全部 FAIL既有日志写入不受影响】
## 1. 统一 Audit Event 首个纵向闭环
- [ ] 1.1 以“受控系统配置更新”为首个简单写纵向切片交付无外键迁移、Audit Event/Event Resource Model、Action/Resource Registry、凭据删除、不可变 Append Writer、结构体注入和同事务失败回滚配置凭据只记录 `credentials_configured` 等安全事实。【主:简单写 + Application/Port/AdapterInfrastructure边界系统配置更新 + 公共写接缝|不迁移:其他配置页面与其他领域|验证:迁移 up/down、Registry/哈希/凭据测试和配置事务集成测试 PASSLSP 无诊断】
- [ ] 1.2 以“Outbox 人工恢复裁决”为第二个高风险纵向切片,接入操作者、原因、批次、事件前后状态和 Audit Event 同事务失败关闭证明多资源与高风险策略可复用。【主Application + Port/AdapterInfrastructure边界既有 Outbox 恢复用例不迁移Outbox Relay 与消费者|验证:成功、重复恢复、有效租约拒绝、审计失败回滚测试 PASSLSP 无诊断】
- [ ] 1.3 交付已确定主要业务资源后的 `failed/denied` 独立短事务和二次失败 critical 日志/指标,以系统配置非法更新及 Outbox 不可恢复事件为代表用例;资源 ID 可空但必须有稳定 Key/快照,只有 actor 而无资源时进入 Access/Security Log不使用裸 goroutine。【主ApplicationInfrastructure/Observability边界失败审计公共接缝不迁移全局错误处理语义验证原业务错误保留、primary 约束、无资源分流、短事务失败指标测试 PASSLSP 无诊断】
- [ ] 1.4 交付 HTTP、OpenAPI、Worker、Scheduler 和 Callback 的 Audit Context 及 `request_id/correlation_id/parent_event_id` 传播接缝,以一个 HTTP→Outbox→Worker 流程证明系统子事件不伪造人工操作者。【主Application + Port/AdapterMiddleware/Asynq边界审计上下文与链路字段不迁移业务状态机验证上下文、载荷结构、父子链路和真实 actor 测试 PASSLSP 无诊断】
- [ ] 1.5 交付批次根事件、资源子事件和 `partial` 统计接缝以现有设备批量分配或资产批量购包小场景验证每个实际变化资源进入自身时间线未处理项不伪造成功。【主ApplicationAsynq/Infrastructure边界公共批量审计形态不迁移批量业务规则验证根子计数、父子链路、部分成功和幂等测试 PASSLSP 无诊断】
## 2. 平台基础审计调查纵向切片
- [ ] 2.1 交付平台身份保护的事件列表和详情纵向切片,包含组合筛选、稳定分页、操作者/入口/结果/风险、多资源角色、身份快照及资源级 before/after平台不做数据行过滤代理/企业/个人后端拒绝。【主QueryAPI/Infrastructure边界全局事件与详情不迁移细粒度权限码、导出和修改验证Fiber 响应、身份隔离、筛选、分页、无 N+1 和数据库 <50ms 测试 PASSLSP 无诊断】
- [ ] 2.2 交付操作者行为视角支持人工账号、OpenAPI、系统任务和外部系统 actor返回历史名称快照及 action/result/risk/resource 筛选。【主Query边界actor 时间线|不迁移:用户画像或风控处置|验证:账号改名/删除、系统 actor、不伪造发起人和稳定分页测试 PASSLSP 无诊断】
- [ ] 2.3 交付 Resource Registry 搜索与通用资源时间线,以卡 ICCID/VirtualNo、设备 VirtualNo/IMEI/SN、店铺、订单和退款为首批 resolver资源删除或换号后使用事件快照解释历史。【主QueryResource Resolver边界注册资源精确/索引搜索与 timeline不迁移任意 JSON 模糊搜索和关系图验证资源搜索、primary/affected/reference、历史快照、稳定分页和查询计划测试 PASSLSP 无诊断】
- [ ] 2.4 新增平台审计 Handler/DTO/RouteSpec 并完成生产结构体注入、路由注册、共享文档 Handler、`cmd/api/docs.go``cmd/gendocs/main.go` 和中文错误码/文档;按 design 8.1-8.4 固化卡、设备、设备卡槽、统一资产、分配、换货、账号、店铺、企业、订单、退款、充值和钱包页面的 `response.data` 字段到目标接口参数映射,并冻结 `investigation_refs` 与降级规则,保证两条 OpenAPI 生成路径包含全部基础调查接口。【主API/Infrastructure边界2.1-2.3 Handler 契约|不迁移:前端实现|验证:逐矩阵 HTTP 参数映射、缺字段隐藏、伪造身份、调查节点跳转、静态/动态路由顺序、两份 OpenAPI 和构建检查 PASSLSP 无诊断】
- [ ] 2.5 交付 Action Registry 标记的敏感读取审计,以受控审计详情或明文业务凭证读取为代表:返回结果前写入读取者、目标资源和字段类别,写入失败则不返回结果;普通列表/详情仍按 N/A 契约执行。【主Query + Application/Port边界敏感读取例外不迁移细粒度权限码与敏感二次查看 UI验证成功读取、审计失败关闭、普通读取不写和安全凭据永不返回测试 PASSLSP 无诊断】
## 3. Integration Log 只读调查中心
- [ ] 3.1 交付 Integration Log 组合筛选列表与稳定 `integration_id` 详情纵向切片,返回 identity/resource/trigger/result/content/linkage/timestamps 分组、原始结果、中文名和派生类别,不直接暴露 GORM Model。【主QueryAPI边界现有 Integration Log 只读投影不迁移Writer 业务语义与恢复操作|验证:全部筛选、空页、不存在、统一响应和无 N+1 测试 PASSLSP 无诊断】
- [ ] 3.2 交付受时间范围约束的 Integration overview覆盖总量、结果/provider/direction、异常、unknown、陈旧 pending、状态变化、平均/P95 耗时与趋势,严格区分 success、processing、indeterminate、failed 和 not_sent。【主Query边界固定聚合维度不迁移告警处置和自动恢复验证聚合语义、时间边界、completed 不计 success 和查询计划测试 PASSLSP 无诊断】
- [ ] 3.3 交付显式 `trigger_series + attempt` 尝试序列,并增量修正新外呼的 series/correlation 传播、correlation 长度、真实资源 ID 和安全可读错误摘要;历史缺字段时标记受限,旧 `request_summary/response_summary/metadata` 返回前按白名单和凭据删除规则重新清理,禁止直接透传或按相似资源/时间猜测重试。【主Infrastructure Adapter + Query边界Integration 链路可解释性与历史读取安全不迁移历史伪回填与多资源子表验证序列排序、correlation 非重试、ICCID hash 兼容、历史 fidelity、敌对历史 JSON 和凭据不返回测试 PASSLSP 无诊断】
- [ ] 3.4 为现有 Integration 表补受控 B-tree 索引并交付 overview/list/detail Handler、RouteSpec、生产装配和两条 OpenAPI 生成入口;明确筛选来自调查输入或关联视角跳转、`integration_id` 来自列表/通知 target/调查节点、身份来自认证上下文,只注册 GET不提供重试、补偿、确认、绑定、恢复、修改、删除或导出路由。【主Query/API + Migration边界Integration 调查中心不迁移JSONB GIN 与任意全文搜索|验证:迁移 up/down、索引命中、通知 `target_key→integration_id`、不可用目标、HTTP/OpenAPI、只读路由扫描和性能目标 PASSLSP 无诊断】
## 4. 代理/企业安全资源活动与旧历史入口隔离
- [ ] 4.1 交付代理资源活动纵向切片:按当前资源店铺及自身/下级店铺范围校验卡、设备、分配、换货、店铺和归属企业,读取 `subject_result/subject_detail`,完全隐藏 `internal_only`、平台操作者、内部原因/备注、风险、内部 before/after、Audit Event ID 和 Integration 内容。【主QueryAPI/数据权限|边界:代理范围资源活动|不迁移:平台审计 DTO 与权限码|验证:各类稳定 identifier、有权、越权/不存在同错、分配详情独立归属、无事件存在性泄露、多资源部分可见测试 PASSLSP 无诊断】
- [ ] 4.2 交付企业资源活动纵向切片:卡与设备分别复用当前有效企业授权关系,禁止在企业缺少 SubordinateShopIDs 时退化为全量撤销授权后不可继续读取资源活动。【主QueryAPI/授权 Store边界企业卡/设备活动|不迁移:企业授权业务规则|验证:有效/撤销授权、无权同错、设备关联卡和安全字段测试 PASSLSP 无诊断】
- [ ] 4.3 交付资源关系级 `internal_only/subject_result/subject_detail`、稳定 `subject_summary` 和 Registry 白名单约束的 `subject_data` 写入契约,以平台退款处理、主体自身操作、内部补偿和外部回调为代表场景,查询时不读取内部 before/after 后删字段生成外部结果。【主Application + Query边界主体可见性快照不迁移前端文案系统验证三种级别、subject_data 白名单、平台过程不可感知、最终业务结论正确和凭据缺失测试 PASSLSP 无诊断】
- [ ] 4.5 新增 `GET /api/admin/agent/resource-activities/{resource_type}/{identifier}``GET /api/admin/enterprise/resource-activities/{resource_type}/{identifier}` 的 Handler/DTO/RouteSpec冻结 `page/page_size`、资源摘要和 activity 字段;按 design 8.1 复用卡 ICCID、设备 VirtualNo、分配单号、换货单号、店铺编号、企业编号及企业受权资产列表字段主体范围只来自认证上下文缺少 identifier 时不展示入口。先上线安全接口,再把旧资产 operation-logs 收缩为仅平台历史入口,并同步生产装配、共享文档 Handler、两个 OpenAPI 生成入口和中文接口文档。【主API/Query边界expand 阶段兼容切换不迁移旧日志迁移、删除旧路由验证design 8.1 全部主体映射、企业禁止 resolve、缺字段隐藏、伪造范围、分配详情独立归属、不可见字段、授权撤销、旧接口身份收缩、两份 OpenAPI 和构建检查 PASSLSP 无诊断】
## 5. 账号、权限、组织与主体写入口迁移
- [ ] 5.1 迁移账号创建、基础资料更新、启停和软删除纵向用例到统一 Writer关联账号、店铺/企业和实际角色资源,已定位账号的失败/拒绝进入短事务;逐用例转绿后停止对应旧 account operation log 写入。【主:简单写/Application + 旧 Service Adapter边界账号生命周期不迁移凭据和第三方绑定验证各状态操作、资源快照、失败/拒绝、旧写逐项归零和 HTTP 测试 PASSLSP 无诊断】
- [ ] 5.2 迁移改密、手机号安全操作、企微绑定及登录/登出安全状态纵向用例记录账号与认证资源但删除密码、验证码、Token、Cookie 等凭据。【主:简单写/Application + 旧 Service Adapter边界账号安全操作不迁移认证协议重构验证成功/拒绝/失败、凭据数据库抽样、actor/request 关联和旧写归零测试 PASSLSP 无诊断】
- [ ] 5.3 迁移角色 CRUD、权限 CRUD 和角色权限配置纵向用例,保存角色/权限 code/name 和前后差异,高风险成功与业务事实同事务。【主:简单写 + Application边界角色权限配置不迁移账号角色分配与新权限体系验证CRUD/拒绝、before/after、同事务和资源时间线测试 PASSLSP 无诊断】
- [ ] 5.4 迁移账号角色、店铺角色的分配与移除纵向用例,分别关联目标账号、角色和店铺并停止对应旧账号日志写入。【主:简单写 + Application边界主体授权关系不迁移权限码重构验证分配/移除/重复/拒绝、资源角色、旧写归零和 actor 查询测试 PASSLSP 无诊断】
- [ ] 5.5 迁移店铺创建、基础资料和层级变更纵向用例,保存店铺编码/名称/上级/层级及操作者,不借审计改变层级规则。【主:简单写 + Application边界店铺身份与层级不迁移状态、业务员和登录限制验证创建/更新/层级拒绝、历史快照和店铺时间线测试 PASSLSP 无诊断】
- [ ] 5.6 迁移店铺启停、删除、业务员绑定和 C 端登录限制纵向用例,主体活动仅输出允许感知结论,逐用例停止旧账号日志借用写入。【主:简单写 + Application边界店铺状态与关键配置不迁移全模块 DDD验证状态/绑定/拒绝、subject 投影、旧写归零测试 PASSLSP 无诊断】
- [ ] 5.7 迁移企业创建、资料/状态/凭据管理纵向用例,关联企业和 owner shop保持现有企业规则并删除安全凭据。【主简单写 + Application边界企业生命周期不迁移资产授权验证CRUD/启停/改密、凭据删除、actor/scope 和时间线测试 PASSLSP 无诊断】
- [ ] 5.8 迁移企业卡授权/回收纵向用例关联企业、owner shop、卡和授权记录并保持现有有效授权语义。【主简单写 + Application边界企业卡授权不迁移设备授权验证授权/撤销/重复/越权、卡活动和企业范围测试 PASSLSP 无诊断】
- [ ] 5.9 迁移企业设备授权/回收纵向用例,关联企业、设备及实际随设备处理的绑定卡,保持现有授权语义。【主:复杂写或简单写事务脚本|边界:企业设备授权|不迁移:卡槽绑定规则|验证:多卡设备授权/撤销/越权、资源关系和企业活动测试 PASSLSP 无诊断】
- [ ] 5.10 迁移个人客户资料、手机号和微信主体纵向用例,记录真实个人 actor不把普通查询或 Token 刷新误记为业务操作。【主:简单写 + Application边界个人客户身份资料不迁移资产绑定验证资料/手机号/微信变更、失败、凭据删除和 subject_detail 测试 PASSLSP 无诊断】
- [ ] 5.11 迁移个人客户卡/设备绑定、解绑和换货绑定迁移纵向用例,关联客户、资产和绑定记录。【主:简单写或复杂写 Application边界客户资产关系不迁移换货其他资金/套餐步骤|验证:绑定/解绑/迁移、资源快照、越权和多资源活动测试 PASSLSP 无诊断】
## 6. 卡、设备、绑定、分配与换货迁移
- [ ] 6.1 迁移 IoT 卡创建/导入落库、基础资料、删除纵向用例,快照含 ID/ICCID/VirtualNo/MSISDN/运营商/店铺/系列/generation。【主简单写 + 旧 Service Adapter边界卡身份生命周期不迁移状态与外部命令验证创建/更新/删除、历史快照、凭据规则和旧资产写归零测试 PASSLSP 无诊断】
- [ ] 6.2 迁移 IoT 卡分配、回收和系列绑定纵向用例,关联分配记录、来源/目标店铺、系列和必要设备关系。【主:复杂写或简单写事务脚本|边界:卡归属与系列|不迁移:卡状态/Gateway验证分配/回收/拒绝、多资源、主体活动和旧写归零测试 PASSLSP 无诊断】
- [ ] 6.3 迁移 IoT 卡停复机、实名策略/状态、限速和人工刷新纵向用例,实际外部尝试写 Integration Log内部状态变化写 Audit Eventunknown 不伪装失败或成功。【主:复杂写 + Gateway Adapter边界卡状态与外部命令不迁移卡归属验证success/failed/denied/unknown、Integration/Audit 分界、主体结论和旧写归零测试 PASSLSP 无诊断】
- [ ] 6.4 迁移设备创建/导入落库、基础资料和删除纵向用例,快照含 ID/VirtualNo/IMEI/SN/型号/店铺/系列/generation。【主简单写 + 旧 Service Adapter边界设备身份生命周期不迁移状态、卡槽与 Gateway验证创建/更新/删除、历史快照和旧资产写归零测试 PASSLSP 无诊断】
- [ ] 6.5 迁移设备分配、回收和系列/实名策略纵向用例,关联分配记录、来源/目标店铺及实际连带卡。【主:复杂写或简单写事务脚本|边界:设备归属与策略|不迁移:设备外部命令|验证:分配/回收/策略、设备卡关系、主体活动和旧写归零测试 PASSLSP 无诊断】
- [ ] 6.6 迁移设备停复机、Wi-Fi、切卡模式、重启和重置纵向用例外部尝试写 Integration Log实际内部变化写 Audit Event。【主复杂写 + Gateway Adapter边界设备外部命令不迁移设备归属与卡槽绑定验证success/failed/unknown、设备时间线、安全结论和旧写归零测试 PASSLSP 无诊断】
- [ ] 6.7 迁移设备绑卡、解绑和当前卡切换纵向用例,把设备、目标卡、旧/新当前卡及每个 binding 作为一等资源,保存 slot/is_current。【主复杂写边界设备卡槽关系完整用例不迁移设备/卡其他状态机验证1-4 卡槽、旧新卡角色、资源双向时间线、事务和主体范围测试 PASSLSP 无诊断】
- [ ] 6.8 迁移单笔资产分配/回收纵向用例,关联分配记录、来源/目标店铺、设备及实际连带卡。【主:复杂写 Application边界单笔资产流转不迁移批量分配和企业授权验证卡/设备流转、多资源、越权和每资源时间线测试 PASSLSP 无诊断】
- [ ] 6.9 迁移批量设备分配纵向用例,批次根事件与每台实际变化设备/绑定卡子事件计数一致。【主:复杂写 + Application/Asynq边界批量设备流转不迁移其他批量任务验证success/partial/failed、根子计数、幂等和店铺范围测试 PASSLSP 无诊断】
- [ ] 6.10 迁移卡换货完整用例,记录换货单、旧/新卡 ICCID+VirtualNo、客户绑定、钱包/流水、套餐权益、店铺和状态变化。【主:复杂写 Domain/ApplicationOutbox/Query边界卡换货创建至完成/取消/迁移|不迁移:设备换货|验证:资金/套餐/客户迁移、失败回滚、多资源时间线和外部安全结论测试 PASSLSP 无诊断】
- [ ] 6.11 迁移设备换货完整用例,记录旧/新设备 VirtualNo+IMEI+SN、实际绑定卡/卡槽、客户绑定、钱包/流水、套餐权益、店铺和换货单。【主:复杂写 Domain/ApplicationOutbox/Query边界设备换货完整流程不迁移无关订单规则验证多卡设备换货、资金/套餐/客户迁移、失败回滚和所有资源时间线测试 PASSLSP 无诊断】
## 7. 套餐、交易与资金纵向迁移
- [ ] 7.1 迁移套餐系列、套餐商品、店铺系列/套餐分配、批量定价和关键价格配置纵向用例,关联店铺、系列、套餐和价格历史,保持现有上架/分配规则。【主:简单写 + Application边界套餐配置与授权不迁移套餐购买流程验证before/after、批量根子、店铺时间线、凭据规则和旧 account 借用写归零测试 PASSLSP 无诊断】
- [ ] 7.2 迁移套餐权益激活、排队、流量重置/扣减、退款失效、资产失效和换货迁移纵向用例,系统任务使用真实 actor权益、订单、套餐和资产作为独立资源。【主复杂写 Domain/ApplicationWorker/Outbox边界package_usage 生命周期|不迁移:套餐列表 Query验证状态条件、重复任务幂等、系统 actor、父子链路和资源时间线测试 PASSLSP 无诊断】
- [ ] 7.3 迁移订单创建、后台代购、C 端/OpenAPI 购买、取消、钱包支付和过期关闭纵向用例,关联买家/操作者、资产、套餐、金额、支付方式、钱包/流水和购买角色。【主:复杂写 Domain/ApplicationOutbox边界订单状态与支付准备不迁移订单只读列表验证各入口 actor、成功/拒绝/失败、资金同事务、系统过期和链路测试 PASSLSP 无诊断】
- [ ] 7.4 迁移支付创建、微信/支付宝/富友预下单、查单和回调确认纵向用例:每次外部尝试写 Integration Log实际改变支付/订单/充值事实时写 Audit Event关联支付单、业务单、渠道交易号和 correlation。【主复杂写 + Infrastructure Adapter边界支付外部与内部终态不迁移渠道协议重构验证success/unknown/迟到回调/重复回调、series/correlation、Domain Ledger 和时间线测试 PASSLSP 无诊断】
- [ ] 7.5 迁移退款申请、审批终态、钱包回充、佣金失效、套餐/资产后处理和通知纵向用例,关联退款、审批、订单、资产、钱包、原扣款/退款流水、佣金和套餐权益,资金事实与审计同事务。【主:复杂写 Domain/ApplicationOutbox/Integration边界退款完整业务链不迁移原路退款等未实现能力验证通过/拒绝/重提/重复终态/失败回滚、资金时间线和代理安全结论测试 PASSLSP 无诊断】
- [ ] 7.6 迁移个人资产充值和代理在线/线下充值纵向用例,关联充值单、提交人、店铺/资产、支付或审批、钱包、交易流水和自动购包;外部回调与系统恢复使用真实 actor。【主复杂写 Domain/ApplicationPayment/Approval/Outbox边界充值创建至入账不迁移新充值渠道验证在线/线下、回调/审批、重复入账、unknown、资金同事务和主体投影测试 PASSLSP 无诊断】
- [ ] 7.7 迁移代理主钱包订单扣款和预占/释放/完成纵向用例,关联订单、钱包、预占、唯一流水和余额前后值。【主:复杂写 Domain/ApplicationOutbox边界订单资金占用与扣款不迁移充值/退款/信用额度|验证:乐观锁/状态条件、唯一业务键、审计失败回滚和失败短事务测试 PASSLSP 无诊断】
- [ ] 7.8 迁移代理主钱包充值入账、人工调整、退款回充和信用额度纵向用例,逐项关联业务单、原流水/新流水和余额前后值。【主:复杂写 Domain/ApplicationOutbox边界代理钱包正向及回退资金不迁移资产钱包与佣金提现验证重复入账/退款上限/人工原因/信用变更、同事务和资金查询测试 PASSLSP 无诊断】
- [ ] 7.9 迁移卡/设备资产钱包充值、扣款、退款和换货迁移纵向用例,关联资产完整标识、钱包、业务单和唯一流水。【主:复杂写 Domain/Application边界资产钱包资金不迁移代理主钱包验证卡/设备、换货、重复业务键、余额前后值和资源时间线测试 PASSLSP 无诊断】
- [ ] 7.10 迁移佣金计算/入账/失效和提现申请/审批/驳回纵向用例,关联店铺、订单、系列、佣金记录、提现单、钱包/流水和金额状态。【主:复杂写 Domain/ApplicationWorker/Outbox边界佣金与提现完整状态机不迁移统计 Query验证计算幂等、审批终态、资金同事务、失败短事务和资金时间线测试 PASSLSP 无诊断】
## 8. 审批、配置、批量与自动入口迁移
- [ ] 8.1 迁移通用审批和企微申请提交、回调同步、主动恢复及终态分发纵向用例,记录真实提交人、外部系统/系统任务 actor、审批实例、业务单、Integration Log 和 Outbox不伪造本地审批人。【主复杂写 Domain/ApplicationWeCom Adapter/Outbox边界审批完整链路不迁移新审批引擎验证提交、unknown、回调、恢复、重复终态、actor/correlation 和资源时间线测试 PASSLSP 无诊断】
- [ ] 8.2 迁移支付配置、运营商、企微应用/成员/场景及其他关键连接配置纵向用例,记录配置身份、状态和“凭据是否已配置”,不记录 Secret、Token、AESKey、私钥或证书正文。【主简单写 + Infrastructure Adapter边界外部连接配置不迁移配置 UI 与渠道业务协议验证CRUD/启停/校验失败、同事务、凭据删除和平台完整业务字段测试 PASSLSP 无诊断】
- [ ] 8.3 迁移卡/设备导入、资产套餐批购、订单套餐失效和导出任务创建/取消纵向用例,记录任务、文件名/目标/操作者和批量根子结果;对象存储签名 URL 不入审计审计中心自身仍不提供导出。【主Application + AsynqObject Storage边界现有导入批量和业务导出任务动作不迁移导出 DataSource 内容|验证:结构化 payload、幂等、partial、凭据删除和任务/资源时间线测试 PASSLSP 无诊断】
- [ ] 8.4 迁移通知生成、投递、单条/全部已读和清理纵向用例,低风险写仍登记 action系统清理使用系统 actorAudit Event 不替代通知或 Outbox 投递事实。【主:简单写/Application + Worker边界通知状态变化不迁移通知查询验证人工/系统 actor、低风险默认展示、Outbox 幂等和资源关联测试 PASSLSP 无诊断】
- [ ] 8.5 迁移轮询配置、并发配置、告警规则、人工触发/取消和状态变化纵向用例;手动轮询表继续承担进度/结果,实际 Gateway 尝试写 Integration Log人工触发动作另写 Audit Event。【主Application + AdapterScheduler/Query边界轮询配置与人工动作不迁移删除手动任务表和普通运行查询验证配置/触发/拒绝、任务 ledger 保留、Integration/Audit 分界和 actor 测试 PASSLSP 无诊断】
- [ ] 8.6 迁移支付、运营商实名和企微等外部 Callback 中实际改变内部事实的入口,使用 external actor无状态变化只保留 Integration Log。【主Application/Infrastructure Adapter边界当前外部回调不迁移渠道协议验证逐回调 action、幂等、资源解析、Integration/Audit 分界和链路测试 PASSLSP 无诊断】
- [ ] 8.7 迁移当前 Worker 中实际改变内部业务事实的入口,使用 system_task actor 并传播 correlation/parent纯投递或技术装配按清单 N/A。【主Application/Asynq边界当前 Worker 写入口不迁移Scheduler 与 Callback验证逐 Worker 覆盖、幂等、失败重试和未登记动作门禁 PASSLSP 无诊断】
- [ ] 8.8 迁移当前 Scheduler 中实际创建任务、改变配置/状态或产生业务事实的入口,使用 scheduled_job actor实施时以当前清单为准不硬编码历史数量。【主Application/Scheduler边界当前计划任务不迁移Worker 消费逻辑|验证:逐 Scheduler 覆盖、重复调度幂等、父子链路和 N/A 理由 PASSLSP 无诊断】
- [ ] 8.9 迁移当前 Outbox 消费者中实际形成新业务事实的入口,保留 Outbox 投递事实并为内部变化写 system_task Audit Event不把投递成功伪装成业务成功。【主Application/Outbox Consumer边界当前可靠事件消费者不迁移Relay 实现|验证:逐消费者覆盖、至少一次幂等、业务/投递结果分离和 correlation 测试 PASSLSP 无诊断】
## 9. 跨视角调查与性能收口
- [ ] 9.1 交付 request 和 correlation 组合时间线,按 `record_source` 组合 Audit Event、Integration Log、Outbox/Asynq 摘要及 Domain Ledger 引用,不扫描 Access Log、不猜测历史链路或技术重试。【主Query边界跨事实只读投影不迁移Access Log 存储与关系图|验证:支付、退款、审批、异步链路、历史缺字段和稳定排序测试 PASSLSP 无诊断】
- [ ] 9.2 交付资金调查时间线,按店铺、钱包、订单、支付、退款、充值、审批、交易号、操作者、时间和 correlation 查询并明确金额权威来自钱包流水及业务表。【主Query边界资金多源投影不迁移资金重算或状态修改验证余额变化、退款、充值、佣金、来源冲突和权限身份测试 PASSLSP 无诊断】
- [ ] 9.3 交付风险 overview/events聚合高风险、资金、安全、失败、拒绝、partial 和 unknown支持跳转事件、资源、actor 和 correlation不建设处置工单或自动封禁。【主Query边界固定风险调查视角不迁移风控决策系统验证计数/趋势、低风险排除、跳转和时间范围性能测试 PASSLSP 无诊断】
- [ ] 9.4 为事件、资源、actor、action/result/risk、scope、request、correlation、parent 及跨视角常用过滤补最小索引,使用先分页 ID 后批量投影避免 N+1不为第一阶段增加 JSONB 任意模糊搜索或 Redis 结果缓存。【主Query/Infrastructure + Migration边界已确认查询路径不迁移数据库月分区与冷热联合查询验证迁移 up/down、EXPLAIN/基准、数据库 <50ms、API P95/P99 和并发分页测试 PASSLSP 无诊断】
- [ ] 9.5 完成跨视角 Handler/DTO/RouteSpec、生产装配、共享文档 Handler、`cmd/api/docs.go``cmd/gendocs/main.go` 和中文功能/API 文档,前端契约必须逐行呈现 design 8.1-8.4 的源页面、前置接口、`response.data` 字段、入口名称/可见条件、目标接口、参数映射和降级行为,不得只罗列审计 API。【主API/Documentation边界9.1-9.3 查询接口|不迁移:前端页面实现|验证:以资产详情、订单、退款、钱包、通知、风险节点逐条演示完整调用链,两条 OpenAPI、README 索引和构建检查 PASSLSP 无诊断】
## 10. 每日冷归档与月初受控清理
- [ ] 10.1 交付 Audit Event + Event Resource 每日归档纵向切片:新增无外键 `tb_log_archive_run`、按 `Asia/Shanghai` 前一完整自然日读取、事件携带完整 resources 的 JSONL+gzip、manifest/SHA-256、对象 metadata 复核、稳定对象 Key 与重复任务幂等;复用现有对象存储和 Asynq不阻塞业务 Writer。【主Infrastructure + Application/Asynq边界Audit 每日冷归档完整闭环不迁移Integration、Access、清理与归档查询验证迁移 up/down、空日/大日、事件资源计数、对象损坏、重复投递、存储故障和业务写入隔离测试 PASSLSP 无诊断】
- [ ] 10.2 交付 Integration Log 每日归档及月度最终 revision 纵向切片:每日保存创建日快照,月初按数据库当前内容复核可变记录,内容变化时创建不可变新 revisionpending 或不一致阻止清理不覆盖旧对象。【主Infrastructure + Application/Asynq边界Integration 冷归档最终性不迁移Integration Writer 语义、恢复和对象存储查询验证pending→success/unknown、跨月更新、hash 变化、重复复核、无法终结和对象版本测试 PASSLSP 无诊断】
- [ ] 10.3 交付月初整月数据库物理删除纵向切片:先完成上月最后一天归档,再校验全部日期的 Audit/Integration manifest、Audit 事件与资源数、Integration 最终 revision、对象大小和 SHA-256通过后使用 GORM、索引和有界批次按先 Event Resource 后 Event、再 Integration 的受控流程物理 `DELETE` PostgreSQL 上月数据,任务可从 ledger 断点继续,并写当前月 `retention_worker` Audit Event审计 Model 不含 `gorm.DeletedAt`不写归档状态、不迁移数据库历史表、不删除对象存储备份。【主Application + Infrastructure/Asynq边界归档门禁与 PostgreSQL 上月数据物理删除不迁移Access Log、Domain Ledger、Outbox、旧 operation log、对象存储生命周期、分区改造和人工清表验证缺日/hash 不一致/部分对象/pending 全部阻断,物理行数归零、批次中断续跑、窗口隔离、对象长期保留、清理事件和数据库膨胀指标测试 PASSLSP 无诊断】
- [ ] 10.4 为平台 Audit/Integration 及代理/企业活动 DTO 增加 `retention{online_from,archived_before,timezone}`显式时间范围早于或跨越在线边界时返回稳定已归档错误默认只查在线窗口ID-only 不存在仍按资源不存在处理不访问对象存储也不新增下载或恢复路由。【主Query/API边界在线查询留存语义不迁移冷热联合查询和前端页面实现验证在线、已归档、跨边界、默认范围、空在线月、路由/OpenAPI 无归档读写能力测试 PASSLSP 无诊断】
- [ ] 10.5 先以“只归档、不清理”开关运行并验收一个完整自然月记录每日成功率、积压、对象大小、压缩率、hash/计数差异、Integration revision、清理预估耗时和告警只有月度演练全部通过后才启用清理开关。【主Release/Observability边界留存能力灰度启用不迁移Access Log、自动恢复和分区改造验证完整月 manifest、故障重试、月度 dry-run、监控阈值、操作手册和启停回滚证据齐全】
## 11. 旧 Writer Contract 与发布门禁
- [ ] 11.1 完成账号、资产及借用旧 account audit 的充值/套餐/支付配置等调用清单逐项归零,删除生产组合根对旧 Writer 的注入和所有裸/双重 goroutine 审计写入;旧表原样保留且不接入新审计 Query既有旧资产历史入口与手动轮询运行写入显式白名单。【主Infrastructure/Governance边界旧 operation log contract不迁移旧数据回填/转换、删除旧表和历史数据|验证:静态扫描、生产装配、统一 Query 无旧表依赖、真实业务写入和旧表无新增测试 PASSLSP 无诊断】
- [ ] 11.2 执行最终 Action/Resource Registry 与当前代码覆盖比对并增量更新 `.scratch/tech-global-audit/审计覆盖基线.md`,确认所有非查询写入口和 Registry 标记的敏感读取均登记、普通查询/N/A 有理由,多资源/设备卡槽/换货/批量/资金/自动入口无遗漏未注册动作或资源使门禁失败。【主治理边界全仓当前入口不迁移未来未提交功能验证覆盖门禁、Registry 清单、基线文件和业务/研发/安全签字证据齐全】
- [ ] 11.3 执行安全与身份发布门禁:平台接口仅 SuperAdmin/Platform、代理店铺层级、企业有效授权、internal_only 不泄露、业务字段平台完整、所有系统安全凭据在库和响应中均不存在、无用户审计导出、对象存储查询/恢复或业务删除路由。【主Security/API边界第一阶段访问与字段契约不迁移细粒度权限码验证敌对 HTTP 场景、数据库抽样、路由/OpenAPI 扫描全部 PASS】
- [ ] 11.4 执行事务、幂等与跨链路发布门禁关键成功审计失败回滚、失败短事务保留原错、批量根子计数一致、correlation/parent/series 正确、Audit/Integration/Domain Ledger/Outbox 边界没有混用。【主Verification边界高风险和代表性流程不迁移业务规则优化验证全部验收测试和业务流程测试 PASS已知错误为零】
- [ ] 11.5 执行迁移、性能、OpenAPI、归档留存和回滚演练增量迁移可在无事实环境 down业务回滚保留在线窗口内事实归档失败阻止清理清理后不恢复旧 Writer或伪造在线历史记录监控阈值和操作手册。【主Release/Infrastructure边界一次 contract 发布|不迁移:数据库分区、冷热联合查询与自动恢复|验证:迁移演练、性能目标、归档 dry-run、两份文档生成、构建、监控和回滚演练全部 PASS】
## 12. 最终文档与交付确认
- [ ] 12.1 在 `docs/feature-504-multi-view-audit-center/` 编写中文总结,包含四类事实边界、字段字典、当前领域/资源/动作矩阵、各资源快照、所有查询视角与 DTO、平台和主体可见性、多卡设备/换货/资金/批量样例、每日归档与月度清理、在线窗口、异常闭环、发布回滚、监控和明确未实现项,并更新 README 索引。【主Documentation边界本 Change 完整交付不迁移旧历史评审文档验证文档逐项引用实现、OpenAPI 和验收证据,无过时字段或路径】
- [ ] 12.2 汇总测试、静态扫描、LSP、迁移、构建、OpenAPI、数据库性能、API P95/P99、身份隔离、凭据抽样、旧写归零、归档完整性、月度清理 dry-run 和业务流程证据,逐条核对 proposal/design/specs/tasks只有全部完成且无已知错误时才标记 Change 可归档。【主:最终验收|边界:全部能力|不迁移:后续权限码、用户导出、对象存储历史查询/恢复、分区、风险处置和自动恢复|验证:全部验收测试 PASS + 全部流程测试 PASS + OpenSpec strict validate PASS】