重置项目上下文与规范文档

This commit is contained in:
2026-08-07 16:18:07 +08:00
parent 6611ca5226
commit 79e2d9ff92
1900 changed files with 1552 additions and 348365 deletions

View File

@@ -1,126 +0,0 @@
Status: ready-for-agent
# PRD客户绑定体系重构与资产归属验证统一
## Problem Statement
C 端用户在使用没有虚拟号的 IoT 卡时,整个业务流程完全失效:登录后无法实名认证、无法购买套餐、无法操作设备。根本原因是系统的客户绑定机制依赖虚拟号(`virtual_no`)作为唯一标识,而虚拟号在 IoT 卡上是可选字段,线上有 12,000+ 张无虚拟号的卡且确认会流向 C 端用户。
与此同时,换货流程在旧卡有虚拟号、新卡无虚拟号时会被错误拦截,即使旧卡实际上没有任何客户绑定记录,换货依然报错"新资产无法承接客户绑定"。
从设计层面看,`tb_personal_customer_iccid` 表(按 ICCID 绑定早已建好Store 层也实现完整,但从未被任何 Service 或 Handler 接入。同时,"判断某张卡/设备是否属于某个客户"这个核心安全规则,散落在 6 个文件中以 4 种不同方式实现,部分实现缺少对已禁用绑定记录的过滤,存在安全缺口。
## Solution
建立统一的 **CustomerBinding 模块**,封装客户与资产之间的绑定创建和归属验证逻辑,屏蔽"有虚拟号走 `tb_personal_customer_device` / 无虚拟号走 `tb_personal_customer_iccid`"的路由细节。所有调用方只需传入资产信息,不感知底层用了哪张表。
在此基础上,修复换货服务中校验与执行逻辑混淆的 bug并将资产解析和 IoT 卡状态字段整理为后续阶段任务。
## User Stories
### C 端用户(无虚拟号的卡)
1. 作为一个持有无虚拟号 IoT 卡的 C 端用户,我想在首次登录时成功绑定我的卡,以便后续能正常使用所有 C 端功能。
2. 作为一个持有无虚拟号 IoT 卡的 C 端用户,我想在绑定卡后能正常提交实名认证,以便激活卡的完整功能。
3. 作为一个持有无虚拟号 IoT 卡的 C 端用户,我想购买流量套餐,以便为我的卡充值续费。
4. 作为一个持有无虚拟号 IoT 卡的 C 端用户,我想在换货后仍能访问新卡并正常操作,以便换货不影响我的使用体验。
### C 端用户(有虚拟号的卡)
5. 作为一个持有有虚拟号 IoT 卡的 C 端用户,我的所有现有功能不受本次改动影响,以便升级对我透明无感知。
6. 作为一个持有有虚拟号 IoT 卡的 C 端用户,当我的绑定关系被禁用后,我不能通过该绑定关系访问资产,以便系统安全边界得到保障。
### 后台运营人员(换货)
7. 作为后台运营人员,我想将一张有虚拟号的旧卡换货为一张无虚拟号的新卡,并且换货能正常完成,以便不再因无虚拟号而被系统错误拦截。
8. 作为后台运营人员,当旧卡有客户绑定记录、新卡无虚拟号时,我希望系统在换货完成后自动将客户绑定迁移到新卡的 ICCID以便客户换货后仍能访问新卡。
9. 作为后台运营人员,当旧卡没有任何客户绑定记录时,无论旧卡是否有虚拟号,换货都应该正常完成,以便换货流程不被无意义的条件拦截。
### 系统(数据一致性)
10. 作为系统,当客户首次绑定一张无虚拟号的卡时,应正确将该卡的 `asset_status` 从在库更新为已销售,以便轮询系统能感知到该卡有真实用户在使用。
11. 作为系统,当客户的绑定关系被禁用(`status=0`)后,归属验证应拒绝该客户访问对应资产,以便被撤销的绑定不再赋予访问权限。
## Implementation Decisions
### 1. 新增 CustomerBinding 模块
`internal/service/` 下建立 `customer_binding` 包,对外暴露两个核心接口:
- `Bind(ctx, tx, customerID, assetType, assetID)` — 创建或更新客户与资产的绑定关系;对于卡资产,有虚拟号写 `tb_personal_customer_device`,无虚拟号写 `tb_personal_customer_iccid`
- `OwnsAsset(ctx, customerID, assetType, assetID) bool` — 验证客户是否持有该资产的有效绑定(`status=1`);按资产类型路由到对应绑定表查询
- `Migrate(ctx, tx, oldAsset, newAsset)` — 换货时迁移绑定关系;处理四种组合(有→有、有→无、无→有、无→无)
`OwnsAsset` 内部统一强制 `status=1` 过滤,解决当前部分实现缺少此过滤的安全缺口。
### 2. 绑定路由规则(卡资产)
| 旧卡 | 新卡 | Migrate 行为 |
|------|------|-------------|
| 有虚拟号,有 pcd 绑定 | 有虚拟号 | 更新 pcd 记录的 virtual_no |
| 有虚拟号,有 pcd 绑定 | 无虚拟号 | 禁用旧 pcd 记录 + 创建 pci ICCID 绑定 |
| 有虚拟号,无绑定 | 无虚拟号 | 跳过(无需迁移) |
| 无虚拟号 | 任意 | 迁移 pci 记录(若存在)到新卡 ICCID |
### 3. 接入 CustomerBinding 的调用点
以下调用点需要替换为 CustomerBinding 模块:
- `client_auth/service.go: bindAsset()` — 使用 `CustomerBinding.Bind()`
- `client_auth/service.go: resolveAssetBindingKey()` — 废弃,逻辑内聚到 CustomerBinding
- `handler/app/client_realname.go: ExistsByCustomerAndDevice()` — 替换为 `CustomerBinding.OwnsAsset()`
- `handler/app/client_device.go: ExistsByCustomerAndDevice()` — 替换为 `CustomerBinding.OwnsAsset()`
- `handler/app/client_wallet.go: isCustomerOwnAsset()` — 替换为 `CustomerBinding.OwnsAsset()`
- `handler/app/client_asset.go: isCustomerOwnAsset()` — 替换为 `CustomerBinding.OwnsAsset()`
- `service/client_order/service.go: checkAssetOwnership()` — 替换为 `CustomerBinding.OwnsAsset()`
- `service/exchange/service.go: switchCustomerBindingWithTx()` — 替换为 `CustomerBinding.Migrate()`
### 4. 修复 switchCustomerBindingWithTx 的校验混淆
当前 `switchCustomerBindingWithTx`(执行函数)在执行阶段重复做了 `ensureNewAssetBindingAvailableWithTx`(校验函数)的判断,且条件更严(不查实际记录数,只看 key 是否为空)。
修复方式:将 `switchCustomerBindingWithTx` 改为调用 `CustomerBinding.Migrate()`,移除函数内的 `newKey==""` 拦截逻辑。`ensureNewAssetBindingAvailableWithTx` 同步更新:当 `newKey==""` 时,不再拦截,因为 `Migrate()` 已能正确处理此情形。
### 5. asset_status 首销标记修复
`bindAsset` 中通过 `firstEverBind`(查 `virtual_no=""` 的记录数)来判断是否首次绑定并触发 `markAssetAsSold()`。无虚拟号的卡因为 key 为空,导致此逻辑失效,`asset_status` 永远停在在库。
修复方式:在 `CustomerBinding.Bind()` 内,对 IoT 卡分别按各自绑定表查询首绑状态,再触发 `markAssetAsSold()`
### 6. 不引入新的数据库表或字段
`tb_personal_customer_iccid` 表和 `PersonalCustomerICCIDStore` 已经存在且完整,本次只是接入,不需要迁移或 schema 变更。
### 7. 现有 personal_customer_device 数据不迁移
有虚拟号的卡现有绑定数据保持不变,继续走 `tb_personal_customer_device` 路径。只有无虚拟号的卡新登录时才写 `tb_personal_customer_iccid`
## Testing Decisions
本项目禁止自动化测试,使用 PostgreSQL MCP 和 Postman/curl 手动验证。
**验证重点场景:**
1. **无虚拟号卡首次登录**:用无虚拟号卡的 ICCID 登录后,`tb_personal_customer_iccid` 应出现绑定记录,`tb_iot_card.asset_status` 应变为 2已销售
2. **无虚拟号卡归属验证**:登录后调用实名认证接口,应返回成功而非"无权限"
3. **无虚拟号卡购买套餐**:购买套餐接口应正常通过归属验证
4. **有虚拟号换无虚拟号(旧卡有绑定)**:换货完成后,`tb_personal_customer_device` 旧记录 status 应为 0`tb_personal_customer_iccid` 应出现新卡 ICCID 的绑定记录
5. **有虚拟号换无虚拟号(旧卡无绑定)**:换货应正常完成,无报错,不写入任何绑定记录
6. **有虚拟号换有虚拟号(回归)**现有流程不受影响pcd 表记录的 virtual_no 正确更新到新卡
7. **禁用绑定后归属验证**:将某条 pcd 或 pci 记录 status 设为 0对应客户调用归属验证应返回无权限
8. **有虚拟号卡回归(全流程)**:确认有虚拟号卡的登录、实名、购买套餐流程无任何变化
## Out of Scope
- **IoT 卡状态字段整理**`status` / `asset_status` / `activation_status` 语义重叠问题):影响面广,单独规划
- **资产解析函数统一**12 个 resolve 变体合并):不影响当前 bug单独规划
- **`tb_personal_customer_iccid` 换货迁移的历史数据回填**:历史上无虚拟号卡的用户无绑定记录,不做回填,用户需重新登录
- **`tb_personal_customer_device``virtual_no=""` 的历史垃圾数据清理**需先确认实际数量SQL 查询),作为独立数据修复任务
## Further Notes
- 线上有 12,000+ 张无虚拟号的 IoT 卡,这是合法业务数据,不是数据质量问题
- `tb_personal_customer_iccid` 的 Store 已有完整的 CRUD 实现Create、ExistsByCustomerAndICCID、GetByCustomerID、CreateOrUpdateLastUsed 等),无需重写
- 换货 bug 的直接触发路径:`completeExchangeWithTx → switchCustomerBindingWithTx`,在执行阶段因 `newKey==""` 报错,即使旧卡实际无任何客户绑定记录
- 建议在上线前执行:`SELECT COUNT(*) FROM tb_personal_customer_device WHERE (virtual_no IS NULL OR virtual_no = '') AND deleted_at IS NULL` 确认是否有历史脏数据需要清理
- **临时线上补丁(知悉)**2026-06-18 已将线上 12,000+ 张无虚拟号的卡手动补充了虚拟号作为应急措施,以保障线上可用性。数据回滚由业务方自行处理,不在本次实现范围内。

View File

@@ -1,50 +0,0 @@
Status: done
# CustomerBinding 模块骨架 + 有虚拟号路径替换(纯重构)
## What to build
建立 `internal/service/customer_binding` 包,对外暴露 `Bind()``OwnsAsset()` 两个接口,将现有有虚拟号路径的逻辑迁入。同时将代码库中 6 处归属验证调用点统一替换为新接口。
**本切片是纯重构,不改变任何现有行为。**
### Bind(ctx, tx, customerID, assetType, assetID)
封装创建客户与资产绑定记录的逻辑,当前只实现有虚拟号路径:
- IoT 卡有虚拟号 → 写 `tb_personal_customer_device`(与现在 `bindAsset` 行为一致)
- 设备 → 写 `tb_personal_customer_device`(设备必然有虚拟号)
- 首次绑定时触发 `markAssetAsSold()`firstEverBind 逻辑保持不变)
### OwnsAsset(ctx, customerID, assetType, assetID) bool
封装归属验证逻辑,当前只实现有虚拟号路径:
-`tb_personal_customer_device WHERE virtual_no = ? AND status = 1`
- 内部统一强制 `status = 1` 过滤(修复现有部分实现缺少此过滤的安全缺口)
### 替换 6 处调用点
以下调用点替换为 `CustomerBinding` 的方法:
| 调用点 | 替换目标 |
|--------|---------|
| `client_auth/service.go: bindAsset()` | `CustomerBinding.Bind()` |
| `handler/app/client_realname.go:92` | `CustomerBinding.OwnsAsset()` |
| `handler/app/client_device.go:82` | `CustomerBinding.OwnsAsset()` |
| `handler/app/client_wallet.go: isCustomerOwnAsset()` | `CustomerBinding.OwnsAsset()` |
| `handler/app/client_asset.go: isCustomerOwnAsset()` | `CustomerBinding.OwnsAsset()` |
| `service/client_order/service.go: checkAssetOwnership()` | `CustomerBinding.OwnsAsset()` |
exchange service 的 `customerOwnsAsset()` 在切片 3 中处理。
## Acceptance criteria
- [ ] `internal/service/customer_binding` 包存在,对外暴露 `Bind``OwnsAsset`
- [ ] 有虚拟号的 IoT 卡登录后,`tb_personal_customer_device` 正常写入绑定记录(与现在一致)
- [ ] 设备登录后,`tb_personal_customer_device` 正常写入绑定记录(与现在一致)
- [ ] 被禁用status=0的绑定记录不能通过 `OwnsAsset` 验证(修复安全缺口)
- [ ] 6 处调用点全部替换完毕原有私有方法resolveAssetBindingKey、isCustomerOwnAsset 等)可删除
- [ ] 有虚拟号卡的实名认证、购买套餐、设备操作全链路与现在行为一致
## Blocked by
None - 可立即开始

View File

@@ -1,52 +0,0 @@
Status: done
# 无虚拟号单卡 C 端完整链路
## Parent
`.scratch/customer-binding-architecture/PRD.md`
## What to build
扩展 `CustomerBinding` 模块,使无虚拟号的独立 IoT 卡(单卡)能够完整走通 C 端业务流程:登录绑定、实名认证、购买套餐。
**仅影响 IoT 卡(单卡)。设备资产必然有虚拟号,不在本切片处理范围内。**
### 扩展 Bind()
当资产为 IoT 卡且 `virtual_no` 为空时:
-`tb_personal_customer_iccid`(按 ICCID 绑定),而非 `tb_personal_customer_device`
- `PersonalCustomerICCIDStore` 已有完整实现,直接注入使用
当资产为 IoT 卡且 `virtual_no` 不为空时:行为与切片 1 一致,不变。
### 修复 firstEverBind + markAssetAsSold
当前 `firstEverBind` 通过查 `tb_personal_customer_device WHERE virtual_no = ""` 来判断是否首次绑定,对无虚拟号的卡完全失效(所有无虚拟号的卡共用同一个空 key
修复方式:在 `Bind()` 内,按路径分别判断首绑:
- 有虚拟号路径:查 `tb_personal_customer_device WHERE virtual_no = ?`
- 无虚拟号路径:查 `tb_personal_customer_iccid WHERE iccid = ?`
首次绑定后正常触发 `markAssetAsSold()`,将卡的 `asset_status` 从在库1更新为已销售2
### 扩展 OwnsAsset()
当资产为 IoT 卡时:
- 若卡有 `virtual_no`:查 `tb_personal_customer_device`(与切片 1 一致)
- 若卡无 `virtual_no`:查 `tb_personal_customer_iccid WHERE iccid = ? AND status = 1`
当资产为设备时:行为不变,只查 `tb_personal_customer_device`
## Acceptance criteria
- [ ] 无虚拟号的单卡 C 端登录后,`tb_personal_customer_iccid` 中出现对应的绑定记录
- [ ] 无虚拟号单卡首次登录后,`tb_iot_card.asset_status` 变为 2已销售
- [ ] 无虚拟号单卡登录后,实名认证接口返回成功(不报"无权限"
- [ ] 无虚拟号单卡登录后,购买套餐接口归属验证通过
- [ ] 有虚拟号卡的全部行为与切片 1 完成后保持一致(无回归)
- [ ] 设备资产的全部行为不受影响
## Blocked by
`.scratch/customer-binding-architecture/issues/01-customer-binding-module-refactor.md`

View File

@@ -1,45 +0,0 @@
Status: done
# 换货绑定迁移修复
## Parent
`.scratch/customer-binding-architecture/PRD.md`
## What to build
`CustomerBinding` 模块上实现 `Migrate(ctx, tx, oldAsset, newAsset)` 方法,替换换货服务中现有的 `switchCustomerBindingWithTx``ensureNewAssetBindingAvailableWithTx` 逻辑,修复有虚拟号旧卡换无虚拟号新卡时被误拦截的 bug。
### Migrate(ctx, tx, oldAsset, newAsset)
处理四种虚拟号组合,仅涉及 IoT 卡(设备必然有虚拟号,只有有→有一种情形):
| 旧卡 | 新卡 | 行为 |
|------|------|------|
| 有虚拟号pcd 有绑定 | 有虚拟号 | 更新 pcd 记录的 virtual_no 为新卡虚拟号 |
| 有虚拟号pcd 有绑定 | 无虚拟号 | 禁用旧 pcd 记录status=0+ 创建新 pci ICCID 绑定 |
| 有虚拟号pcd 无绑定 | 无虚拟号 | 跳过,无需迁移 |
| 无虚拟号pci 有绑定 | 任意 | 迁移 pci 记录到新卡 ICCID或新卡虚拟号路径 |
| 任意 | 任意(无绑定) | 跳过 |
### 修复换货服务
- `switchCustomerBindingWithTx` 替换为调用 `CustomerBinding.Migrate()`,移除函数内 `newKey == ""` 的误拦截逻辑
- `ensureNewAssetBindingAvailableWithTx` 更新:当新卡无虚拟号时,不再拦截换货,因为 `Migrate()` 已能正确处理此情形(无绑定时跳过,有绑定时迁移到 pci
### 根本 bug 说明
`switchCustomerBindingWithTx` 在执行阶段做了 `if newKey == "" { return error }` 的检查,但没有先确认旧资产是否实际存在绑定记录。旧卡有虚拟号但无任何客户绑定时,也会被误拦截报错"新资产无法承接客户绑定"。
## Acceptance criteria
- [ ] 旧卡有虚拟号 + 有客户绑定换货为有虚拟号新卡pcd 记录的 virtual_no 正确更新为新卡虚拟号
- [ ] 旧卡有虚拟号 + 有客户绑定,换货为无虚拟号新卡:旧 pcd 记录 status 变为 0pci 表出现新卡 ICCID 的绑定记录
- [ ] 旧卡有虚拟号 + 无客户绑定,换货为无虚拟号新卡:换货正常完成,不报错,不写入任何绑定记录
- [ ] 旧卡无虚拟号,换货为任意新卡:换货正常完成
- [ ] 有虚拟号换有虚拟号(原有场景):行为与修复前一致,无回归
- [ ] 换货完成后,客户通过新卡(无论有无虚拟号)能正常通过归属验证
## Blocked by
`.scratch/customer-binding-architecture/issues/02-no-virtual-no-card-cend-flow.md`

View File

@@ -1,143 +0,0 @@
Status: ready-for-agent
# PRD企业授权增强 & 资产列表扩展
## Problem Statement
平台运营人员在管理企业授权和查看资产状态时面临以下痛点:
1. **企业维度检索缺失**:卡列表和设备列表无法按企业维度过滤,也无法在列表中直接看到某张卡/设备被授权给了哪个企业,需要跨页面跳转查询。
2. **企业授权操作繁琐**:授权和收回接口只接受精确 ICCID/虚拟号列表,但前端列表有分页,量大时需一个个选,无法按批次号、号段等条件一次性批量操作。
3. **换货历史不可见**:经历过换货的旧卡/设备,在列表和详情中没有任何标记,运营无法判断当前资产是否为"换货遗留"状态或曾经历过换货后被重新激活(转新)的资产。
4. **主钱包流水和退款列表检索维度不足**无法直接通过资产标识ICCID 或虚拟号)快速定位某个资产相关的流水和退款记录。
5. **卡网络状态过滤缺失**standalone 卡列表无法按网络状态(开机/停机)过滤,批量运维操作不便。
## Solution
对六个业务场景进行针对性扩展:
1. 在 standalone 卡列表和设备列表中新增企业维度过滤及企业信息返回字段
2. 将企业授权/收回接口升级为支持 list/range/filter 三(两)模式选资产
3. 在 standalone 卡列表和设备列表响应中暴露换货业务状态和资产世代编号
4. 主钱包流水列表新增资产标识精确检索
5. 退款列表新增资产标识精确检索
6. standalone 卡列表新增网络状态过滤
同时修复卡企业授权表的数据一致性问题(补唯一约束,与设备授权保持一致)。
## User Stories
1. 作为平台运营,我希望在 standalone 卡列表中输入企业 ID 进行过滤,以便快速找到已授权给该企业的所有卡
2. 作为平台运营,我希望在 standalone 卡列表中使用"是否授权企业"开关过滤,以便快速区分已授权和未授权的卡库存
3. 作为平台运营,我希望在 standalone 卡列表中直接看到每张卡被授权给哪个企业企业ID和名称以便不用跳转到企业详情页
4. 作为平台运营,我希望在设备列表中输入企业 ID 进行过滤,以便快速找到已授权给该企业的所有设备
5. 作为平台运营,我希望在设备列表中使用"是否授权企业"开关过滤,以便区分已授权和未授权的设备
6. 作为平台运营,我希望在设备列表中直接看到每台设备被授权给哪个企业,以便快速掌握设备归属
7. 作为平台运营,我希望在授权卡给企业时能通过 ICCID 号段范围一次性选取,以便高效授权大批量卡
8. 作为平台运营,我希望在授权卡给企业时能通过批次号、运营商等筛选条件选取,以便按业务维度批量授权
9. 作为平台运营,我希望在收回企业卡授权时同样支持 list/range/filter 三种模式,以便与授权操作保持一致的操作体验
10. 作为平台运营,我希望在授权设备给企业时能通过虚拟号模糊搜索和批次号筛选一次性选取,以便批量操作设备
11. 作为平台运营,我希望在收回企业设备授权时支持 list/filter 两种模式,以便批量收回
12. 作为平台运营,我希望在 standalone 卡列表中看到每张卡的业务状态(`asset_status`),以便识别已换货但尚未转新的"死档"卡
13. 作为平台运营,我希望在 standalone 卡列表中看到每张卡的世代编号(`generation`),以便识别曾换货后转新重入库的卡
14. 作为平台运营,我希望在设备列表中看到每台设备的业务状态和世代编号,以便同样掌握设备的换货历史
15. 作为平台运营,我希望在主钱包流水列表中输入 ICCID 或虚拟号精确检索,以便快速定位某资产的所有充值/扣款记录
16. 作为平台运营,我希望在退款列表中输入 ICCID 或虚拟号精确检索,以便快速找到某资产相关的所有退款申请
17. 作为平台运营,我希望在 standalone 卡列表中按网络状态(开机/停机)过滤,以便批量处理特定网络状态的卡
## Implementation Decisions
### 数据一致性修复(前置)
- **卡企业授权唯一约束**:在 `tb_enterprise_card_authorization` 表上补充部分唯一索引,约束同一张卡在同一时间只能有一条有效授权记录(`WHERE revoked_at IS NULL AND deleted_at IS NULL`),与设备授权表的 `uq_active_device_auth` 约束保持一致
- **service 层校验补充**:卡授权 service 在执行授权前,需增加"卡是否已授权给其他企业"的校验,目前只校验同一企业重复授权
### 需求1standalone 卡列表 + 设备列表企业字段
**Request 新增过滤条件**`ListStandaloneIotCardRequest``ListDeviceRequest`
- `authorized_enterprise_id *uint`按企业ID过滤只匹配当前有效授权`revoked_at IS NULL`
- `is_authorized_to_enterprise *bool`true=已授权给某企业false=未授权任何企业
**Response 新增字段**`StandaloneIotCardResponse``DeviceResponse`
- `authorized_enterprise_id *uint`:未授权时为 null
- `authorized_enterprise_name string`:未授权时为空字符串
Store 层需通过 JOIN 或子查询 `tb_enterprise_card_authorization` / `tb_enterprise_device_authorization` 表获取企业ID再批量查企业名称
### 需求2企业授权/收回接口升级为多模式
参照 `AllocateStandaloneCardsRequest` / `RecallStandaloneCardsRequest``selection_type` 三模式设计:
**allocate-cards / recall-cards**三模式list / range / filter
- `list``ICCIDs []string`,精确 ICCID 列表
- `range``ICCIDStart string` + `ICCIDEnd string`,号段范围
- `filter`:筛选条件
- allocate-cards filter 字段:`ICCID`(模糊)、`BatchNo``CarrierID``ShopID`/`ShopIDs`
- recall-cards filter 字段:`ICCID`(模糊)、`BatchNo``CarrierID`
**allocate-devices / recall-devices**两模式list / filter不支持 range
- `list``DeviceNos []string`,设备虚拟号列表
- `filter`:筛选条件
- allocate-devices filter 字段:`VirtualNo`(模糊)、`BatchNo``ShopID`
- recall-devices filter 字段:`VirtualNo`(模糊)、`BatchNo`
原有接口的请求结构需要破坏性变更DTO 重构),需与前端联调确认过渡方案
### 需求3换货状态字段
**`StandaloneIotCardResponse``DeviceResponse` 新增字段**
- `asset_status int`业务状态1=在库, 2=已销售, 3=已换货, 4=已停用)
- `asset_status_name string`:对应中文名称
- `generation int`资产世代编号初始值1每次换货+转新后+1
语义约定:
- `asset_status = 3`:该资产已换货且尚未执行"旧资产转新",是永久性死档标记
- `generation > 1`:该资产历史上曾执行过换货+转新,目前仍在流通
### 需求4主钱包流水资产标识检索
`MainWalletTransactionListRequest` 新增:
- `AssetIdentifier string`:精确匹配 `asset_identifier` 快照字段ICCID 或虚拟号),空字符串时不过滤
### 需求5退款列表资产标识检索
`RefundListRequest` 新增:
- `AssetIdentifier string`:精确匹配 `asset_identifier` 快照字段ICCID 或虚拟号),空字符串时不过滤
### 需求6standalone 卡列表网络状态过滤
`ListStandaloneIotCardRequest` 新增:
- `NetworkStatus *int`网络状态过滤0=停机, 1=开机),不传则不过滤
Store 层 `applyStandaloneFilters` 函数中补充 `network_status = ?` 条件
## Testing Decisions
本项目禁止自动化测试,验证方式为:
- **数据库验证**:通过 PostgreSQL MCP 工具验证授权表唯一约束是否生效、字段值是否正确写入
- **接口验证**:通过 curl/Postman 对各接口进行手动联调,重点覆盖以下场景:
- 企业 ID 过滤确认只返回有效授权revoked_at IS NULL的卡/设备
- 是否授权企业过滤true/false 结果集互补且无交集
- 授权时尝试将同一张卡授权给第二个企业,确认被拒绝
- allocate-cards 三种 selection_type 均能正确选取目标卡
- allocate-devices 两种 selection_type 均能正确选取目标设备
- asset_status=3 的卡在列表中可见且字段正确
- generation 字段在换货+转新后正确递增
- 主钱包流水 asset_identifier 精确匹配不漏不多
- 退款 asset_identifier 精确匹配不漏不多
- 网络状态过滤 0/1 结果集互补
## Out of Scope
- 企业卡/设备授权列表接口本身的改造(本次只改授权/收回操作接口)
- `AllocateCardsPreviewReq`(预览接口)是否同步升级为多模式(待后续评估)
- standalone 卡列表和设备列表的导出功能是否同步支持新增过滤条件
- 换货历史的完整时间线展示(仅暴露字段,不新增详情接口)
- `generation` 字段的过滤能力(暂只返回,不支持作为检索条件)
## Further Notes
- 需求1 的企业名称需要 N+1 防护:通过授权查询批量拿到 enterprise_id 后,用 IN 一次性查企业名称,不能逐条查
- 需求2 的 filter 模式对于 allocate-cards 场景卡的范围天然受操作者权限约束代理用户只能授权自己店铺的卡Store 层已有 `middleware.ApplyShopFilter` 处理filter 模式需要同样受此约束
- 卡唯一约束迁移执行前,需确认现有数据中是否存在一张卡同时有多条 `revoked_at IS NULL` 的记录,若有需先清理

View File

@@ -1,28 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
修复卡企业授权的数据一致性问题,使其与设备授权保持一致:一张卡在同一时间只能授权给一个企业。
分两步:
**第一步:数据库迁移**
`tb_enterprise_card_authorization` 表上新增部分唯一索引,约束同一张卡不能同时存在两条有效授权记录(`revoked_at IS NULL AND deleted_at IS NULL`)。迁移执行前需先查询是否存在脏数据(同一 card_id 有多条 revoked_at IS NULL 的记录),若有需在迁移脚本中先行清理。
**第二步service 层校验补充**
卡授权 service`BatchAuthorize`)在执行授权前,增加"目标卡是否已授权给其他企业"的校验。目前代码只跳过已授权给同一企业的卡,不阻止授权给第二个企业。新逻辑:若目标卡已存在有效授权(`revoked_at IS NULL`)且授权对象不是当前企业,则将该卡加入失败列表并给出明确错误原因(如"卡已授权给其他企业,请先收回")。
## Acceptance criteria
- [x] `tb_enterprise_card_authorization` 表存在部分唯一索引,约束 `(card_id) WHERE revoked_at IS NULL AND deleted_at IS NULL`
- [x] 尝试将同一张卡授权给第二个企业时,接口返回失败,错误信息明确说明"已授权给其他企业"
- [x] 已撤回revoked_at 有值)的历史记录不受唯一约束影响,可正常查询
- [x] 同一张卡授权给同一企业时仍返回"已授权给该企业"的原有错误,行为不变
## Blocked by
None - can start immediately

View File

@@ -1,22 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/iot-cards/standalone` 接口的查询条件中新增网络状态过滤。
Request DTO 新增可选字段 `network_status *int`0=停机1=开机不传时不过滤。Store 层 `applyStandaloneFilters` 函数补充对应的 `network_status = ?` WHERE 条件。
## Acceptance criteria
- [x] 传入 `network_status=0` 时只返回停机的卡
- [x] 传入 `network_status=1` 时只返回开机的卡
- [x] 不传 `network_status` 时返回全部(与改动前行为一致)
- [x] 网络状态过滤可与现有其他过滤条件ICCID、运营商等叠加使用
## Blocked by
None - can start immediately

View File

@@ -1,34 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
在 standalone 卡列表响应和设备列表响应中暴露换货相关字段,使运营人员能直接在列表中识别资产的换货历史。
**`StandaloneIotCardResponse` 新增三个字段:**
- `asset_status int`业务状态1=在库, 2=已销售, 3=已换货, 4=已停用)
- `asset_status_name string`:对应中文名称
- `generation int`资产世代编号初始值1每次换货+旧资产转新后 +1
**`DeviceResponse` 同步新增相同三个字段。**
语义约定(写入注释和 description
- `asset_status = 3`:该资产已换货且尚未执行"旧资产转新",不再流通
- `generation > 1`:该资产历史上曾执行过换货后转新,目前仍在使用
Service 层组装响应时直接从 Model 取值,`asset_status_name` 通过 constants 中的方法转换。
## Acceptance criteria
- [x] `/api/admin/iot-cards/standalone` 响应中每条记录包含 `asset_status``asset_status_name``generation` 三个字段
- [x] `/api/admin/devices` 响应中每条记录包含相同三个字段
- [x] 已换货未转新的卡/设备返回 `asset_status=3``asset_status_name="已换货"`
- [x] 经过换货转新重入库的卡/设备返回 `generation=2`(或更高)且 `asset_status=1`
- [x] 普通未经历换货的资产返回 `generation=1`
## Blocked by
None - can start immediately

View File

@@ -1,23 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/shops/{shop_id}/main-wallet/transactions` 接口的查询条件中新增资产标识精确检索。
`MainWalletTransactionListRequest` 新增可选字段 `asset_identifier string`,非空时对 `asset_identifier` 列做精确匹配(`= ?`,不做模糊匹配)。`asset_identifier` 字段存储的是下单时资产的标识符快照ICCID 或虚拟号),空字符串时不过滤。
## Acceptance criteria
- [x] 传入有效的 ICCID 时只返回该卡相关的主钱包流水
- [x] 传入有效的设备虚拟号时只返回该设备相关的主钱包流水
- [x] 传入不存在的标识符时返回空列表total=0不报错
- [x] 不传 `asset_identifier` 时行为与改动前完全一致
- [x] 可与 `transaction_type``start_date``end_date` 等现有条件叠加使用
## Blocked by
None - can start immediately

View File

@@ -1,23 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/refunds` 接口的查询条件中新增资产标识精确检索。
`RefundListRequest` 新增可选字段 `asset_identifier string`,非空时对退款记录的 `asset_identifier` 列做精确匹配(`= ?`。该字段存储的是下单时资产的标识符快照ICCID 或虚拟号),空字符串时不过滤。
## Acceptance criteria
- [x] 传入有效的 ICCID 时只返回该卡相关的退款申请
- [x] 传入有效的设备虚拟号时只返回该设备相关的退款申请
- [x] 传入不存在的标识符时返回空列表total=0不报错
- [x] 不传 `asset_identifier` 时行为与改动前完全一致
- [x] 可与 `status``order_id``shop_id` 等现有条件叠加使用
## Blocked by
None - can start immediately

View File

@@ -1,33 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/iot-cards/standalone` 接口中新增企业维度过滤条件,并在响应中返回当前有效授权的企业信息。
**Request 新增过滤条件(`ListStandaloneIotCardRequest`**
- `authorized_enterprise_id *uint`按企业ID过滤只匹配 `tb_enterprise_card_authorization``revoked_at IS NULL` 的有效授权
- `is_authorized_to_enterprise *bool`true=只返回当前已授权给某企业的卡false=只返回未授权任何企业的卡
**Response 新增字段(`StandaloneIotCardResponse`**
- `authorized_enterprise_id *uint`当前有效授权的企业ID未授权时为 null
- `authorized_enterprise_name string`:对应企业名称,未授权时为空字符串
Store 层在 `applyStandaloneFilters` 中增加对 `authorized_enterprise_id``is_authorized_to_enterprise` 的处理,通过子查询 `tb_enterprise_card_authorization``revoked_at IS NULL AND deleted_at IS NULL`实现过滤。响应组装时批量查询企业名称IN 查询),不逐条查询。
## Acceptance criteria
- [x] 传入 `authorized_enterprise_id` 时只返回当前有效授权给该企业的卡
- [x] 传入 `is_authorized_to_enterprise=true` 时只返回已授权给某企业的卡
- [x] 传入 `is_authorized_to_enterprise=false` 时只返回未授权任何企业的卡
- [x] 已撤回的历史授权不影响过滤结果(已撤回视为未授权)
- [x] 响应中每张卡包含 `authorized_enterprise_id``authorized_enterprise_name`,未授权卡对应字段为 null/空字符串
- [x] 企业名称通过批量查询获取,不触发 N+1 查询
- [x] 新增过滤条件可与现有条件ICCID、运营商、店铺等叠加使用
## Blocked by
- `issues/01-card-enterprise-auth-unique-constraint.md`

View File

@@ -1,32 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
`/api/admin/devices` 接口中新增企业维度过滤条件,并在响应中返回当前有效授权的企业信息。
**Request 新增过滤条件(`ListDeviceRequest`**
- `authorized_enterprise_id *uint`按企业ID过滤只匹配 `tb_enterprise_device_authorization``revoked_at IS NULL` 的有效授权
- `is_authorized_to_enterprise *bool`true=只返回当前已授权给某企业的设备false=只返回未授权任何企业的设备
**Response 新增字段(`DeviceResponse`**
- `authorized_enterprise_id *uint`当前有效授权的企业ID未授权时为 null
- `authorized_enterprise_name string`:对应企业名称,未授权时为空字符串
Store 层通过子查询或 JOIN `tb_enterprise_device_authorization` 实现过滤。响应组装时批量查询企业名称IN 查询),不逐条查询。
## Acceptance criteria
- [x] 传入 `authorized_enterprise_id` 时只返回当前有效授权给该企业的设备
- [x] 传入 `is_authorized_to_enterprise=true` 时只返回已授权给某企业的设备
- [x] 传入 `is_authorized_to_enterprise=false` 时只返回未授权任何企业的设备
- [x] 已撤回的历史授权不影响过滤结果(已撤回视为未授权)
- [x] 响应中每台设备包含 `authorized_enterprise_id``authorized_enterprise_name`,未授权设备对应字段为 null/空字符串
- [x] 企业名称通过批量查询获取,不触发 N+1 查询
## Blocked by
None - can start immediately

View File

@@ -1,42 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
将企业卡授权和收回接口升级为支持 list/range/filter 三种模式批量选取卡,替代原来只接受精确 ICCID 列表的方式。
**涉及接口:**
- `POST /api/admin/enterprises/{id}/allocate-cards`
- `POST /api/admin/enterprises/{id}/recall-cards`
**`AllocateCardsReq` 重构为:**
- `selection_type string`(必填,`list``range``filter`
- list 模式:`iccids []string`ICCID 列表最多1000个
- range 模式:`iccid_start string` + `iccid_end string`(号段范围)
- filter 模式过滤字段:`iccid string`(模糊)、`batch_no string``carrier_id *uint``shop_id *uint``shop_ids []uint`
- `remark string`(备注,所有模式均可选)
**`RecallCardsReq` 重构为:**
- `selection_type string`(必填,`list``range``filter`
- list 模式:`iccids []string`
- range 模式:`iccid_start string` + `iccid_end string`
- filter 模式过滤字段:`iccid string`(模糊)、`batch_no string``carrier_id *uint`
filter 和 range 模式下allocate 操作的候选卡集受操作者权限约束(代理用户只能授权自己店铺的卡),与 `applyStandaloneFilters` 中的 `ApplyShopFilter` 逻辑保持一致。
## Acceptance criteria
- [x] `allocate-cards` 接口接受 `selection_type=list` + `iccids`,行为与改动前一致
- [x] `allocate-cards` 接口接受 `selection_type=range` + 号段,批量授权号段内所有匹配的卡
- [x] `allocate-cards` 接口接受 `selection_type=filter` + 过滤条件,批量授权所有匹配的卡
- [x] `recall-cards` 接口同样支持三种模式,分别正确收回对应卡的企业授权
- [x] filter/range 模式下代理用户只能操作自己店铺的卡,超出范围的卡进入失败列表
- [x] 任何模式下尝试授权"已授权给其他企业"的卡,该卡进入失败列表并附带原因
- [x] 响应中 `success_count``fail_count``failed_items` 准确反映实际执行结果
## Blocked by
- `issues/01-card-enterprise-auth-unique-constraint.md`

View File

@@ -1,38 +0,0 @@
Status: ready-for-human
## Parent
`.scratch/enterprise-auth-and-asset-enhancements/PRD.md`
## What to build
将企业设备授权和收回接口升级为支持 list/filter 两种模式批量选取设备,替代原来只接受精确设备号列表的方式。
**涉及接口:**
- `POST /api/admin/enterprises/{id}/allocate-devices`
- `POST /api/admin/enterprises/{id}/recall-devices`
**`AllocateDevicesReq` 重构为:**
- `selection_type string`(必填,`list``filter`
- list 模式:`device_nos []string`(设备虚拟号列表)
- filter 模式过滤字段:`virtual_no string`(模糊)、`batch_no string``shop_id *uint`
**`RecallDevicesReq` 重构为:**
- `selection_type string`(必填,`list``filter`
- list 模式:`device_nos []string`
- filter 模式过滤字段:`virtual_no string`(模糊)、`batch_no string`
filter 模式下allocate 操作的候选设备集受操作者权限约束代理用户只能授权自己店铺的设备。filter 模式命中的设备数量无硬性上限,但单次事务处理建议分批,超大批次需记录日志。
## Acceptance criteria
- [x] `allocate-devices` 接口接受 `selection_type=list` + `device_nos` 列表,行为与改动前一致
- [x] `allocate-devices` 接口接受 `selection_type=filter` + 过滤条件,批量授权所有匹配设备
- [x] `recall-devices` 接口接受 `selection_type=list` + `device_nos` 列表,行为与改动前一致
- [x] `recall-devices` 接口接受 `selection_type=filter` + 过滤条件,批量收回所有匹配设备
- [x] filter 模式下代理用户只能操作自己店铺的设备,超出范围的设备进入失败列表
- [x] 响应中 `success_count``fail_count``failed_items` 准确反映实际执行结果
## Blocked by
None - can start immediately

View File

@@ -1,124 +0,0 @@
# PRDIoT 卡风险状态限制 / 导入任务操作人与批量失效 / 凭证多图 / 预充值备注
Status: ready-for-agent
---
## Problem Statement
运营团队在日常工作中遇到四个独立痛点:
1. **风险停机/已销户卡无法有效拦截**:运营商侧将某些卡置为"风险停机"或"已销户"状态后,系统仍允许对这些卡发起复机操作,且轮询系统仍持续轮询这些卡,浪费资源并可能产生错误数据。
2. **导入任务无操作人信息**IoT 卡导入任务和设备导入任务的列表中没有"操作人"字段,无法追溯是谁发起的批量操作。同时缺少批量失效订单套餐的操作入口,运营只能逐条手动处理。
3. **凭证只能上传一张**:后台创建订单和发起退款时,只允许上传一张凭证图片,无法满足多张凭证的业务场景(如多笔转账记录、多页合同等)。
4. **代理预充值缺少备注字段**:代理预充值操作无法附加运营备注,导致事后无法追溯充值的业务原因。
---
## Solution
1. 在复机接口和轮询系统中增加对 `gateway_extend` 字段的检查,当独立卡处于"风险停机"或"已销户"状态时,拒绝复机并停止轮询。
2. 在所有导入任务中快照操作人姓名;新增"批量失效订单套餐"导入任务,支持通过 CSV 批量将订单下的套餐置为失效状态。
3. 将订单、退款、代理预充值的凭证字段改为支持最多5张的 jsonb 数组存储。
4. 在代理预充值记录上新增 `remark` 备注字段,供运营创建时填写,并在列表/详情接口中返回。
---
## User Stories
1. 作为运营人员,当我尝试对"风险停机"状态的独立卡发起复机时,我希望系统拒绝并提示原因,以免产生无效操作。
2. 作为运营人员,当我尝试对"已销户"状态的独立卡发起复机时,我希望系统拒绝并提示原因,以免对已注销的卡发起无意义请求。
3. 作为运营人员,我希望"风险停机"和"已销户"的独立卡不再参与系统轮询,以免浪费轮询资源并产生噪音数据。
4. 作为运营人员,当轮询系统检测到某张独立卡的网关状态变为"风险停机"或"已销户"时,我希望系统自动将该卡的轮询关闭,而不需要手动干预。
5. 作为运营人员,我希望在 IoT 卡导入任务列表中看到"操作人"姓名,以便追溯每次批量导入是谁发起的。
6. 作为运营人员,我希望在设备导入任务列表中看到"操作人"姓名,以便追溯每次批量导入是谁发起的。
7. 作为运营人员,我希望通过上传 CSV 文件批量将一批订单的套餐置为失效,以便快速处理异常订单。
8. 作为运营人员我希望在创建批量失效任务时能填写备注和上传凭证最多5张以便记录操作原因。
9. 作为运营人员,我希望批量失效任务完成后能看到成功数、失败数,以及失败原因(如订单号不存在),以便核查处理结果。
10. 作为运营人员,我希望批量失效套餐只影响套餐记录本身,不触发退款和其他业务流程,让轮询系统自行根据套餐状态处理停机。
11. 作为运营人员我希望在创建后台订单时可以上传最多5张凭证以便完整记录线下付款证明。
12. 作为运营人员我希望在发起退款申请时可以上传最多5张凭证以便完整记录退款证明材料。
13. 作为运营人员,我希望在代理预充值列表中看到备注内容,以便了解每笔预充值的业务背景。
14. 作为运营人员,我希望在创建代理预充值订单时填写备注,以便记录本次充值的原因或说明。
15. 作为开放 API 调用方,当我通过 Open API 对"风险停机"或"已销户"的独立卡调用复机接口时,我希望收到明确的错误响应。
---
## Implementation Decisions
### 需求一:风险停机/已销户卡限制
- **判断字段**`tb_iot_card.gateway_extend` 原文值 = `"风险停机"``"已销户"`。新增两个常量 `GatewayCardExtendRiskStop``GatewayCardExtendCancelled`
- **判断范围**:仅 `is_standalone = true` 的独立卡(未绑定设备的卡)。绑定设备的卡不受此限制。
- **复机拦截**:在 `ManualStartCard`(后台管理员入口)和 `ResumeCard`Open API 入口)两个函数中,调用网关前先读取 DB 中已落库的 `gateway_extend`,若命中则返回业务错误,不实时查询网关。
- **轮询停止**:轮询状态处理器(`PollingCardStatusHandler`)在将新的 `gateway_extend` 写入 DB 后,检查若命中风险状态且 `is_standalone=true`,则调用现有的 `UpdatePollingStatus(false)``enable_polling` 写为 `false`,并跳过 `requeueCard`,终止该卡的轮询循环。
- **错误码**:复机被拒绝时返回业务错误,提示信息明确说明原因("风险停机"/"已销户")。
### 需求二:导入任务操作人与批量失效
**操作人快照:**
- `tb_iot_card_import_task``tb_device_import_task` 表各新增 `creator_name varchar` 字段。
- 新的批量失效任务表同样包含此字段。
- 创建任务时从当前登录用户快照姓名写入,旧数据留空(不回填)。
- 列表响应 DTO 新增 `creator_name` 字段。
**批量失效订单套餐任务:**
- 新建模型、Store、Service、Handler参照现有 IoT 卡导入任务的结构CSV 上传 → 异步任务处理)。
- CSV 格式:单列 `order_no`(无表头行约定沿用现有导入任务的处理方式)。
- 创建接口额外支持:`remark`字符串备注和最多5张凭证jsonb 存储)。
- 处理逻辑:按行读取 `order_no` → 查询 `Order` → 找到 `PackageUsage WHERE order_id = ? AND status NOT IN (3, 4)` → 批量更新 `status = 4`(已失效)。
- 订单不存在记为失败fail继续处理后续行。
- 订单存在但旗下套餐全部已是终态status 3 或 4记为成功success
- 不触发退款,不直接操作 IoT 卡停机,由轮询系统根据套餐状态自行评估。
- 任务表命名:`tb_order_package_invalidate_task`
### 需求三:凭证多图改造
- `tb_order.payment_voucher_key`varchar 500`jsonb`,存储 `[]string`最多5个元素。
- `tb_refund.refund_voucher_key`varchar 500`jsonb`,存储 `[]string`最多5个元素。
- `tb_agent_recharge_record.payment_voucher_key`varchar 500`jsonb`,存储 `[]string`最多5个元素。
- **数据迁移**(同一 Migration 语句):非空旧值 `"some_key"``["some_key"]`;空字符串 → `[]`。生产环境由运营手动执行 Migration。
- 所有涉及凭证的请求 DTO 字段由 `string` 改为 `[]string`validate 标签限制 `max=5`,每项 `max=500`
- 所有涉及凭证的响应 DTO 字段改为 `[]string`
- 不限制文件类型(上传 URL 获取接口层面不变,仅存储 key 列表)。
### 需求四:代理预充值备注
- `tb_agent_recharge_record` 新增 `remark text` 字段(可为空)。
- `CreateAgentRechargeRequest` 新增 `remark` 字段(`omitempty`,无最大长度强限制,建议 1000 字符内)。
- `AgentRechargeResponse` 新增 `remark` 字段(列表和详情接口均返回)。
- 创建后不可修改,无需新增修改接口。
---
## Testing Decisions
本项目禁止自动化测试。验证方式:
- **PostgreSQL MCP**:核查 `tb_iot_card.enable_polling` 在风险状态卡被轮询后是否正确置为 `false`;核查 `tb_package_usage.status` 在批量失效任务执行后是否按预期更新;核查 jsonb 字段迁移结果。
- **curl / Postman**:对风险停机卡调用复机接口,验证返回正确错误码;上传多张凭证创建订单,验证存储和返回正确;创建代理预充值时附带备注,验证列表返回。
---
## Out of Scope
- 已绑定设备的卡(`is_standalone=false`)不受"风险停机/已销户"限制约束。
- 批量失效订单套餐不触发退款流程。
- 批量失效不直接调用网关停机,依赖轮询系统自行评估。
- 历史导入任务的 `creator_name` 不回填。
- 凭证文件类型校验(文件类型限制不在本次范围内)。
- 代理预充值备注创建后不可修改(无修改接口)。
---
## Further Notes
- 凭证字段改为 jsonb 后,前端需同步更新上传组件支持多选;本 PRD 只覆盖后端改造。
- 批量失效任务的凭证和备注字段,与代理预充值备注字段、以及需求三的多凭证设计保持一致的数据形态,便于后续统一处理。
- "风险停机"和"已销户"的 `gateway_extend` 原文值来自上游网关,若上游修改返回值需同步更新常量。

View File

@@ -1,28 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
新增两个 gateway_extend 常量(`GatewayCardExtendRiskStop = "风险停机"``GatewayCardExtendCancelled = "已销户"`),然后在后台复机入口(`ManualStartCard`)和 Open API 复机入口(`ResumeCard`)中增加前置拦截:
- 仅对 `is_standalone = true` 的独立卡生效
- 读取 DB 中已落库的 `gateway_extend` 字段(不实时查网关)
- 若命中 `"风险停机"``"已销户"`,立即返回业务错误,不再继续调用网关
- 错误提示需明确说明被拒绝的原因(如"该卡已被运营商风险停机,不允许复机"
无 Schema 变更,不需要 Migration。
## Acceptance criteria
- [ ] `pkg/constants/iot.go` 新增 `GatewayCardExtendRiskStop``GatewayCardExtendCancelled` 两个常量
- [ ] 后台 `ManualStartCard` 在调用网关前检查独立卡 `gateway_extend`,命中风险值时返回业务错误
- [ ] Open API `ResumeCard` 在调用网关前检查独立卡 `gateway_extend`,命中风险值时返回业务错误
- [ ] 已绑定设备的卡(`is_standalone = false`)不受此限制,复机正常进行
- [ ] 返回的错误信息能区分"风险停机"和"已销户"两种情况
## Blocked by
None - can start immediately

View File

@@ -1,28 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
`PollingCardStatusHandler` 中,当轮询查询网关后将新的 `gateway_extend` 写入 DB若新值命中风险常量`GatewayCardExtendRiskStop``GatewayCardExtendCancelled`)且该卡 `is_standalone = true`,则:
1. 调用现有的 `UpdatePollingStatus(false)``enable_polling` 写为 `false`
2. 跳过末尾的 `requeueCard`,终止该卡的轮询循环
检查时机:在 `UpdateFields` 写入 `gateway_extend` 成功之后,`requeueCard` 调用之前。
不需要 Schema 变更,不需要 Migration。
## Acceptance criteria
- [ ] 轮询处理器在写入 `gateway_extend` 后检查风险状态
- [ ] 独立卡(`is_standalone=true`)命中风险值时调用 `UpdatePollingStatus(false)``tb_iot_card.enable_polling` 被置为 `false`
- [ ] 该卡不再入队(不调用 `requeueCard`),轮询循环终止
- [ ] 已绑定设备的卡(`is_standalone=false`)命中风险值时不触发此逻辑,轮询正常继续
- [ ] 可通过 PostgreSQL MCP 验证:风险状态卡处理后 `enable_polling = false`
## Blocked by
- `issues/01-risk-resume-block.md`(依赖 `GatewayCardExtendRiskStop``GatewayCardExtendCancelled` 常量)

View File

@@ -1,30 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
在现有两个导入任务表中新增操作人快照字段,并在列表接口返回:
- `tb_iot_card_import_task` 新增 `creator_name varchar(100)` 字段
- `tb_device_import_task` 新增 `creator_name varchar(100)` 字段
- 创建任务时从当前登录用户快照姓名写入(不可为 null旧数据留空字符串
- IoT 卡导入任务列表响应 DTO 新增 `creator_name` 字段
- 设备导入任务列表响应 DTO 新增 `creator_name` 字段
历史数据不回填,旧记录 `creator_name` 为空字符串。
## Acceptance criteria
- [ ] Migration 为两个现有任务表各添加 `creator_name varchar(100) not null default ''` 字段
- [ ] IoT 卡导入任务创建时写入当前用户姓名快照
- [ ] 设备导入任务创建时写入当前用户姓名快照
- [ ] IoT 卡导入任务列表接口返回 `creator_name` 字段
- [ ] 设备导入任务列表接口返回 `creator_name` 字段
- [ ] 旧记录 `creator_name` 为空字符串,不报错
## Blocked by
None - can start immediately

View File

@@ -1,43 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
新增"批量失效订单套餐"导入任务,完整实现 Model → Store → Service → Handler → Route → Worker 全链路,参照现有 IoT 卡导入任务的结构。
**数据模型**(新建 `tb_order_package_invalidate_task`
- 参照 `tb_iot_card_import_task` 的基础字段(任务编号、状态、计数、失败/跳过明细、文件存储 key、时间戳等
- 额外字段:`creator_name varchar(100)``remark text``voucher_keys jsonb`(存储 `[]string`最多5个元素
**上传接口**(创建任务):
- 接受 CSV 文件(单列 `order_no`
- 额外参数:`remark`(字符串,可选)和 `voucher_keys``[]string`最多5个可选
- 写入 `creator_name` 快照
**Worker 处理逻辑**
- 逐行读取 `order_no`
-`order_no` 查询 `tb_order` 获取 `order_id`;找不到则记为失败,继续处理
- 查询 `tb_package_usage WHERE order_id = ? AND status NOT IN (3, 4)`,批量更新 `status = 4`(已失效)
- 订单存在但套餐全部已是终态status 3 或 4记为成功
- 不触发退款,不调用网关,不直接停机
**列表/详情接口**:参照现有导入任务接口结构。
## Acceptance criteria
- [ ] Migration 创建 `tb_order_package_invalidate_task` 表,包含所有必要字段
- [ ] 创建任务接口:接受 CSV + remark + voucher_keys最多5个写入 creator_name 快照
- [ ] Worker 按行处理:`order_no` 不存在 → 失败并记录原因,继续下一行
- [ ] Worker 按行处理:订单存在且有可失效套餐 → 批量更新 `status=4` → 记为成功
- [ ] Worker 按行处理:订单存在但套餐全为终态 → 记为成功
- [ ] 处理过程中不调用退款接口、不操作网关
- [ ] 列表接口返回 `creator_name``remark`、任务状态和计数
- [ ] 详情接口返回失败明细(含 order_no 和原因)
- [ ] 已在路由和文档生成器中注册
## Blocked by
None - can start immediately

View File

@@ -1,36 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
一次 Migration 完成三个现有表的凭证字段类型变更,并同时新增代理预充值备注字段:
**字段类型变更**varchar(500) → jsonb
- `tb_order.payment_voucher_key`
- `tb_refund.refund_voucher_key`
- `tb_agent_recharge_record.payment_voucher_key`
**数据转换规则**(在同一 Migration 的 USING 子句中完成):
- 非空旧值 `"some_key"``["some_key"]`
- 空字符串 `""``[]`
- NULL → `[]`
**新增字段**
- `tb_agent_recharge_record.remark text`(可为空,默认 null
生产环境由运营手动执行Migration 文件需包含完整的 UP 和 DOWN 语句。
## Acceptance criteria
- [ ] Migration UP三个凭证字段成功从 varchar 转为 jsonb历史单值数据转换为单元素数组
- [ ] Migration UP空字符串/NULL 转换为空数组 `[]`
- [ ] Migration UP`tb_agent_recharge_record` 新增 `remark text` 字段
- [ ] Migration DOWN可回滚jsonb → varchar取数组第一个元素remark 字段删除)
- [ ] 迁移后可用 PostgreSQL MCP 验证数据格式正确
## Blocked by
None - can start immediately

View File

@@ -1,27 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
将后台创建订单接口的凭证字段从单个字符串改为最多5个字符串的列表
- `CreateAdminOrderRequest.payment_voucher_key``string``[]string`validate 限制 `max=5`,每项 `max=500`
- `OrderResponse.payment_voucher_key`(以及所有订单相关响应 DTO`string``[]string`
- Order Model 的 `PaymentVoucherKey` 字段类型改为适配 jsonb 的 `pq.StringArray` 或自定义 JSON 类型
- Order Service 中读写 `payment_voucher_key` 的逻辑同步更新
Schema 变更已由 `issues/05-voucher-multi-migration.md` 完成。
## Acceptance criteria
- [ ] 后台创建订单接口接受 `payment_voucher_key []string`0-5个元素offline 支付时至少需要1个
- [ ] 订单列表/详情接口返回 `payment_voucher_key []string`
- [ ] 传入超过5个凭证 key 时返回参数校验错误
- [ ] 现有无凭证的订单wallet 支付)返回空数组,不报错
## Blocked by
- `issues/05-voucher-multi-migration.md`

View File

@@ -1,29 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
将退款申请接口的凭证字段从单个字符串改为最多5个字符串的列表
- `CreateRefundRequest.refund_voucher_key``string``[]string`validate 限制 `max=5`,每项 `max=500`required至少1个
- `ResubmitRefundRequest.refund_voucher_key``*string``*[]string`(重新提交时可选替换)
- `RefundResponse.refund_voucher_key`(以及所有退款相关响应 DTO`string``[]string`
- Refund Model 的 `RefundVoucherKey` 字段类型改为适配 jsonb 的类型
- Refund Service 中读写 `refund_voucher_key` 的逻辑同步更新
Schema 变更已由 `issues/05-voucher-multi-migration.md` 完成。
## Acceptance criteria
- [ ] 创建退款申请接口接受 `refund_voucher_key []string`1-5个元素必填
- [ ] 重新提交退款接口的 `refund_voucher_key` 可选,传入时替换全部凭证
- [ ] 退款列表/详情接口返回 `refund_voucher_key []string`
- [ ] 传入超过5个凭证 key 时返回参数校验错误
- [ ] 传入空数组时返回参数校验错误至少需要1个
## Blocked by
- `issues/05-voucher-multi-migration.md`

View File

@@ -1,35 +0,0 @@
Status: done
## Parent
`.scratch/iot-risk-import-voucher-remark/PRD.md`
## What to build
同时完成代理预充值的凭证多图改造和备注字段接入:
**凭证多图**
- `CreateAgentRechargeRequest.payment_voucher_key``string``[]string`validate 限制 `max=5`,每项 `max=500`offline 支付时至少1个微信支付时忽略
- `AgentRechargeResponse.payment_voucher_key``string``[]string`
- `AgentRechargeRecord.PaymentVoucherKey` Model 字段类型改为适配 jsonb 的类型
**备注字段**
- `CreateAgentRechargeRequest` 新增 `remark string`可选建议不超过1000字符
- `AgentRechargeResponse` 新增 `remark string`(列表和详情接口均返回)
- `AgentRechargeRecord.Remark` 在创建时写入,后续不可修改
AgentRecharge Service 中读写相关字段的逻辑同步更新。
Schema 变更payment_voucher_key 类型 + remark 字段)已由 `issues/05-voucher-multi-migration.md` 完成。
## Acceptance criteria
- [ ] 创建代理预充值接口接受 `payment_voucher_key []string`offline 时至少1个和可选 `remark`
- [ ] 代理预充值列表/详情接口返回 `payment_voucher_key []string``remark`
- [ ] 传入超过5个凭证 key 时返回参数校验错误
- [ ] 不新增 remark 修改接口,创建后备注字段只读
- [ ] 微信支付创建预充值时可不传凭证(与现有逻辑一致)
## Blocked by
- `issues/05-voucher-multi-migration.md`

View File

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

View File

@@ -1,18 +0,0 @@
# 00 — 建立全系统审计面与公共能力决策基线
**What to build:** 对整个现有系统而非仅七月迭代执行一次可复核的审计面盘点,形成动作注册表输入和机器可检查的覆盖基线。每个状态变更、敏感读取、关键拒绝或失败入口都必须明确 Audit Event、Domain Ledger、Integration Log、Outbox 的使用决策或给出不适用理由,后续需求以此基线增量维护。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure 治理,辅助通道为 Query 验证。
**完整业务边界:** 本票收口 Handler、Application、Domain、Service、Worker 和回调入口的全仓审计分类、动作编码、事务策略、资源关系、失败策略和测试责任人。明确不迁移任何业务代码、不改变已评审业务规则、不把 Access Log、Audit Event、Domain Ledger 和 Integration Log 合并。
- [ ] 盘点全部 HTTP/Worker/定时任务/回调入口,至少覆盖创建、修改、删除、状态流转、资金、权限、关键配置、导入导出、人工恢复、敏感读取、拒绝和关键失败;记录代码入口与业务所有者。(自动扫描候选已生成,待三方确认不存在启发式漏项)
- [ ] 每个入口明确 `Audit Event / Domain Ledger / Integration Log / Outbox / N/A` 决策;选择 N/A 必须写明原因,禁止空白或“以后处理”。(自动分类候选已生成,待三方逐入口评审确认)
- [ ] 对需要审计的入口填写稳定动作编码、中文名称、风险、主要及受影响资源、操作者与来源、前后数据、事务边界、失败策略和自动化测试接缝。(结构已固定,待三方修正并确认业务语义)
- [x] 建立可被静态检查或测试读取的动作注册与覆盖基线,未经登记的新敏感操作不能通过发布门禁。
- [x] 对旧账号、资产、手动轮询 Writer 及全部直接写旧日志表的位置建立准确清单,作为 0409 迁移票和 19 号切换票的输入。
- [ ] 更新《审计覆盖基线》,经业务、研发和安全评审确认不存在未分类入口;评审只确认覆盖与分类,不借机扩大各业务 PRD 范围。

View File

@@ -1,21 +0,0 @@
# 01 — 交付不可变 Audit Event 写入闭环
**What to build:** 业务用例可以通过统一 Audit Writer 写入不可变、多资源、可串联且默认脱敏的审计事件。成功事件能够与业务事实共用事务,失败或拒绝事件使用独立短事务保留;未注册动作、缺少主要资源或审计写入失败时按明确策略阻止错误事实落地。
**Blocked by:**
- 00 — 建立全系统审计面与公共能力决策基线
- `.scratch/tech-public-foundation/issues/01-public-migration-ownership-and-gates.md` — 01 — 建立公共迁移所有权与检查门禁
**Status:** ready-for-agent
**架构通道:** 主通道为 Application + Port/Adapter辅助通道为 Infrastructure。
**完整业务边界:** 本票收口 Audit Event、事件资源、动作注册表、统一 Sanitizer、内容哈希、大小控制和 Writer 可靠性,并用一个代表性敏感写操作验证公开接缝。明确不迁移其他旧 Service不提供完整审计查询中心不用 Audit Event 替代领域流水。
- [ ] Audit Event 支持稳定事件 ID、操作者与入口快照、结果、风险、前后数据、请求/关联/父事件 ID、HTTP 摘要、内容哈希和创建时间;事件资源支持 `primary/affected/reference`,且每个事件至少包含一个 `primary`
- [ ] 动作注册表集中定义稳定动作编码、中文名称、类别、默认风险、允许资源类型和敏感字段规则;未经注册的动作不能写入生产审计。
- [ ] 统一 Sanitizer 删除禁止字段;`before_data``after_data``metadata` 分别执行 16KB 上限,超限后保存截断标志、原字节数、摘要和受控制品引用。
- [ ] `content_hash` 基于脱敏、标准化后的不可变内容生成,不包含数据库自增 ID相同内容哈希稳定Repository 不提供 Update/Delete 能力。
- [ ] `AppendWithTx` 与业务事实使用同一 GORM 事务,任一步失败整体回滚;已经回滚的 `failed/denied` 使用独立短事务,二次失败保留原业务错误并产生 critical 日志和指标。
- [ ] 真实 PostgreSQL 集成测试覆盖不可变约束、多资源、动作注册、风险默认值、哈希、截断、禁止字段、重复事件幂等和事务回滚。

View File

@@ -1,18 +0,0 @@
# 02 — 交付可恢复的 Integration Log 尝试闭环
**What to build:** 外部调用和未实际发出的同步尝试都能以稳定 Integration ID 建立可恢复事实,并通过受控条件更新进入明确终态。入站回调在业务处理前保存脱敏摘要和幂等标识,出站响应未知时保留未知结论而不伪装成普通失败。
**Blocked by:** `.scratch/tech-public-foundation/issues/01-public-migration-ownership-and-gates.md` — 01 — 建立公共迁移所有权与检查门禁
**Status:** completed
**架构通道:** 主通道为 Infrastructure辅助通道为 Application + Port/Adapter。
**完整业务边界:** 本票收口 Integration Log 模型、Writer、执行态到终态的条件更新、入站摘要和未发送结果语义。明确不接管 Gateway、支付、企微或运营商的业务状态机不把外部交互记录作为业务权威状态。
- [x] Integration Log 支持 provider、方向、operation、外部单号、资源、触发来源/场景/序列/尝试、计划与开始时间、结果、渠道摘要、脱敏请求响应摘要、耗时、状态变化标志和关联 ID。
- [x] 方向固定为 `inbound/outbound`;公开终态至少覆盖成功、失败、未找到、无效载荷、忽略、合并、限频、提前完成和取消,并返回对应中文名称。
- [x] 实际外部调用前持久化稳定尝试身份,完成后使用预期状态条件更新;并发完成、重复回调或重复消费不能改写既有终态。
- [x] 请求已发出但响应未知时记录明确的未知结论和恢复策略,不自动把具有副作用的请求当成普通失败盲目重发。
- [x] 入站回调先保存脱敏摘要、内容哈希和幂等标识;原始密文、完整正文、签名、附件和密钥不进入普通记录。
- [x] PostgreSQL 集成测试覆盖出站成功、明确失败、响应未知、未发送终态、入站回调、重复回调、条件更新冲突和 Audit Event 关联。

View File

@@ -1,21 +0,0 @@
# 03 — 完成 Access Log 全路由敏感信息防泄漏
**What to build:** 当前项目所有敏感 HTTP 路由均使用统一的请求、响应、Query 和 Header 脱敏及路由级安全摘要策略。即使正文不是 JSON、解析失败或属于文件载荷也不会把可复用凭证、完整回调或文件内容写入 Access Log。
**Blocked by:**
- `.scratch/tech-public-foundation/issues/09-access-log-recursive-redaction.md` — 09 — 统一 Access Log 请求与响应递归脱敏
- `.scratch/tech-public-foundation/issues/10-sensitive-route-safe-summaries.md` — 10 — 为敏感接口提供安全摘要策略
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票将公共 Access Log 安全能力应用到当前仓库真实路由并建立固定回归矩阵。明确不修改业务响应,不创建审计事实,不把 Access Log 升级为业务权威存储。
- [x] 登录、Token、支付与企微配置路由只记录字段存在性、长度和安全结果不记录密码、验证码、Token、Secret、密钥或完整配置值。
- [x] 支付、企微和运营商回调只记录事件类型、安全资源标识、大小、内容类型、摘要哈希与处理结果,不记录密文、完整正文、签名或附件。
- [x] 上传、下载和导出路由不记录文件字节、Base64、multipart 正文、临时凭证或签名 URL只保留脱敏文件元数据和任务标识。
- [x] Query 和 Header 覆盖 token、secret、sign、nonce、authorization、cookie 等大小写变体;请求和响应均先脱敏再执行 50KB 截断。
- [x] 非 JSON、XML、表单、二进制及解析失败场景均按路由策略安全降级不能回退记录原文。
- [x] 真实 Fiber 测试捕获最终 Access Log覆盖敏感路由矩阵并断言测试凭证、操作密码、回调原文、签名 URL、Authorization 和 Cookie 均未落盘。

View File

@@ -1,18 +0,0 @@
# 04 — 迁移账号、角色与权限敏感操作到统一审计
**What to build:** 账号、角色和权限敏感操作通过统一 Audit Writer 记录操作者、动作、目标资源、结果、风险和前后变化。关键成功审计与业务修改同事务,拒绝和失败审计可可靠保留,这些入口不再调用旧账号审计 Writer。
**Blocked by:** 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 主通道为简单写 Application辅助通道为旧 MVC Adapter。
**完整业务边界:** 本票收口现有账号、角色和权限敏感写入口的统一审计接入。明确不重构整个账号模块,不迁移无关读取,不删除或回填旧账号历史表。
- [ ] 账号创建、修改、启停、删除、角色分配及权限变更使用注册动作和统一资源类型,成功、拒绝和失败语义明确。
- [ ] 账号角色或权限关键成功事件与业务事实使用同一 GORM 事务;审计失败时业务修改回滚。
- [ ] 权限拒绝与业务失败通过独立短事务记录,不向客户端泄露底层错误或资源是否存在。
- [ ] 事件包含操作者与入口快照、请求/关联标识、变更前后事实和直接受影响资源,敏感字段按统一规则删除或脱敏。
- [ ] 生产装配中的上述入口不再调用旧账号审计 Service也不直接创建旧账号日志模型静态检查和真实用例测试可证明该边界。
- [ ] 测试覆盖事务成功、审计失败回滚、拒绝、业务失败、重复请求和数据权限边界,不改变旧历史查询结果。

View File

@@ -1,18 +0,0 @@
# 05 — 迁移卡资产生命周期操作到统一审计
**What to build:** 卡分配、回收、删除、停复机、实名策略和状态变化等资产生命周期操作统一产生可关联的 Audit Event并以多资源关系表达卡、设备、店铺、订单等直接影响对象。复杂卡状态规则仍由完整业务用例收口不在审计 Adapter 中复制。
**Blocked by:** 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 复杂卡状态用例采用 Application/Domain其他入口采用旧 MVC Adapter。
**完整业务边界:** 本票迁移现有卡资产生命周期写入口的审计能力。明确不重新设计周期轮询、队列、卡资格或重排策略,不迁移本需求未触碰的资产规则,不删除旧资产历史表。
- [ ] 卡分配、回收、删除、手工停复机、实名策略和业务状态变化均映射到已注册动作,成功、拒绝和失败结果保持一致。
- [ ] 事件至少关联一个主要卡资源,并按实际影响关联设备、来源/目标店铺、订单或其他直接资源;不递归制造资源关系。
- [ ] 关键状态变更成功审计与业务事实同事务;失败或拒绝使用独立短事务,重复状态请求不制造重复业务副作用或虚假变化。
- [ ] `request_id/correlation_id/parent_event_id` 在 HTTP、业务用例和后续异步链路中按各自职责传播。
- [ ] 当前卡资产入口不再调用旧资产审计 Writer 或直接创建旧资产日志模型,但兼容历史读取仍可工作。
- [ ] 测试覆盖权限拒绝、状态条件更新、事务回滚、多资源时间线、敏感 ICCID 默认脱敏和重复请求。

View File

@@ -1,18 +0,0 @@
# 06 — 迁移设备与资产导入操作到统一审计
**What to build:** 设备分配、回收、绑定、控制操作以及卡和设备导入任务通过统一 Audit Writer 留下稳定、脱敏、可检索的事件。批量操作只在事件中保存命令摘要和计数,逐项明细继续由业务任务或受控制品承担。
**Blocked by:** 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 主通道为简单写 Application/旧 MVC Adapter辅助通道为 Infrastructure。
**完整业务边界:** 本票收口设备生命周期与现有资产导入入口的审计接入。明确不重构整个设备 Service不建立通用批量任务表不把大批量明细塞入 Audit Event JSON。
- [ ] 设备分配、回收、系列绑定、绑卡/解绑、停复机、限速、WiFi 设置、切卡、重启和恢复出厂映射为注册动作。
- [ ] 卡和设备导入任务创建、完成与失败事件关联任务、操作者、来源文件安全摘要和结果计数,不保存文件内容、签名 URL 或完整逐项数据。
- [ ] 批量事件超过大小限制时记录截断元数据、原始计数、摘要及受控任务/制品引用,查询仍能定位权威明细。
- [ ] 关键业务修改与成功审计同事务;拒绝和失败可靠保留,重复任务或重复控制请求不制造重复成功事件。
- [ ] 相关生产入口不再调用旧资产审计 Writer自动检查覆盖直接旧模型 Create 和隐藏装配注入。
- [ ] 真实用例测试覆盖单项与批量、事务回滚、部分结果摘要、多资源关系、权限范围和敏感字段删除。

View File

@@ -1,18 +0,0 @@
# 07 — 迁移店铺、套餐和关键配置操作到统一审计
**What to build:** 店铺敏感归属、套餐管理以及支付、企微和关键系统配置的现有写操作统一记录注册动作、操作者、目标资源和前后变化。关键配置与权限相关成功审计和业务事实同事务,敏感配置原值永不进入审计正文。
**Blocked by:** 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 主通道为简单写 Application辅助通道为旧 MVC Adapter。
**完整业务边界:** 本票迁移当前已有店铺、套餐和关键配置敏感入口。明确不接管各业务 PRD 尚未实现的状态机,不主动迁移无关 CRUD不创建配置专用审计表。
- [ ] 店铺敏感业务员归属、关键账号关联和其他已识别敏感变更使用稳定动作及店铺/人员资源关系。
- [ ] 套餐创建、修改、上下架、授权或其他已存在敏感管理入口记录业务命令摘要和直接受影响资源。
- [ ] 支付、企微及关键系统配置变更只记录 Key、安全类型与脱敏前后摘要不保存 Secret、Token、EncodingAESKey、私钥、公钥原文或签名材料。
- [ ] 关键配置和权限相关成功审计与业务事实同事务;拒绝、校验失败和持久化失败按公共失败策略记录。
- [ ] 相关旧账号/资产审计调用与零散配置日志不再承担这些操作的权威审计,生产装配可被自动检查验证。
- [ ] 测试覆盖配置审计失败回滚、店铺越权、套餐批量摘要、敏感字段删除、重复更新和统一中文错误。

View File

@@ -1,21 +0,0 @@
# 08 — 迁移现有资金与订单敏感操作到统一审计
**What to build:** 当前钱包变更、充值、退款和订单资金敏感操作通过统一 Audit Event 关联业务单、钱包、钱包流水、操作者和完整业务链路。金额结论继续以 Domain Ledger 为准,关键成功审计与资金事实同事务且只产生一次。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- `.scratch/tech-public-foundation/issues/02-transactional-public-outbox-write.md` — 02 — 在业务事务中可靠写入公共 Outbox
**Status:** ready-for-agent
**架构通道:** 主通道为复杂写 Application/Domain辅助通道为 Port/Adapter。
**完整业务边界:** 本票迁移仓库当前存在的钱包、充值、退款和订单资金敏感入口,完整收口所触碰用例的金额、并发、幂等和可靠事件边界。明确不用 Audit Event 替代钱包流水、订单或退款事实,不重写未触碰资金用例。
- [ ] 钱包余额变更、代理钱包回退、线下充值入账、人工退款结果及现有订单资金操作使用注册动作和稳定 correlation。
- [ ] Audit Event 关联审批或业务单、钱包、钱包流水及直接受影响资产;每条投影明确领域流水才是金额权威。
- [ ] 余额、流水、成功审计和必要 Outbox 在同一 GORM 事务内提交;审计或 Outbox 写入失败时资金事实整体回滚。
- [ ] 状态条件更新、钱包版本或稳定业务键保证重复请求、Worker 重投和并发处理不重复改变余额、流水或成功审计。
- [ ] 明确失败、拒绝和结果异常使用独立短事务记录安全摘要,不把第三方支付密钥、完整凭证或底层错误返回客户端。
- [ ] PostgreSQL 集成测试覆盖正常资金变化、审计失败、Outbox 失败、乐观锁冲突、重复业务键、并发处理和金额事实对账。

View File

@@ -1,24 +0,0 @@
# 09 — 承接手动轮询状态并记录同步外部尝试
**What to build:** 现有手动触发、进度和监控接口保持用户契约但运行状态由公共异步任务契约承接Gateway 实际请求及合并、互斥、限频、提前完成等未发送尝试进入 Integration Log只有状态变化、人工强制、连续失败或高风险异常进入 Audit Event。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- 02 — 交付可恢复的 Integration Log 尝试闭环
- `.scratch/tech-public-foundation/issues/02-transactional-public-outbox-write.md` — 02 — 在业务事务中可靠写入公共 Outbox
- `.scratch/tech-public-foundation/issues/03-outbox-at-least-once-delivery.md` — 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
- `.scratch/tech-public-foundation/issues/06-unified-async-task-contract.md` — 06 — 冻结统一异步任务五态和查询契约
**Status:** ready-for-agent
**架构通道:** 主通道为 Application + Infrastructure辅助通道为 Query。
**完整业务边界:** 本票收口旧手动轮询日志承担的运行状态替代、外部尝试记录和兼容接口。明确不改变轮询配置、间隔、Redis 分片队列、任务类型、卡级开关、并发控制或失败重排,不新建同步专用运行表。
- [ ] 手动触发、进度与监控公开契约继续可用,状态、计数、失败摘要和恢复行为映射到公共异步任务五态。
- [ ] 实际 Gateway 请求在调用前建立 Integration Log完成后记录结果、耗时、状态变化和脱敏上游摘要。
- [ ] 合并、互斥、限频、已达预期和取消即使未产生 HTTP 请求也有可解释终态,不伪造 HTTP 状态。
- [ ] 同步序列传播 `request_id/correlation_id/series_id/attempt`同序列查询可连续看到立即、3 分钟、5 分钟的全部尝试。
- [ ] 普通无变化成功只写 Integration Log状态变化、人工强制、连续失败或高风险异常按注册规则关联 Audit Event。
- [ ] 回归测试固定现有轮询配置、队列、开关和监控基线,验证切换前后调度事实不变且旧手动轮询表不再承担新增运行状态。

View File

@@ -1,18 +0,0 @@
# 10 — 提供旧审计历史统一只读投影
**What to build:** 审计查询可以把旧账号、旧资产和旧手动轮询记录规范化为统一只读历史事件,并与新 Audit Event 稳定分页排序。现有资产操作日志兼容接口继续可用,但不再直接绑定旧资产日志表。
**Blocked by:** 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口三个旧历史来源的 `UNION ALL` 投影、确定性历史键和资产兼容读取。明确不在线回填新表,不修改或删除旧记录,不伪造历史中不存在的 correlation、风险或多资源关系。
- [ ] 旧账号、资产和手动轮询记录分别返回 `legacy_account/legacy_asset/legacy_polling` 来源及确定性历史事件键。
- [ ] 字段映射保留可证明的操作者、操作、资源、结果、时间和摘要;不存在的关联、风险或资源关系显式为空而非推测填充。
- [ ] 新旧记录使用稳定时间加 ID/历史键排序,分页跨越切换时间时无重复、漏项或顺序漂移。
- [ ] 资产操作日志兼容接口同时读取新 Audit Event 与旧资产投影,并保持原业务调用方所需响应兼容性。
- [ ] Query 不返回可用于业务写入的聚合对象,不对旧 JSON 做无索引模糊扫描,不产生 N+1 查询。
- [ ] PostgreSQL 投影测试覆盖三类历史样本、字段映射、确定性键、分页排序、新旧交界和兼容接口。

View File

@@ -1,23 +0,0 @@
# 11 — 交付全局事件和资源轨迹查询闭环
**What to build:** 获得授权的平台用户可以分页查看全局审计事件与详情,先搜索业务资源候选,再打开包含新事件和历史投影的资源时间线。资源权限与字段脱敏由后端重新校验,代理或企业用户只能在原业务范围查看允许的资源轨迹。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- 05 — 迁移卡资产生命周期操作到统一审计
- 06 — 迁移设备与资产导入操作到统一审计
- 10 — 提供旧审计历史统一只读投影
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口事件列表/详情、资源候选搜索和资源时间线 API 及权限、分页、索引和 DTO 投影。明确不递归遍历资源图,不扫描 JSONB不通过聚合根读取不实现其他审计视角。
- [ ] 事件列表和详情支持注册动作、操作者、来源、结果、风险、时间、资源和请求标识等有索引过滤,默认 20、最大 100并稳定按时间与 ID 排序。
- [ ] 资源搜索先查询业务读模型返回类型、ID、Key 和显示名;静态搜索路由先于动态资源路由注册。
- [ ] 资源时间线组合新 Audit Event 和旧投影,`include_related=true` 只展开事件直接关联资源,不递归遍历关系图。
- [ ] 超级管理员、平台角色、代理和企业按权限码与原数据范围取交集;越权查询不泄露资源是否存在。
- [ ] DTO 默认脱敏并返回结果、风险等中文名称;公共关键词不对 JSONB 执行无索引模糊扫描。
- [ ] 真实 Fiber、认证、GORM 和 PostgreSQL 测试覆盖路由顺序、分页、筛选、权限、历史交界、无 N+1 和常用查询索引;新增 Handler 同步两个文档生成器。

View File

@@ -1,21 +0,0 @@
# 12 — 交付人员行为和风险事件查询闭环
**What to build:** 获得相应权限的平台用户可以查看操作者列表、行为摘要、个人事件时间线以及风险概览和风险事件列表。所有聚合均由服务端完成,并严格隔离代理、企业和无对应权限的平台角色。
**Blocked by:**
- 04 — 迁移账号、角色与权限敏感操作到统一审计
- 11 — 交付全局事件和资源轨迹查询闭环
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口人员与风险两个读取视角、服务端汇总和权限边界。明确不建设自动封禁、风险处置工单、实时行为分析或前端全量下载聚合。
- [ ] 操作者查询支持 actor kind、稳定 ID、名称快照、入口、动作、结果、风险和时间过滤并提供摘要及稳定分页事件时间线。
- [ ] 风险概览按风险等级、结果、类别、动作和时间窗口进行有索引汇总,风险事件列表可进一步检索详情。
- [ ] `audit:actor:view``audit:risk:view` 独立授权;代理和企业账号不能进入人员或风险全局视角。
- [ ] 平台角色的权限与数据范围取交集,失败、拒绝、高风险和严重事件不会因缺失可选关联而被错误过滤。
- [ ] 汇总使用服务端 SQL 和批量投影,不下载全量事件、不对 JSONB 模糊扫描、不产生操作者或资源 N+1。
- [ ] HTTP 与性能测试覆盖超级管理员、不同平台角色、代理、企业、空态、筛选空态及代表性数据量OpenAPI 文档同步更新。

View File

@@ -1,24 +0,0 @@
# 13 — 交付请求、业务链路和外部集成时间线
**What to build:** 运维人员可以按 request ID 或 correlation ID 查看同一请求或完整业务链路中的 Audit Event、Integration Log、Outbox 和异步任务摘要,并可按外部提供方、操作、方向、结果、资源、场景和序列检索外部尝试。
**Blocked by:**
- 02 — 交付可恢复的 Integration Log 尝试闭环
- 09 — 承接手动轮询状态并记录同步外部尝试
- 11 — 交付全局事件和资源轨迹查询闭环
- `.scratch/tech-public-foundation/issues/02-transactional-public-outbox-write.md` — 02 — 在业务事务中可靠写入公共 Outbox
- `.scratch/tech-public-foundation/issues/06-unified-async-task-contract.md` — 06 — 冻结统一异步任务五态和查询契约
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口请求时间线、关联时间线和 Integration Log 列表/详情查询。明确不在 API 请求中扫描本地 Access Log不复制 Outbox/任务完整载荷,不把外部交互结果解释成业务权威状态。
- [ ] Request Timeline 组合可关联的审计事件、外部尝试、Outbox 和任务安全摘要,并返回 Access Log 检索标识而不读取日志文件。
- [ ] Correlation Timeline 以稳定 correlation 串联跨请求、回调和异步处理,`parent_event_id` 只表达直接因果,不替代 correlation。
- [ ] Integration 查询支持 provider、operation、方向、结果、资源、触发来源、场景、序列、尝试和时间过滤列表默认 20、最大 100。
- [ ] 同一同步序列能够按尝试顺序连续展示立即、3 分钟、5 分钟结果,包括合并、限频和提前完成等未发送终态。
- [ ] 原始外部正文、密文、签名、附件、Outbox 载荷和任务敏感失败明细不进入普通查询 DTO。
- [ ] 真实链路测试覆盖单请求、多请求 correlation、异步传播、重复尝试、0/3/5 序列、权限隔离和常用查询索引OpenAPI 文档同步更新。

View File

@@ -1,21 +0,0 @@
# 14 — 交付资金审计时间线
**What to build:** 财务人员可以按业务单、钱包、审批、资产和时间查看组合时间线,统一展示 Audit Event、钱包流水、订单、退款、充值和审批实例并清楚区分每条记录来源及金额权威。
**Blocked by:**
- 08 — 迁移现有资金与订单敏感操作到统一审计
- 13 — 交付请求、业务链路和外部集成时间线
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口资金审计的只读组合投影、权限、分页和对账语义。明确不通过查询修改资金状态,不把 Audit Event 当作金额账本,不迁移未触碰的资金写用例。
- [ ] 时间线组合 Audit Event、钱包流水、订单、退款、充值和审批实例每条返回明确 `record_source`、时间、业务标识和安全摘要。
- [ ] 金额、余额和资金结论始终取自 Domain Ledger审计事件只解释操作者、动作、风险、前后变化和关联关系。
- [ ] 支持钱包、店铺、业务单、审批、资产、动作、结果、风险和时间等有索引过滤,并使用稳定时间加来源 ID 排序。
- [ ] `audit:finance:view` 独立授权,代理和企业不能进入全局资金视角;平台角色权限与原资金数据范围取交集。
- [ ] 金额、手机号、第三方交易号等字段默认按权限脱敏,不因某条关联缺失泄露其他资源存在性。
- [ ] 对账和性能测试覆盖多来源同链路、退款与充值、审批关联、无重复漏项、无 N+1 和代表性数据量OpenAPI 文档同步更新。

View File

@@ -1,22 +0,0 @@
# 15 — 交付敏感值二次查看与自审计闭环
**What to build:** 审计详情默认返回掩码或安全状态;具有独立敏感查看权限的用户可以针对明确事件或资源二次请求允许展示的完整值,而该查看行为本身会产生高风险 Audit Event并保留被查看对象、操作者和请求链路。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- 11 — 交付全局事件和资源轨迹查询闭环
- 12 — 交付人员行为和风险事件查询闭环
**Status:** ready-for-agent
**架构通道:** 主通道为 Query辅助通道为简单写 Application。
**完整业务边界:** 本票收口敏感字段受控读取、字段级授权和读取自审计。明确不恢复已被策略删除的原值,不允许超级管理员绕过二次审计,不授予导出权限。
- [ ] 普通详情对手机号、IP、ICCID、钱包金额和第三方交易号等受控字段默认脱敏禁止字段始终不可恢复。
- [ ] 二次查看要求 `audit:sensitive:view`、明确目标和字段范围,并重新校验平台角色、数据范围与资源权限。
- [ ] 成功或被拒绝的敏感查看产生注册的高风险 Audit Event关联被查看事件/资源、操作者、入口和 request ID。
- [ ] 审计写入失败时敏感值不返回;重复查看每次都保留独立读取事实,不因已有全局权限跳过。
- [ ] 无权限、历史字段不存在、原值已删除和资源越权返回明确但不泄密的统一错误或状态。
- [ ] 真实 Fiber 和 PostgreSQL 测试覆盖授权查看、越权、审计失败、禁止字段、历史缺失、并发请求及自审计事件查询OpenAPI 文档同步更新。

View File

@@ -1,24 +0,0 @@
# 16 — 交付审计导出与字段授权快照
**What to build:** 获得审计导出权限的用户可以从支持的审计视角创建异步导出任务。任务创建时固化过滤条件、数据范围、字段授权和脱敏等级,后续角色变更或任务重试不会扩大导出内容。
**Blocked by:**
- 11 — 交付全局事件和资源轨迹查询闭环
- 12 — 交付人员行为和风险事件查询闭环
- 13 — 交付请求、业务链路和外部集成时间线
- 14 — 交付资金审计时间线
- 15 — 交付敏感值二次查看与自审计闭环
**Status:** ready-for-agent
**架构通道:** 主通道为 Query + Export DataSource辅助通道为 Application。
**完整业务边界:** 本票收口审计导出 Scene、字段能力、权限快照、异步任务和导出审计。明确不创建第二套导出框架不把敏感查看权限等同敏感导出权限不在权限解析失败时回退全字段。
- [ ] 审计导出复用统一 Export DataSource 和任务契约,支持经批准的事件、资源、链路、集成、风险和资金过滤条件。
- [ ] 创建任务时快照操作者、权限、数据范围、字段集合、脱敏等级和过滤条件Worker 只使用快照执行。
- [ ] `audit:export``audit:sensitive:view` 独立;导出完整敏感字段需要额外字段授权,解析失败一律拒绝。
- [ ] 文件不包含禁止字段、完整外部报文、密钥、签名 URL 或未授权业务数据;下载继续遵守安全摘要和临时访问策略。
- [ ] 创建、完成、失败和下载等敏感导出行为产生统一 Audit Event并能关联导出任务与请求链路。
- [ ] 测试覆盖权限快照、角色变更、字段空集、敏感权限分离、任务重试、导出审计、越权和文件内容检查。

View File

@@ -1,21 +0,0 @@
# 17 — 交付审计保留、清理和运行监控
**What to build:** 运维人员可以观察 Audit/Integration 写入、旧表意外新增、Integration Log 增长与清理、敏感读取和导出,并由受控维护身份按保留策略分批清理可清理记录。在线应用始终不能更新或删除 Audit Event。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- 02 — 交付可恢复的 Integration Log 尝试闭环
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为 Query。
**完整业务边界:** 本票收口保留分类、Integration Log 分批清理、权限边界、指标与告警。明确不由在线应用删除 Audit Event不预建尚无必要的复杂分区系统不清理已有生产审计事实作为回滚手段。
- [ ] 审计事件按资金/权限/审批/关键配置与普通资产/业务分类表达 5 年或 2 年保留策略,在线应用账号无 Update/Delete 能力。
- [ ] Integration Log 默认保留 180 天Gateway 无变化成功记录保留 30 天;清理按稳定时间与主键小批量执行并记录结果。
- [ ] 清理只允许受控维护身份执行,使用条件范围和可恢复进度,不影响异常、状态变化或仍被业务链路引用的记录。
- [ ] 指标和告警覆盖 Audit/Integration 写入失败、失败短事务失败、旧三表意外新增、Outbox 积压、Integration 增长/清理、敏感读取和导出次数。
- [ ] 日志与指标标签只使用安全 ID、结果和计数不包含正文、敏感值或高基数未受控载荷。
- [ ] 测试覆盖保留边界、分批重跑、并发清理、在线账号拒绝、旧表新增告警和不删除未到期/受保护记录。

View File

@@ -1,25 +0,0 @@
# 18 — 冻结审计中心跨仓前端契约与验收包
**What to build:** 前端仓库获得稳定、框架无关的审计工作台契约和可执行验收数据,能够实现全局事件、人员行为、资源轨迹、请求/业务链路、资金审计、风险事件和外部集成七个视角,并正确处理权限、敏感查看、导出、空态与错误状态。
**Blocked by:**
- 11 — 交付全局事件和资源轨迹查询闭环
- 12 — 交付人员行为和风险事件查询闭环
- 13 — 交付请求、业务链路和外部集成时间线
- 14 — 交付资金审计时间线
- 15 — 交付敏感值二次查看与自审计闭环
- 16 — 交付审计导出与字段授权快照
**Status:** ready-for-agent
**架构通道:** Query/API 契约。
**完整业务边界:** 本票收口跨仓页面、状态、权限和验收契约以及 OpenAPI/样本数据。当前仓库不含前端源码,明确不虚构组件目录、状态库或技术栈,不在本票实现前端页面。
- [ ] 契约覆盖 `/operations/audit` 七个视角、权限控制 Tab、服务端筛选分页、稳定 URL 参数和详情抽屉字段分组。
- [ ] 资源轨迹定义先搜索候选再查看时间线;请求与 correlation 可相互跳转Integration 序列连续展示立即、3 分钟、5 分钟尝试。
- [ ] 敏感字段默认掩码,二次查看重新请求受控接口;导出、敏感查看和各视角权限分别处理。
- [ ] 明确定义加载、真实空态、筛选空态、403、404/不可见、失败重试、历史字段不存在和数据已删除等状态。
- [ ] 提供不含真实敏感数据的七视角验收样本、权限矩阵和人工验收清单,后端 OpenAPI 与真实路由保持一致。
- [ ] 文档明确前端隐藏不是授权边界,第一版不实现自动封禁、风险工单或自由拖拽关系图。

View File

@@ -1,41 +0,0 @@
# 19 — 执行一次性审计切换与停机发布门禁
**What to build:** 发布负责人可以在一次停机窗口内完成新结构、Writer、现有敏感入口、历史查询、权限、监控和跨仓契约切换并通过自动化门禁决定是否开放流量。任一关键检查失败时整体停止发布一旦新系统产生生产事实只能前向修复不能恢复旧 Writer 制造分裂历史。
**Blocked by:**
- 00 — 建立全系统审计面与公共能力决策基线
- 03 — 完成 Access Log 全路由敏感信息防泄漏
- 04 — 迁移账号、角色与权限敏感操作到统一审计
- 05 — 迁移卡资产生命周期操作到统一审计
- 06 — 迁移设备与资产导入操作到统一审计
- 07 — 迁移店铺、套餐和关键配置操作到统一审计
- 08 — 迁移现有资金与订单敏感操作到统一审计
- 09 — 承接手动轮询状态并记录同步外部尝试
- 10 — 提供旧审计历史统一只读投影
- 11 — 交付全局事件和资源轨迹查询闭环
- 12 — 交付人员行为和风险事件查询闭环
- 13 — 交付请求、业务链路和外部集成时间线
- 14 — 交付资金审计时间线
- 15 — 交付敏感值二次查看与自审计闭环
- 16 — 交付审计导出与字段授权快照
- 17 — 交付审计保留、清理和运行监控
- 18 — 冻结审计中心跨仓前端契约与验收包
- 20 — 接入公共系统配置正式审计 Adapter
- 21 — 接入 Outbox 人工恢复正式审计 Adapter
- `.scratch/tech-public-foundation/issues/12-foundation-release-gate-and-integration-contract.md` — 12 — 建立公共基础发布门禁和下游接入契约
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为 Application/Query 验收。
**完整业务边界:** 本票收口全局审计一次性停机切换、迁移验证、旧写收缩、对账、性能、安全、监控和发布回滚边界。明确不允许长期双写或局部放量,不删除旧历史,不在新事实产生后通过删表回滚。
- [ ] 停机顺序明确覆盖暂停流量与 Worker、前置检查、增量迁移、权限初始化、新 Writer 装配、旧写护栏、历史对账、Worker 恢复和开放流量。
- [ ] 自动切换清单证明生产产物不再调用旧账号/资产审计 Writer、不再直接 Create 旧三表、启动装配不再注入旧 Writer旧表意外新增会立即告警或失败。
- [ ] 《审计覆盖基线》的全部入口均已分类且无空白项;所有 Audit Event 动作已注册,所有 N/A 均有评审理由,系统配置更新和 Outbox 人工恢复已装配正式 Audit Writer。
- [ ] 门禁覆盖关键业务与审计同事务、审计失败回滚、失败短事务、Integration 结果未知、历史新旧交界、Access Log 敏感矩阵和权限隔离。
- [ ] Query 性能验证常用过滤使用索引、无 JSONB 全表模糊扫描、无资源/操作者 N+1并满足项目 P95/P99 目标。
- [ ] 任一迁移、旧写清单、样本对账、事务、安全、权限、性能、监控或跨仓验收失败均在开放流量前整体终止发布,不能让局部模块继续写旧表。
- [ ] 发布与回滚说明明确:未产生新事实时可回退兼容应用;已产生 Audit/Integration/Outbox 事实后保留全部记录、停止异常生产者并前向修复,禁止恢复旧 Writer 或删除事实。
- [ ] 中文功能总结覆盖关键流程、前后端契约、异常闭环、发布回滚、监控恢复和待决策项README 增加索引;完整停机演练通过后方可放量。

View File

@@ -1,20 +0,0 @@
# 20 — 接入公共系统配置正式审计 Adapter
**What to build:** 在系统配置更新的 Application 组合根注入正式 Audit Writer Adapter使关键配置变更进入统一 Audit Event并按公共基础已经确认的事务边界失败关闭。由真实全局审计替换公共基础的未装配占位不创建临时审计表或旁路 goroutine。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- `.scratch/tech-public-foundation/issues/08-controlled-system-config-update.md` — 08 — 交付系统配置更新、权限和审计闭环
**Status:** ready-for-agent
**架构通道:** 主通道为 Application + Port/Adapter辅助通道为 Infrastructure。
**完整业务边界:** 本票仅收口公共系统配置更新既有用例的正式审计装配、事务一致性、脱敏和测试。明确不新增配置 Key、不建设配置前端页面、不迁移其他业务审计。
- [ ] 注册系统配置更新的稳定动作编码、中文名称、风险等级及允许资源类型,并写入全系统审计覆盖基线。
- [ ] 系统配置更新的业务事实与 Audit Event 使用同一 GORM 事务;审计失败时配置更新回滚,敏感配置只记录“是否配置”和安全摘要,不记录原值。
- [ ] 组合根显式注入正式 Audit Writer Adapter未装配时失败关闭禁止回退到空实现、旧审计表或裸 goroutine。
- [ ] PostgreSQL 集成测试覆盖成功同事务、审计失败整体回滚、敏感值不落库和未装配失败关闭。
- [ ] 发布检查能证明生产组合根已装配正式 Adapter并由 19 号一次性切换门禁纳入停机演练。

View File

@@ -1,20 +0,0 @@
# 21 — 接入 Outbox 人工恢复正式审计 Adapter
**What to build:** 在 Outbox 人工恢复的 Application 组合根注入正式 Audit Writer Adapter使每次受控恢复完整记录操作者、原因、批次和状态变化并与恢复事实同事务失败关闭。由真实全局审计替换公共基础的未装配占位不创建临时审计表或旁路 goroutine。
**Blocked by:**
- 01 — 交付不可变 Audit Event 写入闭环
- `.scratch/tech-public-foundation/issues/04-outbox-observability-and-recovery.md` — 04 — 提供 Outbox 监控和受控恢复能力
**Status:** ready-for-agent
**架构通道:** 主通道为 Application + Port/Adapter辅助通道为 Infrastructure。
**完整业务边界:** 本票仅收口 Outbox 人工恢复既有用例的正式审计装配、事务一致性和测试。明确不改变恢复资格与租约规则、不建设 Outbox 前端页面、不迁移其他业务审计。
- [ ] 注册 Outbox 人工恢复的稳定动作编码、中文名称、风险等级及允许资源类型,并写入全系统审计覆盖基线。
- [ ] Outbox 状态变更与 Audit Event 使用同一 GORM 事务;记录操作者、中文原因、恢复批次、事件标识及恢复前后状态,审计失败时恢复回滚。
- [ ] 组合根显式注入正式 Audit Writer Adapter未装配时失败关闭禁止回退到空实现、旧审计表或裸 goroutine。
- [ ] PostgreSQL 集成测试覆盖成功同事务、审计失败整体回滚、重复恢复裁决、有效租约不可恢复和未装配失败关闭。
- [ ] 发布检查能证明生产组合根已装配正式 Adapter并由 19 号一次性切换门禁纳入停机演练。

View File

@@ -1,268 +0,0 @@
# 全系统审计覆盖基线
状态2026-08-06 当前源码最终重扫及业务语义、研发边界、安全字段三视角复核已完成;后续入口变更由静态门禁持续校验
## 可复核制品
- 显式逐入口清单:[`审计覆盖清单.json`](审计覆盖清单.json),当前共 692 项;七月旧 490 项只用于遗漏比对,不再作为现行契约。
- 扫描范围331 个 HTTP RouteSpec、39 个 Asynq Worker 注册语句35 个唯一 TaskType、12 个 Asynq 定时任务、33 个 Application、5 个 Domain、180 个旧 Service 公共业务入口、78 个 Integration Log 调用点、14 个 Outbox Consumer 注册点和 0 个旧 Writer 调用点。
- 生成入口:`go run ./cmd/audit-coverage`
- 发布门禁:重新执行 `go run ./cmd/audit-coverage`并通过清单差异、gopls、构建和数据库数据核对确认入口与决策同步。
- 每项均固定代码入口、业务所有者、中文摘要以及待评审的 Audit Event / Domain Ledger / Integration Log / Outbox 分类候选,并预留动作、风险、资源、操作者来源、事务边界、失败策略、前后数据、敏感策略和确认核对接缝。
- 当前 `Audit Event=N/A` 候选均填写逐入口理由;评审前不能据此宣称全系统分类已经确认。
生成器只负责产生待评审候选,不能自行代表评审通过。更新清单时必须核对业务语义,不能仅运行生成命令后直接提交。本轮复核已按下述证据完成,后续新增入口仍需重新执行同样流程。
## 2026-08-06 当前入口最终重扫结论
### 现行矩阵与旧清单差异
- 当前逐入口矩阵固定 `code_entry/kind/owner/action/actor/resource/transaction/visibility`,并分别登记 Audit Event、Domain Ledger、Integration Log、Outbox 或 N/A 理由692 项不存在空白决策字段,所有 Audit Event N/A 项均有理由。
- RouteSpec 从旧 270 项增至当前 331 项Worker 从 24 项增至 39 个注册语句35 个唯一 TaskTypeAsynq 定时任务从 4 项增至 12 项。当前另显式登记 78 个 Integration Log 调用点、14 个 Outbox Consumer 注册点和 0 个旧 Writer 调用点,避免用模块级泛化描述替代真实入口。
- 扫描器排除无 `context.Context` 的依赖注入 `SetXxx`、构造器、注册/装配函数和纯分发壳;普通 GET、购买检查和资产验证均按只读 N/A 登记。`SetSpeedTier` 因包含真实业务上下文和 Gateway 副作用保留为业务入口。
- Callback 使用 `external_system/callback` actorWorker 与消费者使用 `system_task/asynq`Scheduler 只投递任务时为 N/A真实业务变化归对应 Worker/Application。
### 敏感读取单列
| 入口 | 资源与安全决定 | 四类事实 |
|---|---|---|
| 企微应用配置 `GET /applications` | 返回 Secret、CallbackToken、EncodingAESKey返回前必须 fail-closed Audit凭据不得写入 Audit/Integration | Audit=必须Domain Ledger=只读配置Integration/Outbox=N/A |
| 资产实时状态 `GET /:identifier/realtime-status` | 设备响应含 WiFi 明文密码且实时访问 Gateway | Audit=必须Domain Ledger=只读Integration=每次尝试必须Outbox=N/A |
| IoT 卡实名链接 `GET /:iccid/realname-link` 与 C 端 `/realname/link` | 返回短期实名业务凭证 | Audit=必须Domain Ledger=只读卡事实;实际外部尝试写 IntegrationOutbox=N/A |
| 导出任务详情 `GET /export-tasks/:id` | 完成态返回预签名 `download_url` | Audit=必须导出任务是运行事实Integration=N/AOutbox=N/A |
| 批量下载/上传预签名 URL | 对象 Key 和 URL 受资源权限约束,签名 URL 不入审计正文 | Audit=必须Domain Ledger 按目标业务资源对象存储只写基础设施日志Outbox=N/A |
操作密码是否设置、实名状态、普通账号/资产/订单列表与统计均为普通受权读取Audit Event=N/AJSSDK 配置只返回客户端初始化签名,真实 token 回源由 Integration Log 记录,不把短期客户端配置误判为系统凭据读取。
### Integration Log 与旧 Writer 复核
- Integration Log 唯一 Repository 写入口为 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`,当前已覆盖运营商回调、支付 H5 下单/查单/回调、卡观测、卡限速及企微 token/通讯录/模板/附件/提交/详情/回调;支付宝 WAP URL 本地签名不伪造外部尝试。
- 设备 Gateway、IoT 卡 Gateway、统一资产实时状态、支付、企微与运营商回调均已按真实外部尝试接入 Integration Log扫描器覆盖 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`,不能用 Audit Event 或旧 Asset Operation Log 代替。
- 旧账号和旧资产 Writer、直接业务调用及裸/双重 goroutine 均已归零;旧账号表与旧资产表原样保留,旧资产仅保留独立历史只读入口。
### 已落地代表切片
- 受控系统配置更新使用 `system_config.updated`:成功与 `tb_system_config` 同事务,已注册只读/非法值拒绝及事务失败在业务未落地后同步写独立短事务;未注册 Key、空 Key 或无 actor 不生成无资源 Audit Event。
- Outbox 人工重放与过期租约释放分别使用 `outbox.replayed``outbox.expired_lease_released`:成功事件关联批次内全部 Outbox 资源并保存状态/租约前后值;重复恢复、有效租约等完整定位资源后的拒绝写 `denied`,业务事务或成功审计失败写 `failed`,均保持 Outbox 原状态。
- 失败/拒绝审计二次写入失败保留原业务错误,使用进程内原子计数器累计,并输出含 `severity=critical`、稳定 action/resource/request/correlation/error code 的安全日志;全链路无裸 goroutine。
### 三视角复核记录
- 业务语义:逐 RouteSpec、Application/Service、Worker/Callback/Consumer 追踪真实副作用,确认通知已读属于低风险写,购买检查/资产验证属于 N/A多资源、资金、批量和系统动作未按名称一刀切。
- 研发边界:核对 GORM 事务、Domain Ledger、Integration Repository、Outbox consumer/relay、旧 Writer 调用链Setter、注册器、纯查询和分发壳已剔除。
- 安全字段单列企微明文凭据、WiFi 密码、实名链接、预签名 URL/导出下载;平台内部、代理/企业安全投影和 `internal_only` 可见性已逐项填写,越权与不存在保持同错。
## 分类边界
| 入口类型 | Audit Event | Domain Ledger | Integration Log | Outbox |
|---|---|---|---|---|
| 状态变更、资金、权限、关键配置 | 必须;关键成功与业务事实同事务 | 既有业务表仍是权威 | 存在外部调用时必须 | 存在提交后可靠副作用时必须 |
| 失败、拒绝 | 业务回滚后独立短事务 | 不伪造领域事实 | 外部尝试仍记录真实结果 | 不为已回滚事实制造事件 |
| 普通读取 | N/A逐项记录理由 | 只读投影 | N/A | N/A |
| 敏感读取、下载和导出 | 返回敏感结果前必须,自审计失败则不返回 | 业务数据仍是权威 | N/A | 异步导出按任务契约决定 |
| 外部回调、轮询和 Gateway 调用 | 状态变化、人工触发、连续失败或高风险异常时必须 | 业务状态仍在领域表 | 每次实际或未发送尝试都必须 | 需要可靠后续处理时必须 |
| Domain 方法 | 不直接依赖审计基础设施,由 Application 写入 | 维护业务不变量 | 由 Application/Adapter 负责 | 只记录领域事件,由 Application 持久化 |
## 七月测试环境冻结期增量登记
`complete-july-iteration-test-release` 已明确把 Audit Event Writer 与发布门禁延期到任务 6.5。以下登记仅说明测试环境阶段的临时分类不代表生产评审通过也不得删除通知事实、Access Log 或公共 Outbox
| 入口 | Audit Event | Domain Ledger | Integration Log | Outbox |
|---|---|---|---|---|
| 明确后台账号通知事件消费与幂等写入 | N/A测试环境冻结生产前由 6.5 重新评审通知失败与系统告警治理) | `tb_notification` 是通知投递与接收人已读状态的权威事实 | N/A无外部系统调用 | 消费公共 Outbox 的稳定事件,不复制 Outbox |
| 当前后台账号单条通知已读 | N/A低风险个人阅读状态普通已读操作只进入 Access Log | `tb_notification.is_read/read_at` 是权威状态 | N/A | N/A |
| 当前后台账号未读数与基础列表 | N/A普通读取不返回其他接收人数据或敏感业务正文 | 只读 `tb_notification` 投影 | N/A | N/A |
| 代理主钱包订单统一扣款 | 使用 `agent_wallet.order_debit`,关联订单、主钱包和唯一成功流水,保存余额前后值;成功与订单、钱包、流水及 Outbox 同事务,审计失败回滚,已定位订单后的失败/拒绝在业务回滚后写独立短事务 | `tb_order``tb_agent_wallet``tb_agent_wallet_transaction``tb_payment` 与套餐使用记录在同一事务形成权威事实;现有行锁、乐观锁和唯一业务引用保持不变 | N/A不调用外部系统 | 同事务写入 `wallet.agent_main.debited`为余额预警等后续消费者提供稳定事实Audit Event 不替代 Outbox |
| 代理主钱包订单资金预占、释放与完成扣除 | 使用 `agent_wallet.order_reserve/order_release/order_complete`,关联订单、主钱包、预占事实及完成时的唯一扣款流水,保存余额和冻结余额前后值;成功与原资金事务同写,重复终态不伪造事件 | `tb_agent_wallet_reservation` 是预占金额、付款钱包与唯一终态的权威事实;钱包与完成扣除流水同事务更新。当前生产仅取消订单调用 releasefreeze/complete Application 接缝暂无生产调用者,已接好审计但不借本任务新增业务调用 | N/A不调用外部系统 | 同事务写入 `wallet.agent_main.reservation.changed`;完成扣除同时写入 `wallet.agent_main.debited`消费者按权威事实幂等确认Audit Event 不替代 Outbox |
| 代理主钱包充值与人工调整正向入账 | 充值沿用 `agent_recharge.credit`,以一条事件关联充值单、提交人、店铺、主钱包和唯一流水,避免为同一入账重复造事件;人工调整使用 `agent_wallet.adjust_balance`,要求保留人工原因并关联主钱包、唯一调整流水及余额前后值。成功与原资金事务同写,重复业务引用不伪造成功 | 充值/人工调整业务事实、`tb_agent_wallet` 与唯一成功流水在同一事务形成权威事实;当前没有人工调整生产入口,统一 Posting 接缝已覆盖但不借审计新增接口 | N/A本接缝不调用支付或审批外部系统 | 同事务写入 `wallet.agent_main.credited`,消费者按成功流水复核;支付/审批 Integration Log 由代理充值外部流程负责Audit Event 不替代 Outbox |
| 代理主钱包实际信用额度更新 | 使用 `agent_wallet.change_credit`,关联主钱包及所属店铺,保存 balance/frozen_balance 不变事实、credit_enabled/credit_limit/version 前后值;成功与信用字段版本条件更新同事务,已定位钱包后的资金占用拒绝、版本冲突或审计回滚失败使用独立短事务 | `tb_agent_wallet` 是实际信用开关、额度、余额、冻结余额和版本的权威事实;保持现有可用额度及资金占用校验,不产生钱包流水 | N/A本地信用配置不调用外部系统 | N/A信用额度更新不产生可靠异步副作用 |
| 代理在线充值支付链接创建 | 延期(测试环境冻结;创建人、店铺、金额和支付方式由充值单与支付单留痕,生产前按 6.5 复核 Audit Event | `tb_agent_recharge_record``tb_payment` 同事务保存待支付事实和收款身份快照 | 微信 v3 H5/v2 MWEB 下单每次真实外呼写 Integration Log支付宝 WAP URL 仅本地签名N/A后续查单与回调仍逐次记录 | N/A创建阶段不产生可靠异步副作用支付确认后才同事务写入钱包入账 Outbox |
| 代理订单主钱包退款回充 | `refund.approve` 与退款单、订单、原扣款钱包、原扣款流水和唯一退款流水同事务;重复退款流水不重复写成功事件 | 原成功扣款流水定位付款钱包并限定金额;退款审批、`tb_agent_wallet` 与唯一成功退款流水同事务形成权威事实 | N/A本资金接缝不调用渠道或审批外部系统原路渠道退款尚未实现 | 同事务写入 `wallet.agent_main.refunded`,消费者复核退款流水、原扣款事实、金额上限和资产快照 |
| 代理商资金概况信用投影 | N/A普通受权读取不返回其他数据范围的资金事实不执行资金或配置变更 | 只读投影 `tb_shop`、主/佣金钱包、提现汇总和主账号;派生金额不另建事实表 | N/A无外部系统调用 | N/A纯 Query 不产生可靠副作用) |
| 受控系统配置更新 | N/A用户已明确取消全局 Audit Event仅超级管理员可更新代码注册 Key未知 Key、非法类型和值域均拒绝 | `tb_system_config` 是配置值、类型、模块及更新人的 PostgreSQL 权威事实,更新后失效 Redis 缓存 | N/A配置更新不调用外部系统不得写 Integration Log 冒充配置审计) | N/A配置更新不产生可靠异步副作用 |
| 电信实名结果回调 | 实名状态首次实际变化时写 `iot_card.realname_callback_sync`,使用 `external_system/callback`,关联 IoT 卡和入站 Integration LogAudit Event 与卡状态、首次实名时间及实名变化 Outbox 同事务,审计失败回滚。已解析卡后的业务失败写独立短事务;重复成功不伪造事件 | `tb_iot_card` 是实名状态、首次实名时间和逆转窗口的权威事实 | 每次入站先写 `tb_integration_log`,仅保存正文摘要;覆盖 `invalid_payload/ignored/not_found/conflict/success/failed` 终态;未改变实名状态时只保留 Integration Log | 实名事实首次变化时由公共 `ApplyCardObservation` 同事务写入实名状态变化 Outbox重复成功不重复写事件 |
| 移动实名成功回调 | 实名状态首次实际变化时写 `iot_card.realname_callback_sync`,使用 `external_system/callback`,关联 IoT 卡和入站 Integration LogAudit Event 与卡状态、首次实名时间及实名变化 Outbox 同事务,审计失败回滚。已解析卡后的业务失败写独立短事务;重复成功不伪造事件 | `tb_iot_card` 是实名状态、首次实名时间和逆转窗口的权威事实 | 每次入站先写 `tb_integration_log`,仅保存正文摘要;覆盖 `invalid_payload/not_found/conflict/success/failed` 终态,并通过 pending 租约恢复中断处理;未改变实名状态时只保留 Integration Log | 仅合法成功报文进入公共 `ApplyCardObservation`;实名事实首次变化时同事务写入 Outbox重复成功不重复写事件 |
| 联通实名成功回调 | 实名状态首次实际变化时写 `iot_card.realname_callback_sync`,使用 `external_system/callback`,关联 IoT 卡和入站 Integration LogAudit Event 与卡状态、首次实名时间及实名变化 Outbox 同事务,审计失败回滚。已解析卡后的业务失败写独立短事务;重复成功不伪造事件 | `tb_iot_card` 是实名状态、首次实名时间和逆转窗口的权威事实;上游 `dateChanged` 只用于幂等和留痕 | 每次入站先写 `tb_integration_log` 正文摘要;覆盖 `invalid_payload/not_found/conflict/success/failed`,关闭时记录 `ignored`;未改变实名状态时只保留 Integration Log | 仅合法成功报文进入公共 `ApplyCardObservation`;实名事实首次变化时同事务写入 Outbox重复成功不重复写事件 |
| 联通解除实名回调 | N/A只识别并留痕外部解除通知不把单次回调作为本地实名逆转事实 | `tb_iot_card` 保持原实名状态、首次实名时间、检查时间及逆转计数,回调不写领域事实 | 每次入站先写 `tb_integration_log` 正文摘要;覆盖 `invalid_payload/not_found/conflict/ignored/failed` 终态并支持 pending 租约恢复 | N/A不调用公共实名观测不产生状态变化、停机或套餐事件 |
## 七月确认范围增量登记
| 入口 | Audit Event | Domain Ledger | Integration Log | Outbox |
|---|---|---|---|---|
| 换货迁移套餐在原订单退款后失效 | N/A退款终态的自动后处理不新增人工决定人工申请与审批审计沿用退款入口 | 原订单、换货新旧资产关系和 `tb_package_usage` 是套餐权益来源及失效状态的权威事实;仅失效该订单迁移后的对应权益 | N/A本地数据库后处理不调用企微、支付或 Gateway | 企微退款沿用 `approval.terminal_decision.recorded` 驱动;存量旧退款沿用原同步链路,本修复不新增 Outbox |
| 订单渠道、资产标识、退款/充值/换货提交人和卡/设备实名筛选 | N/A均为字段来源修正或受权只读投影不改变订单、资产、实名或审批事实 | 只读既有订单 `purchase_role`、卡 ICCID、设备 VirtualNo/IMEI、创建人账号及有效卡绑定实名事实不另建领域账本 | N/A查询使用本地批量账号查询和 EXISTS不实时调用外部系统 | N/A纯查询和创建时既有字段赋值不产生新增可靠副作用 |
| 换货创建前未终结退款拦截 | 已识别旧卡或旧设备后分别使用 `exchange.card.create``exchange.device.create` 记录 `denied`,关联稳定换货单 Key、旧资产和店铺 | `tb_refund_request` 的未终结状态是拦截依据;拒绝后不创建换货单、不修改资产 | N/A本地前置校验不调用企微或其他外部系统 | N/A校验拒绝不产生业务事实或可靠副作用 |
| 店铺 C 端新登录限制配置与登录拦截 | 店铺更新入口记录操作者 `updater`、字段前后值进入 Access Log本 Change 不新建店铺专用 Audit Writer统一店铺配置 Audit Event 接缝登记为后续治理项 | `tb_shop.client_login_disabled` 是是否允许新登录的权威事实;已有 Token 不修改,拦截时不创建新 Token | N/A配置更新与资产登录判断均为本地数据库操作 | N/A同步配置与登录前拦截不产生必须可靠投递的提交后副作用 |
| 卡/设备实名策略单个及批量更新 | IoT 卡使用 `iot_card.realname_policy_update` / `iot_card.realname_policy_batch_update`,设备使用 `device.realname_policy_update` / `device.realname_policy_batch_update`;均记录真实后台操作者、目标资产、实际绑定卡/设备与卡槽引用、策略前后值及 success/failed/denied批量根子事件与实际策略变化同事务 | `tb_iot_card.realname_policy``tb_device.realname_policy` 是策略权威事实C 端仅实时计算 `effective_realname_policy` | N/A策略更新和读取均不调用运营商或其他外部系统 | N/A策略同步更新不产生可靠异步副作用 |
| 下架套餐当前使用者续费与普通列表过滤 | N/A普通受权查询与既有订单创建规则订单创建继续沿用原订单/资金审计接缝) | 当前套餐使用记录决定续费资格,新订单、订单明细、套餐当前配置和支付记录是新购买权威事实;不修改历史订单 | 第三方支付仍沿用既有支付 Integration Log本资格判断不新增外部调用 | 新订单支付、钱包扣款、自动购包及佣金继续沿用既有任务/Outbox本切片不新增事件类型 |
| 卡/设备 C 端支付方式配置更新 | 复用 `systemconfig.UpdateService` 的事务内 `AuditWriter`,记录操作者、请求标识及配置前后值;审计 Writer 已装配时写入失败会回滚配置更新 | `tb_system_config` 是卡、设备允许支付方式集合及更新人的 PostgreSQL 权威事实Redis 仅为可失效缓存 | N/A配置更新不调用外部系统 | N/A提交后仅失效可重建缓存不产生必须可靠投递的业务副作用 |
| C 端读取支付方式与后端订单/充值校验 | N/A普通受权读取和业务规则校验不产生独立敏感事实拒绝原因进入 Access Log | 只读 `tb_system_config`,订单创建后由 `tb_order.payment_method` 固化所选方式,充值与支付事实沿用既有订单、充值单和支付记录 | 第三方支付请求继续沿用既有支付集成日志接缝,本配置策略本身不新增外部调用 | 强充支付成功后的自动购包继续沿用既有 Asynq/业务幂等链路,本配置读取不新增 Outbox |
| 主钱包首次跌破 100 元通知店铺业务员 | N/A由已审计资金事实派生的内部提醒不新增人工操作或敏感读取 | `tb_agent_wallet_transaction``wallet.agent_main.debited` 是余额前后值的权威事实,`tb_notification` 保存最终通知与已读状态 | N/A不调用外部系统 | 扣款事实消费者仅在 `balance_before >= 10000 && balance_after < 10000` 时同事务幂等写入明确后台账号通知 Outbox无有效业务员时正常结束 |
| 创建物流换货单并提醒关联个人客户 | 卡/设备换货分别使用 `exchange.card.create``exchange.device.create` 记录真实后台操作者、换货单、旧资产和店铺,成功事实与换货单及通知 Outbox 同一 GORM 事务 | `tb_exchange_order` 是物流换货申请及状态的权威事实,`tb_notification` 是接收人通知与已读状态的权威事实 | N/A不调用外部系统 | 换货单与每个启用关联客户的 `notification.personal_customer.direct.requested` 在同一 GORM 事务写入;事件 ID 使用换货单和客户 ID 稳定防重,消费端按事件与接收人唯一键幂等 |
| 套餐临期列表、数量与每日/手动 15/7/3 天节点提醒 | N/A列表和数量是普通受权读取手动入口仅允许超级管理员提交同一幂等扫描任务操作者进入 Access Log 和任务日志,不直接修改业务事实) | `tb_package_usage` 的计时条款快照和到期队列是预计最终到期的权威事实,`tb_notification` 保存店铺账号、平台业务员和个人账号通知及已读状态 | N/A不调用企业微信、短信、邮件或其他外部系统 | 每日或手动任务按资产、到期日和节点生成稳定事件 ID同一 GORM 事务内向店铺动态接收人写入 `notification.admin.dynamic.requested`,并向绑定个人账号写入 `notification.personal_customer.direct.requested`;列表和数量纯 Query 不产生 Outbox |
| 主套餐过期接续与孤儿恢复 | N/A系统自动推进套餐生命周期不包含人工敏感操作 | `tb_package_usage` 是套餐待生效、生效、用完和过期状态的权威事实 | N/A仅使用本地 PostgreSQL 与 Redis不新增外部请求 | 成功激活继续在同一事务复用 `card.observation.series.requested`,稳定事件 ID 基于套餐使用记录 ID不新增套餐激活事件类型或消费者 |
| 企业微信应用连接配置保存与明文读取 | 配置保存和默认发起人分别使用 `wecom.application.save``wecom.application.save_default_creator`,只记录应用标识、状态和真实 `credentials_configured` 布尔事实;明文列表读取仅允许超级管理员,返回前以独立短事务同步写 `wecom.application.credentials_read`审计缺失或失败均不返回结果Audit Writer 不接收 Secret、回调 Token 或 EncodingAESKey | `tb_wecom_application` 是 corp_id、agent_id、应用状态及明文 Secret、回调 Token、EncodingAESKey 的权威事实;管理响应按用户确认向超级管理员返回明文 | 保存和读取本身不调用企微;连接测试或 token 缓存未命中时,每次真实回源均写 `tb_integration_log`,请求和响应摘要不含 Secret、回调凭据或 access_token | N/A连接配置提交后仅同步失效可重建 token 缓存,不产生必须可靠投递的业务副作用) |
| 支付连接配置 CRUD 与启停 | 使用 `payment_config.create/update/delete/activate/deactivate`,保存配置 ID、名称、渠道、启用状态、非敏感商户/应用标识和各渠道 `credentials_configured` 安全事实;不保存 Secret、Token、AESKey、支付密钥、私钥、公钥正文或证书正文。创建字段校验、删除生效配置或在途业务等已定位资源后的拒绝写独立短事务激活其他配置时为被自动停用的原配置另写同事务停用事件 | `tb_wechat_config` 是配置及明文渠道凭据的权威事实CRUD/启停与 Audit Event 同一 GORM 事务,审计失败回滚配置事实,提交后 Redis 缓存仅 best-effort 失效 | N/A配置 CRUD/启停不调用渠道;支付、查单和回调的真实外部尝试仍由 7.4 Integration Log 负责) | N/A配置提交不产生可靠异步副作用 |
| 运营商配置 CRUD 与启停 | 使用 `carrier.create/update/delete/update_status`,保存配置 ID、编码、名称、类型、状态、流量重置日和实名链接业务配置重复编码、非法模板配置等已定位资源后的拒绝进入独立短事务 | `tb_carrier` 是运营商及实名链接配置权威事实;成功审计与 CRUD/状态变更同一 GORM 事务 | N/A本切片只改本地运营商配置不调用 Gateway 或运营商) | N/A同步配置不产生可靠异步副作用 |
| 运营商回调开关与卡/设备支付方式等受控连接策略 | 继续复用 `system_config.updated`,记录注册 Key、模块、前后值、操作者和请求链路只读、非法值和事务失败沿用已落地的拒绝/失败短事务策略 | `tb_system_config` 是回调启停和支付方式策略权威事实,代码 Registry 默认值与 Redis 缓存不替代 PostgreSQL | N/A配置本身不调用运营商或支付渠道真实回调/支付尝试由各自 Integration Log 记录) | N/A提交后仅失效可重建缓存 |
| 企业微信可见成员同步与账号显式绑定 | 成员同步使用 `wecom.application.sync_members`,记录应用身份、状态、同步数量和时间,成员快照替换与 Audit Event 同一事务;账号绑定继续使用 `account.bind_wecom`,记录操作者、目标账号及绑定前后 `(corp_id, userid, name)`,不记录手机号或邮箱 | `tb_wecom_member` 是最近同步的应用可见成员选择快照,`tb_account.wecom_*` 是管理员确认后的账号绑定事实;不建立部门组织模型 | 每次真实调用应用可见成员接口均写 `tb_integration_log`,仅记录应用、根部门、成员数量、状态码和耗时,不保存 access_token 或成员列表正文 | N/A同步和绑定均为同步事务不产生必须可靠投递的提交后副作用 |
| 企业微信审批业务场景与模板控件映射 | 配置保存使用 `wecom.approval_scene.save`,记录场景 ID、业务类型、应用 ID、模板 ID/名称、状态、指纹和最近校验时间;配置与审计同事务,不保存凭据、审批节点或审批人规则到审计数据 | `tb_wecom_approval_scene` 是两个稳定业务类型的当前模板、控件映射、模板最小快照和启用状态权威事实 | 保存前每次真实调用模板详情接口均写 `tb_integration_log`,记录应用、模板 ID、状态码、控件数量和耗时不保存 access_token 或完整外部响应 | N/A配置保存为同步事务不产生必须可靠投递的提交后副作用 |
| 企业微信默认发起人与审批提交 | 默认发起人配置继续复用事务内 `systemconfig.AuditWriter`;业务事务创建通用审批时写 `approval.request`,关联真实提交账号、退款或线下充值业务单及 `approval.submission.requested` Outbox。企微提交 Worker 以 `system_task/worker``approval.sync_submission` 的 success/failed/unknown不以默认成员伪造操作者 | `tb_wecom_application.default_creator_*` 是当前默认发起配置,`tb_approval_instance``tb_wecom_approval_context` 是审批和提交状态权威事实Audit Event 仅保存状态变化及稳定引用 | 每次附件上传和 applyevent 均写精确 Integration Log摘要不含 Secret、access_token、media_id、附件正文或完整企微响应提交审计引用对应 `integration_id`,但不替代或冻结其可变结果 | 业务事务写入 `approval.submission.requested`Worker 条件领取后只提交一次,明确失败和结果未知均终结自动重试,禁止盲目创建第二张审批单 |
| 企业微信审批加密回调与详情终态同步 | 首次权威终态写 `approval.sync_decision`,回调使用真实 `external_system/callback` actor关联审批实例、业务单、真实提交账号、入站回调/详情 Integration 及终态 Outbox不记录本地或企微审批人为操作者。重复终态未改变审批事实时不伪造成功事件 | `tb_integration_log` 保存回调和详情外部事实,`tb_wecom_approval_context.latest_detail_snapshot`、通用审批实例及决策投递表保存权威标准终态Audit Event 与审批状态、决策投递和终态 Outbox 同事务 | 入站回调先写 Integration Log pending详情任务完成后置 completed每次 `getapprovaldetail` 写独立出站 Integration Log。审计只引用稳定 `integration_id` 与身份字段,不保存 access_token、完整响应或尚可变化的 Integration result | 回调只入队结构化 `wecom:approval:sync` 任务;权威终态原子写 `approval.terminal_decision.recorded`,退款和线下充值消费者继续使用既有业务/资金审计,并传播终态 Outbox 的 correlation/parent |
| 企业微信审批主动恢复、未终态轮询与审批人读取投影 | 唯一确认结果未知提交时写 `approval.recover_submission`:回调恢复使用 `external_system/callback`,主动恢复使用 `scheduled_job/scheduler`;轮询取得首次权威终态写 `approval.sync_decision`。普通审批人读取投影仍为 N/A重复恢复或重复终态未改变事实时不写成功事件 | `tb_wecom_approval_context.submission_attempted_at/last_recovery_at/sp_no/latest_detail_snapshot` 与通用审批实例是恢复和展示的权威本地事实;只有唯一候选可从结果未知转为审批中 | 每次 `getapprovalinfo` 分页和 `getapprovaldetail` 均写独立出站 Integration Log恢复审计关联实际提交或查询 Integration只保存安全摘要不保存 Secret、access_token、media_id、附件正文或完整响应 | Scheduler 仅提交 `wecom:approval:recovery`;恢复和轮询只提交结构化 `wecom:approval:sync`,不写新的审批提交 Outbox、不调用 `applyevent`,标准终态继续使用既有终态 Outbox |
| 员工线下代充值申请与企微终态入账 | 申请保存以真实提交人及明文业务快照留痕;资金成功 Audit Event 延期至既有统一钱包治理任务,企微自动终态不伪造人工审批人 | `tb_agent_recharge_record``tb_approval_instance``tb_wecom_approval_context` 同事务保存申请事实approved 通过 `topup + recharge_record_id` 唯一成功钱包流水幂等入账,其他终态不修改钱包,通过后撤销不自动冲正;`tb_notification` 保存到账通知 | 申请创建本身不外呼后续附件上传、applyevent、详情与恢复沿用企微 Integration Log业务参数按用户确认保存明文日志仍不记录 Secret、access_token、media_id 或附件正文 | 创建事务写 `approval.submission.requested`;标准终态写 `approval.terminal_decision.recorded`approved 入账事务再写 `wallet.agent_main.credited` 和目标店铺的 `notification.admin.dynamic.requested`;在线充值复用同一入账接缝;新审批单禁止旧人工确认或驳回入口绕过 |
| 代理充值列表、详情与支付状态读取 | N/A普通受权读取请求进入 Access Log不改变充值、支付或钱包事实 | 只读 `tb_agent_recharge_record`、相关支付单和店铺名称;平台账号不限制,代理账号统一按当前上下文的自身及下级店铺 ID 过滤 | N/A不调用支付渠道或企微 | N/A纯 Query不产生 Outbox |
| 退款申请与企微终态处理 | 申请、人工/企微通过与拒绝、退回、重提使用 `refund.create/approve/reject/return/resubmit`;佣金实际回扣使用 `refund.invalidate_commission`,资产后处理完成使用 `refund.process_asset`。成功事实与各自 Domain Ledger 同事务,已定位退款后的失败/拒绝使用独立短事务;代理/企业仅看到“已提交/已通过/已拒绝/处理完成”等安全结论 | `tb_refund_request`、通用审批实例和企微上下文同事务保存approved 条件更新订单与退款单,代理主钱包按 refund ID、资产钱包按退款单号复核成功回款佣金按记录锁定并失效套餐按订单及换货迁移关系幂等失效`tb_notification` 保存退款完成通知。Audit Event 关联退款、审批、订单、资产、钱包、原扣款/退款流水、佣金、实际失效套餐权益和通知 Outbox但不替代这些权威事实 | 申请创建不外呼附件、applyevent、详情、回调和恢复沿用企微 Integration Log业务参数明文保存在业务/审批快照中但不复制到 Integration LogSecret、access_token、media_id 和附件正文仍禁止记录原路渠道退款未实现Integration Log 明确 N/A | 创建事务写 `approval.submission.requested`;终态写 `approval.terminal_decision.recorded`;退款事务幂等写目标店铺的 `notification.admin.dynamic.requested`;通知继续以 Outbox 为权威投递事实Audit Event 仅保存稳定引用;业务消费者只有在订单、钱包、佣金和资产后处理完成后才确认投递成功,失败释放租约重试 |
| 退款与线下代充值旧审批入口发布切换 | N/A部署环境开关控制旧入口是否可用不新增业务操作实际旧入口操作继续沿用各自既有审计口径 | `approval_instance_id IS NULL` 是存量旧 provider 的兼容边界,非空记录只接受企微标准终态;关闭开关不修改或删除任何业务事实 | N/A开关判断不调用外部系统也不得写 Integration Log 冒充发布审计) | N/A开关判断不产生可靠副作用企微 Worker 继续消费既有标准终态 Outbox |
| IoT 卡/设备导入与设备 CSV 批量操作 | 创建使用 `iot_card_import_task.create``device_import_task.create`,完成使用对应 `*.complete` 根事件;任务只保存 ID/单号、文件名、运营商或操作目标和操作者,不保存 StorageKey、签名 URL 或文件正文。每张实际新增卡/设备继续使用既有 `iot_card.create/device.create` 业务子事件并以完成根为 parent设备 CSV 分配/系列/回收继续复用既有批量根子审计。跳过项不伪造子事件,根事件 success/partial/failed 与实际子事件及失败统计一致 | `tb_iot_card_import_task``tb_device_import_task` 保存任务输入、进度和逐项结果;卡、设备、资产标识、卡槽绑定、钱包及分配/系列事实仍由原业务表权威保存Audit Event 不替代任务或资产事实 | 对象存储下载只读取任务表中的 StorageKey不写 Integration Log审计源头只使用安全文件名。设备批量操作本身不新增外部调用 | HTTP 创建继续提交结构化 `IotCardImportPayload/DeviceImportPayload`Worker 使用任务单号 correlation 和稳定完成根 parent重试通过稳定 EventID 与原业务幂等规则避免重复子事件 |
| 单列 CSV 资产套餐批量订购 | 使用 `asset_package_batch_order_task.create/complete` 记录任务、文件名、套餐目标、支付方式、操作者和实际根子统计;既有 `order.create` 作为每个真实订单子事件并继承任务 correlation/parent不另造同义订单审计。StorageKey、VoucherKey、签名 URL 和 CSV 正文不进入审计 | `tb_asset_package_batch_order_task` 保存输入参数和逐行结果,成功行以 `tb_order`、订单明细、套餐使用、支付记录及代理钱包成功流水为权威业务事实 | 对象存储上传和下载沿用现有存储日志,不把文件正文写入 Integration Log本切片不新增外部支付或 Gateway 调用 | 创建接口提交结构化 `asset:package:batch_order` Asynq 任务;逐行钱包订单继续沿用既有钱包扣款 Outbox 和佣金任务,重复任务由状态条件与订单幂等规则阻断 |
| 订单套餐 CSV 批量失效 | 使用 `order_package_invalidate_task.create/complete` 记录任务根,使用 `order_package_invalidate_task.item` 为每个实际变化或已识别失败订单写子事件;订单为主要资源,实际失效套餐权益逐条关联并保存状态 `before→4`。无有效权益的幂等行不伪造变化子事件;任务 UI 统计与审计实际子事件统计分别保留 | `tb_order_package_invalidate_task` 保存文件任务状态和失败明细,`tb_package_usage.status` 是套餐权益终态权威事实;逐订单状态更新与子事件同一事务,任务终态与完成根同一事务 | 对象存储只用于读取 CSVStorageKey、VoucherKey、签名 URL 和文件正文不进入 Audit/Integration | 创建接口提交结构化 `InvalidateTaskPayload`Worker 原子 Claim审计失败时任务恢复待处理以便安全重试稳定子事件 ID 防止重复写入 |
| ICCID 批量生成待失效订单号 CSV | N/A脚本复用现有受权资产套餐查询属于运维只读投影请求进入 Access Log不改变套餐或订单事实 | N/A只读取 `tb_package_usage` 的状态和订单号快照,不写入领域账本;实际失效仍由既有批量失效任务负责) | N/A只调用本系统后台接口不调用 Gateway、支付或其他外部系统 | N/A纯查询和本地 CSV 生成,不产生 Outbox 或异步任务) |
| CSV 批量修改生效中套餐过期时间 | 每条修改复用既有 `asset_package_expires_at` 资产操作审计,记录操作者、资产、套餐使用记录及过期时间前后值和 success/failed 结果 | `tb_package_usage.expires_at` 是套餐过期时间权威事实;脚本只通过受权接口逐条修改,不直连数据库 | N/A仅调用本系统后台接口不调用 Gateway、支付或其他外部系统 | N/A同步修改不产生新增 Outbox后续套餐过期推进沿用既有轮询任务 |
| IoT 卡与套餐业务导出 | 创建/取消统一使用 `export_task.create/cancel`记录任务单号、scene、format、真实操作者和店铺范围取消状态或取消请求与 Audit Event 同事务。导出 Query、FileKey、签名下载 URL 和导出内容不进入审计;导出数据读取本身仍为 N/A | N/A导出只读取现有卡、套餐使用、套餐和分配事实不写入领域账本 | N/A不调用外部业务系统对象存储文件生成和下载沿用现有导出基础设施日志 | 沿用现有 `export:dispatch``export:shard``export:finalize` Asynq 链路,不新增业务 Outbox审计中心自身不注册导出路由 |
| 钱包流水与代理充值业务导出 | 创建/取消统一使用 `export_task.create/cancel`,只保存任务身份、格式、操作者和受控店铺范围,不复制 Query、业务凭证 Key、FileKey、签名 URL 或导出内容 | N/A只读取主钱包流水、充值记录和本地通用审批实例金额及余额使用既有权威事实不写入领域账本 | N/A不实时调用支付渠道或企业微信明文业务凭证 Key 只进入有权导出结果,不复制到 Audit/Integration且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch``export:shard``export:finalize` Asynq 链路,不新增业务 Outbox取消重复请求不伪造新的状态变化事件 |
| 退款与换货业务导出 | 创建/取消统一使用 `export_task.create/cancel`;平台或代理 actor、scene/format 和当前权限范围进入任务资源Query、收货资料、业务凭证、FileKey、签名 URL 与结果正文不进入审计 | N/A只读取退款、订单、套餐使用、换货资产快照和本地审批事实金额及处理标记沿用既有权威事实不写入领域账本 | N/A不实时调用企业微信、支付或 Gateway明文业务凭证和收货资料只进入有权导出结果不复制到 Audit/Integration且仍禁止记录 Secret、access_token、media_id 和附件正文 | 仅沿用现有 `export:dispatch``export:shard``export:finalize` Asynq 链路,不新增业务 Outbox审计中心不提供用户导出 |
| 站内通知生成、已读与保留清理 | Outbox 消费实际生成通知使用 `notification.deliver`,只保存通知 ID、事件 ID、接收人、类别、类型、严重级别和受控资源引用不复制标题或正文后台账号与个人客户首次单条/全部已读使用 `notification.read/read_all`,重复已读不伪造事件;保留清理使用 `notification.cleanup/cleanup_item`,由 `system_task/worker` 记录批次和每条实际删除通知。通知创建、已读更新或物理删除与对应 Audit Event 共用 GORM 事务,审计失败回滚业务事实 | `tb_notification` 继续是通知内容、接收人、展示期限和已读状态的权威事实Audit Event 只解释生成、阅读和清理动作,不替代通知正文或接收状态 | N/A站内通知不调用短信、邮件、企微或其他外部系统受控 `ref_id/ref_key` 禁止 URL系统安全凭据和通知正文不进入审计 | 业务事务仍只写现有三类结构化通知请求 OutboxOutbox 投递事实与通知生成 Audit Event 分离,重复消费由事件+接收人唯一键幂等;清理继续复用 `notification:cleanup` Asynq 计划任务,不新增 Outbox |
| IoT 卡固定档位限速 | 统一动作 `iot_card.speed_tier_set` 记录认证上下文中的真实操作者、目标卡 ID/ICCID、请求档位编码与名称如 128Kbps/1Mbps/恢复不限速、Integration Log ID、Integration 是否终结及 success/failed/unknown 结果;设备无入口且不通过绑定卡间接限速 | N/A不在本地保存或修改卡当前限速状态Gateway 是外部执行方 | 每次实际 Gateway 调用前写 pending按 success/failed/unknown 终结;超时 unknown 保存按 ICCID 人工核对策略,摘要不含操作者或 Secret、access_token、完整响应正文 | N/A单次外部命令无后续可靠副作用结果未知禁止盲目重发不创建自动补偿 Outbox |
| IoT 卡人工实名状态纠偏 | `iot_card.realname_status_update` 记录真实后台操作者、目标卡、实名状态前后值及是否实际变化状态事实、Audit Event 和实名变化 Outbox 在同一 GORM 事务,失败写独立短事务 | `tb_iot_card.real_name_status`、首次实名时间及激活字段是内部实名事实 | N/A人工纠偏不直接调用 Gateway | 状态实际变化时复用 `card.realname.changed`,未变化不伪造变化事件 |
| IoT 卡后台/个人人工刷新 | 后台使用 `iot_card.manual_refresh`,个人客户使用 `iot_card.personal_refresh`;记录真实 actor、目标卡、实际状态变化及最终 success/partial/failed/unknown任一结果未知不写成成功 | 卡网络、实名、流量及 `last_sync_time` 是内部观测事实;各实际变化与统一 Audit Event 同事务 | 网络、实名、流量查询的每次真实 HTTP 尝试分别写 Integration Log失败/超时逐次终结,成功尝试在内部应用结果确定后记录 `state_changed` | 实名、网络、流量实际变化沿用各自 Card Observation Outbox人工刷新汇总不另造可靠事件 |
| IoT 卡人工/OpenAPI/自动/保护期停复机 | 分别使用 `iot_card.manual_stop``iot_card.manual_start``iot_card.openapi_start``iot_card.auto_stop``iot_card.auto_start``iot_card.auto_stop_reason_update`;记录真实人工/OpenAPI/system actor、目标卡、关联设备与卡槽、停复机原因、状态前后值、Integration ID 及 success/failed/denied/unknown保护期强制修正复用同一服务不再旁路审计 | `tb_iot_card.network_status/stopped_at/resumed_at/stop_reason/gateway_extend` 是内部状态权威事实 | 保持既有内外层重试次数和退避;每次真实 Gateway HTTP 分别写同一 `trigger_series` 下单调递增 attempt超时为 unknown成功尝试待状态事务结束后终结 | 状态、Audit Event 与 `card.observation.series.requested` 同事务;停复机完成后沿用轮询重排,不新增补偿 Outbox |
| 单笔资产分配/回收 | 当前无独立通用单笔写入口IoT 卡和设备同步分配/回收接口在请求仅含一个资产时,分别复用 `iot_card.allocate/recall``device.allocate/recall` 单资源子事件,记录真实后台或代理操作者、资产、分配记录、来源/目标店铺;设备同时关联实际连带变化的卡和卡槽 binding。每个已识别资源的拒绝/失败仍进入对应子事件,不另造 `asset.*` 重复动作 | 卡/设备归属与状态、实际连带卡归属及 `tb_asset_allocation_record` 是权威业务事实;单项事实、分配记录和 Audit Event 沿用 6.2/6.5 已有同一 GORM 事务 | N/A本地资产归属事务不调用 Gateway、支付、企微或其他外部系统 | N/A同步流转不产生新增可靠副作用提交后缓存失效和轮询通知保持既有顺序。批量请求根事件及 CSV 批量任务分别沿用 6.2/6.5 和 6.9,不在本切片重复实现 |
| 设备 CSV 批量分配、设置套餐系列或回收 | Worker 使用真实 `system_task/worker` 上下文,每个任务以稳定 `task_no` 作为 correlation 和批次键,仅对尚未达到目标关系的设备调用一次现有设备批量服务,复用 `device.allocate_batch`/`device.allocate``device.series_binding_batch`/`device.series_binding``device.recall_batch`/`device.recall` 根子事件;根事件统计与已识别设备子事件一致,实际绑定卡和卡槽随设备子事件进入各自资源时间线,不再另写 CSV 专用设备事实审计。任务查询的 `target_name` 仅批量投影当前店铺或套餐系列名称 | 设备归属、实际绑定卡归属、`tb_asset_allocation_record``series_id` 是权威业务事实;成功事实与 Audit Event 共用设备服务原 GORM 事务,审计失败回滚业务,不另建领域账本 | N/ACSV 解析、分配、系列绑定和回收均为本地数据库操作,不调用 Gateway、支付或企微对象存储沿用现有存储日志 | 复用 `device:import` Asynq 任务;完成/失败状态阻止终态任务重复执行,处理中断恢复时已达到目标关系的设备不再写业务事实或成功子事件,其余设备仍使用相同 `task_no` 保持审计幂等,不新增业务 Outbox |
| 设备停复机、Wi-Fi、切卡模式、重启和重置 | 使用 `device.stop``device.start``device.set_wifi``device.set_switch_mode``device.reboot``device.reset`,记录后台、个人或 OpenAPI 的真实 actor/source、设备、实际绑定卡与卡槽引用、命令参数、Integration ID 及 success/failed/denied/unknownWi-Fi 只记录 `credentials_configured`,不记录密码;当前卡切换由独立卡槽关系用例记录 | 停复机只修改实际处理卡的 `network_status/stopped_at/resumed_at/stop_reason`Wi-Fi、模式、重启和重置无同步本地状态变化只记录外部命令结论不伪造设备字段已生效`enabled` 当前未下发,登记 N/A | 保留 Gateway 既有重试与退避,每次真实 HTTP 尝试写同一 `trigger_series` 下的独立 attempt停复机按实际卡记录其他命令按设备记录超时为 unknown成功尝试在本地事务结果明确后终结 | 停复机的卡状态、Audit Event 与既有 `card.observation.series.requested` 同一事务,并保留保护期和轮询缓存失效;其他命令成功后沿用 best-effort 观测分发。Worker/Scheduler/Callback 不直接执行这些设备命令,自动停复机继续由 6.3 卡级审计覆盖 |
| 设备绑卡、解绑与当前卡切换 | 使用 `device.bind_card``device.unbind_card``device.switch_current_card`;记录后台账号、个人客户或 OpenAPI 的真实 actor/source设备、目标卡、旧/新当前卡及实际相关 binding 均为独立资源binding 快照及 before/after 固化 `slot_position/is_current`;设备导入和删除产生的隐式绑卡/解绑继续使用既有 `device.create/delete`,但同样关联每张卡和 binding已识别设备后的拒绝、失败和 unknown 使用独立短事务 | `tb_device_sim_binding.bind_status/slot_position/is_current` 是设备 1-4 卡槽及当前卡的权威内部事实;绑卡创建、解绑状态和切卡当前标识与 Audit Event 同一 GORM 事务,卡的 `device_virtual_no` 仍保持原提交后 best-effort 快照语义 | 绑卡、解绑 N/A纯本地事务切卡每次真实 Gateway HTTP 尝试写相同 `trigger_series` 下的独立 Integration Log attempt超时为 unknown目标卡必须是当前设备有效绑定卡Integration 只表达外呼结果,不提前声称本地状态已变化 | 切卡成功后保留既有 Card Observation best-effort 分发,用 Gateway 后续观测校准真实当前槽位;观测 Worker 的自动回写属于 8.7,本切片不重复审计;不新增 Outbox 类型,不迁移设备/卡其他状态机。后台绑卡/解绑只有平台入口,切卡三类入口汇聚共享 Service三组旧资产 operation log 已停止 |
| 订单创建、取消、钱包支付与过期关闭 | 使用 `order.create``order.cancel``order.wallet_pay``order.expire_close`;后台创建/代购记录真实账号C 端记录个人客户,代理 OpenAPI 记录 OpenAPI actor资产套餐批量订购逐笔记录 `system_task/worker`,过期关闭记录 `scheduled_job/scheduler`。事件关联订单、买家、卡或设备、套餐、实际钱包/流水和 Payment订单快照保存金额、支付方式/状态、购买角色及操作者;成功与原订单事务同写,已识别订单或资产后的拒绝/失败使用独立短事务。普通订单列表和详情仍为受权读取 N/A | `tb_order``tb_order_item``tb_payment``tb_package_usage``tb_agent_wallet`/`tb_asset_wallet` 及对应唯一流水继续是订单、套餐和资金权威事实Audit Event 不替代余额、支付或权益账本。钱包下单及待支付后的钱包支付均在原 GORM 事务内追加审计,审计失败回滚业务;重复命中且未发生状态变化时不伪造成功事件 | 本切片不直接调用支付渠道或 GatewayIntegration Log=N/A微信、支付宝、富友预下单、查单和回调属于 7.4,不能用订单事件替代外部尝试记录 | 代理钱包扣款继续在同一事务写既有钱包 Outbox佣金任务和套餐观测沿用原提交后链路Audit Event 不替代 Outbox。OpenAPI/CSV 批量根事件与 partial 统计属于 8.3,本切片只记录每笔实际订单,不提前伪造批次根事件 |
| 支付外部尝试与内部终态 | 使用 `payment.create``payment.confirm``payment.fail`;没有独立 Payment 的旧订单回调使用 `order.online_pay`。支付单为主要资源,订单或充值单为业务单资源,渠道交易号保存在支付快照;个人资产充值确认同时关联实际变化的钱包和唯一流水。支付创建、明确关闭及回调确认均与对应 Payment、订单、充值或钱包 Domain Ledger 共用原 GORM 事务,重复回调未发生状态变化时不重复写成功 Audit Event | `tb_payment``tb_order``tb_recharge_order``tb_agent_recharge_record`、钱包及唯一流水继续是支付、订单、充值和资金权威事实Audit Event 只解释操作者、回调来源、资源关系及前后状态,不替代支付状态、到账金额或渠道交易号 | 微信/富友真实预下单、代理充值微信/支付宝查单及所有已识别的微信/支付宝/富友回调逐次写 Integration Log使用稳定 `trigger_series+attempt`、支付单号 correlation 和渠道交易号幂等;成功回调仅在内部状态真实变化时标记 `state_changed`。支付宝 WAP URL 由本地签名生成Integration Log=N/A当前没有富友主动查单实现登记 N/A不虚构外部尝试 | 支付确认后既有佣金、套餐恢复、自动购包及代理充值入账 Outbox 保持原链路Audit Event 和 Integration Log 均不替代 Outbox。个人/代理充值完整创建至入账、钱包资金专项分别留给 7.67.9,本切片不修改渠道协议、金额校验、价格、佣金、套餐激活、钱包算法或状态机 |
| 个人资产充值与代理在线/线下充值 | 个人资产充值继续以 `payment.create/confirm/fail` 记录支付生命周期,并补齐真实个人客户、卡/设备、资产钱包和充值单资源;代理充值使用 `agent_recharge.create/credit/close` 记录线下申请、审批终态、在线/线下真实入账和关闭,支付事件补齐提交账号、目标店铺及主钱包。支付回调使用 `external_system/callback`,主动恢复使用 `scheduled_job/scheduler`Outbox 入账和自动购包使用 `system_task/worker`,不伪造最初提交人;重复支付、重复审批和重复入账未改变事实时不重复写成功事件 | `tb_recharge_order``tb_agent_recharge_record``tb_payment``tb_asset_wallet`/`tb_agent_wallet` 及唯一成功流水继续是充值与资金权威事实;成功 Audit Event 与充值状态、钱包余额、唯一流水及必要 Outbox 共用原 GORM 事务,审计失败回滚业务。`asset_recharge.auto_purchase` 与自动创建订单、钱包扣款流水、Payment、套餐权益和充值单自动购包状态同事务最终失败状态同样与 failed 审计同事务 | 微信、支付宝、富友预下单、回调和代理主动查单沿用 7.4 的逐次 Integration Logunknown 只表示外部结果未确认,不推进充值或钱包事实,也不伪造成功 Audit Event。线下申请创建和本地钱包入账不外呼Integration Log=N/A企微提交、终态同步与主动恢复继续由审批链 Integration Log 负责 | 代理在线支付确认继续写 `agent_recharge.payment_confirmed.v1`,由 Outbox 消费者幂等入账;线下申请继续写审批提交 Outbox审批通过后同事务写钱包 credited Outbox个人资产充值到账后沿用自动购包 Asynq自动购包继续写观测 Outbox。旧线下人工确认的账号 operation log 裸 goroutine 已停止;不新增充值渠道,不改变金额、钱包、审批、套餐激活或自动购包规则 |
### `deliver-july-iteration-confirmed-scope` 任务覆盖映射
- 1.1 对应“换货迁移套餐在原订单退款后失效”1.21.4 对应“订单渠道、资产标识、提交人与实名筛选”1.5 对应“换货创建前未终结退款拦截”。
- 2.12.4 分别对应店铺登录限制、实名策略、下架套餐续费、支付方式配置及后端校验。
- 3.13.3 分别对应物流换货提醒、主钱包低余额提醒、套餐临期提醒。
- 4.14.6 分别由企业微信应用、成员绑定、场景模板、默认发起人与提交、加密回调、主动恢复六行覆盖。
- 5.15.3 分别由员工线下代充值、退款企微终态、旧审批入口发布切换三行覆盖。
- 6.16.6 分别由批量订购、三组业务导出、IoT 卡限速、设备批量分配六行覆盖。
- 0.1、1.6、7.1、7.37.5 只产生证据、API 契约、静态检查或联调文档,不运行生产入口,因此 Audit Event、Domain Ledger、Integration Log 与 Outbox 均为 N/A7.2 即本基线维护动作。
## 旧 Writer 与旧表写入口清单
### 旧账号审计
- contract 结果:旧 Writer、Store、生产装配和全部借用旧账号日志的调用已删除`tb_account_operation_log` 表及存量数据原样保留,不回填、不转换、不接入统一 Query。
- 已切换入口:账号创建、基础资料更新、独立启停、软删除、管理员改密、本人改密、企微绑定及后台登录/登出已改用统一 WriterPostgreSQL 账号安全事实与审计同事务,登录/登出审计为提交后 best-effort失败不撤销 Token 也不改变原认证结果;刷新保持原单次刷新协议,不因审计重写。
- 角色创建/更新/启停/删除/默认信用、权限创建/更新/删除及角色权限分配/移除已切换统一 Writer角色、权限和关联变化与 Audit Event 同一 GORM 事务,批量配置不再逐项自动提交,权限快照保存 code/name 和资源级 before/after。
- 账号角色分配/移除及店铺默认角色分配/移除已切换统一 Writer账号或店铺为主要资源实际变化角色保存 ID/name/type/status 与分配前后值,关系变化与审计共用同一 GORM 事务;权限缓存在提交后 best-effort 失效Redis 失败不回滚业务。账号两处旧 operation log 写入已移除;店铺侧原本不存在旧账号日志写入。
- 店铺创建与基础资料变化已切换统一 Writer创建事件保存店铺编码、名称、上级和层级并与店铺、初始账号、默认角色及钱包初始化共用原有事务更新事件只记录实际变化的基础资料状态、业务员和 C 端登录限制留给 5.6。父子层级仍仅在创建时按既有七级规则确定,相关入口原本不存在旧账号日志写入。
- 店铺状态、业务员归属、C 端登录限制及删除已切换统一 Writer混合 PUT 按实际字段差异分别记录 `shop.enable/disable``shop.update_business_owner``shop.update_client_login_limit`,主体投影只保存安全结论;删除与账号禁用、账号资源变化及审计使用同一 GORM 事务,相关缓存在提交后 best-effort 失效。`Service.Enable/Disable` 当前无生产调用方,生产启停仍由 Update 状态字段承载;这些入口原本不存在旧账号日志写入。
- 企业创建、基础资料、状态和账号改密已切换统一 Writer分别使用 `enterprise.create``enterprise.update``enterprise.update_status``enterprise.update_password`,企业为主要资源,归属店铺为引用资源,实际企业账号为受影响资源;成功审计与企业/账号事实共用 GORM 事务,改密仅保存 `credentials_configured/state` 安全事实,不借审计改变原令牌行为。普通企业列表保持 N/A资产授权明确留给 5.8/5.9;这些入口原本不存在旧 operation log 写入。
- 企业卡授权、回收和授权备注已切换统一 Writer分别使用 `enterprise_card.allocate_cards``enterprise_card.recall_cards``enterprise_card.update_record_remark`企业为主要资源owner shop 为引用资源,实际变化的 IoT 卡和授权记录为受影响资源;授权事实与 Audit Event 共用 GORM 事务,重复有效授权不伪造变化。卡资源仅保存 `subject_result`安全结论,授权记录及备注保持 `internal_only``BatchAuthorize``RevokeAuthorizations` 无生产调用方、`AllocateCardsPreview` 为普通读取,均登记 Audit Event N/A。设备授权留给 5.9,本切片不改变卡授权有效性规则,也不引入第二套 Writer。
- 企业设备授权与回收已切换统一 Writer使用 `enterprise_device.allocate_devices``enterprise_device.recall_devices`企业为主要资源owner shop 为引用资源,实际变化的设备、设备授权、随设备处理的绑定卡及卡授权为受影响资源,实际卡槽绑定仅作为引用快照。授权创建与 Audit Event 共用 GORM 事务并锁定设备与当前卡槽绑定;回收改为事务内行锁和条件更新,按事务内真实命中项返回计数,不再调用持有独立 `db` 的 Store 方法形成伪事务。Service 边界显式拒绝空筛选和非法选取模式,参数错误不写 Audit Event企业/设备越权统一同错,零成功及并发全项冲突使用独立短事务写 `denied`。企业账号被明确拒绝,平台/代理继续复用 `CanManageEnterprise`,代理设备范围保持既有“仅本店设备”规则;设备与卡仅保存 `subject_result`,授权记录与卡槽绑定保持 `internal_only`。本切片不改变 1-4 卡槽绑定规则,不停止设备或卡的旧资产 Writer对应后续 6.x 用例)。
- 个人客户资料、手机号与微信主体已切换统一 Writer使用 `personal_customer.update_profile``personal_customer.bind_phone``personal_customer.change_phone``personal_customer.update_wechat_identity`,个人客户为主要资源,实际手机号或 OpenID 关系为受影响资源;资料更新、手机号绑定/换绑、客户或 OpenID 实际创建/同步与 Audit Event 共用 GORM 事务已识别客户后的业务拒绝或失败使用独立短事务。actor/source 固定为真实 `personal_customer/personal_api`,主体投影为 Registry 白名单约束的 `subject_detail`验证码、JWT、Cookie 和 Redis Token 不进入审计。重复微信登录且资料/OpenID 无变化不写资料事件,普通 `GetProfile`、资产令牌签发、登录 Token 签发与读取保持 N/A。
- 个人客户资产关系已切换统一 Writer使用 `personal_customer.bind_asset``personal_customer.unbind_asset``personal_customer.migrate_asset_binding`,个人客户为主要资源,实际新增、删除或迁移的 `tb_personal_customer_device`/`tb_personal_customer_iccid` 绑定为受影响资源,卡或设备以稳定标识快照进入同一事件。绑定沿用真实 `personal_customer/personal_api`,换货迁移和旧资产重置解绑沿用真实后台账号上下文;所有成功事件与原绑定写入、换货迁移或清理共用既有 GORM 事务,幂等绑定及无有效迁移记录不伪造成功事件。本切片不改变资产校验、首次绑定售出、换货资金/套餐迁移或无虚拟号旧资产重置规则。
- IoT 卡身份生命周期已切换统一 Writer实际导入落库使用 `iot_card.create`,每张新增卡与 `tb_asset_identifier`、Audit Event 共用原批次 GORM 事务actor/source 固定为真实 `system_task/worker`,以导入任务单号关联链路;已存在卡不写成功事件,导入任务创建及任务级根事件仍留给 8.3。单卡、批量删除分别使用 `iot_card.delete``iot_card.batch_delete`,实际删除卡与根子事件共用事务,缓存失效和轮询回调保持提交后执行,对应旧资产删除日志已停止。卡快照保存 ID、ICCID、VirtualNo、MSISDN、运营商、店铺、系列和 generation当前没有独立卡创建或基础资料更新生产入口后者登记 N/A。本切片不迁移分配/回收/系列、状态、实名、限速或 Gateway 命令。
- IoT 卡分配、回收和系列绑定已切换统一 Writer分别使用 `iot_card.allocate_batch`/`iot_card.allocate``iot_card.recall_batch`/`iot_card.recall``iot_card.series_binding_batch`/`iot_card.series_binding` 根子动作;实际卡归属、原有本地状态、`tb_asset_allocation_record``series_id` 更新与 Audit Event 共用原 GORM 事务,审计失败回滚业务,已识别资源后的拒绝/失败使用独立短事务。分配记录继续作为 Domain Ledger卡子事件关联来源/目标店铺、套餐系列以及实际存在的设备和卡槽绑定;绑定设备导致分配/回收拒绝时只记录真实既有关系不修改设备或绑定。缓存失效和轮询回调保持原提交后顺序对应三组旧资产操作日志已停止Integration Log、Outbox 均为 N/A本地事务不调用外部系统且无新增可靠副作用不迁移 6.3 的卡状态命令、实名、限速或 Gateway 行为。
- 设备身份生命周期已切换统一 Writer设备导入 Worker 使用 `device.create`actor/source 固定为 `system_task/worker`correlation 使用导入任务单号;每台设备的主表、资产标识、既有卡槽绑定与卡设备号快照、设备钱包及设备 Audit Event 保持在原单行 GORM 事务,审计只关联设备资源,不提前迁移卡槽语义。平台单删使用 `device.delete`,现有解绑、设备软删、资产标识清理和成功 Audit Event 同事务,失败/拒绝在业务未落地后写独立短事务;对应 `AssetAuditOpDeviceDelete` 调用已归零。设备快照保存 ID、VirtualNo、IMEI、SN、名称、型号、类型、制造商、店铺、系列和 generation当前不存在设备基础资料更新或批量删除生产入口均登记 N/A不为未来入口创建 Action 或 Service。导入任务创建的旧任务级审计仍留给 8.3;本切片不迁移状态、实名策略、分配回收、卡槽资源或 Gateway。
- 设备归属与策略已切换统一 Writer后台与 CSV Worker 共用 `device.allocate_batch`/`device.allocate``device.recall_batch`/`device.recall``device.series_binding_batch`/`device.series_binding`,实名策略使用 `device.realname_policy_batch_update`/`device.realname_policy_update`。分配、回收的设备与实际绑定卡归属/状态、分配记录和 Audit Event 共用 GORM 事务,系列与实名策略事实亦与审计同事务;子事件关联真实来源/目标店铺、分配记录、前后套餐系列、实际绑定卡及当前有效卡槽。已识资源的全拒绝和业务回滚失败使用独立短事务,二次失败保留原业务错误并记录 critical部分成功只为实际变化设备写 success 子事件。三组旧资产操作审计及 CSV 专用事实双写已停止;企业设备授权/回收已由 5.9 的 `enterprise_device.*` 覆盖,本切片登记 N/A不重复迁移。Integration Log 与 Outbox 均为 N/A均为本地事务且无新增可靠副作用设备外部命令明确留给 6.6。
- 设备外部命令已切换统一 Writer后台设备停复机、后台/个人 Wi-Fi、后台切卡模式以及后台/个人/OpenAPI 重启和重置均在共享设备 Service 接入。每次真实 Gateway HTTP 尝试写 Integration Log保留原重试与退避超时记录 unknown。停复机只为实际变化卡在原状态事务内写 `device.stop`/`device.start`,并关联入口设备、目标卡和现有 binding其他命令无同步内部字段变化Audit Event 只记录已下发、失败或结果未知的外部命令事实。Wi-Fi 密码不进入 Audit/Integration当前 `enabled` 未实际下发登记 N/A当前卡切换由后续卡槽关系切片覆盖。上述六组旧资产 operation log 调用已停止。
- 设备卡槽关系已切换统一 Writer后台绑卡/解绑分别使用 `device.bind_card``device.unbind_card`,平台后台、个人端和代理 OpenAPI 共用的当前卡切换使用 `device.switch_current_card`。设备、目标卡、旧/新当前卡和实际相关 binding 均进入独立资源时间线binding 快照及 before/after 保存 `slot_position/is_current`;设备导入创建 binding 和删除设备批量解绑则在原 `device.create/delete` 事件中补齐每张卡与 binding不另造重复动作。绑卡创建、解绑及切卡本地 `is_current` 与 Audit Event 共用 GORM 事务,审计失败回滚本地事实。切卡只允许当前设备有效绑定卡,每次 Gateway HTTP 尝试写独立 Integration Log超时或本地收口失败不伪装成功成功后仍保留原 Card Observation 分发校准真实当前槽位。三组旧资产 operation log 调用已归零;本切片不迁移设备/卡其他状态机,也不改变卡设备虚拟号提交后 best-effort 维护。
- 卡换货完整用例已切换统一 Writer物流创建、个人客户填写收货信息、后台发货、完成、取消和换出旧卡转新分别使用 `exchange.card.create``exchange.card.submit_shipping_info``exchange.card.ship``exchange.card.complete``exchange.card.cancel``exchange.card.renew`;直接换货创建即完成,只记录一次完成事件。成功事件与原换货单、卡状态、客户绑定迁移、资产钱包和流水、套餐权益及通知 Outbox 共用既有 GORM 事务,审计失败回滚业务;已识别换货单或旧卡后的拒绝/失败使用独立短事务并保留原错。完成事件分别关联换货单、旧/新卡 ICCID+VirtualNo、店铺、实际客户绑定、旧新钱包、迁移流水和实际迁移套餐权益客户绑定仍保留独立 `personal_customer.migrate_asset_binding` 事件,钱包流水和套餐权益仍是 Domain Ledger。个人收货资料、内部备注不进入审计主体仅看到安全结果旧卡转新为 `internal_only`。换货不调用外部系统Integration Log 为 N/A物流创建通知继续使用既有 Outbox。本切片不改变钱包迁移、PCI 解绑、套餐、资产归属或换货状态规则。
- 设备换货完整用例已切换统一 Writer物流创建、个人客户填写收货信息、后台发货、完成、取消和换出旧设备转新分别使用 `exchange.device.create``exchange.device.submit_shipping_info``exchange.device.ship``exchange.device.complete``exchange.device.cancel``exchange.device.renew`;直接换货仍只记录一次完成事件。成功事件继续与原换货事务、通知 Outbox、客户绑定、钱包/流水和套餐权益共用同一 GORM 事务,失败或拒绝使用独立短事务。完成事件关联换货单、旧/新设备 VirtualNo+IMEI+SN、店铺、实际客户绑定、旧新钱包、实际迁移流水和套餐权益并逐张关联旧/新设备当前有效绑定卡及 `device_sim_binding` 的 slot/is_current。现有业务不会在设备换货时迁移卡槽因此绑定卡和卡槽按真实状态记录为 reference不伪造变化客户绑定、钱包及套餐权益仅在实际变化时记录 affected。客户绑定独立事件、Domain Ledger、Integration Log N/A 和通知 Outbox 边界保持不变;本切片未修改设备卡槽、钱包、套餐、资产归属或换货状态规则。
- 套餐配置与授权已切换统一 Writer套餐系列使用 `package_series.create/update/delete/update_status`,套餐商品使用 `package.create/update/delete/update_status/update_shelf_status`,店铺套餐上下架、零售价和生效条件分别使用 `shop_package.update_shelf_status``package.update_retail_price``shop_package.update_expiry_base`,系列授权使用 `shop_series_grant.create/update/manage_packages/delete`。批量套餐分配与批量成本价调整分别使用 `shop_package.batch_allocate`/`shop_package.allocate``shop_package.batch_update_pricing`/`shop_package.update_pricing_item` 根子事件;全量价格锁拒绝为 `denied`,成功与拒绝混合为 `partial`,未实际变化或已存在而跳过的资源不伪造成功子事件。系列、套餐、店铺、系列授权、套餐授权和价格历史均作为独立资源进入各自时间线,配置与价格 before/after 只记录本次相关字段;成功事件与原配置、授权及价格写入共用 GORM 事务,已识别资源后的拒绝或回滚失败使用独立短事务并保留原业务错误。`tb_shop_package_allocation_price_history` 继续是价格变化的 Domain LedgerAudit Event 不替代它Integration Log 与 Outbox 均为 N/A本切片仅本地配置事务不发生外部交互或可靠异步副作用`shop_package_batch_allocation` 借用旧账号 operation log 的写入已停止,原上架、直属下级授权、佣金天花板、成本价锁定、赠送套餐和分配规则保持不变;套餐购买及 `package_usage` 权益生命周期明确留给 7.27.3,不在本切片迁移。
- 套餐权益生命周期已切换统一 Writer实际激活、到期及加油包级联失效、流量扣减、日/月/年重置、退款精准失效和按资产失效分别使用 `package_usage.activate/expire/deduct_traffic/reset_traffic/invalidate_refund/invalidate_asset`,只在状态条件更新或真实数值变化命中时写 success重复任务和无变化分支不伪造事件。`package_usage` 为主要/受影响资源,关联订单、套餐商品、当前卡或设备以及退款单均作为独立 reference 资源进入各自时间线;成功事件与 `tb_package_usage` 及每日流量详单 Domain Ledger 共用原 GORM 事务审计失败回滚业务已定位权益后的事务失败使用独立短事务并保留原错。Scheduler 使用真实 `scheduled_job/scheduler`Asynq、Outbox 消费和旧退款异步后处理使用真实 `system_task/worker`,人工实名激活保留 HTTP 账号 actorOutbox 消费以当前 envelope EventID 作为直接 parent沿用既有 request/correlation。退款审批标准 Outbox 同样把决策 EventID 传播为后处理 parent。换货完成继续复用 7.1 已有的同事务完成事件,不重复新增套餐迁移动作,并补齐迁移权益的激活/到期等快照以及独立订单、套餐引用;`tb_package_usage` 仍是权益权威事实。流量观测 Outbox 继续承担可靠传递Audit Event 不替代 Outbox套餐生命周期本地写不产生外部请求Integration Log 为 N/A。`InvalidateAllPackagesByAsset` 当前没有生产调用者,登记为已接好审计但生产覆盖 N/A不借本任务新增“资产停用即权益失效”规则既有套餐列表 Query 保持 N/A。`fix-package-activation-starvation` 的孤儿 CTE、每载体一个名额、同步恢复和两事务接续设计均作为独立专项修复保留本切片不改其业务规则。
- 订单生命周期已切换统一 Writer后台创建与代购、C 端普通购买、代理 OpenAPI 每笔购买、手工取消、待支付订单钱包支付及计划任务过期关闭分别使用 `order.create/cancel/wallet_pay/expire_close`。账号、个人客户、OpenAPI、批量 Worker 和 Scheduler 均保留真实 actor/source订单为主要资源买家、卡或设备、套餐、实际钱包/流水及 Payment 为独立关联资源,金额、支付状态、方式和购买角色保存为快照。成功事件与订单、明细、资金、支付、套餐权益及既有 Outbox 共用原 GORM 事务,审计失败回滚业务;拒绝和事务失败保留原错误并写独立短事务,幂等无变化分支不伪造成功。支付渠道 Integration Log 留给 7.4,代理及资产钱包自身资金动作分别留给 7.7/7.9OpenAPI/CSV 批量根事件留给 8.3;本切片未修改价格、佣金、套餐激活、钱包算法、授权或订单状态机,普通订单 Query 保持 N/A。
- 支付外部与内部终态已接入统一边界Payment 创建、渠道明确关闭和回调确认分别使用 `payment.create/confirm/fail`,没有 Payment 的旧订单回调使用 `order.online_pay`;支付单、订单或充值单、渠道交易号以及回调实际变化的钱包/流水进入同一事件,成功审计与对应 Domain Ledger 共用原 GORM 事务,重复回调不伪造成功事件。微信/富友真实预下单、代理充值微信/支付宝查单及微信/支付宝/富友已识别回调写 Integration Log支付宝 WAP 本地签名和当前未实现的富友主动查单均明确 N/A。Integration 使用支付单号 correlation回调用渠道交易号幂等结果未知不伪装失败或成功本切片未重构渠道协议也未修改价格、佣金、套餐激活、钱包、授权、金额规则或状态机充值与资金专项审计仍由 7.67.9 收口。
- 个人资产和代理充值已切换统一 Writer个人资产充值的 `payment.create/confirm/fail` 事件补齐个人客户、卡/设备、充值单、资产钱包和唯一流水;代理线下申请、真实入账及拒绝/关闭分别使用 `agent_recharge.create/credit/close`关联提交账号、目标店铺、审批实例、支付单、主钱包和唯一流水。外部回调、主动恢复、Outbox 消费分别保留 `external_system``scheduled_job``system_task` actor重复终态不伪造成功。自动购包使用 `asset_recharge.auto_purchase`,与订单、支付、钱包扣款流水、套餐权益及充值单自动购包状态同事务,最终失败状态同样原子记录;既有 Integration Log、审批/入账 Outbox、观测 Outbox 和自动购包 Asynq 边界不变。旧线下人工确认的账号 operation log 裸 goroutine已停止本切片不新增渠道也未修改金额、钱包、审批、套餐激活、佣金或自动购包规则。
- 代理主钱包订单资金已切换统一 Writer直接扣款使用 `agent_wallet.order_debit`,预占、释放和完成分别使用 `agent_wallet.order_reserve/order_release/order_complete`;订单为主要资源,主钱包、预占事实及实际成功流水为受影响资源,钱包记录 balance/frozen_balance 前后值流水保留唯一业务引用。Audit Event 与既有行锁、乐观锁、状态条件、唯一流水和钱包 Outbox 共用原事务,写入失败回滚全部业务事实;订单事务整体失败后按已尝试的钱包动作写独立短事务,重复扣款或重复预占终态不伪造成功事件。完成预占由 `order_complete` 同时关联扣款流水,不再重复生成 `order_debit` 审计。当前生产只有直接扣款和取消订单 release 调用freeze/complete Application 接缝暂无生产调用者,本任务只接入审计,不新增调用、不修改钱包、订单、套餐、佣金、充值、退款或信用额度规则。
- 代理主钱包正向及回退资金已完成专项收口:充值继续复用 7.6 的 `agent_recharge.credit`,退款继续复用 7.5 的 `refund.approve`,两者均已在一条业务事件中关联主钱包、原流水/新流水和余额前后值,不新增同义钱包事件。人工调整使用 `agent_wallet.adjust_balance`,在既有 Posting/Outbox 事务内关联唯一调整流水并强制记录原因;当前无生产入口,只接好现有 Application 能力,不新增接口。实际信用额度更新使用 `agent_wallet.change_credit`,主钱包为主要资源、店铺为引用资源,保存余额/冻结余额不变和信用字段/version 前后值;成功审计失败回滚信用更新,已识别钱包后的拒绝/失败使用独立短事务并保留原错。Integration Log 均为 N/A本切片未修改充值、退款上限、钱包算法、信用占用、审批、佣金或资产钱包规则。
- 卡/设备资产钱包资金已完成专项收口:充值复用 `payment.confirm`,扣款复用 `order.wallet_pay`,退款复用 `refund.approve`,换货迁移复用 `exchange.card.complete/exchange.device.complete`,不新增同义钱包动作。四类成功事件均在原业务事务内关联卡的 ICCID/VirtualNo 或设备的 VirtualNo/IMEI/SN、资产钱包、充值/订单/退款/换货单及唯一流水,并保存余额前后值;充值事件只保留一条受影响钱包资源,订单扣款的钱包和流水明确标记为 affected。支付、订单、退款状态条件及换货状态机继续阻止重复业务键或重复终态伪造成功无余额迁移不创建流水钱包流水继续作为 Domain Ledger。充值渠道外部尝试沿用 Integration Log其余本地钱包动作 N/A本切片未修改钱包余额/冻结/乐观锁、退款上限、换货迁移、套餐、客户绑定或代理主钱包规则。
- 佣金与提现状态机已切换统一 Writer订单 Worker 使用 `commission.calculate` 记录真实 `system_task/worker`、订单佣金状态结果、全部佣金记录、归属店铺和套餐系列;每笔自动入账及待审人工入账使用 `commission.credit`,关联佣金记录、订单、店铺、系列、佣金钱包及实际存在的钱包流水和余额前后值;待审人工失效使用 `commission.invalidate`。退款回扣继续只使用既有 `refund.invalidate_commission`,不重复造佣金失效事件。提现申请、通过、驳回分别使用 `commission_withdrawal.request/approve/reject`,提现单为主要资源,店铺、佣金钱包及冻结/扣除/解冻流水为独立资源,收款账户 JSON 不进入审计。成功事件与现有佣金、订单、钱包、流水和提现事实同事务,已定位订单、佣金记录或提现单后的拒绝/失败使用独立短事务;幂等完成订单不伪造重复成功。现有“待审佣金人工入账”业务只更新佣金记录和钱包、不创建钱包流水,本切片按真实事实关联钱包但不伪造 Domain Ledger统计 Query、佣金公式、阶梯规则、提现金额/手续费、冻结算法和审批状态机均未修改。Integration Log 与 Outbox 为 N/A这些链路没有外部调用或新增可靠副作用
- 轮询配置与人工动作已切换统一 Writer轮询配置创建、更新、删除和启停使用 `polling_config.create/update/delete/update_status`,并发配置更新与计数重置使用 `polling_concurrency.update/reset`,告警规则创建、更新和删除使用 `polling_alert.create_rule/update_rule/delete_rule`;成功事件与对应 PostgreSQL 配置事实共用 GORM 事务审计失败回滚配置写入。Redis 并发计数重置无法与 PostgreSQL 原子提交,审计失败时恢复重置前计数;人工单卡去重键在日志/审计事务失败或队列写入失败时移除,避免阻断原有重试。
- 单卡、批量、条件筛选人工触发和取消分别使用 `polling_manual_trigger.trigger_single/trigger_batch/trigger_by_condition/cancel_trigger`,记录真实后台账号、手动任务和实际卡资源;配置重名、每日触发上限、重复入队、越权取消和已结束任务取消等已定位资源的拒绝写独立短事务,二次失败保留原业务错误并记录 critical。`tb_polling_manual_trigger_log` 继续承担进度、结果与历史查询,不被 Audit Event 替代或停写;审计不复制 `CardIDs`、条件正文、通知渠道正文或其他安全凭据,只保存任务类型、触发方式、数量、状态及“条件/通知渠道是否配置”等安全事实。
- 实名、流量和卡状态轮询的每次真实 Gateway 尝试继续写 Integration Log套餐/保护期轮询自身不伪造 Gateway 尝试,实际停复机复用共享 `StopResumeService` 的 Audit/Integration 边界。轮询配置、并发状态、告警历史、人工任务状态/历史和监控页面等普通运行查询均为 N/A通知投递、任务 ledger、Audit Event 与 Integration Log 保持独立事实。`polling_cleanup` 数据清理配置未包含在 8.5 明确边界,本轮不借轮询审计扩展其 CRUD 或手动清理动作,继续保留在覆盖清单等待对应显式切片。
- 外部 Callback 已按当前生产路由收口:微信/支付宝/富友支付回调继续复用 7.4 的支付、订单和充值事务审计,企微审批回调继续复用 8.1 的权威终态审计;电信、移动、联通实名成功回调在共享 `ApplyCardObservation` 事务中使用 `iot_card.realname_callback_sync`,真实 actor 为各运营商 `external_system/callback`,关联 IoT 卡和入站 Integration Log。重复支付、重复审批、重复实名及无状态变化只保留 Integration Log不伪造成功 Audit Event联通解除实名仍仅留痕且不改变本地实名事实。本切片不修改支付、运营商或企微协议。
- 当前 31 个 Asynq Worker 已按 8.7 逐项复核:`iot_card:import``device:import``order_package:invalidate``asset_package:batch_order``commission:calculate``package:first_activation``package:queue_activation``order:expire``notification:cleanup``auto_purchase:after_recharge``wecom:approval:sync``wecom:approval:recovery``agent_recharge:recovery` 已复用前序纵向切片的统一 Audit Event本切片新增 `card_observation:series``polling:realname/carddata/card_status``system_task/worker` 上下文,仅在实名、流量、网络或设备字段/当前卡槽实际变化时分别写 `iot_card.worker_realname_sync``iot_card.worker_traffic_sync``iot_card.worker_network_sync``device.worker_observation_sync`,并关联对应 Gateway Integration Log。`polling:package/protect` 不另造轮询动作,实际停复机继续复用 `iot_card.auto_stop/auto_start/auto_stop_reason_update`。上述链路传播 request/correlation/parent重试失败写 failed 短事务,无实际变化仅保留 Integration Log 或任务运行事实,不伪造 success。
- Worker N/A 与后续边界:`email:send` 仅模拟邮件投递;`export:dispatch/shard/finalize` 仅做导出技术装配;`commission_stats:update/sync/archive` 仅维护 Redis/PostgreSQL 统计投影;`polling:alert_check` 仅生成告警运行事实;`polling:data_cleanup` 仅执行既有轮询数据清理;`package:expiry_reminder` 仅生成可靠通知 Outbox`daily_traffic:flush` 仅把 Redis 日流量 Domain Ledger 落盘,均不重复创建 Audit Event。`outbox:deliver` 自身是可靠投递技术入口Relay/投递事实 N/A其各业务消费者是否形成新业务事实按任务 8.9 逐项收口,不在 8.7 提前迁移。Scheduler 仅投递或创建任务的边界留给 8.8。
- Scheduler 已按当前注册清单逐项复核:代理在线充值恢复、订单过期关闭和企微审批恢复在真实业务事实变化时使用 `scheduled_job/scheduler`;轮询 Scheduler 直接执行的套餐到期、后续权益接续、流量周期重置及套餐到期停机检查同样保留 `scheduled_job/scheduler` 操作者。套餐到期后的异步停机检查使用不受调度 tick 取消影响的原审计上下文,继续传播 actor、correlation 和 parent不伪造人工操作者。
- Scheduler N/A 与幂等边界Asynq 周期注册、轮询心跳、队列深度检查、分片/手动队列出队、重复调度的 `Unique` 去重与失败回队只是技术调度或任务事实,不写 Audit Event也不把后续 Worker 结果伪装成 Scheduler 成功。告警检查、轮询数据清理、通知保留清理、套餐临期提醒和日流量落盘继续按 8.7 的 Worker/Domain Ledger/Outbox 边界处理;重复调度依赖既有状态条件、领取租约、稳定事件 ID 和 Asynq `Unique`,无实际变化不伪造 success。Worker 消费逻辑未在 8.8 迁移。
- 当前 14 个 Outbox 事件注册、12 个消费者实现已按 8.9 逐项复核:企微审批提交终态、审批标准决策分发、卡实名/流量/网络变化后续处理、代理在线充值入账和三类站内通知生成会形成新的内部业务事实,均通过 `outbox:deliver` 入口取得 `system_task/worker` actor并沿用信封的 request/correlation/parent实际变化继续复用 8.1、7.2、7.6 和 8.4 已接入的同事务 Audit Event。状态条件、处理租约、稳定事件 ID、业务唯一键和通知 `CreateIdempotent` 保证至少一次投递不会伪造重复 success消费者返回成功只代表本次业务处理完成Outbox 的 delivered 状态仍是独立投递事实。
- Outbox 消费者 N/A 边界:卡观测序列请求仅幂等创建后续观测任务,代理主钱包预占/入账/退款消费者仅复核既有 Domain Ledger扣款消费者仅复核流水并按阈值幂等追加新的通知 Outbox均不把“校验通过、任务触发或二次投递成功”写成业务 Audit Event后续观测或通知实际改变业务事实时由对应 Worker/Application 自身写 Audit Event。Relay 的领取、入队、续租、delivered/failed、退避和重投保持技术投递事实不在 8.9 修改或审计化。
- request/correlation 组合时间线只按非空稳定 ID 精确读取 `tb_audit_event``tb_integration_log``tb_outbox_event`,以 `record_source` 保留 Audit、外部交互及可靠投递边界Outbox 只展示当前投递摘要,不把 delivered 解释为业务成功。Asynq 没有通用 PostgreSQL 历史表Query 仅从已落库的导入、批量购包、套餐失效、设备批量和导出任务资源生成 `asynq_task` 摘要,不扫描 Redis、不展示技术重试。订单、支付、退款、充值、钱包/流水、套餐权益、审批和佣金等只生成 `domain_ledger_ref` 稳定引用,金额与状态仍以业务表为准并留给 9.2 专业资金视角。Access Log 只返回 request ID 供开发检索,不读取日志文件;历史缺少 request/correlation/parent、直接 Audit 关联或稳定资源时通过 fidelity 标记原样降级,不按相近时间、相似资源或相同 correlation 猜测技术尝试。节点统一按发生时间、`record_source`、稳定节点 ID 升序排列。
- 资金调查时间线以平台只读 Query 组合 Audit Event、代理/资产钱包流水、代理钱包预占、订单、支付、退款、代理/个人资产充值、佣金、提现和审批当前业务事实;支持从店铺、钱包、订单、支付、退款、充值、审批、第三方交易号、操作者、时间或 correlation 中任一稳定条件进入,并在服务端解析已持久化关联,不要求前端补齐整条链路。节点按发生时间、`record_source`、稳定节点 ID 倒序分页,统一返回调查跳转引用;钱包流水的 `amount/balance_before/balance_after` 及各业务表金额字段明确标记为权威Audit Event 金额只作操作摘要,冲突时不改写历史事件并以对应 Domain Ledger 为准。平台身份复用统一审计 Query 的 SuperAdmin/Platform 后端校验,不增加店铺数据范围,也不提供资金重算、状态修改、导出或恢复能力。
- 风险调查视角只读聚合统一 Audit Event高/严重风险、涉及订单/支付/退款/充值/钱包/佣金等资金资源、安全类别,以及 `failed/denied/partial/unknown` 结果进入固定风险集合普通低风险成功事件明确排除。overview 在最长 31 天的显式时间范围内按风险、结果、action、来源和小时/日趋势聚合events 复用统一事件批量投影、稳定倒序分页及 `investigation_refs`可继续跳转事件、资源、actor 和 correlation。该视角不读取或修改业务状态不产生 Audit Event、Domain Ledger、Integration Log 或 Outbox也不提供处置工单、自动封禁、导出或恢复能力。
- 跨视角查询性能仅补已确认路径的 PostgreSQL B-tree保留事件时间、actor、scope、correlation 和资源时间线既有索引,补 action、result、risk、category、source 的稳定倒序分页索引,将 request/parent 索引补齐时间与 ID并为资源 type+id/key 到事件的反向关联补索引。全局、actor、资源和风险事件先分页事件 ID再批量投影事件与资源Integration 列表同样先分页 ID再批量读取列表字段不加载正文 JSON。第一阶段不增加 JSONB 任意搜索、Redis 结果缓存、月分区或冷热联合查询。
- 跨视角 HTTP 契约复用既有平台 `AuditHandler` 和生产/文档装配,仅新增 request、correlation、finance、risk 的只读 GET Handler/DTO/RouteSpec身份继续只取认证上下文未增加写路由、导出、处置、恢复或前端实现。前端导航文档逐行冻结 design 8.1-8.4 的源页面、前置接口、`response.data` 字段、入口可见条件、目标参数和降级规则,并以资产、订单、退款、钱包、通知、风险节点演示完整调用链。
- Audit Event 与 Event Resource 每日冷归档使用独立 `audit:daily:archive` Asynq 任务和 `tb_log_archive_run` 轻量账本:按 `Asia/Shanghai` 前一完整自然日、事件 `created_at` 半开区间分页读取,每行保存一个完整事件及其全部 `resources[]`,生成 JSONL+gzip、manifest 和 SHA-256对象 Key 按日期与 revision 稳定生成,上传后回读 metadata 核对大小、hash、事件数和资源数成功重复任务直接复用失败或对象不一致使用新 revision 且不覆盖旧对象。该基础设施任务不写业务 Audit Event不读取 Integration/Access/Domain Ledger/Outbox/旧 operation log不清理数据库也不向业务 Writer 注入对象存储;对象存储失败只更新归档账本并由 Asynq 重试,业务审计写入继续正常执行。
- Integration Log 冷归档复用同一对象存储、归档 Service 和 `tb_log_archive_run``integration:daily:archive``created_at` 保存前一自然日的结构化 JSONL+gzip 创建日快照;`integration:monthly:finalize` 在月初逐日按数据库当前内容重新生成并比较记录数与 SHA-256首次终结、内容变化或对象 metadata 不一致时创建新的不可变 revision旧对象不覆盖。月度复核仅把无 `pending` 记录且最终对象、manifest 均复核成功的日期标记 `is_final``pending`、对象损坏或复核失败会使任务失败并明确阻止后续清理。该切片不修改 Integration Writer、恢复语义或业务状态也不删除 PostgreSQL 数据、不提供对象存储查询/恢复接口。
- 月度留存清理使用 `audit:monthly:retention` Asynq 任务,在 `Asia/Shanghai` 每月 1 日 06:00 处理上一完整自然月:先补齐最后一日 Audit/Integration 归档并完成 Integration 最终 revision再逐日核对 ledger、manifest、对象 metadata、压缩对象实际大小/SHA-256 及数据库数量。全月硬门禁通过后仅按 Event Resource → Audit Event → Integration Log 顺序对 `tb_audit_event_resource``tb_audit_event``tb_integration_log` 以 1000 行有界批次执行 GORM 物理删除,并复用 `tb_log_archive_run.cleanup_started_at/cleaned_at` 断点续跑;对象存储归档和 manifest 长期保留。清理结果以 `retention_worker/system_task` 写当月 `audit.retention_cleanup` Audit Event资源为 `log_archive_month`Domain Ledger、Integration Log 新写、Outbox 均为 N/A因该事实是内部留存执行结果不是业务状态、外部交互或可靠投递。Access Log、订单/支付/退款/钱包等 Domain Ledger、Outbox、Asynq 运行事实、手动轮询、旧 operation log 及其他业务表明确不删除。
- 在线审计查询以 `tb_log_archive_run.cleaned_at/range_end` 作为真实清理边界:平台 Audit/Integration、request/correlation/finance/risk 及代理/企业活动响应统一返回 `retention{online_from,archived_before,timezone}`;缺省时间范围只查 PostgreSQL 在线窗口,显式早于或跨越边界返回 `CodeAuditDataArchived` 和当前边界。稳定事件或 Integration ID 只在在线库查找,不存在仍返回资源不存在;历史资源快照搜索和 Integration 尝试序列同样受边界限制。该 Query 切片不访问对象存储,不新增归档下载、恢复、冷热联合查询、导出或写路由,普通读取仍为 Audit Event/Domain Ledger/Integration Log/Outbox N/A。
- 留存灰度默认使用 `worker.audit_retention_cleanup_enabled=false`:月初 `audit:monthly:retention` 只读复核完整月 ledger、manifest、对象 metadata/大小/SHA-256、Audit 计数和 Integration 最终 revision并记录三类在线行数、manifest 数、校验耗时和预计 1000 行删除批次;不会写 `cleanup_started_at/cleaned_at`,也不会执行 DELETE。完整自然月可在显式确认的隔离测试数据库中按真实日界构造无需等待现实时间流逝dry-run 通过后仅在该测试环境启用清理,验证目标月三张日志表归零且月前/月后哨兵、其他业务事实和对象归档不受影响。2026-08-06 使用 `.env.local``junhong_cmp_test` 完成 `2001-02` 仿真56/56 归档成功、28/28 Integration final、最大 revision/attempt=2、压缩率 0.5047、dry-run 无清理断点、清理后目标月三表归零且 6 条边界哨兵保留、56 条账本清理断点完整并写入 1 条清理审计。生产开关仍保持关闭;该 Release/Observability 切片不产生 Domain Ledger、Integration Log 或 Outbox。
## 2026-08-06 最终 Registry 与覆盖门禁复核
- 业务复核692 个当前源码入口均进入显式清单;状态变更、资金、权限、关键配置、批量、多资源、设备卡槽、换货、自动入口及 5 组敏感读取均已在上方领域矩阵给出 Audit Event 或逐项 N/A 决定。39 个 Worker 注册语句对应 35 个唯一 TaskType重复注册来自对象存储可用/不可用两条互斥装配分支,不代表重复消费。
- 研发复核219 个 `AuditAction` 常量与 219 个 `actionsByCode` 注册项一一对应16 个兼容 `AuditOperation``actionsByOperation` 一一对应;生产使用未注册动作、动作字符串字面量均为 0。61 个资源类型常量与 61 个 Resource Registry 条目一一对应146 个资源角色常量均有生产引用,生产 `ResourceInput` 未使用资源类型或角色字符串字面量。Writer 对未知 action、未知 resource、主要资源不匹配及不完整关系保持 fail-closed。
- 查询复核:通用资源时间线直接以 Resource Registry 判定支持类型,不再维护独立 13 类白名单;资源搜索仍按第一阶段契约只开放卡、设备、店铺、订单和退款 5 类 resolver。统一 Query 不读取旧账号/资产 operation log。
- 外部与可靠链路复核78 个 Integration Log 调用点覆盖 `Start/Complete/RecordInbound/ClaimExpiredInboundPending`14 个 Outbox Consumer 注册点均显式登记Consumer 注册本身为技术装配 N/A实际状态变化沿用对应 Consumer/Application 动作Outbox 投递事实不伪装成业务成功。
- 安全复核:普通 GET 继续逐项 N/A企微明文应用凭据、资产实时状态、后台/个人实名链接、完成态导出任务详情等敏感读取保持返回前 fail-closed 审计。密码、验证码、Token、Secret、私钥、Cookie、签名 URL 和支付密钥不进入 Audit/Integration 或主体投影。
- contract 复核:旧 Writer 调用为 0旧账号表与旧资产表原样保留旧资产历史入口只读手动轮询 ledger 继续读写。扫描清单 692 项必填分类字段缺失为 0Audit Event N/A 无理由为 0静态扫描、gopls、全仓构建与 `git diff --check` 作为本 Change 禁止自动化测试约束下的完成证据。
## 2026-08-06 安全与身份发布门禁复核
- 身份边界:平台 Audit/Integration Query 仅允许 `SuperAdmin/Platform`,代理与企业即使通过通用后台认证也在 Query 层统一 fail-closed代理范围只取认证上下文中的自身及下级店铺企业只取认证上下文企业 ID并要求卡/设备授权 `revoked_at IS NULL AND deleted_at IS NULL`
- 主体投影:不支持资源类型与不存在/越权统一返回“无权限操作该资源或资源不存在”;活动投影返回前再次核验目标资源当前归属或有效授权。`internal_only` 不进入统计、列表和关联资源,`subject_result` 强制返回空 `subject_data`,只有 `subject_detail` 解码持久化白名单数据;主体 DTO 不包含 actor、risk、内部 before/after、Audit Event ID、request/correlation 或 Integration 内容。
- 平台完整性:平台事件详情继续完整投影已存业务 metadata、资源身份快照及各资源 before/after不对手机号、IP、ICCID、VirtualNo、金额和交易号执行展示掩码系统安全凭据仍由 Writer/Sanitizer 在持久化前删除。
- 凭据门禁:统一 Sanitizer 已覆盖驼峰字段名、裸 token/api/payment key 及凭据文本值Audit/Integration 标量同样在落库和历史响应投影前清理。`.env.local` 测试 PostgreSQL 只读抽样中Audit Event JSON、Audit Resource JSON、Audit 标量、Audit Resource 标量、Integration 标量、Integration 禁止键和 Integration 凭据值命中均为 0。历史 Integration 的 129 条初始正则命中全部是允许的 `token_present` 布尔安全事实,不包含 token 值。
- 能力面:`internal/routes/audit.go` 及生成的 `docs/admin-openapi.yaml` 中 15 条平台/代理/企业审计路径全部只有 GET未注册审计导出、对象存储归档查询/恢复、Integration 重试/补偿/修改/删除或 Audit Event 业务删除路由。旧资产 operation-logs 继续作为仅平台可读的独立历史白名单入口。
- 验证约束:本 Change 明确禁止新增、修改或运行自动化测试;本门禁使用敌对身份调用链静态复核、数据库只读抽样、路由/OpenAPI 扫描、全仓构建、gopls 与 `git diff --check` 作为完成证据。
## 2026-08-06 事务、幂等与跨链路发布门禁复核
- 同事务失败关闭:系统配置更新在同一 GORM 事务内完成业务写和 `WriteConfigChange`;代理主钱包扣款/入账在同一事务内完成条件更新、唯一流水和 Audit EventWriter 错误直接返回并回滚业务事务。Outbox 恢复、审批终态及其他高风险纵向切片沿用相同 `tx` 接缝,不使用提交后的补写冒充原子性。
- 失败短事务:统一 `Writer.RecordFailure` 仅在原业务返回后开启独立短事务,`fillFailureInput` 保留原 `AppError` code/message二次写失败只递增 `secondaryWriteFailures` 并输出含 action、资源、request/correlation 和原错误码的 critical 日志,不替换原业务错误。
- 批量根子:`AppendBatch` 在落库前拒绝负数、`success+fail>total`、子事件少于成功数或多于已处理数,并为子事件补齐稳定 parent/correlation。该门禁发现并修复 IoT 卡批量轮询开关原先缺少根/子 `EventID` 的生产缺口,现按请求批次和卡 ID 生成稳定 ID并以实际有效卡数计数。测试库只读核对中批量计数错误、非法结果、缺少/多个主要资源均为 0当前在线窗口没有带批次统计的根事件因此不伪造动态样本结论。
- Audit 链路:测试库中 `child_without_parent_online``child_without_correlation``parent_correlation_mismatch` 均为 0。跨事实 Query 分别读取 Audit Event、Integration Log、Outbox并以独立 `record_source` 投影Domain Ledger 和 Asynq 仅作为 `reference_only` 稳定引用,金额权威仍由资金业务表提供。
- Integration 技术序列:只读核对发现存量代理充值查单及卡观测记录存在重复 `trigger_series + attempt`。新写入已前向修复:自动 attempt 在序列级 PostgreSQL advisory lock 的同一短事务内分配;卡观测按 `series_id + sync_type` 区分不同 Gateway operation。历史数据不回填、不猜测详情保留每条真实 operation并以 `attempt_sequence_reliable=false` 明确重复、断档或混合 operation 的受限保真度。
- 幂等边界Audit Event 继续以稳定 `event_id` 冲突不重复写;资金使用业务唯一流水和状态/版本条件Outbox 保持独立至少一次投递状态。Audit、Integration、Domain Ledger 和 Outbox 之间只通过 request/correlation/parent、稳定业务 ID 或只读引用关联,没有把外部尝试、投递成功或审计摘要伪装成业务成功。
- 验证约束:未新增、修改或运行自动化测试;完成证据使用代表性事务源码调用链、测试 PostgreSQL 只读不变量查询、覆盖清单、OpenAPI、全仓构建、gopls 与 `git diff --check`
## 2026-08-06 迁移、性能、OpenAPI、归档留存与回滚发布门禁复核
- 迁移现状:`.env.local` 测试 PostgreSQL 的 `schema_migrations``205/dirty=false`Audit、Integration、归档账本关键增量索引已存在。共享测试库已有 Audit 和归档事实,未对 `public` 执行 down。
- 无事实 down/up在同一测试 PostgreSQL 的隔离 schema 中使用最小前置表结构执行 `000199`~`000205` up再按 `205→199` 逆序 down上行后目标表/列/索引存在,回滚后 Audit/归档表移除且 Outbox parent/订单预占列恢复。`000199.down` 自带事务并提交外层事务,隔离 schema 已显式删除且核对不存在;该注意项已写入操作手册。
- 性能观测:复用已完成 9.4 的分页 ID + 批量投影与索引证据;当前库只读 `EXPLAIN (ANALYZE, BUFFERS)`事件列表、资源时间线、Integration 列表、风险聚合执行时间分别为 `0.315ms/0.116ms/0.186ms/0.123ms`,均低于 50ms。当前 Audit 在线样本很小PostgreSQL 对部分路径选择顺序扫描Integration 已命中 provider 索引未因小样本增加缓存、分区或额外抽象。API P95/P99 沿用 9.4 完成证据及发布后 Access Log 阈值,本门禁不重复运行接口压测。
- OpenAPI`go run ./cmd/gendocs` 生成 `docs/admin-openapi.yaml`;运行时文档入口已生成 `logs/openapi.yaml`。两份制品均包含 15 条平台 Audit/Integration 及代理/企业资源活动路径。
- 归档留存:复用 10.5 的 `2001-02` 完整月证据56/56 归档成功、28/28 Integration final、最大 revision/attempt=2、压缩率 0.5047、dry-run 零清理断点,物理清理后目标三表为 0、6 条边界哨兵保留、56 条清理断点完整、1 条当月清理审计且对象/manifest 未删除。归档失败、缺日、pending、计数/SHA-256/对象 metadata 不一致均在 DELETE 前 fail-closed生产清理开关仍关闭。
- 回滚:现有归档手册已补充 contract 回滚 SOP 与全局监控阈值。业务回滚保留版本 205 结构、当前在线 Audit/Integration、全部 Domain Ledger/Outbox 和对象存储归档;只能回到旧 Writer 已停写的 contract 基线,不恢复旧 Writer不对已清理月份回填或伪造在线历史。
- 静态门禁:覆盖清单重新生成 692 项;全仓 `go build ./...`、已改 Go 文件 `gopls check``git diff --check` 通过。Go 只输出 module stat cache 无权限警告,构建退出码为 0。本 Change 未新增、修改或运行 `_test.go`;当前门禁按用户要求不再执行接口压测。
### 与审计接入分开保留的独立修复
- 企业、企业卡和企业设备的权限校验、空筛选防全量、批量边界、越权同错及企业设备 TOCTOU/真实命中计数作为独立安全与并发修复保留,不视为审计所需的业务重构。
- 手机号绑定/换绑的行锁和事务内二次复检、账号状态与代理越权校验、管理员账号改密后撤销 Token 作为独立修复保留;企业账号改密不扩展同样的 Token 行为。
- 角色/权限变化和店铺删除后的权限缓存清理能力作为独立修复保留,但只在数据库提交后 best-effort 执行,不让 Redis 失败反向回滚业务事实。
- 支付配置和员工线下充值的外层裸 goroutine、旧账号 Writer 及旧资产 Writer 的异步写入均已归零。
- 迁移责任04、07、0819 号票验证生产装配和直接旧表写入归零。
### 旧资产审计
- contract 结果:旧资产 Create/Writer、全部业务调用、裸/双重 goroutine 和 Worker 装配已删除;卡/设备停用、轮询开关及套餐人工调整已切换统一 Writer业务事实与成功审计同事务已识别资源后的失败/拒绝使用独立短事务。
- 历史只读白名单:`internal/service/asset_audit/``internal/store/postgres/asset_operation_log_store.go``internal/handler/admin/asset.go``internal/routes/asset.go``internal/model/dto/asset_operation_log_dto.go` 仅保留旧资产历史查询;主进程只注入只读 Store/ServiceWorker 不再装配旧资产 Store。
- `tb_asset_operation_log` 表及存量数据原样保留,不回填、不转换、不接入统一 Query生产业务不再新增记录。
- 迁移责任05、06、0919 号票验证生产装配和直接旧表写入归零。
### 旧手动轮询日志
- 状态与写入:`internal/service/polling/manual_trigger_service.go``internal/store/postgres/polling_manual_trigger_store.go``internal/model/polling.go`
- 装配与接口:`internal/bootstrap/services.go``internal/bootstrap/stores.go``internal/handler/admin/polling_manual_trigger.go`
- 当前继续承担运行状态、进度、结果和历史查询8.5 仅为人工触发/取消补充统一 Audit Event不得停写、删除或以 Audit Event 替代该 ledger最终旧写护栏必须将其列入显式白名单。
## 评审门禁
本轮已按当前代码完成以下三视角复核;后续源码入口、事务、敏感字段或可见性变化会使覆盖门禁失败并要求重新复核:
- 业务评审逐入口业务所有者、资源、Domain Ledger 与 N/A 理由准确,没有改变已评审业务范围。
- 研发评审:事务边界、失败策略、旧 Writer 清单、动作编码和测试接缝能由对应迁移票落地。
- 安全评审:风险等级、敏感字段策略、拒绝/失败覆盖和外部正文摘要策略完整。
后续评审发现错误时应修改对应显式条目和生成分类规则,并重新运行覆盖门禁;禁止仅手改统计数字。

File diff suppressed because it is too large Load Diff

View File

@@ -1,132 +0,0 @@
# PRDTECH 公共站内通知与受控跳转
Status: 后端代码与契约完成、验证延期
---
## Problem Statement
七月迭代的套餐临期、钱包低余额、企微审批结果和系统异常都需要向系统用户发送消息,但当前代码没有统一的站内通知存储、未读状态、消息中心或受控跳转能力。若每个需求自行建表和接口,会产生不同的防重、接收人、已读和权限规则;若直接把任意 URL 放进消息,又会形成越权跳转和开放重定向风险。
站内通知不承担企业微信审批待办,也不等同于旧轮询告警。业务资金、审批和套餐事务不能因为通知投递暂时失败而回滚,但通知又必须在 Outbox/Asynq 至少一次投递下保持不重不漏。
## Solution
建立一套轻量站内通知写模型业务事务只可靠发布稳定事件Notification Worker 根据受控通知类型、模板和接收人生成每人一条通知,并以事件和接收人唯一键防重。后台账号与个人客户使用各自认证上下文查询自己的未读数、分页列表和已读状态,任何接口都不接受前端传入接收人 ID。
通知只保存受控 `ref_type/ref_id/ref_key`,目标解析接口把它转换为白名单 `target_type` 和结构化目标参数,不保存、不返回任意 URL。跳转后的业务详情继续执行原资源权限校验。
## User Stories
1. 作为后台或代理账号,我希望在顶部看到可靠的未读数,并在通知中心查看自己的审批、临期、同步和系统消息。
2. 作为个人客户,我希望看到与自己订单、套餐和资产有关的简化消息,不看到平台运维消息。
3. 作为用户,我希望重复 Worker 投递不会生成重复消息,重复点击已读也不会报错。
4. 作为用户,我希望点击通知只能进入系统允许的业务页面,权限变化后不能借通知越权查看资源。
5. 作为业务开发者,我希望新增消息场景只注册常量、模板、接收人和目标类型,不复制一套通知表和 Handler。
6. 作为运维人员,我希望没有接收人、模板错误和投递失败均可追踪,不会无限重试或影响原业务提交。
7. 作为前端用户,我希望铃铛、抽屉和通知中心的未读状态一致,请求失败时不会把已有未读数闪回零。
## Implementation Decisions
### 架构与可靠投递
- 站内通知是简单写模型,采用 `Application + Query + Infrastructure`,不创建无业务价值的通知聚合根。
- 关键业务在原事务中写 Outbox提交后由 Relay 投递结构化 Asynq 载荷Notification Worker 负责解析接收人、渲染模板并幂等写通知。非关键系统告警可直接入队,但必须携带预先生成的稳定 `event_id`
- 本需求复用七月公共 `tb_outbox_event`、Relay 和处理租约,不在通知模块复制一套 Outbox。调用项目 `EnqueueTask` 时传 struct 或 map禁止传预序列化 `[]byte`
- 业务提交只依赖 Outbox 同事务成功不等待通知表写入Worker 失败按队列策略重试,不回滚已完成的资金、审批或套餐事务。
- 每个最终接收人独立一行,唯一键为 `event_id + recipient_kind + recipient_id`。同一事件重复消费不重复写,同一事件的不同接收人互不影响已读状态。
- 接收人解析失败分为:暂无可用接收人记 `no_recipient` 并成功结束;数据库/模板等瞬时失败返回任务错误;模板字段永久缺失达到最大重试后进入失败监控,禁止生成残缺正文。
### 数据与常量
- 新建 `tb_notification`,字段至少包括:`id``event_id``recipient_kind``recipient_id``category``type``severity``title``body``ref_type``ref_id``ref_key``is_read``read_at``expires_at``created_at`
- `recipient_kind` 为类型字段,使用 string`account``personal_customer`
- `category` 为 string`approval``expiry``sync``system``severity` 为 string`info``warning``error``critical`。具体 `type` 使用点分业务常量,例如 `package.expiring``wallet.low_balance``wecom.approval.approved``card_sync.failed`
- 所有常量及中文说明统一放在 `pkg/constants/`;通知类型到类别、默认级别、模板和允许目标的映射使用代码注册表,前端不得自行猜测。
- `title/body` 是发送时的纯文本快照,不保存任意 HTML。正文不得包含密码、操作密码、Token、Secret、完整证件、完整敏感回调、长期对象存储 URL 或企微 `media_id`
- 唯一索引覆盖 `event_id, recipient_kind, recipient_id`;未读和分类索引均以 `recipient_kind, recipient_id` 开头并包含 `created_at DESC`;过期时间建立部分索引。禁止数据库外键。
- 第一版未读数直接查询 PostgreSQL不维护 Redis 未读计数,避免通知表与缓存双写不一致。
### 接收人规则
- Notification Worker 只接受稳定用户 ID不按用户名、手机号等可变文本投递。具体用户由业务事件携带角色类接收人在消费时批量解析当前启用账号。
- 审批结果发送给申请人;通过后撤销且资金已执行发送给申请人和当前可用财务角色账号;企微模板/系统配置异常发送给当前可用平台超管或指定运维角色。
- 套餐临期和钱包低余额的店铺接收人为当前启用的店铺主账号及当前仍可用的店铺业务员;去重后逐账号写通知。上级代理可查看下级数据不代表自动成为通知接收人。
- 个人套餐/订单/资产通知使用 `recipient_kind=personal_customer` 和客户 ID。个人客户接口不得返回 `sync/system` 运维消息。
- 账号停用、软删除或业务员关系失效时跳过;已经生成的历史通知仍按原接收人可读,不因后续关系变化转移给其他人。
### API 契约
- 后台、平台、代理和企业账号统一使用当前认证的 `/api/admin`
- `GET /api/admin/notifications/unread-count`
- `GET /api/admin/notifications/unread-summary`
- `GET /api/admin/notifications`
- `PUT /api/admin/notifications/read-all`
- `PUT /api/admin/notifications/{id}/read`
- `GET /api/admin/notifications/{id}/target`
- 个人客户使用:
- `GET /api/c/v1/notifications/unread-count`
- `GET /api/c/v1/notifications`
- `PUT /api/c/v1/notifications/read-all`
- `PUT /api/c/v1/notifications/{id}/read`
- 静态 `/read-all` 路由必须先于 `/{id}` 动态路由注册。后台分类汇总第一版返回 `total` 与四个固定类别C 端第一版只提供总未读数。
- `unread-count` 返回 `count:int64``display_count:string`0 返回 `"0"`199 返回十进制文本,超过 99 返回 `"99+"`
- 列表过滤为 `category``type``severity``is_read``page``page_size`;固定按 `created_at DESC, id DESC`,默认每页 20最大 50。过期通知不进入列表和未读统计。
- 通知项返回 `id/category/type/severity/title/body/ref_type/ref_id/ref_key/is_read/read_at/created_at`,使用统一响应外层和 ISO 8601 时间。
- 单条已读执行带接收人的条件更新。通知不存在、属于别人或已读均幂等返回成功,不泄露通知是否存在;首次成功写同一 `read_at`,重复请求不覆盖。
- `read-all` 的可选 `category` 为空时只更新当前接收人的全部未过期未读通知,返回实际更新数量;非法类别返回参数错误。
- `/target` 先固定当前接收人查询通知,再通过后端白名单返回 `target_type``target_id/target_key``available`;不得返回 URL。目标资源不存在或当前无权访问时 `available=false`,不得泄露更多资源信息。
### 受控目标
- 第一版白名单至少覆盖本期实际场景:退款详情、代理充值详情、企微审批详情、卡详情、设备详情、临期资产列表、店铺资金概况、审计外部集成和系统配置。
- `card_sync` 不指向不存在的独立同步执行页;平台运维消息解析为统一审计中心外部集成目标,并携带受控资源或 Integration Log 标识。
- 前端维护 `target_type -> route builder` 白名单;未知类型和 `available=false` 只展示消息正文,不跳转。拥有通知不等于拥有目标资源权限。
### 前端交互
- 登录布局挂载后立即请求未读数,每 30 秒刷新;页面不可见时暂停,恢复可见时立即刷新。失败保留上次成功数值并提供静默重试,不闪回 0。
- 顶部铃铛固定宽度0 时不显示徽标199 显示数字,超过 99 显示 `99+`。点击打开最近 10 条抽屉,支持全部、审批、临期、同步/系统分类及进入 `/notifications`
- 通知中心支持类别、类型、严重级别、已读状态和服务端分页,并提供当前筛选类别的全部已读。普通消息使用中性色,错误/严重消息才使用警告视觉。
- 点击通知先进入已读视觉状态并调用已读接口,再解析受控目标;已读调用失败时以下次服务端刷新为准。目标解析或跳转失败不把通知恢复为未读。
- C 端使用简化消息列表,只展示与当前客户有关的审批结果、套餐、订单和资产消息。
### 审计、保留与发布
- 普通通知读取和已读只进入 Access Log通知模板/接收人解析失败、系统告警生成和管理性排查进入统一 Audit/Integration Log。不得使用用户通知列表作为管理员查看他人消息的入口。
- 套餐临期展示至到期并保留数据 180 天;审批结果不自动过期并保留 365 天;同步异常展示 30 天、保留 180 天;系统告警按事件指定展示期限、最长保留 365 天。
- 低峰清理任务按主键/时间分批删除超出数据保留期限的通知;用户不提供删除接口,清理不修改业务审计和领域流水。
- 未来短信或企微消息使用独立 Delivery 消费同一业务事件;不得在 Notification Handler/Worker 写完站内消息后同步循环调用外部渠道。
- 新增管理端和 C 端 Handler 后同步注册路由和两个 OpenAPI 文档生成器;公共通知能力先于 UR#33、UR#97 和企微结果通知启用。
## 公共能力发布依赖
- 实现阶段复用公共 Outbox 与异步任务,不自行复制表或状态机;最终 08 号发布票必须阻塞于公共基础 12 号票和全局审计 19 号票。
- 每个下游通知生产者仍须登记事件类型、载荷版本、消费者幂等键、接收人、失败明细和通知策略;“通知基础完成”不代表业务生产者已经接入。
## Testing Decisions
- Application/Worker 测试覆盖重复事件、多个接收人、接收人去重、停用/删除接收人、无接收人、模板字段缺失、Outbox/Asynq 重试和过期时间。
- PostgreSQL 集成测试验证唯一索引、未读/分类查询、固定排序、最大分页、过期排除、单条/批量条件更新和并发重复消费。
- HTTP 集成测试穿过真实后台/C 端认证、Handler、Query、GORM 和统一响应;验证前端无法传入或篡改 `recipient_id`,管理员也不能从用户接口查看别人通知。
- 越权测试对“别人通知 ID”“已删除通知”“无权目标资源”返回相同安全语义重复已读保持成功且 `read_at` 不变。
- 目标解析契约测试覆盖全部白名单、未知 `ref_type`、目标删除、权限变化和审计外部集成目标,证明响应中不存在任意 URL。
- 前端测试/人工验收覆盖 0、1、99、100 条徽标30 秒刷新、隐藏页暂停、失败保留旧数、抽屉最近 10 条、筛选、全部已读和点击顺序。
- 使用开发 PostgreSQL/Redis 与测试 Outbox Relay/Asynq Worker 验证至少一次投递;不得给真实用户生成测试通知。
- 生成 OpenAPI 并核对静态 `read-all` 路由未被动态 ID 路由吞掉。
## Out of Scope
- 不建设 WebSocket/SSE 推送,第一版使用 30 秒未读轮询。
- 不在本系统复制企业微信“待我审批”待办,不发送套餐临期企业微信消息。
- 不实现短信、企微消息或邮件 Delivery只保留独立扩展边界。
- 不允许管理员从通知接口查看、修改或删除其他用户消息。
- 不保存富文本 HTML、任意 URL、永久附件链接或外部回调原文。
- 不用 Redis 维护未读数,不让用户自行删除通知。
- 不在本需求实现各业务场景的触发规则UR#33、UR#97 和企微审批需求分别负责发布业务事件。
## Further Notes
- 当前仓库没有通知模型、接口和消息中心,只有轮询告警等运维模型,不能把后者改名充当业务通知。
- 前端仓库不在当前工作区;本 Spec 的路由、状态和错误交互是跨仓契约,实际组件目录以对应前端仓库为准。
- 公共通知和公共 Outbox 是多个单需求的依赖,但不把这些单需求合并成一份整轮迭代 PRD。

View File

@@ -1,20 +0,0 @@
# 01 — 向明确后台账号可靠投递首条站内通知
**What to build:** 业务事件携带稳定后台账号 ID 后,可以经公共 Outbox、Relay 和 Notification Worker 为该账号幂等生成一条纯文本站内通知;当前登录账号可以查询自己的未读数和分页列表,并将单条通知幂等标记为已读。重复投递不会重复写入,过期通知不进入用户视图,任何用户接口都不能指定或篡改接收人。
**Blocked by:** `.scratch/tech-public-foundation/issues/03-outbox-at-least-once-delivery.md` — 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
**Status:** 代码完成、验证延期
**架构通道:** 主通道为简单写 Application辅助通道为 Infrastructure 与 Query。
**完整业务边界:** 本票收口后台明确账号通知的存储、受控类型注册、Worker 幂等消费、未读数、基础列表和单条已读闭环。明确不实现角色或店铺动态接收人、个人客户通知、分类汇总、全部已读、目标解析、前端组件或具体业务场景触发规则,也不复制公共 Outbox 和 Relay。
- [x] 通知事实包含稳定事件 ID、接收人类型与 ID、类别、类型、级别、纯文本标题正文、受控资源引用、已读与过期时间并通过事件 ID、接收人类型和接收人 ID 唯一约束防止重复消费。
- [x] 通知常量、中文说明、类型到类别、默认级别、模板和允许目标的注册关系统一管理;未注册类型、模板字段永久缺失或正文包含禁止敏感内容时不生成残缺通知。
- [x] Worker 只接受结构化载荷,重复事件和并发消费最多为同一后台账号生成一条通知;瞬时数据库错误返回任务错误,原业务事务不因通知写入失败而回滚。
- [x] 当前后台账号可以获得准确的 `count:int64``display_count:string`,其中 0、199、100 以上分别显示 `0`、十进制文本和 `99+`,且未读数只查询 PostgreSQL。
- [x] 后台列表只读取当前认证账号的未过期通知,固定按创建时间和 ID 倒序,默认每页 20、最大 50并返回统一响应与 ISO 8601 时间。
- [x] 单条已读使用接收人条件和未读条件更新;别人通知、不存在通知和已读通知均幂等成功,首次写入的 `read_at` 在重复请求中保持不变。
- [ ] PostgreSQL、Worker 和真实后台认证 HTTP 集成测试覆盖唯一约束、重复消费、过期排除、分页排序、接收人篡改、越权隔离及重复已读。(按本 Change 测试环境豁免转任务 6.1
- [x] 新增后台 Handler 后完成路由、RouteSpec 和两个 OpenAPI 文档生成器注册,且静态路由顺序不会被动态通知 ID 路由吞掉。

View File

@@ -1,19 +0,0 @@
# 02 — 交付后台通知筛选、分类汇总与全部已读
**What to build:** 当前后台账号可以按通知类别、类型、严重级别和已读状态分页查看自己的消息,获得总未读数及审批、临期、同步、系统四个固定类别的汇总,并将当前类别或全部未过期通知一次性标记为已读。筛选、汇总和更新始终绑定认证账号,不因管理员身份扩大到其他用户。
**Blocked by:** 01 — 向明确后台账号可靠投递首条站内通知
**Status:** 代码完成、验证延期
**架构通道:** 主通道为 Query辅助通道为简单写 Application。
**完整业务边界:** 本票收口后台通知中心所需的筛选、固定分类汇总和批量已读用例。明确不实现个人客户接口、动态接收人、目标跳转、前端页面、Redis 未读计数或管理员查看他人通知能力。
- [x] 列表支持类别、类型、严重级别、已读状态、页码和每页数量组合过滤,所有条件使用 AND 语义并保持创建时间、ID 倒序。
- [x] 非法类别、严重级别、已读参数或越界分页返回统一参数错误,不向客户端拼接底层校验信息。
- [x] 未读汇总固定返回 `total``approval``expiry``sync``system`,过期通知不计入任何分类。
- [x] 全部已读在类别为空时更新当前账号全部未过期未读通知,在类别有效时只更新该类别,并返回实际更新数量。
- [x] 批量更新使用当前接收人、未读状态、未过期和可选类别条件;重复调用返回零更新且保持成功,不覆盖既有 `read_at`
- [x] `/read-all` 等静态路由先于 `/{id}` 动态路由注册,生成的 OpenAPI 与真实路由、请求参数和响应结构一致。
- [ ] PostgreSQL 与真实后台认证 HTTP 集成测试覆盖组合筛选、固定汇总、最大分页、过期排除、非法类别、并发批量已读及无法操作他人通知。(按本 Change 测试环境豁免转任务 6.1

View File

@@ -1,19 +0,0 @@
# 03 — 向个人客户投递并提供简化通知中心
**What to build:** 业务事件携带稳定个人客户 ID 后,可以为该客户幂等生成与其订单、套餐或资产有关的站内通知;当前登录个人客户可以查询自己的未读数和分页列表,并执行单条或全部已读。个人客户永远看不到同步、系统等平台运维消息,也不能通过请求参数读取或修改其他客户通知。
**Blocked by:** 01 — 向明确后台账号可靠投递首条站内通知
**Status:** 代码完成、验证延期
**架构通道:** 主通道为 Query辅助通道为简单写 Application 与 Infrastructure。
**完整业务边界:** 本票收口个人客户通知的投递、读取和已读闭环,复用既有通知表、注册表和 Worker。明确不实现 C 端分类汇总、后台动态接收人、平台运维消息展示、C 端受控目标接口或前端组件。
- [x] Worker 能以 `personal_customer` 接收人类型和稳定客户 ID 幂等生成通知,同一事件的后台账号与个人客户通知相互独立。
- [x] 个人客户通知类型注册明确允许的业务类别和资源引用;`sync``system` 及未对 C 端开放的类型不会出现在个人客户查询中。
- [x] C 端未读数遵循 0、199、100 以上的显示规则,列表固定倒序、默认每页 20、最大 50并排除过期通知。
- [x] 单条已读、全部已读只作用于当前认证客户;不存在、已删除、属于别人或已读的通知使用相同幂等安全语义。
- [x] 请求 DTO 不接受接收人 ID额外或恶意接收人参数不能改变查询与更新范围。
- [ ] 真实个人客户认证、Handler、Query、GORM 集成测试覆盖重复投递、运维类别隔离、跨客户越权、分页、过期排除、重复已读和批量已读。(按本 Change 测试环境豁免转任务 6.1
- [x] 新增 C 端 Handler 后同步个人客户路由、RouteSpec 与两个 OpenAPI 文档生成器,接口统一挂载在约定认证上下文中。

View File

@@ -1,24 +0,0 @@
# 04 — 接入账号、角色与店铺动态接收人解析
**What to build:** Notification Worker 可以按业务场景把审批申请人、当前平台角色账号或目标店铺解析为一组稳定、去重且当前可用的后台账号接收人。店铺场景只包含当前启用的店铺主账号和当前仍可用的店铺业务员;账号停用、软删除或关系失效时跳过,上级代理不会因为可查看下级数据而自动收到通知。
**Blocked by:**
- 01 — 向明确后台账号可靠投递首条站内通知
- `.scratch/ur96-shop-business-owner/issues/06-business-owner-recipient-resolution-and-release.md` — 06 — 提供业务员通知接收人解析并完成发布验证
**Status:** 代码完成、验证延期
**架构通道:** 主通道为 Infrastructure Adapter辅助通道为简单写 Application。
**完整业务边界:** 本票收口公共通知 Worker 对明确申请人、平台角色和店铺接收人的解析、可用性复核、去重及无接收人语义。明确不实现 UR#33、UR#97 或企微审批的业务触发规则,不改变账号、角色、店铺层级或业务员归属,不自动转派历史通知,也不发送短信或企微消息。
- [x] 明确申请人场景只使用业务事件携带的稳定系统账号 ID并在消费时跳过已停用或软删除账号不使用企微代提交身份替代真实业务提交人。
- [x] 角色场景批量解析当前启用、未删除且仍持有指定平台角色的账号,结果按稳定账号 ID 去重,不按用户名或手机号投递。
- [x] 店铺场景解析当前启用的店铺主账号,并复用 UR#96 接缝解析当前可用业务员;不沿父店铺、祖先店铺或代理数据权限向上扩散。
- [x] 同一账号同时以主账号、业务员或角色命中时只生成一条通知,同一事件的其他接收人仍分别拥有独立已读状态。
- [x] 暂无可用接收人记为 `no_recipient` 并成功结束,不进入无限重试;数据库等瞬时错误继续返回任务错误。
- [x] 已生成通知不会因账号关系后续变化而转移给新接收人,历史接收人仍可在自身认证上下文中读取原通知。
- [ ] Application、Worker 和 PostgreSQL 集成测试覆盖申请人、角色批量解析、店铺主账号与业务员去重、停用、软删除、关系失效、无接收人及重复投递。
自动化与真实 PostgreSQL/Redis/Asynq 验证按本 Change 测试环境豁免转任务 6.1、6.3;本票已完成 `gofmt``git diff --check``go build ./...` 代码门禁。

View File

@@ -1,24 +0,0 @@
# 05 — 交付通知受控目标解析与权限复核
**What to build:** 当前后台账号点击自己的通知时,后端只返回白名单目标类型和结构化目标标识,不保存也不返回任意 URL。退款、代理充值、企微审批、卡、设备、临期资产、店铺资金、审计外部集成和系统配置等目标在解析时重新执行当前资源权限检查目标不存在或权限已经变化时统一返回 `available=false`,不泄露资源详情。
**Blocked by:**
- 01 — 向明确后台账号可靠投递首条站内通知
- `.scratch/tech-global-audit/issues/13-request-correlation-integration-timeline.md` — 13 — 交付请求、业务链路和外部集成时间线
**Status:** 代码完成、验证延期
**架构通道:** 主通道为 Query辅助通道为 Application + Port/Adapter。
**完整业务边界:** 本票收口后台通知受控目标注册、解析、当前权限复核和安全不可用语义。明确不返回 URL、不实现前端路由构造、不把通知所有权当成目标资源权限、不创建独立卡同步执行页也不迁移各目标业务详情的既有授权规则。
- [x] 通知只保存受控资源类型、数值 ID 或稳定 Key目标响应只包含白名单 `target_type`、结构化 `target_id/target_key``available`,任何字段均不能承载任意 URL。
- [x] 第一版注册表至少覆盖退款详情、代理充值详情、企微审批详情、卡详情、设备详情、临期资产列表、店铺资金概况、审计外部集成和系统配置。
- [x] 目标解析先按当前接收人固定查询通知,再调用对应业务权限 Adapter 复核资源;拥有通知不授予目标资源访问权。
- [x] 别人通知、不存在通知和已删除通知不泄露通知事实;目标不存在、已删除或当前无权时统一返回 `available=false`,不返回资源差异信息。
- [x] `card_sync` 等同步消息解析为统一审计中心外部集成目标并携带受控资源或 Integration Log 标识,不指向不存在的同步执行页。
- [x] 未知通知引用或尚未支持的目标只允许展示正文,不产生开放重定向、自由路径或自动回退 URL。
- [ ] 契约与越权测试覆盖全部白名单、未知引用、通知越权、目标删除、权限变化、外部集成目标,并断言响应和持久化数据不存在任意 URL。
代理充值与企微审批目标类型已预注册,但其下游业务表和权限 Adapter 尚未由后续任务交付,因此当前安全返回 `available=false`。自动化与真实 PostgreSQL/HTTP 验证按本 Change 测试环境豁免转任务 6.1;本票已完成 OpenAPI 生成、`gofmt``git diff --check``go build ./...` 代码门禁。

View File

@@ -1,25 +0,0 @@
# 06 — 交付通知保留清理与失败可观测闭环
**What to build:** 系统可以按通知场景计算展示期限和数据保留期限,并在低峰按稳定主键和时间分批删除已超过保留期的通知。无接收人、模板错误、系统告警生成和投递失败具有可追踪、有限重试和安全摘要,既不会无限重试,也不会影响已经提交的资金、审批、套餐或其他业务事实。
**Blocked by:**
- 01 — 向明确后台账号可靠投递首条站内通知
- `.scratch/tech-global-audit/issues/01-audit-event-write-loop.md` — 01 — 交付不可变 Audit Event 写入闭环
- `.scratch/tech-global-audit/issues/02-integration-log-attempt-loop.md` — 02 — 交付可恢复的 Integration Log 尝试闭环
**Status:** 代码完成、验证延期
**架构通道:** 主通道为 Infrastructure辅助通道为简单写 Application。
**完整业务边界:** 本票收口通知展示期限、数据保留、分批清理、失败分类和统一审计/外部集成可观测接缝。明确不删除 Audit Event、Integration Log、领域流水或 Outbox不提供用户删除接口不建设管理员查看他人消息入口也不改变公共 Relay 的租约算法。
- [x] 套餐临期、审批结果、同步异常和系统告警按约定计算展示与保留期限,审批结果不自动过期,系统告警展示期限不超过允许上限。
- [x] 清理任务按时间和稳定主键小批量删除超过数据保留期限的通知,可中断重跑且只清理通知事实,不级联业务资源或审计记录。
- [x] 暂无接收人记录 `no_recipient` 后成功结束;数据库、队列等瞬时错误按有限策略重试;模板永久缺失达到最大重试后进入失败监控且不写残缺正文。
- [x] 系统告警直接入队时必须携带预先生成的稳定事件 ID重复执行仍由通知唯一键防重。
- [ ] 模板或接收人解析失败、系统告警生成和管理性排查写入统一 Audit/Integration 接缝;普通通知读取和已读只进入 Access Log。
- [x] 日志、监控和审计只记录事件 ID、通知类型、失败类别、计数及安全资源标识不记录敏感模板数据、完整回调、Token、Secret 或任意长期 URL。
- [ ] 测试覆盖各类期限边界、分批清理可重入、无接收人、瞬时失败、永久模板失败、最大重试、系统告警重复入队和业务事实不回滚。
统一 Audit Event/Integration 管理性写入按本 Change 冻结转任务 6.5,不阻塞测试环境代码交付;普通 HTTP 仍由 Access Log 覆盖。自动化与真实 PostgreSQL/Redis/Asynq 验证转任务 6.1、6.3。本票已完成 `gofmt``git diff --check``go build ./...` 代码门禁。

View File

@@ -1,25 +0,0 @@
# 07 — 冻结后台与 C 端通知前端契约及验收包
**What to build:** 前端仓库获得稳定、框架无关的后台铃铛、最近通知抽屉、通知中心和个人客户简化列表契约,以及可执行的验收数据。前端可以实现 30 秒未读轮询、页面隐藏暂停、失败保留旧值、筛选和全部已读,并按“先已读、再解析受控目标”的顺序处理点击,而无需猜测类别、级别或后端目标路径。
**Blocked by:**
- 02 — 交付后台通知筛选、分类汇总与全部已读
- 03 — 向个人客户投递并提供简化通知中心
- 05 — 交付通知受控目标解析与权限复核
**Status:** 契约完成、前端与人工验收延期
**架构通道:** 主通道为 Query/API 跨仓契约,辅助通道为前端验收契约。
**完整业务边界:** 本票收口当前后端仓库能够交付的 OpenAPI、交互状态、目标白名单说明和验收数据不在本仓库实现前端组件。明确不引入 WebSocket/SSE不承诺 Redis 未读计数,不为未知目标提供自由 URL也不代替前端仓库自身的组件测试。
- [x] 契约明确布局挂载后立即请求、每 30 秒刷新、页面不可见暂停、恢复立即刷新,以及失败保留上次成功未读数且不闪回零。
- [x] 徽标验收覆盖 0、1、99、1000 时隐藏、199 显示数字、100 显示 `99+`,并约定固定宽度避免布局抖动。
- [x] 后台抽屉按最近 10 条和约定分类展示,通知中心支持类别、类型、严重级别、已读状态、服务端分页及当前类别全部已读。
- [x] 点击顺序固定为先进入已读视觉状态并调用已读接口,再解析受控目标;已读失败以下次服务端刷新为准,目标失败不恢复未读。
- [x] 前端目标白名单只根据 `target_type` 和结构化标识构造内部路由,未知类型或 `available=false` 只展示正文且不跳转。
- [x] C 端契约只展示当前客户相关的审批结果、套餐、订单和资产消息,不暴露后台同步或系统运维分类。
- [x] OpenAPI、中文契约文档、示例响应与验收矩阵保持一致并明确前端代码位于外部仓库、需按对应仓库流程实施和联调。
前端源码、浏览器联调与真实人工验收不在当前仓库,保持延期到任务 6.6。

View File

@@ -1,31 +0,0 @@
# 08 — 完成公共通知发布门禁与下游接入契约
**What to build:** 发布负责人可以通过一套公共站内通知整体验收确认通知表、注册表、Worker、后台和 C 端接口、受控目标、清理任务及运行监控已经就绪。验收使用真实 PostgreSQL、Redis、公共 Outbox Relay 和 Asynq 接缝验证至少一次投递与重复消费,并向 UR#33、UR#97 和企微结果通知提供稳定的事件、接收人、模板和目标注册方式。
**Blocked by:**
- 02 — 交付后台通知筛选、分类汇总与全部已读
- 03 — 向个人客户投递并提供简化通知中心
- 04 — 接入账号、角色与店铺动态接收人解析
- 05 — 交付通知受控目标解析与权限复核
- 06 — 交付通知保留清理与失败可观测闭环
- 07 — 冻结后台与 C 端通知前端契约及验收包
- `.scratch/tech-public-foundation/issues/12-foundation-release-gate-and-integration-contract.md` — 12 — 建立公共基础发布门禁和下游接入契约
- `.scratch/tech-global-audit/issues/19-one-time-audit-cutover-gate.md` — 19 — 执行一次性审计切换与停机发布门禁
**Status:** 代码与发布契约完成、验证延期
**架构通道:** 主通道为 Infrastructure辅助通道为 Application、Query 与跨仓契约。
**完整业务边界:** 本票收口公共通知的迁移验证、端到端可靠性、OpenAPI、文档、发布回滚和下游接入说明。明确不实现 UR#33 套餐临期、UR#97 钱包低余额或企微审批结果的业务触发规则,不发送真实用户测试通知,不引入外部 Delivery 渠道,也不借发布验收迁移未触碰旧模块。
- [ ] 空数据库和兼容环境可执行通知正向迁移、索引校验与允许的结构回滚;已有通知事实后不得通过降级删表清理,应用回滚允许保留数据。
- [ ] PostgreSQL、Redis、公共 Relay 和 Asynq 端到端测试覆盖事务事件、至少一次投递、入队成功后重复、并发 Worker、多接收人、接收人去重和最终通知唯一性。
- [ ] 真实后台与个人客户认证测试覆盖所有公开接口、统一响应、静态路由顺序、接收人不可篡改、跨用户隔离、过期排除和受控目标权限变化。
- [x] 运行门禁覆盖 Worker 失败、永久模板错误、无接收人、Outbox 积压、清理滞后和审计/外部集成记录异常,并给出停止放量和恢复步骤。
- [x] 下游接入契约明确稳定事件 ID、结构化载荷、受控通知类型、接收人解析、模板字段、过期策略和目标引用调用统一队列客户端时禁止传预序列化字节。
- [x] 发布顺序明确为迁移与校验、Worker 与监控、后端 API、前端、下游生产者下游不得在消费者和监控就绪前制造不可见积压。
- [x] 新增管理端和 C 端 Handler 已同步路由、RouteSpec、两个 OpenAPI 文档生成器,并生成 OpenAPI 核对 `/read-all` 未被动态 ID 路由吞掉。
- [x] 中文功能总结覆盖关键流程、前后端契约、异常闭环、监控、发布回滚和待决策项README 增加入口;测试数据使用隔离标识且不向真实用户生成通知。
迁移演练、真实 PostgreSQL/Redis/Relay/Asynq、真实认证 HTTP 与并发验证按本 Change 豁免保持未勾选,并转任务 6.1、6.3;前端人工验收转 6.6。当前已完成 OpenAPI 生成、`gofmt``git diff --check``go build ./...` 与 OpenSpec 校验。

View File

@@ -1,291 +0,0 @@
# PRDTECH 七月迭代公共开发基础
Status: completed
## Problem Statement
七月迭代同时包含企微审批、支付与充值、钱包入账、站内通知、卡状态事件、批量订购、设备批量分配、导出和低余额预警等需求。这些需求都需要可靠事件投递、重复请求防护、异步任务状态、动态配置、增量迁移和日志脱敏,但它们不应各自实现一套互不兼容的基础设施。
当前仓库已经具备可复用的基础GORM 显式事务、PostgreSQL 唯一约束和条件更新、钱包 `version` 乐观锁、Fiber `request_id`、Redis、Asynq 客户端与 Handler、部分任务的五态常量及进度字段以及请求 JSON 的递归脱敏。与此同时,公共 `tb_outbox_event`、受控 `tb_system_config` 尚未落地已有异步任务状态并不完全一致Redis 防重、状态条件更新、乐观锁和 Worker 抢占的职责没有形成统一契约Access Log 的响应体仍可能原样记录敏感字段,非 JSON 和敏感接口也缺少明确策略。
如果没有先冻结公共责任边界,将产生以下风险:
- 同一业务事务可能只写业务事实却丢失审计或异步事件,或者在事务内直接调用外部系统,造成不可恢复的不一致。
- 各业务建立不同 Outbox 表、Relay、重试状态和 Asynq 载荷,重复投递时缺乏稳定 `event_id`,消费者无法可靠幂等。
- 将 Redis 锁、`request_id`、状态条件更新、钱包版本号和 Worker 租约误当成同一种幂等机制,甚至把 Redis 当作最终业务事实。
- 批量订购、设备分配和导出各自创造不同的“部分成功”状态、错误结构和轮询语义,前端无法形成统一交互。
- 动态配置演变成任意 Key-Value 数据库,未经注册的 Key 可被写入,类型、值域、权限、缓存失效和审计无法保证。
- Access Log 在登录、Token、支付、企微回调和文件接口中泄露凭证、签名、支付链接、回调原文或文件内容。
- 公共基础设施需求侵入 Audit Event、Integration Log、站内通知或各业务领域最终重新合成一个无法独立交付的巨型需求。
本 PRD 的目标不是重新探索业务需求,也不是统一重构全仓库,而是把已评审通过的跨需求约定归拢成一个可先行交付、可被下游复用、所有权清晰的公共基础设施边界。
## Solution
交付一个边界明确的 `tech-public-foundation`,覆盖以下八类已确认能力:
1. 公共表、索引和约束的增量迁移护栏,以及上线前检查、失败退出和数据安全回滚边界。
2. 基于现有 GORM 用法的显式事务契约,使业务事实与契约要求的 Audit Event 或 Outbox 在同一事务提交。
3. 权威公共 `tb_outbox_event`、Relay、领取租约、重试、监控、恢复及结构化 Asynq 投递契约。
4. `request_id + 请求指纹`、PostgreSQL 唯一约束、状态条件更新、钱包 `version`、Worker 租约和 Redis 防并发的公共幂等原语与选择规则。
5. 面向业务任务和前端的统一五态、结果计数、错误摘要、失败明细、恢复与轮询语义,但不建设万能任务表。
6. 受控系统配置壳层,包括 Key 注册、校验、缓存、权限、API 和统一审计接缝。
7. Access Log 请求/响应递归脱敏和敏感接口安全摘要。
8. 加载、空态、权限不足、失败重试和异步任务恢复的前端公共交互契约;当前仓库不实现前端代码。
责任边界固定如下:
| 责任方 | 拥有内容 | 复用但不拥有 |
|---|---|---|
| `tech-public-foundation` | 公共 Outbox 与 Relay、公共幂等原语、系统配置壳层、统一异步状态、迁移护栏、Access Log 脱敏 | Audit Event、Integration Log、通知和业务消费者 |
| `tech-global-audit` | Audit Event、Integration Log、多视角 Query、审计前端、旧审计 Writer 一次性切换 | 公共事务接缝、迁移护栏和共享脱敏策略 |
| `tech-inapp-notifications` | 通知表、模板、接收人解析、Notification Worker、通知 API 和前端通知中心 | 公共 Outbox、Relay、Worker 租约和统一审计 |
| 各业务 PRD | 业务事件定义、业务状态机、业务唯一键、业务任务表、失败明细和消费者行为 | 公共 Outbox、幂等选择规则、五态任务契约和系统配置壳层 |
以上三项公共需求保持独立,不把公共基础、全局审计和站内通知重新合并为一个巨型基础设施需求。
## User Stories
1. 作为发布负责人,我希望每张公共表、每个公共索引和约束都有唯一迁移所有者,以便避免多个下游 PRD 重复创建或互相回滚。
2. 作为发布负责人,我希望迁移前检查能发现重复业务键、非法状态、空值、类型不兼容和未完成任务,并在不满足前置条件时明确失败退出,而不是带病上线。
3. 作为运维人员,我希望数据迁移可重入、可观测,并能区分“可安全回滚结构”与“只能停止生产者后向前修复的数据事实”。
4. 作为业务开发者,我希望继续使用现有 GORM 显式事务在一个清晰边界内提交业务事实、Audit Event 或 Outbox而不必引入 UnitOfWork、工厂层或迁移未触碰模块。
5. 作为业务开发者,我希望通过一个权威 Outbox 模型发布事件,稳定携带 `event_id``event_type`、聚合/资源定位、`request_id``correlation_id` 和结构化载荷。
6. 作为业务开发者,我希望 Outbox 写入失败会令业务事务整体回滚,事务成功后即使 Asynq 暂时不可用,事件仍可恢复投递。
7. 作为 Relay 运维人员,我希望多个 Worker 能并发领取事件但不会长期重复处理同一行Worker 崩溃后过期租约可以自动恢复。
8. 作为 Relay 运维人员,我希望看到待投递量、最老积压时长、投递速率、重试次数、租约过期数和最终失败数,并能按受控流程恢复失败事件。
9. 作为事件消费者,我希望重复投递始终携带同一个 `event_id` 和业务关联标识,以便用业务状态、唯一约束或消费记录实现自己的幂等。
10. 作为 API 调用方,我希望同一作用域内相同 `request_id` 和相同请求指纹返回原结果,而同一 `request_id` 携带不同业务内容时获得明确冲突。
11. 作为资金业务开发者,我希望钱包 `version`、唯一流水和事务边界继续作为资金正确性来源,公共幂等能力不会用 Redis 锁替代资金约束。
12. 作为状态机业务开发者,我希望状态条件更新负责状态流转幂等,更新未命中时按当前状态判断“已处理、冲突或资源不可见”。
13. 作为批量任务开发者,我希望继续拥有本业务的任务表和逐项失败明细,同时复用固定的五态、结果计数、租约恢复和轮询语义。
14. 作为前端用户,我希望任务出现部分成功时仍显示“已完成”,并通过总数、成功数、失败数和失败明细了解结果,而不是看到一个新的“部分成功”状态。
15. 作为前端用户,我希望刷新页面后可以通过 `task_id` 恢复任务进度,页面隐藏时停止轮询,重新可见时立即刷新。
16. 作为超级管理员,我希望查询按模块组织的受控系统配置,并通过与类型匹配的控件更新已注册且允许修改的 Key。
17. 作为安全负责人,我希望未注册配置默认不可写,错误类型、越界值和只读配置更新均被拒绝并留下统一审计。
18. 作为业务模块开发者,我希望只注册本模块的配置 Key、类型和值域公共基础负责存储、缓存、权限和审计但不接管具体业务校验。
19. 作为运维人员我希望系统配置缓存失效失败会告警Redis 故障时读取可回退 PostgreSQL而不会把缓存当作唯一事实。
20. 作为安全负责人,我希望 Access Log 对请求和响应进行同一套递归脱敏,且敏感接口无法因解析失败而回退记录原文。
21. 作为排障人员,我希望脱敏后仍保留 `request_id`、方法、路径、安全查询参数、状态码、耗时、用户与终端信息及截断标记,以便关联问题。
22. 作为前端用户,我希望公共页面都有一致的加载、空态、权限不足、失败和重试反馈,不把权限不足伪装成空数据。
23. 作为测试人员,我希望通过真实 Fiber、GORM、PostgreSQL、Redis、Relay 和 Asynq Handler 验证公共外部行为,而不是依赖私有函数或目录结构。
24. 作为下游需求负责人,我希望清楚知道公共基础提供什么、不提供什么,以及哪些契约必须先冻结,避免为赶进度复制临时基础设施。
## Implementation Decisions
### 1. 架构与责任边界
- 采用触碰式渐进迁移。公共能力放在可复用的 Application、Persistence、Queue 和 Middleware 接缝中,但不主动迁移未被七月需求触碰的旧 Service。
- 复杂写操作仍由各业务 UseCase 和 Domain 收口业务不变量;简单写操作可使用 Application 事务脚本;读取继续由 Query 负责。公共基础不创建新的业务聚合。
- 不引入 Java 风格 UnitOfWork、事务工厂、Repository 工厂或全仓事务抽象。现有 GORM `Transaction` 用法和显式传递事务句柄是权威基础。
- 公共基础提供契约、模型和运行机制;事件含义、业务状态、消费者副作用、业务唯一键和失败明细始终由业务 PRD 所有。
- 数据库关联使用 ID 显式维护,不建立外键约束,不通过 GORM 关联标签扩大耦合。
### 2. 增量迁移与发布基础
- `tech-public-foundation``tb_outbox_event``tb_system_config` 及其公共索引、唯一约束和必要初始化数据的唯一迁移所有者。
- `tech-global-audit``tech-inapp-notifications` 和各业务 PRD 分别拥有自己的表、字段、业务索引、业务唯一约束和数据迁移。公共基础不得接管所有业务迁移。
- 发布清单必须维护数据库对象所有权。同一表、字段、索引或约束只能在一个迁移中创建或修改;下游只能声明依赖,不得复制公共 DDL。
- 每个增量迁移包含正向和回滚边界。结构创建、兼容字段和未产生业务数据的初始化可按验证结果回滚已经产生的业务事实、Audit Event、Outbox、通知和任务结果不得通过降级删除。
- 上线前检查至少覆盖:目标对象是否存在且定义一致、唯一键冲突、必填字段空值、枚举非法值、待处理/处理中任务、未投递 Outbox、长租约和依赖版本。任何破坏正确性的异常都必须非零退出并阻断发布。
- 数据回填按稳定主键分批、可重复执行并记录进度;重复执行不能生成重复事实。`IF NOT EXISTS` 只能用于安全重入,不能掩盖已有对象定义不一致。
- 正向迁移完成后执行后置校验,包括约束生效、行数守恒、异常计数归零、关键索引可用和读写冒烟。检查输出只包含计数与安全标识,不泄露敏感数据。
- 推荐发布顺序为:迁移及前后置检查、兼容 API、Outbox Relay/Worker、依赖消费者、前端。生产者不得早于消费者和监控就绪而开始制造不可见积压。
- 回滚优先顺序为:停止新写入和 Relay 领取、保留已有事实与 Outbox、回滚无数据风险的应用版本、修复后向前恢复。公共表已有生产数据后不允许通过删除表完成回滚。
- 发布必须设置停止条件迁移异常、Outbox 持续积压或租约大量过期、系统配置读写不一致、Access Log 脱敏回归失败、关键任务无法恢复时停止继续放量。
### 3. GORM 事务边界
- 事务由 Application UseCase 或简单写事务脚本开启Handler 不拼接事务逻辑Domain 不依赖 GORM。
- 一次事务只包含需要原子提交的 PostgreSQL 写入。业务事实与契约要求的 Audit Event、Outbox 必须使用同一事务句柄;任一写入失败,整体回滚。
- 是否写 Audit Event、Outbox 或两者,由业务契约决定:需要同步查询的不可变审计事实写 Audit Event需要跨进程消费的可靠事件写 Outbox同一用例同时需要时两者同事务写入。Audit Event 的模型和 Writer 仍归 `tech-global-audit`
- Redis、Asynq、HTTP、企微、支付、Gateway、运营商和对象存储调用不得放进数据库事务。事务提交后由 Relay 或后置动作执行;缓存失效在提交成功后发生。
- 事务内生成并持久化稳定 `event_id`、业务唯一键和必要快照,禁止由 Relay 或消费者在重试时重新生成身份标识。
- 事务应短小,避免在事务内解析大文件、渲染报表或执行慢查询。并发正确性由唯一约束、条件更新、行锁或版本号承担,而不是扩大事务范围。
### 4. 公共 Outbox
#### 权威模型
- 全仓只使用公共 `tb_outbox_event` 表承载需要跨进程可靠投递的领域/应用事件。业务模块不得再创建自己的 Outbox 表或 Relay。
- 权威字段语义至少包括:内部主键;全局稳定且唯一的 `event_id`;稳定的 `event_type`;载荷版本;来源聚合类型与标识;主要资源类型、标识和可选业务键;`request_id``correlation_id`;结构化 JSON payload投递状态重试次数下次可领取时间租约所有者和过期时间最后错误码与脱敏摘要创建、更新和成功投递时间。
- Outbox 状态是 Relay 内部生命周期,使用整数:`1=待投递、2=投递中、3=已投递、4=投递失败`。它不等同于面向用户的五态业务任务,也不产生“部分成功”状态。
- `event_id` 建立最终唯一约束;领取路径按状态、下次可领取时间和租约到期时间建立索引;聚合/资源和关联标识建立满足排障与恢复的查询索引。所有索引归公共基础所有。
- `event_type` 和 payload schema 归发布事件的业务 PRD 所有公共基础只要求事件类型稳定、payload 带版本且能够向后兼容。禁止把任意业务对象完整序列化后无约束写入。
- `request_id` 表示本次入口请求或命令标识;`correlation_id` 表示跨事务、跨队列和外部交互的业务链路。没有 HTTP 请求的定时任务使用稳定命令标识,并以其或 `event_id` 建立关联链路。Relay 和消费者必须原样传播这些标识。
#### 写入、Relay 与至少一次投递
- 业务 UseCase 在保存业务事实的同一 GORM 事务内插入 Outbox。不能先提交业务再补写 Outbox也不能在事务内直接入 Asynq。
- Relay 按小批量领取到期的待投递或可重试事件,通过数据库条件更新/跳锁机制取得有期限的处理权。领取、续租、完成和失败都必须校验当前状态与租约所有者。
- Relay 将公共事件信封作为 struct 或 map 调用项目 `EnqueueTask`;禁止传入预序列化 `[]byte`,避免二次序列化成为 Base64 字符串。直接使用 Asynq 原生任务构造器的既有代码不属于该调用方式,必须自行且只序列化一次。
- Asynq 入队成功后将 Outbox 标记为已投递。若进程在“入队成功、数据库标记前”崩溃,同一事件会再次投递,这是被接受的至少一次语义。
- 入队失败记录安全错误码与摘要,按有上限的指数退避设置下次领取时间;达到最大重试或判定永久错误后保持失败并告警,不删除记录。
- 处理中 Worker 超过租约未完成时,可由恢复扫描重新置为可领取;恢复保留原 `event_id`、payload 和关联标识,不生成新事件。
- 受控重放只能作用于明确选择的失败/滞留事件,记录操作者、原因和批次,不修改已投递事件内容。重放仍使用原 `event_id`,因此消费者必须幂等。
- 监控至少提供待投递量、最老待投递年龄、处理中与过期租约数、投递成功率、重试分布、最终失败数和按 `event_type` 的积压。阈值越界进入现有告警通道。
- 公共 Outbox 只保证“事件最终至少被送达队列”不保证业务副作用只发生一次。消费者仍需用业务状态、PostgreSQL 唯一约束、消费记录或稳定业务键实现自己的幂等。
### 5. 公共幂等原语
- 公共基础提供选择规则和可复用构件,不建设要求所有请求进入同一张表的万能幂等平台。
- 创建类命令采用调用方稳定提供的 `request_id`作用域至少包含调用主体与操作类型。请求指纹由会影响业务结果的规范化字段计算排除时间戳、签名、Token 等易变传输字段,并带算法版本。
- 同一作用域内,`request_id + 相同指纹` 返回已存在结果;`request_id + 不同指纹` 返回幂等冲突,不覆盖原事实;并发首次写入由 PostgreSQL 唯一约束裁决。
- PostgreSQL 唯一约束、业务状态和账务流水是最终正确性来源。应用层预查只用于友好返回,不能替代数据库约束。
- 状态流转使用 `WHERE 当前状态=预期状态` 的条件更新;影响行数为零时重新读取当前状态,区分已完成、非法转换和统一资源不可见语义。
- 钱包余额、冻结金额和其他并发数值写使用 `version` 乐观锁或业务明确要求的行锁并与唯一流水、业务事实、Audit Event/Outbox 同事务。公共基础不拥有钱包规则。
- Worker 租约只解决“谁在这一时刻处理”,不证明业务副作用未发生。领取条件、租约所有者、过期时间和完成条件必须落在 PostgreSQL消费者仍执行自己的业务幂等检查。
- Redis `SETNX`、分布式锁和短期防重键只用于减少重复并发和热点压力。Redis 缺失、过期、故障或主从切换不能造成重复业务事实;锁必须设置过期并确保释放。
- 稳定 `event_id` 解决事件身份,稳定业务唯一键解决副作用身份,`request_id` 解决入口命令身份,三者不得混为一个万能键。
### 6. 统一异步任务契约
- 面向业务和前端的任务状态固定为:`1=待处理、2=处理中、3=已完成、4=已失败、5=已取消`。响应同时提供对应中文状态名。
- “已完成”表示任务已到达处理终点,不表示每个业务项都成功。部分成功由 `total_count``success_count``failed_count` 表达,禁止增加“部分成功”状态。
- 业务数据逐项完成但存在业务校验失败时,任务状态为已完成;只有文件无法解析、任务无法建立、关键基础设施持续失败或整体执行无法到达业务终点时,任务状态才为已失败。
- 每类任务继续拥有自己的业务任务表、业务项表和失败明细。批量订购、设备批量分配和导出不得为了统一状态而迁移到一张万能任务表。
- 公共查询语义至少统一 `task_id`、状态与状态名、总数/成功数/失败数、进度、脱敏失败摘要、开始时间、完成时间和更新时间。失败明细由业务定义字段并分页或受限返回。
- 失败摘要使用稳定错误码与用户可见中文说明,不暴露 SQL、堆栈、外部密钥、回调原文或内部网络信息。可下载失败文件时只返回受控短期访问能力不写永久公开地址。
- 待处理任务通过条件更新领取为处理中处理中任务必须具有租约或等价的可恢复执行记录。Worker 崩溃、进程重启或队列重复投递后,过期任务可以重新领取并从业务事实恢复。
- 终态只能通过满足预期状态的条件更新进入。重复 Handler 看到已完成、已失败或已取消时不得重新制造业务副作用。
- 取消只适用于业务明确支持取消的任务;不支持取消的业务仍返回五态中的实际状态,不能把失败伪装成取消。
- Asynq 载荷使用最小结构化标识,通常只包含 `task_id`、必要分片标识和关联标识;调用 `EnqueueTask` 时必须传 struct 或 map禁止传 `[]byte`
### 7. 公共系统配置
- 新建公共 `tb_system_config`,至少保存唯一 `config_key`、字符串化 `config_value``value_type`、所属模块、中文说明、只读标记、创建/更新人与时间。`value_type` 限定为 `string``int``bool``json`
- Key 使用稳定的 `module.group.name` 命名。代码中的受控 Key 注册表是可写配置的权威来源,定义 Key、模块、类型、值域/枚举、默认值、是否只读、是否敏感和前端控件提示。
- 未注册 Key 默认不可写;数据库中已存在但未注册的 Key 最多按只读、可诊断方式展示。禁止通过 API 创建任意 Key禁止提供原始 JSON 自由编辑器把它扩展成通用 Key-Value 配置中心。
- 注册表重复 Key、类型冲突或不合法默认值必须在启动或验证阶段失败不能以后注册者静默覆盖前者。
- 查询 API 为已认证超级管理员提供按模块过滤的配置列表,返回脱敏后的值、类型、值域、说明、只读状态和更新时间。更新 API 按 Key 修改单项配置,不提供无约束批量覆盖。
- 更新流程依次执行认证与超级管理员授权、Key 注册检查、类型解析、值域/业务边界校验、GORM 事务更新和统一 Audit Event。审计模型与 Writer 复用 `tech-global-audit`,公共基础不另建配置审计表。
- Redis Key 固定按配置 Key 生成默认缓存五分钟。PostgreSQL 是唯一事实来源;读取缓存未命中或 Redis 不可用时查询数据库并尝试回填。
- 配置更新提交成功后立即失效对应 Redis 缓存。失效失败不回滚已提交事实,但必须告警;短 TTL 限制旧值持续时间,后续读取可按版本/更新时间避免回填旧值。
- 数据库值无法解析、越界或读取失败时不得静默使用错误值。按注册策略使用最后一个已验证值或代码安全默认值,并产生可定位告警。
- UR#48 负责注册具体支付方式 Key、支付业务值域和启停校验本 PRD 只负责存储、注册、权限、缓存、API 和审计壳层。
### 8. Access Log 脱敏
- Access Log 对 query、请求体和响应体执行同一套递归脱敏覆盖嵌套对象与数组。字段匹配大小写不敏感并支持公共敏感字段注册表与路由级策略。
- 通用敏感字段至少覆盖密码/口令、Token、Authorization、Cookie、密钥/Secret、签名、Nonce、验证码、支付凭证和私密 URL。脱敏值不可逆不允许只遮盖中间几位后保留可复用凭证。
- 登录和 Token 接口:请求中的密码、验证码全部替换;响应中的访问令牌、刷新令牌、会话标识全部替换,只保留成功状态和必要主体标识。
- 支付接口:不记录支付凭证、银行卡敏感信息、二维码原文、支付跳转链接、渠道密钥和完整签名;保留安全订单号、渠道类型、结果码和金额等排障摘要。
- 企微回调不记录原始加密包、解密正文、签名、Nonce、通讯录敏感字段或完整回调响应保留事件类型、安全资源标识、载荷大小、摘要哈希和处理结果。
- 文件上传、下载和导出接口:不记录 multipart/binary、Base64 内容、文件字节、临时凭证和签名下载地址;只记录脱敏文件名、类型、大小、数量、任务标识和结果。
- 敏感路由的 JSON/XML/表单解析失败时采用“字段存在性、长度、内容类型、安全哈希和截断标记”的摘要策略,禁止回退记录原始 body。普通非敏感文本接口也必须经过路由策略后才可记录。
- 保留方法、路径、脱敏 query、状态码、耗时、`request_id`、IP、User-Agent、用户标识以及请求/响应摘要。请求体和响应体分别遵守现有 50KB 上限,先脱敏再截断,并明确记录截断状态。
- 脱敏器是公共可复用能力Access Log 中间件归本 PRD。Audit Event 和 Integration Log 的模型、Writer、查询、保留策略及其字段级脱敏仍归 `tech-global-audit`,本 PRD 不重复实现。
### 9. 前端公共交互契约
- 当前仓库没有前端源码,本 PRD 只冻结跨仓 API 与交互验收契约,不虚构前端目录、组件名或状态管理实现。
- 加载态:首次加载展示明确占位并阻止重复提交;已有数据刷新时保留可辨识的旧内容和刷新提示,不闪回空白或零值。
- 空态:只有成功请求且确实无数据时展示空态;筛选无结果与系统暂无数据使用不同文案,并提供清除筛选或返回入口。
- 权限不足:按统一 403 语义展示无权限,不展示重试按钮,不用 404/空列表泄露资源是否存在。
- 失败与重试:瞬时网络或服务错误保留用户输入和已有结果,展示安全错误摘要与显式重试;参数错误定位可修正字段,不自动无限重试。
- 创建异步任务成功后,前端保存 `task_id` 到刷新后可恢复的页面状态,不能只存在内存。重新进入页面时通过 `task_id` 或业务任务列表恢复最新状态。
- 轮询状态为待处理或处理中时继续;建议间隔按 2 秒、3 秒、5 秒逐步退避,最长 10 秒。到达已完成、已失败或已取消后停止。
- 页面进入隐藏状态时暂停轮询;恢复可见时立即刷新一次,再按当前状态恢复退避。网络恢复或页面刷新不能创建重复任务。
- 已完成且 `failed_count > 0` 时展示部分成功摘要和失败明细入口;已失败展示失败摘要与业务允许的重试/重建入口;已取消展示取消原因且不自动重建。
### 10. 可观测性与故障恢复
- 公共日志和指标使用 `request_id``correlation_id``event_id``task_id` 和安全资源标识串联,但不得把完整 payload 或敏感配置值作为标签或日志字段。
- Outbox、系统配置缓存和 Access Log 脱敏失败均需提供中文、可操作告警;告警内容包含组件、错误码、时间窗口和安全标识。
- Worker 和 Relay 重启后以 PostgreSQL 状态和租约恢复,不依赖进程内内存或 Redis 锁推断任务是否完成。
- 恢复工具只暴露受控的查询、重试和租约释放能力,所有人工操作写统一 Audit Event禁止直接删除 Outbox、篡改业务终态或跳过消费者幂等检查。
## Testing Decisions
### 1. 测试层级与真实接缝
- 系统配置通过真实 Fiber 路由、认证、Application、GORM 和 Redis 验证,覆盖查询、更新、超级管理员权限、未注册 Key、只读 Key、类型/值域错误、事务回滚、缓存命中与更新后失效。
- Outbox 通过“业务事务写入 → Relay → Asynq Handler → 可观察消费结果”的完整链路验证。断言基于数据库事实、队列可观察结果和消费者公开结果,不直接调用 Relay 私有函数完成测试。
- PostgreSQL 集成测试验证事务回滚、`event_id`/业务键唯一约束、状态条件领取、并发领取、租约恢复、重试调度和重复投递。
- Access Log 使用真实 Fiber 测试请求和响应,捕获最终 JSON 日志检查递归脱敏、敏感接口特殊策略、解析失败安全降级、50KB 截断及 `request_id`/状态/耗时保留。
- Redis 和 PostgreSQL 使用隔离测试数据、唯一前缀或独立测试空间,测试只清理自己创建的数据,不执行全库/全缓存清空。
- 不依赖真实支付、企微、Gateway 或运营商网络;外部边界使用可观察的测试 Adapter。公共链路仍使用真实 PostgreSQL、Redis 和 Asynq 组件。
- 只测试公共外部行为和稳定契约,不绑定私有函数、未导出类型或内部文件组织。
### 2. 迁移与事务测试
- 在空数据库和带兼容存量数据的数据库分别执行正向迁移、后置校验和允许的回滚,验证公共对象只创建一次、定义一致且迁移版本可继续前进。
- 构造重复 `event_id`、非法配置、唯一键冲突、处理中任务和未投递事件,验证前置检查明确失败退出且不执行破坏性写入。
- 数据回填中断后重复运行,验证已完成批次不重复、未完成批次继续、行数守恒且错误报告不含敏感值。
- 在业务事实、Audit Event、Outbox 任一步注入失败,验证同一 GORM 事务完全回滚;提交成功后再模拟 Redis、Asynq 或外部 Adapter 失败,验证业务事实不回滚且可恢复。
### 3. Outbox 与幂等测试
- 验证业务提交成功时 Outbox 与业务事实同时可见业务回滚时二者均不可见Relay 不读取未提交事务中的事件。
- 并发启动多个 Relay 领取同一批事件,验证同一时刻只有租约所有者可完成该行;终止 Worker 后等待租约过期,验证其他 Worker 使用原 `event_id` 恢复。
- 模拟入队成功但 Outbox 未标记成功,验证再次投递同一 `event_id`,消费者通过业务唯一约束或状态检查只产生一次业务副作用。
- 覆盖瞬时失败退避、最大重试、永久失败告警、受控重放、积压年龄和不同 `event_type` 的监控聚合。
- 直接向 `EnqueueTask``[]byte` 必须失败;传 struct/map 能由公开 Handler 正确解析。重复 Marshal 造成 Base64 的回归必须被测试阻止。
-`request_id + 指纹` 覆盖相同请求重放、同 ID 不同内容冲突、不同主体同 ID、并发首次提交和 Redis 不可用,验证 PostgreSQL 约束始终裁决最终结果。
- 分别验证状态条件更新、钱包版本冲突和 Worker 租约,证明三者只承担各自并发职责,不能相互替代。
### 4. 异步任务契约测试
- 对所有新接入的批量订购、设备分配和导出公开 DTO 做契约测试,状态只能为 1 至 5 且状态名一致。
- 覆盖全成功、部分成功、全部业务项失败、整体执行失败和取消:部分/全部业务项处理完均为已完成并由计数表达;整体无法执行才为已失败。
- 验证 `total_count = success_count + failed_count` 的业务终态守恒;若业务存在明确跳过项,必须在业务 PRD 中定义其归类,不能由公共层凭空新增状态。
- 验证失败摘要不泄露内部错误,失败明细分页/受限,终态重复消费不改变计数,过期处理中任务能够恢复。
- 前端契约验收覆盖加载、真实空态、筛选空态、权限不足、失败重试、2/3/5 秒退避、页面隐藏暂停、恢复立即刷新和通过 `task_id` 恢复。
### 5. 系统配置与脱敏测试
- 系统配置测试覆盖 `string/int/bool/json` 类型、枚举/范围校验、未注册和只读 Key、超级管理员与非授权用户、并发更新、审计事实和安全默认值。
- 模拟 Redis 未命中、超时、写入失败和失效失败,验证 PostgreSQL 仍为事实来源、更新不丢失、旧缓存有期限且告警可观察。
- Access Log 使用嵌套对象、数组、大小写变体、query、JSON/XML/表单、二进制、超长 body 和无法解析内容建立测试矩阵。
- 登录/Token、支付、企微回调和文件接口分别有固定回归样例日志中不得出现测试密码、Token、签名、Nonce、支付链接、回调原文、文件字节或临时凭证。
- 验证脱敏后仍保留方法、路径、安全 query、状态、耗时、`request_id`、用户标识、body 摘要和截断标记,可用于从 Access Log 关联到 Audit/Integration 记录。
## Out of Scope
- Audit Event 和 Integration Log 的具体模型、Writer、查询、前端、保留策略及旧审计 Writer 一次性切换;这些归 `tech-global-audit`
- 站内通知业务包括通知表、模板、接收人、Notification Worker、通知 API 和前端通知中心;这些归 `tech-inapp-notifications`
- 企微、支付、Gateway 和运营商 Adapter以及其签名、协议映射、回调业务和外部补偿逻辑。
- 钱包、退款、充值、支付、套餐订购、卡状态、低余额预警等领域规则、状态机、金额校验和业务消费者行为。
- 各业务表、业务索引、业务唯一键和业务数据迁移;它们继续由对应业务 PRD 所有。
- 所有旧模块的全仓事务重构、DDD 重构、Repository 重写或异步任务迁移。
- 万能任务表、万能幂等表、全请求幂等平台和任意 Key-Value 配置中心。
- 强制所有任务使用同一个业务任务模型,或把批量订购、设备分配、导出迁移到公共表。
- 精确一次投递承诺。公共 Outbox 提供至少一次投递,消费者幂等由业务负责。
- 在当前仓库实现前端组件或虚构前端目录;这里只冻结跨仓交互和验收契约。
- 真实支付、企微、Gateway、运营商网络或生产数据上的集成测试。
## Further Notes
### 已确认的仓库基础
- GORM 已关闭默认事务并普遍使用显式事务;公共实现应沿用这一方式,不新增平行事务框架。
- 项目 Asynq 客户端已经统一使用 Redis 配置,并拒绝 `EnqueueTask` 接收 `[]byte`;仍存在直接使用 Asynq 原生客户端并自行序列化的业务代码,迁移时需区分两种调用契约。
- Fiber 已生成并传播 `request_id`;仓库已有 PostgreSQL 条件更新、钱包 `version`、Redis 防并发和导出任务五态的可复用实践,但尚未形成完整公共契约。
- 当前没有公共 Outbox 和受控系统配置实现。Access Log 已对 JSON 请求递归脱敏并限制 50KB但响应体仍直接截断记录敏感非 JSON/文件接口也缺少安全降级策略。
- 当前仓库没有前端源码,前端交付由对应前端仓库按本 PRD 的公开契约验收。
### 下游依赖与阻塞关系
本 PRD 是公共契约和发布顺序的前置项,但只阻塞下游对公共接缝的集成与上线,不接管下游业务设计:
| 下游 PRD | 被本 PRD 阻塞的公共接缝 | 下游仍自行负责 |
|---|---|---|
| `tech-global-audit` | 公共迁移所有权、同事务写入接缝、关联标识和共享脱敏策略 | Audit Event、Integration Log、Query、前端和旧 Writer 切换 |
| `tech-inapp-notifications` | `tb_outbox_event`、Relay、结构化载荷和租约恢复 | 通知模型、模板、接收人、Worker、API 和前端 |
| UR#37 企微审批基础 | Outbox、至少一次投递、Worker 租约、关联标识 | 企微场景、实例状态机、扫码绑定和 Adapter |
| UR#34 代理充值 | `request_id + 指纹`、Outbox、两阶段投递和任务恢复 | 支付事实、充值状态机、钱包入账和唯一流水 |
| UR#94 卡状态事件与回调 | 公共事件信封、Outbox、结构化 Asynq 载荷 | 卡状态规则、事件类型、消费者和外部回调行为 |
| UR#97 钱包低余额预警 | Outbox 与可靠消费接缝 | 阈值规则、钱包变更事件、接收人和通知内容 |
| UR#36 批量订购 | 五态任务、计数、失败语义、幂等和租约契约 | 批量任务/明细表、逐行校验、订购行为 |
| UR#42 统一导出字段权限 | 五态、结构化任务载荷、轮询和恢复语义 | 导出 DataSource、字段权限、文件生成和业务任务表 |
| UR#49 设备批量分配 CSV | 五态、部分成功、失败明细和租约契约 | 设备分配规则、CSV 校验和业务任务表 |
| UR#38 代理主钱包授信 | GORM 事务、PostgreSQL 最终幂等和 `version` 职责边界 | 钱包规则、授信/扣款、流水与领域事件 |
| UR#48 支付方式配置 | `tb_system_config`、受控 Key 注册、缓存、权限、API 和审计壳层 | 具体支付 Key、值域、支付业务校验和生效规则 |
上述下游可以在公共契约稳定后并行开发自己的领域部分;在公共 Outbox、任务状态或系统配置接缝尚未可用时不得复制临时 Outbox、万能任务表、幂等表、通知表或配置中心作为替代。
### 交付判定
- 公共能力的公开契约、迁移所有权、故障恢复和可观测性全部通过本 PRD 的真实接缝测试后,才可认为公共基础就绪。
- `tech-global-audit``tech-inapp-notifications` 保持独立交付;公共基础就绪不等于审计或通知业务已经完成。
- 下游业务 PRD 的领域测试、外部 Adapter 测试和业务前端验收仍是各自发布门槛,不能用公共基础测试替代。

View File

@@ -1,18 +0,0 @@
# 01 — 建立公共迁移所有权与检查门禁
**What to build:** 发布负责人能够通过统一清单确认公共数据库对象的唯一迁移所有者,并在迁移前后运行可重入检查。对象定义冲突、唯一键冲突、非法状态、必填字段空值、未完成任务、未投递事件、长租约或依赖版本不满足时,检查以中文安全摘要和非零状态阻断发布;正常结果给出可核对的计数与安全标识。
**Blocked by:** None — can start immediately
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口公共数据库对象的所有权登记、迁移前置检查、后置检查、失败退出、可重入和安全输出契约。明确不创建下游业务表,不迁移历史业务数据,不接管审计、通知或业务 PRD 拥有的迁移。
- [x] 公共 Outbox、系统配置及其公共索引、约束和初始化数据均有唯一迁移所有者下游只能声明依赖不能复制公共 DDL。
- [x] 检查能够发现目标对象定义不一致、唯一键冲突、必填字段空值、非法枚举、未完成任务、未投递事件、长租约和依赖版本问题,并以非零状态阻断发布。
- [x] 前置和后置检查可以重复运行;重复执行不产生新业务事实,`IF NOT EXISTS` 不会掩盖已有对象定义不一致。
- [x] 后置检查验证约束、关键索引、异常计数和读写冒烟,输出仅包含计数、错误码与安全标识,不泄露敏感值。
- [x] 发布说明明确哪些结构可安全回滚、哪些已有事实只能停止生产者后向前修复,以及迁移异常时的停止条件。
- [x] 自动化验证覆盖空数据库、兼容存量数据库、异常数据和重复执行场景,不执行全库清理。

View File

@@ -1,18 +0,0 @@
# 02 — 在业务事务中可靠写入公共 Outbox
**What to build:** 业务开发者可以沿用现有 GORM 显式事务,在提交业务事实的同一事务中写入权威公共 Outbox 事件。事件身份和关联标识在事务内稳定持久化,业务写入或 Outbox 写入任一步失败都会整体回滚,事务中不会调用 Redis、Asynq 或外部系统。
**Blocked by:** 01 — 建立公共迁移所有权与检查门禁
**Status:** completed
**架构通道:** Application + Port/Adapter。
**完整业务边界:** 本票收口公共 Outbox 模型、事件信封、事务内追加 Port 和一个可观察的示例写入链路。明确不定义下游业务事件含义,不实现业务消费者,不引入 UnitOfWork、事务工厂或全仓事务重构。
- [x] 公共 Outbox 具有稳定唯一的事件 ID、事件类型、载荷版本、聚合与资源定位、请求与关联标识、结构化载荷、投递生命周期、重试、租约和安全错误摘要字段。
- [x] Outbox 内部状态固定为 `1=待投递、2=投递中、3=已投递、4=投递失败`,常量、模型注释和公开说明保持一致。
- [x] 业务事实和 Outbox 使用同一 GORM 事务句柄;任一写入失败时二者均不可见,未提交事件不会被投递侧读取。
- [x] 事件 ID、业务键及必要快照在事务内生成并持久化重试过程中不会重新生成事件身份。
- [x] 事务内不执行 Redis、Asynq、HTTP、对象存储或其他外部调用Domain 不依赖 GORM。
- [x] PostgreSQL 集成测试覆盖事务成功、业务写入失败、Outbox 写入失败、事件 ID 唯一约束和回滚行为。

View File

@@ -1,18 +0,0 @@
# 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
**What to build:** 多个 Relay 实例可以并发领取到期 Outbox 事件,并通过有期限租约把公共事件信封可靠投递到 Asynq。瞬时失败会退避重试进程崩溃后过期租约可恢复入队成功但数据库标记前崩溃时允许重复投递但始终传播原事件 ID 和关联标识。
**Blocked by:** 02 — 在业务事务中可靠写入公共 Outbox
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口 Relay 的领取、租约、续租、投递、完成、失败、退避与恢复闭环,并通过公开 Asynq Handler 验证结构化信封。明确不实现业务消费者副作用,不承诺精确一次,不迁移未触碰的旧队列生产者。
- [x] Relay 以小批量条件领取或跳锁方式取得处理权,领取、续租、完成和失败均校验当前状态与租约所有者。
- [x] Relay 调用统一队列客户端时传 struct 或 map传入 `[]byte` 被明确拒绝并有回归测试防止二次序列化为 Base64。
- [x] 入队成功后事件标记为已投递;模拟入队成功但标记前崩溃时,恢复投递仍使用原事件 ID、载荷和关联标识。
- [x] 瞬时失败按有上限的指数退避安排下次领取,达到最大重试或永久失败时保留记录并产生中文安全告警。
- [x] 多 Relay 并发时同一时刻只有租约所有者能够完成事件Worker 崩溃后其他实例可在租约过期后恢复领取。
- [x] 真实 PostgreSQL、Redis 和 Asynq 链路测试覆盖事务写入、Relay、公开 Handler 与可观察消费结果,不依赖 Relay 私有函数断言。

View File

@@ -1,18 +0,0 @@
# 04 — 提供 Outbox 监控和受控恢复能力
**What to build:** 运维人员可以查看 Outbox 待投递量、最老积压、处理中和过期租约、成功率、重试分布、最终失败及按事件类型聚合的状态,并能对明确选择的失败或滞留事件执行受控重放或租约释放。恢复操作保留原事件内容和身份,并通过统一审计接缝记录操作者与原因。
**Blocked by:** 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
**Status:** completed
**架构通道:** 主通道为 Query辅助通道为 Infrastructure。
**完整业务边界:** 本票收口 Outbox 运行状态查询、指标、告警和受控恢复用例。明确不实现 Audit Event 模型或查询,不删除 Outbox不修改已投递事件内容不跳过消费者幂等检查。
- [x] 查询和指标覆盖待投递量、最老待投递年龄、处理中、过期租约、成功率、重试分布、最终失败和按事件类型的积压。
- [x] 日志、指标和告警使用事件 ID、关联 ID 与安全资源标识串联,不把完整载荷或敏感值作为日志字段或指标标签。
- [x] 受控重放只接受明确选择的失败或滞留事件,保留原事件 ID、载荷和关联标识并记录操作者、原因和恢复批次。
- [x] 租约释放只作用于符合状态与过期条件的事件,不能越过当前租约所有者直接修改正在有效处理的事件。
- [x] 人工恢复通过统一审计 Port 记录;审计不可用时遵循明确的失败策略,但本票不创建独立审计表。
- [x] 测试覆盖积压统计、阈值告警、最终失败、选择性重放、租约释放和越权/非法状态拒绝。

View File

@@ -1,18 +0,0 @@
# 05 — 提供创建命令幂等与并发职责契约
**What to build:** 创建类用例可以使用调用主体、操作类型和稳定请求 ID 建立幂等作用域,并对影响业务结果的规范化字段计算带版本的请求指纹。相同请求返回原结果,同一请求 ID 携带不同业务内容时返回明确冲突,并发首写最终由 PostgreSQL 唯一约束裁决Redis 故障不会制造重复业务事实。
**Blocked by:** None — can start immediately
**Status:** completed
**架构通道:** Application + Port/Adapter。
**完整业务边界:** 本票收口请求指纹、幂等作用域、冲突分类和并发职责的可复用构件与公开示例。明确不建立万能幂等表,不迁移未触碰的旧订单、钱包或状态机,不用 Redis 替代数据库事实。
- [x] 请求指纹只包含影响业务结果的规范化字段排除时间戳、签名、Token 等易变传输字段,并携带算法版本。
- [x] 同一作用域内相同请求 ID 和相同指纹返回原结果;不同指纹返回统一幂等冲突且不覆盖既有事实。
- [x] 作用域至少区分调用主体与操作类型;不同主体使用相同请求 ID 不会相互污染。
- [x] 并发首次提交由 PostgreSQL 唯一约束裁决应用层预查仅用于友好返回Redis 不可用、过期或主从切换不影响最终正确性。
- [x] 文档和测试明确区分请求 ID、事件 ID、业务唯一键、状态条件更新、钱包版本、Worker 租约和 Redis 防并发的职责。
- [x] 集成测试覆盖相同请求重放、指纹冲突、不同主体、并发首写和 Redis 故障,不修改未触碰业务模块。

View File

@@ -1,18 +0,0 @@
# 06 — 冻结统一异步任务五态和查询契约
**What to build:** 新接入的业务任务和前端可以复用固定五态、中文状态名、进度、结果计数、失败摘要、时间信息、租约恢复和轮询语义。任务到达业务处理终点时即为已完成,部分成功或全部业务项失败通过计数表达;只有整体无法执行时才进入已失败。
**Blocked by:** None — can start immediately
**Status:** completed
**架构通道:** 主通道为 Application辅助通道为 Query。
**完整业务边界:** 本票收口公共任务状态、公开投影、领取与终态更新、结构化队列载荷及前端轮询契约,并以现有导出任务作为兼容基准。明确不创建万能任务表,不迁移批量订购、设备分配、导入任务或其他未触碰业务任务。
- [x] 公共状态固定为 `1=待处理、2=处理中、3=已完成、4=已失败、5=已取消`,响应同时返回对应中文状态名。
- [x] 公开查询契约至少包含任务 ID、状态与名称、总数、成功数、失败数、进度、安全失败摘要、开始、完成和更新时间。
- [x] 业务项处理完成后满足 `total_count = success_count + failed_count`;部分成功和全部业务项失败均为已完成,不增加“部分成功”状态。
- [x] 待处理领取、终态进入和取消均使用预期状态条件更新;重复 Handler 遇到终态不会重新制造业务副作用。
- [x] 处理中任务具有租约或等价恢复记录,进程重启或重复投递后能从 PostgreSQL 事实恢复;队列载荷只包含最小结构化标识且禁止 `[]byte`
- [x] 契约测试覆盖全成功、部分成功、全部业务项失败、整体失败、取消、重复消费和过期任务恢复,并记录跨仓轮询、隐藏暂停与刷新恢复语义。

View File

@@ -1,18 +0,0 @@
# 07 — 交付受控系统配置注册与查询闭环
**What to build:** 超级管理员可以按模块查询系统注册的配置 Key并获得脱敏值、类型、值域提示、中文说明、只读状态和更新时间。业务模块只需注册自己拥有的 Key、类型、默认值和校验规则公共壳层负责注册冲突检查、持久化读取、Redis 缓存与 PostgreSQL 回退,未注册数据库记录最多只读展示。
**Blocked by:** 01 — 建立公共迁移所有权与检查门禁
**Status:** completed
**架构通道:** 主通道为 Query辅助通道为 Infrastructure。
**完整业务边界:** 本票收口系统配置表、代码注册表、启动校验、缓存读取和超级管理员列表 API。明确不注册支付等具体业务 Key不允许创建任意 Key不提供原始 JSON 自由编辑器;新增 Handler 必须同步文档生成器。
- [x] 配置存储支持唯一 Key、字符串化值、`string/int/bool/json` 类型、模块、中文说明、只读与敏感属性、创建更新人与时间,且无外键或 GORM 关联标签。
- [x] 注册表定义稳定 Key、模块、类型、值域或枚举、默认值、只读、敏感和控件提示重复 Key、类型冲突或非法默认值在启动或验证阶段失败。
- [x] 只有已认证超级管理员可按模块分页查询配置;权限不足返回统一 403不伪装为空数据。
- [x] 未注册数据库 Key 默认不可写,最多按只读、可诊断方式展示;敏感配置值按注册策略脱敏。
- [x] PostgreSQL 是唯一事实来源Redis 未命中、超时或不可用时回退数据库并尝试回填,缓存 Key 和默认 TTL 遵循公共常量。
- [x] 真实 Fiber、认证、GORM 和 Redis 测试覆盖模块过滤、四种类型、缓存命中与回退、未注册 Key、敏感值和权限边界OpenAPI 文档生成器同步更新。

View File

@@ -1,18 +0,0 @@
# 08 — 交付系统配置更新、权限和审计闭环
**What to build:** 超级管理员可以按 Key 更新单个已注册且允许修改的配置。系统依次完成授权、注册检查、类型解析和值域校验,在 GORM 事务中保存事实并通过统一审计接缝记录变更;提交成功后立即失效对应缓存,缓存失效失败不会回滚数据库事实但会产生可操作告警。
**Blocked by:** 07 — 交付受控系统配置注册与查询闭环
**Status:** completed
**架构通道:** 主通道为简单写 Application 事务脚本,辅助通道为 Port/Adapter。
**完整业务边界:** 本票收口单 Key 更新、权限、类型和值域校验、事务写入、缓存失效和审计 Port。明确不实现全局 Audit Event 模型,不提供无约束批量覆盖,不接管各业务模块的具体值域或生效规则;新增 Handler 必须同步文档生成器。
- [x] 只有超级管理员可以更新配置;未注册、只读、类型错误、非法 JSON、越界或枚举外值均返回统一中文错误且不修改事实。
- [x] 配置更新使用现有 GORM 显式事务完成事实写入和审计接缝调用;任一步事务内写入失败时整体回滚。
- [x] 审计信息包含操作者、操作类型、中文描述、变更前后事实和请求关联标识,但本票不创建独立配置审计表。
- [x] 提交成功后失效对应 Redis 缓存;失效失败不回滚 PostgreSQL产生包含组件、错误码、时间窗口和安全标识的中文告警。
- [x] 数据库值不可解析或越界时不能静默使用错误值,按注册策略返回最后验证值或安全默认值并告警。
- [x] 真实 Fiber、认证、GORM 和 Redis 测试覆盖更新成功、并发更新、权限、未注册、只读、四种类型、事务回滚、缓存失效失败和审计事实OpenAPI 文档生成器同步更新。

View File

@@ -1,18 +0,0 @@
# 09 — 统一 Access Log 请求与响应递归脱敏
**What to build:** Access Log 对 query、请求体和响应体使用同一套大小写不敏感的递归脱敏能力覆盖嵌套对象和数组。日志先脱敏再执行 50KB 截断,并继续保留请求 ID、方法、路径、安全 query、状态、耗时、用户与终端信息及明确截断标志方便排障而不泄露可复用凭证。
**Blocked by:** None — can start immediately
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口公共敏感字段注册、query/请求/响应 JSON 脱敏、截断与访问日志元数据。明确不修改 Audit Event 或 Integration Log 模型、Writer 与查询,不改变业务响应内容。
- [x] 公共敏感字段至少覆盖密码、口令、Token、Authorization、Cookie、密钥、Secret、签名、Nonce、验证码、支付凭证和私密 URL匹配大小写不敏感。
- [x] query、请求 JSON 和响应 JSON 复用同一递归规则,嵌套对象、数组、非字符串敏感字段均被不可逆替换。
- [x] 请求体和响应体分别先脱敏后按 50KB 截断,并输出可机器识别的截断状态,不因序列化失败回退记录未脱敏 JSON。
- [x] 脱敏后仍保留方法、路径、安全 query、状态、耗时、请求 ID、IP、User-Agent、用户标识及请求/响应摘要。
- [x] 日志、注释和告警均使用中文,用户可见响应继续使用统一错误与响应格式。
- [x] 真实 Fiber 测试捕获最终 JSON 日志覆盖嵌套结构、数组、大小写变体、query、请求与响应、超长 body 和无法序列化场景。

View File

@@ -1,18 +0,0 @@
# 10 — 为敏感接口提供安全摘要策略
**What to build:** 登录与 Token、支付、企微回调、文件上传下载和导出等敏感接口按路由策略记录安全摘要。无论载荷是 JSON、表单、XML、multipart、二进制还是解析失败都不会回退记录原文只保留事件类型、安全资源标识、大小、内容类型、摘要哈希、处理结果和截断信息等排障字段。
**Blocked by:** 09 — 统一 Access Log 请求与响应递归脱敏
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口路由级敏感策略、非 JSON 安全降级和固定回归矩阵。明确不实现登录、支付、企微、文件或导出业务逻辑,不记录完整回调正文、文件内容或临时访问能力。
- [x] 登录和 Token 接口不记录密码、验证码、访问令牌、刷新令牌或会话标识,只保留成功状态和必要主体标识。
- [x] 支付接口不记录支付凭证、银行卡敏感信息、二维码原文、跳转链接、渠道密钥或完整签名,只保留安全订单号、渠道类型、结果码和金额摘要。
- [x] 企微回调不记录加密包、解密正文、签名、Nonce、通讯录敏感字段或完整响应只保留事件类型、安全标识、大小、哈希和处理结果。
- [x] 文件与导出接口不记录 multipart、二进制、Base64、文件字节、临时凭证或签名下载地址只保留脱敏文件名、类型、大小、数量、任务标识和结果。
- [x] 敏感路由解析失败时只记录字段存在性、长度、内容类型、安全哈希和截断标志;普通非敏感文本也必须经过明确路由策略才可记录。
- [x] 真实 Fiber 回归矩阵覆盖 JSON、XML、表单、multipart、二进制、超长和不可解析载荷并断言日志中不存在测试凭证、签名、回调原文或文件字节。

View File

@@ -1,18 +0,0 @@
# 11 — 验证公共对象迁移与数据安全回滚边界
**What to build:** 发布负责人可以在空数据库和带兼容存量数据的数据库上执行公共 Outbox 与系统配置的正向迁移、后置校验及允许的回滚。公共对象只创建一次且定义一致;存在重复身份、非法配置、处理中任务、未投递事件或长租约时,流程明确失败且不执行破坏性写入。
**Blocked by:** 02 — 在业务事务中可靠写入公共 Outbox07 — 交付受控系统配置注册与查询闭环
**Status:** completed
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口公共 Outbox 和系统配置对象的迁移链路、兼容数据验证、可重入回填、后置校验和回滚边界。明确不迁移下游业务表不删除已产生的业务事实、Outbox、审计、通知或任务结果。
- [x] 空数据库和兼容存量数据库均可执行正向迁移,公共表、索引、约束和初始化数据只创建一次且定义符合契约。
- [x] 构造重复事件 ID、非法配置、唯一键冲突、处理中任务、未投递事件和长租约时前置检查以非零状态失败且不执行破坏性写入。
- [x] 需要回填时按稳定主键分批、记录进度并可中断重跑;重复执行不生成重复事实,最终行数守恒。
- [x] 后置校验覆盖约束生效、异常计数归零、关键索引可用和读写冒烟,输出不包含敏感数据。
- [x] 未产生业务数据的新增结构可在验证后回滚;已有 Outbox 或配置事实后,回滚流程停止生产者和 Relay、保留事实并向前修复禁止删表清理。
- [x] 迁移说明记录发布顺序、停止条件、恢复步骤和测试数据隔离策略,并通过相关迁移与集成测试。

View File

@@ -1,19 +0,0 @@
# 12 — 建立公共基础发布门禁和下游接入契约
**What to build:** 发布负责人可以通过一套公共基础整体验收判断是否允许下游接入和放量。验收使用真实 Fiber、GORM、PostgreSQL、Redis、Relay 和 Asynq 接缝,覆盖事务可靠性、至少一次投递、幂等职责、任务恢复、配置读写和 Access Log 安全,并给出下游接入顺序、运行监控、故障恢复和前端交互契约。
**Blocked by:** 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环04 — 提供 Outbox 监控和受控恢复能力05 — 提供创建命令幂等与并发职责契约06 — 冻结统一异步任务五态和查询契约08 — 交付系统配置更新、权限和审计闭环10 — 为敏感接口提供安全摘要策略11 — 验证公共对象迁移与数据安全回滚边界
**Status:** completed
**架构通道:** 主通道为 Infrastructure辅助通道为 Application 与 Query 契约。
**完整业务边界:** 本票收口公共能力的端到端发布门禁、运行手册、跨仓前端契约和下游接入说明。明确不实现前端代码、Audit Event、Integration Log、站内通知、业务事件消费者或任何下游领域规则不借验收迁移未触碰旧模块。
- [x] 整体验收通过公开接缝验证业务事务写入、Outbox Relay、Asynq Handler、重复投递、租约恢复和可观察消费结果。
- [x] PostgreSQL、Redis 和 Asynq 测试使用隔离数据与唯一前缀,只清理本次创建的数据,不执行全库或全缓存清空。
- [x] 发布顺序明确为迁移与检查、兼容 API、Relay/Worker、依赖消费者、前端生产者不得在消费者和监控就绪前制造不可见积压。
- [x] 停止条件覆盖迁移异常、Outbox 持续积压或租约大量过期、配置读写不一致、脱敏回归失败和关键任务无法恢复。
- [x] 下游接入说明明确公共基础提供与不提供的能力,以及审计、通知和各业务 PRD 自行拥有的模型、状态机、业务唯一键、失败明细和消费者幂等。
- [x] 前端跨仓契约记录加载、真实空态、筛选空态、403、失败重试、任务 ID 恢复、2/3/5 秒退避、最长 10 秒、页面隐藏暂停和恢复立即刷新。
- [x] 中文功能总结覆盖关键流程、异常闭环、发布回滚、监控恢复和待决策项README 增加索引;所有公共外部行为测试通过后方可标记基础就绪。

View File

@@ -1,129 +0,0 @@
# PRDUR#33 套餐临期查询、Dashboard 与站内提醒
Status: ready-for-agent
---
## Problem Statement
系统尚无统一临期资产列表、Dashboard 汇总和站内提醒。各页面如果直接使用当前套餐到期时间,会忽略排队主套餐并产生不同的临期数量。现有代码也没有七月迭代要求的通用站内通知基础设施,旧轮询告警模型不能承担面向业务用户的消息中心职责。
平台和不同层级代理看到的数据范围不同临期列表、Dashboard 数量和通知接收人都必须严格复用现有店铺层级权限,不能建立绕过权限的全局统计。
## Solution
以 UR#46 的预计最终到期 Query 为唯一事实来源,按 `Asia/Shanghai` 自然日将剩余 015 天定义为临期。提供受权限约束的临期列表,并把临期卡数、设备数作为通用 Dashboard 的首个业务卡片。
每日任务只负责在 15/7/3 天节点产生站内通知和防重记录页面、Dashboard 和导出始终实时查询,不读取每日任务快照。
## User Stories
1. 作为平台人员,我希望查看当前权限范围内全部临期卡和设备。
2. 作为不同层级代理,我希望 Dashboard 数量和列表只包含自己有权查看的店铺层级数据。
3. 作为运营人员,我希望 03 天资产在临期页优先展示,而普通资产列表只高亮不改排序。
4. 作为店铺主账号或业务员,我希望在 15、7、3 天节点收到一次站内提醒。
5. 作为 C 端客户,我希望资产临期时看到续费入口。
6. 作为维护人员,我希望任务漏跑后只补一个最近节点,重试不会重复发消息。
7. 作为运营人员,我希望临期列表能沿用统一导出任务下载结果。
## Implementation Decisions
### 临期规则
- 唯一到期来源是 UR#46`estimated_final_expires_at``days_until_final_expiry`
-`expiry_estimate_status=exact` 且剩余上海自然日为 015 的资产属于临期。
- 已过期(负数)、`waiting_activation``none``invalid_data` 均不进入临期列表、Dashboard 数量和提醒扫描。
- 展示等级固定815 天 `pink=粉红色`47 天 `purple=紫色`03 天 `red=红色`;后端返回 `expiry_level``expiry_level_name`
- 临期独立列表先按“03 天优先”排序,再按预计最终到期时间升序、资产类型和资产 ID 稳定排序。
- 普通卡/设备列表只使用 UR#46 字段高亮,保持原排序。
### 临期列表 API
- 新增 `GET /api/admin/expiring-assets`
- 参数:`asset_type`(可选 `iot_card|device`)、`keyword``shop_id``package_id``days_min``days_max``expires_from``expires_to``page``size`
- `keyword` 按现有资产标识解析能力匹配卡 ICCID/MSISDN/虚拟号或设备稳定标识;其他筛选与 keyword 按 AND 组合。
- `days_min/days_max` 必须在 015 且最小值不大于最大值;日期按上海自然日解析;分页默认 20、最大 100。
- 每项返回 `asset_type``asset_id``identifier``shop_id``shop_name``package_usage_id``package_name``estimated_final_expires_at``days_until_final_expiry``expiry_level``expiry_level_name``can_renew`
- `total` 和 items 使用完全相同的最终到期、筛选和权限条件;不得先取一页资产再在内存过滤临期。
- `can_renew` 只表示当前登录主体是否有合法续费入口,具体可售性继续由 UR#40 的统一套餐可售策略在下单时复核。
### 通用 Dashboard
- 新增通用 `GET /api/admin/dashboard/overview`,本需求交付首个 `expiring_assets` 卡片,不建立无业务内容的抽象插件框架。
- 响应首期结构:`expiring_assets.card_count``device_count``total_count``window_days=15`,并返回服务端计算时间。
- 平台、代理及不同层级代理调用同一接口。统计 Query 必须使用当前账号既有数据权限:平台按平台范围,各级代理按自己被授权的店铺及下级范围,因此不同主体看到不同数量。
- Dashboard 数量与不带额外条件的临期列表使用相同 Query 和权限范围;三个数量必须可由列表结果复核。
- 点击卡数、设备数或合计数进入临期页并携带对应 `asset_type`,不新增第二套详情接口。
- 后续需求可以在 `overview` 响应增加其他业务卡片,但本期不预测未来字段。
- Dashboard 不做跨用户共享缓存如需缓存key 必须包含稳定权限范围版本并确保权限变化立即失效。首期优先实时聚合。
### 权限与接收人
- 临期列表、Dashboard、资产列表、详情和导出分别复用其执行时或任务创建时的权限快照不通过字段筛选扩大行范围。
- `shop_id` 只会缩小现有数据范围;越权店铺按“无权限或资源不存在”处理。
- 每个临期资产的站内接收人为所属店铺主账号及仍有效绑定的平台业务员;去重后逐账号生成通知。
- 不因上级代理可以在列表看到下级数据,就自动向所有上级代理逐级发送通知;接收人以店铺主账号和业务员规则为准。
- 没有可用接收人时记录可观测告警,不创建无接收人的消息。
### 站内通知与防重
- 本需求依赖七月公共站内通知能力:业务事务/任务通过 Outbox 发布通知事件Notification Worker 按 `event_id + recipient_kind + recipient_id` 幂等写 `tb_notification`
- 新增 `tb_expiry_push_record`,唯一键为 `package_usage_id + recipient_id + channel + push_node``channel` 本期固定为站内通知。
- `package_usage_id` 使用最终到期队列中最后一条主套餐使用记录,使新增续费套餐后能够形成新的 15/7/3 提醒周期。
- 每日任务按上海时区运行,扫描当前仍在 015 天内的资产。
- 节点为 15、7、3。命中或漏跑时每次只选择“当前最近且尚未发送”的一个节点剩 14 天补 15剩 6 天补 7剩 2 天补 3不得一次补发多个旧节点。
- 防重记录与 Outbox 在同一数据库事务内写入任务、Relay 和 Worker 至少一次重试不得重复通知。
- 通知使用受控 `ref_type/ref_id` 指向资产或临期列表,不保存任意 URL前端点击后先标记已读再通过受控路由跳转。
- 本期不发送企业微信、短信或邮件临期消息。
### 其他接口与前端
- 卡/设备列表、后台详情和 `GET /api/c/v1/asset/info` 使用 UR#46 的统一字段C 端 `is_expiring=true && can_renew=true` 时展示续费入口。
- 代理首页调用通用 Dashboard而不是临期专用 summary API。
- 通知中心使用公共通知 API展示临期分类、15/7/3 节点文案和受控跳转。
- 前端颜色只根据 `expiry_level`,不重新计算天数阈值。
- 页面加载、空态、权限错误和重试遵循七月公共交互规范。
### 导出
- `POST /api/admin/export-tasks` 新增/注册 `scene=expiring_asset`,查询 filters 与临期列表参数保持同语义。
- 使用现有 DataSource 框架;场景只负责 Count、Headers、Fetch不另建导出队列、文件或下载链路。
- Count 与 Fetch 必须应用同一临期条件和任务创建时保存的 `ScopeShopIDs` 权限快照。
- Fetch 使用稳定 offset/limit 排序,行字段与 Headers 一致Worker 不读取实时登录上下文扩大权限。
- 同步更新 scene 常量、创建 DTO 校验、Registry 和支持场景判断。
### 架构、索引与发布
- 临期列表和 Dashboard 是 Query 层投影,直接使用 GORM/DTO不经过聚合根通知扫描复用同一查询核心。
- 不创建临期状态快照表。`tb_expiry_push_record` 只保存通知防重事实。
- 为最终到期批量计算、临期范围和接收人解析建立必要索引,使用代表性数据验证;禁止按资产或接收人 N+1。
- 实施顺序UR#55 快照 → UR#46 Query → 公共站内通知 → 本需求列表/Dashboard/任务/导出/前端。
## Testing Decisions
- Query 测试覆盖 0、1、3、4、7、8、15、16 和负数天边界,以及所有不可预计状态。
- 验证临期页 03 天优先和稳定分页,普通资产列表排序不变。
- HTTP 集成测试使用真实开发 PostgreSQL、Redis、JWT 和进程内 Fiber App覆盖全部筛选、AND、非法范围、分页和统一错误。
- 创建平台、不同层级代理及不同店铺范围数据,验证临期列表和 Dashboard 数量分别受权限约束,且 Dashboard 可由列表复核。
- 验证越权 `shop_id`、任务创建时权限快照和后续权限变化不会导致导出扩大数据范围。
- 通知测试覆盖精确命中、14/6/2 天漏跑补偿、重复任务、Outbox 重试、多接收人、无接收人、新续费使用记录和已过期跳过。
- 验证同一使用记录/接收人/节点只生成一条通知,不同接收人均能收到。
- DataSource 验证 Count/Fetch 行数及筛选一致、单一表头、CSV/XLSX、分片稳定排序和字段权限。
- 对 100 项列表、Dashboard 和每日扫描验证固定批量查询数量并执行 `EXPLAIN ANALYZE`
- 前端验收临期颜色、置顶、Dashboard 跳转、C 端续费入口、通知已读与受控跳转。
## Out of Scope
- 不发送企业微信、短信或邮件临期提醒。
- 不提供可编辑临期阈值或颜色配置。
- 不维护临期状态或 Dashboard 数量快照。
- 不在本需求实现公共站内通知中心基础设施本身。
- 不提前设计 Dashboard 未来卡片或插件系统。
- 不改变套餐续费可售规则;由 UR#40 负责。
## Further Notes
- 当前仓库没有业务 Dashboard 路由;`GET /api/admin/dashboard/overview` 是通用 Dashboard 的首个正式契约。
- 旧轮询告警的通知字段不是业务通知中心,不得复用为 `tb_notification` 的替代品。
- 导出部分遵循项目 DataSource 体系:任务框架负责分片、文件、对象存储和下载,临期场景只负责数据语义。

View File

@@ -1,291 +0,0 @@
# PRDUR#34 代理在线扫码充值与平台线下代充值
Status: ready-for-agent
---
## Problem Statement
当前代理充值接口只创建本地充值记录,没有按当前配置复用微信 H5/MWEB 与支付宝 WAP 支付能力返回可供前端渲染的支付 URL。在线回调直接在一次事务中尝试完成钱包入账支付事实与钱包处理结果无法独立表达回调也没有完整校验支付配置、金额、第三方交易号和业务关联。网络超时、回调丢失、重复回调、钱包事务失败和第三方迟到成功都缺少可靠恢复边界。
当前同一个创建接口还允许代理或平台提交目标 `shop_id`,平台可以替任意代理创建在线支付,容易混淆真实付款人和受益钱包。线下代充值仍通过本地 `offline-pay``reject` 和全局操作密码完成人工入账,没有接入已经冻结的企业微信审批公共能力;附件仍是字符串 Key 列表,审批结论、充值状态和钱包入账状态也未独立建模。
本需求必须把代理自主在线充值与平台线下代充值分成两条不可混用的路径,复用现有支付和钱包基础能力,保证第三方真实收款、企微审批结论、钱包实际入账和通知都可独立追踪,并在重复回调、重复 Worker、进程中断、查单未知和审批异常下保持资金不重不漏。
## Solution
保留代理充值资源,建立两条严格隔离的创建路径:代理只为当前店铺主钱包选择 `wechat``alipay` 发起在线充值,不提交目标店铺,也不进入审批;平台或超级管理员只为指定代理店铺发起 `offline` 线下代充值,提交固定金额、备注和 15 个结构化付款凭证,并接入 UR#37 的真实企业微信审批。
在线充值为每次用户主动创建生成新的充值单和支付单。微信 H5/MWEB 或支付宝 WAP 返回的 HTTPS URL 通过 `qr_content` 原样返回,前端自行渲染二维码,后端不生成二维码图片。前端支付状态轮询只读取本地状态;后端通过支付回调和受控查单任务同步第三方真实状态。支付成功先可靠固化收款事实,再由独立 Worker 幂等增加代理主钱包并写唯一流水,钱包失败不回滚支付成功事实。
线下充值创建时把充值单、唯一企微审批实例和提交 Outbox 原子落库。企微只能同意或拒绝固定金额;同意后复用与在线充值相同的钱包入账 Worker拒绝后原单终结。无论在线还是线下只要主钱包实际入账成功都向目标代理发送防重的站内到账通知。
## User Stories
1. 作为代理账号,我希望只为当前所属店铺的主钱包充值,不需要也不能选择目标店铺。
2. 作为代理付款人,我希望在创建时选择微信或支付宝,并在订单创建后不能切换支付方式。
3. 作为代理付款人,我希望每次主动点击创建支付都得到一张新的充值单和支付单,而不是复用相同金额的旧单。
4. 作为代理付款人,我希望网络重试不会为同一次提交创建多张充值单。
5. 作为代理付款人,我希望后端返回支付平台给出的付款字符串,由前端稳定渲染二维码。
6. 作为代理付款人,我希望页面不展示一个本地猜测的精确失效倒计时,以免把第三方实际已经失效的码显示为有效。
7. 作为代理付款人,我希望关闭页面后旧支付单继续等待回调或查单收敛,不因页面关闭而被取消。
8. 作为代理付款人,我希望即使创建了多张支付单,每张真实付款的订单都能分别准确入账。
9. 作为代理付款人,我希望付款完成后页面先显示第三方已收款,再准确展示钱包是否已经到账。
10. 作为代理付款人,我希望第三方已经收款但钱包暂时失败时,系统明确显示“支付成功、入账重试中”,而不是显示支付失败。
11. 作为代理付款人,我希望第三方成功回调晚于本地失败或关闭状态时,系统仍能核实并补充入账,不吞掉已付资金。
12. 作为代理付款人,我希望回调丢失时系统能够通过第三方查单补偿,而不要求我再次付款。
13. 作为代理付款人,我希望微信或支付宝不可用时页面只展示真实可用的支付方式。
14. 作为代理付款人,我希望钱包实际到账后收到一条站内到账通知。
15. 作为非主账号的在线充值提交人,我希望自己和目标店铺主账号都能收到到账通知。
16. 作为平台员工,我希望选择目标代理店铺、金额、备注和付款凭证发起线下代充值。
17. 作为平台员工,我希望线下代充值金额大于 0 即可,不受代理在线充值 100 元起付限制。
18. 作为平台员工,我希望使用本人绑定的企微身份提交线下充值审批,业务系统同时保存我是真实提交人。
19. 作为审批人,我希望企微表单展示充值单号、目标代理、固定金额、真实提交人、备注和付款凭证。
20. 作为审批人,我只需要同意或拒绝线下代充值,不能修改金额,也不需要回业务系统输入操作密码。
21. 作为平台提交人,我希望企微拒绝后原充值单只读终结;修正资料后通过创建入口提交一张新单。
22. 作为平台提交人,我希望审批通过和钱包实际到账是两个独立状态,不会把审批通过误认为余额已增加。
23. 作为目标代理主账号,我希望平台线下代充值实际到账后收到站内到账通知。
24. 作为真实业务提交人,我希望线下审批通过或拒绝继续收到 UR#37 的审批结果通知。
25. 作为代理管理者,我希望按既有店铺层级和充值查看权限查看本店及可管理下级店铺的充值记录,而不是只看自己创建的记录。
26. 作为代理查看者,我希望看到充值金额、付款方式、业务凭证、真实提交人、审批状态和入账结果,但看不到平台内部审批人、意见、审批附件或支付配置。
27. 作为平台财务,我希望钱包入账持续失败或审批通过后撤销时收到内部异常通知,以便人工处理。
28. 作为财务人员,我希望已经入账后发生企微通过后撤销时系统不自动扣减代理余额,避免未经核实的反向资金动作。
29. 作为系统维护人员,我希望支付回调、第三方查单、企微回调、企微轮询和重复 Worker 都进入统一幂等用例,不重复加钱。
30. 作为审计人员,我希望充值单、支付单、企微审批实例、代理主钱包、钱包流水、通知和外部交互能够完整关联。
31. 作为前端开发者,我希望在线支付、线下审批和钱包处理状态分别返回,不需要通过单个状态猜测业务阶段。
32. 作为验收人员,我希望本地自动化覆盖完整后台业务和异常恢复,真实微信/支付宝留到部署测试环境人工扫码,真实企微仍在实现阶段参与线下充值验收。
## Implementation Decisions
### 范围、依赖与架构边界
- UR#34 只负责代理在线充值、平台线下代充值、支付状态同步、企微终态消费、钱包入账、充值 Query 和前端业务页面。企微连接、平台账号绑定、模板版本、附件上传企微、提交、加密回调、轮询和审批详情复用 UR#37,不实现第二套企微客户端或审批状态机。
- 站内到账通知复用 TECH 公共站内通知;支付和钱包关键事实使用全局 Audit Event第三方支付与企微调用使用 Integration Log不新建充值私有审计或通知体系。
- 在线创建、支付确认、线下创建、企微终态消费和钱包入账均涉及远程调用、状态机、金额、并发与可靠事件,采用 `Handler → Application UseCase → Domain → Repository/Infrastructure` 的复杂写通道。充值列表、详情和轻量支付状态采用 Query 通道。
- 只迁移完成 UR#34 所需的最小完整充值用例。现有旧 Service 可以暂时作为迁移门面,但支付确认和钱包入账不变量不得一半留在旧 Service、一半放在新 Domain。
- 当前支付配置通常只有一套微信参数和一套支付宝参数。本期不建设多通道池、优先级、权重、自动切换或故障转移;代理选择的是支付方式,不选择也不感知具体支付通道。
### 角色与两条创建路径
- 代理账号只能创建 `payment_method=wechat|alipay` 的在线充值。目标店铺和主钱包从当前认证上下文确定;请求不接受 `shop_id`,代理不能给本店下级或其他店铺创建在线支付。
- 平台账号和超级管理员只能创建 `payment_method=offline` 的线下代充值,必须提交目标 `shop_id`,并受既有目标店铺数据范围和管理权限约束。平台与超级管理员不能替代理创建微信或支付宝支付码。
- 代理账号不能创建线下代充值;企业账号不能创建或访问任一代理充值路径。
- 创建权限和查看权限分开处理。创建路径使用上述严格角色规则;列表、详情和支付状态继续使用既有充值业务权限与店铺层级数据范围。
- 新创建不接受历史 `bank` 支付方式;历史 `bank` 记录只读兼容,不改变原事实。
### 可用支付方式接口
- `GET /api/admin/agent-recharges/payment-methods` 只供代理在线充值页面使用,返回当前真正配置完整且能够创建支付的 `wechat` 和/或 `alipay`
- `data` 固定包含:`methods:string[]``min_amount:int64=10000``max_amount:int64=100000000`。没有可用方式时 `methods=[]`,接口本身仍成功。
- 接口不返回商户号、应用私钥、支付通道、支付配置 ID或具体缺失的敏感配置。支付方式顺序固定为 `wechat``alipay`,仅保留可用项。
- 创建接口必须再次校验所选支付方式。配置在查询列表后失效时拒绝创建,不能只信任前端先前取得的列表。
- 微信只有在当前微信支付配置支持对应协议的 H5/MWEB 下单、回调验签和查单能力时才视为可用;支付宝只有在 WAP 支付链接签名、回调验签和查单所需参数完整时才视为可用。
### 创建接口与请求幂等
- 保留 `POST /api/admin/agent-recharges`,根据 `payment_method` 使用严格的判别请求契约。
- 在线请求只接受:`amount``payment_method=wechat|alipay``request_id``amount` 为分,范围 `10000100000000`;不接受 `shop_id`、付款凭证或备注。
- 线下请求只接受:`shop_id``amount``payment_method=offline``attachments`、可选 `remark``request_id``amount` 为分,范围 `1100000000``remark` 最多 500 字。
- 线下 `attachments` 必传 15 项,每项固定包含 `file_key``file_name``file_size`。后端使用现有真实对象存储元数据能力校验对象存在、当前上传主体、允许的文件类型和实际大小;请求元数据作为业务快照,不能替代对象事实。
- `request_id` 只防止同一次用户提交因超时、重试或重复点击产生重复业务单。幂等范围为真实提交账号与 `request_id`,并保存请求指纹;同账号、同 ID、同载荷返回原业务结果载荷变化返回冲突。
- 用户每次主动点击创建或拉起支付必须生成新的 `request_id`。后端每次创建全新的充值单、支付单和 `qr_content`,不按相同金额、支付方式或既有待支付记录复用旧单。
- 在线和线下创建都必须通过数据库唯一约束和请求指纹保证持久化幂等Redis 只能减少并发,不能成为唯一正确性依据。
### 在线充值创建与付款内容
- 在线创建先在同一 PostgreSQL 事务写充值单和 `tb_payment`。支付单增加稳定 `order_type=agent_recharge`,关联充值 ID、支付方式、金额、创建时支付配置 ID和支付单号充值单初始 `status=1`,支付单初始 `status=0`,处理状态为 0。
- 微信按 `provider_type` 使用 v3 H5 返回 `h5_url` 或 v2 MWEB 返回 `mweb_url`;支付宝复用 C 端 `alipay.trade.wap.pay` 签名 URL。前端统一将 HTTPS URL 渲染为二维码,不要求开通微信 Native 或支付宝当面付。
- 微信或支付宝返回的二维码码串、协议字符串或 HTTPS URL 原样保存为 `qr_content` 并返回。前端使用二维码组件渲染;后端不生成、上传或返回二维码图片,也不新增“生成二维码”“重新生成二维码”接口。
- 创建成功的在线 `data` 至少包含:`recharge_id``recharge_no``amount``payment_method``qr_content``status/status_name``payment_status/payment_status_name``processing_status/processing_status_name``approval_source=none`。不返回 `expires_at``payment_channel` 或支付配置 ID。
- 本地计算时间不能代表第三方支付订单真实有效期。后台在线充值接口不返回 `expires_at`,前端不展示精确倒计时,也不根据本地时间判定二维码仍然有效。
- 用户关闭页面只停止当前页面展示,不调用支付取消或关单。此前支付单继续等待支付回调或后端查单自然收敛;用户再次主动创建得到独立新单。
- 多张在线支付单若都被第三方确认真实付款,每张分别进入幂等钱包入账,不能因为存在更新的支付单而吞掉旧单资金。
### 预下单失败和结果未知
- 第三方明确返回预下单失败时,支付单改为 `2=已失败`并保存稳定失败码/脱敏摘要,充值单改为 `4=已关闭`;创建接口返回统一支付错误。用户再次主动尝试使用新的 `request_id` 创建新单。
- 网络超时、连接中断或响应丢失导致预下单结果未知时,不能直接把原支付单判失败,也不能以同一个 `request_id` 创建另一张支付单。必须保留原业务事实,并使用创建时支付配置和原支付单号执行安全恢复。
- 结果未知恢复先向第三方查单:明确已支付则进入统一支付确认;明确不存在时可用同一支付单号重新预下单;明确未支付且第三方支持幂等恢复付款内容时保存并返回原支付单的 `qr_content`;仍无法判断时保持未知并继续可靠恢复,不伪造失败或成功。
- 同一 `request_id` 重试只能返回原充值/支付事实或当前稳定错误,不能创建新单。用户主动使用新 `request_id` 创建的新单不取消未知旧单,旧单仍需查单收敛。
- 预下单结果未知、恢复尝试和最终结论写 Integration Log外部原始响应、商户密钥和完整付款内容不得进入普通日志或审计详情。
### 支付状态、充值状态与处理状态
- `tb_payment` 状态统一沿用当前 Go 常量:`0=待支付``1=已支付``2=已失败``3=已退款`。本需求不新增“已关闭”支付状态;第三方明确关闭或失效时使用 `2=已失败`并保存结构化失败原因。
- 充值业务状态沿用 `16``1=待支付/线下待审批``2=已支付或审批通过后正在入账``3=已完成``4=已关闭``5=已退款``6=已驳回`。不新增 `7=已退回`
- 钱包处理状态统一为:`0=未触发``1=处理中``2=处理成功``3=处理失败`。所有响应返回对应 `processing_status_name`,不再提供同义字段 `wallet_posting_status`
- 在线记录另返回支付状态及 `payment_status_name`;线下记录没有第三方支付单,支付状态字段为不适用,不用伪造“已支付”。审批状态完全复用 UR#37 的公共企微状态。
- 状态类字段使用 `int`,方式/来源类使用 `string`。DTO description 必须从公共 constants 原文复制,不能按迁移注释或旧文档重新编号。
- 历史 `tb_payment` 建表迁移的注释和默认值与当前 Go `03` 常量不一致。实施前必须按状态、业务类型和创建入口盘点存量值;新迁移统一默认值、注释和约束到 `03`,遇到无法证明含义的存量值必须中止并输出异常清单,禁止盲目整体加减 1。
### 支付回调、查单补偿与迟到成功
- 微信、支付宝回调先完成渠道协议验签/解密,再按支付单号和 `order_type=agent_recharge` 分发到统一支付确认用例,不再只依赖充值单号前缀猜测业务类型。
- 支付确认必须核验:原支付方式、创建时支付配置、商户订单号、业务关联、第三方交易号、支付金额和第三方成功状态。金额或关联不一致时拒绝入账并记录严重 Integration Log/Audit Event不能把异常回调当成功。
- 第三方交易号必须具备持久化唯一性或等价防重;同一支付单重复回调、回调与查单并发、第三方重复通知都只固化一次支付成功事实。
- 前端每 3 秒调用轻量支付状态接口时只查询本地数据库,不直接请求微信或支付宝。页面不可见时暂停,恢复时立即刷新;到达本地终态或离开页面时停止。
- 后端可靠任务按受控、可运维调整的频率查询仍待支付或预下单结果未知的支付单。具体频率是实现配置,不成为前端或业务契约;任务必须有限流、租约和批次上限,不能形成无限并发查单。
- 查单与回调进入同一支付确认用例。只有第三方明确返回已支付、已关闭/失效或订单不存在时才推进对应本地结论;超时、限流和未知状态继续保持原状态并重试。
- 第三方明确关闭或失效且未支付时,支付单改为已失败,充值单改为已关闭。不得仅根据本地 `expire_at` 或页面停留时间关闭第三方订单。
- 本地曾因第三方明确关闭而标记失败后,如果又收到可验证的迟到成功回调,以真实收款事实为准:支付单改为已支付,原充值单进入入账流程。已关闭业务状态不能吞掉已付资金。
### 两阶段钱包入账与资金幂等
- 在线支付确认采用两阶段处理。第一阶段在一个事务中把支付单条件更新为已支付,保存第三方交易号和支付时间,把充值单更新为 `2=已支付``processing_status=1`,并写钱包入账 Outbox。事务成功后即可向支付渠道返回成功不等待钱包余额更新。
- 线下企微首次同意同样只把充值单推进到 `2``processing_status=1`并写钱包入账 Outbox企微同步线程不直接修改钱包。
- 钱包入账 Worker 在独立事务中锁定目标代理主钱包,使用钱包版本/条件更新增加余额,创建唯一充值流水,把充值单改为 `3=已完成``processing_status=2`并写资金 Audit Event。
- 钱包流水以充值单号或等价稳定业务键建立唯一约束。若流水已经存在而充值单尚未完成,重试只核验并补齐充值单状态和审计关联,不再次增加余额。
- Worker 通过处理状态、条件更新和有期限租约领取。失败时支付或审批事实保持不变,充值单 `processing_status=3`并保存脱敏错误摘要,由可靠任务重试;前端没有人工“再次入账”按钮。
- 重复支付回调、重复企微终态、重复 Outbox、Asynq 至少一次投递、租约过期和进程中断均不能重复加钱、重复流水或重复到账通知。
- 充值增加主钱包账面余额;钱包原余额为负时允许正常入账并自然冲减欠款,不因欠款状态拒绝充值。
### 线下代充值与企微终态
- 创建线下充值前必须确认 `offline_recharge_approval` 场景、当前模板和当前平台/超级管理员本人的企微绑定可用。任一前置不可用时,在充值单、审批实例和 Outbox 落库前失败,前端保留表单。
- 线下充值单、真实业务提交人快照、结构化申请附件、唯一企微审批实例和提交 Outbox 在同一 PostgreSQL 事务创建。远程企微异步提交;创建成功返回充值详情及 `approval_source=wecom`、审批“提交中”。
- 企微表单至少展示充值单号、目标代理店铺、充值金额、真实业务提交人、申请备注和 15 个付款凭证。金额提交后固定;企微只允许同意或拒绝,不提供金额编辑、退回修改或本地审批动作。
- 企微拒绝把原充值单条件更新为 `6=已驳回`,处理状态保持未触发,保存拒绝原因和审批快照。原单、凭证和拒绝原因只读保留;修正后必须通过创建接口生成新的充值 ID、单号、资料和企微审批。
- 系统不新增 `7=已退回`,不提供 `resubmit`,也不注册旧 `offline-pay` 或业务单级 `reject`。企微同意后自动入账,不校验全局操作密码。
- 企微在通过前撤销或删除时,充值单改为 `4=已关闭`且不入账,可以重新创建。企微通过后、钱包尚未入账时发生撤销,终止后续入账并关闭充值单。
- 钱包已经入账后再收到通过后撤销时,不自动扣减代理余额,充值单保持已完成;系统记录 `critical` Audit Event并向平台财务/运维发送异常通知,交由人工处理。
### 到账通知与异常通知
- 无论在线扫码还是平台线下代充值,只有钱包入账事务成功后才产生“充值到账”可靠事件;支付成功或企微同意本身不提前发送到账通知。
- 目标代理店铺主账号必须收到到账通知。在线充值的真实提交人若不是店铺主账号,也收到到账通知;同一账号同时符合多种身份时按“充值单 + 接收人”唯一键只生成一条。
- 平台线下充值提交人不因为提交身份收到代理到账通知,但继续通过 UR#37 收到审批通过/拒绝结果通知。若平台提交人同时也是目标代理接收账号,则仍按接收人防重。
- 通知至少展示充值单号、金额、支付方式/线下代充来源、到账时间和受控充值详情引用,不保存任意 URL。通知投递失败不回滚资金事务并由公共通知 Worker可靠重试。
- 入账持续失败、金额异常、重复第三方交易号和通过后撤销只通知有权的平台财务/运维,代理页面显示业务化处理状态,不暴露数据库、支付配置或第三方原始错误。
### 查询、权限与信息投影
- 保留 `GET /api/admin/agent-recharges``GET /api/admin/agent-recharges/{id}`,新增 `GET /api/admin/agent-recharges/{id}/payment-status`。列表、详情和支付状态必须使用相同认证、充值业务权限和店铺层级数据范围。
- 充值记录不按创建账号隔离。代理在具备充值查看权限时,可查看本店及有权管理的下级店铺记录;平台和超级管理员沿用既有数据范围;企业账号统一拒绝。
- 代理可见充值金额、支付方式、业务付款凭证、真实提交人、充值状态、企微状态和钱包处理结果;不可见企微审批人、内部意见、审批人附件、企微成员 ID、支付配置 ID、商户号、支付通道和内部失败详情。
- 平台和超级管理员只有同时具备充值业务查看权限时,才能读取 UR#37 提供的完整审批详情。企微运营权限不能绕过充值业务权限。
- 资源不存在与无权访问统一返回禁止访问语义,不能借列表、详情、支付状态或附件下载的差异探测其他店铺充值。
- 列表沿用 `page/page_size`,默认第 1 页、每页 20、最大 100默认 `created_at DESC, id DESC`。店铺、充值状态、支付方式、提交人和起止时间等筛选按 AND 组合,空参数不改变查询。
- 列表项与 UR#44 对齐,至少返回充值 ID/单号、店铺、金额、支付方式、真实提交人、充值状态及名称、`approval_source`、审批状态及名称、代理投影为空的当前审批人摘要、处理状态及名称、创建和完成时间。
- 详情返回完整充值业务资料、结构化申请附件、支付摘要、根级处理状态以及按主体投影的 `approval` 对象。在线固定 `approval_source=none``approval=null`;新线下固定 `approval_source=wecom`;历史本地审批使用 `legacy`,不伪造企微时间线。
- 轻量支付状态接口只适用于在线充值,至少返回:充值 ID/单号、充值 `status/status_name``payment_status/payment_status_name``processing_status/processing_status_name`、可空 `paid_at/completed_at`。不返回 `qr_content`、钱包余额、审批详情、`expires_at`、支付通道或配置。
### 数据模型、索引与迁移
- `tb_agent_recharge_record` 保留现有业务主表,并补充:`request_id`、请求指纹、结构化申请附件、真实提交人显示快照、`approval_instance_id`、处理状态/错误/开始/完成时间、处理租约、乐观锁版本及可靠执行所需字段。数据库不建立外键,也不使用 GORM 关联标签。
- 新线下附件使用结构化 `{file_key,file_name,file_size}` 列表保存。历史 `payment_voucher_key` 字符串或字符串数组只读兼容;迁移不能丢失旧凭证,也不能把短期预签名 URL 写成业务事实。
- `tb_payment` 增加 `order_type=agent_recharge` 常量及本需求需要的 `qr_content`、稳定失败码/摘要和预下单恢复事实。付款内容不得写普通访问日志API 仅在在线创建成功响应中返回。
- 充值记录对 `(user_id,request_id)` 建有效记录唯一约束并保存请求指纹;`approval_instance_id` 对新线下充值保持一单一审批唯一性;为待处理状态/租约、店铺+创建时间和支付单业务关联建立必要索引。
- 支付单号、第三方交易号和钱包充值流水分别建立符合其语义的唯一约束。软删除不能允许相同第三方资金事实再次入账。
- 数据库金额约束继续允许历史及线下 `amount>=1`;在线 `amount>=10000` 和两条路径的 `amount<=100000000` 必须在 Domain 中按支付方式重校验。不得用一个全局最低 100 元约束破坏线下小额代充。
- 发布前盘点充值状态、支付状态、重复第三方交易号、重复充值流水、孤立支付单、待处理线下单和历史附件格式。任何不确定资金记录进入异常清单,不由迁移脚本猜测完成状态。
### 停机发布、存量迁移与回滚
- 采用已确认的停机发布同时切换迁移、API、Worker、支付回调分发、企微终态消费和前端。新版本不保留旧本地审批兼容窗口。
- 历史已完成、已关闭、已退款和已驳回的线下充值只读保留并标记/投影为 `approval_source=legacy`,不补企微审批,也不伪造审批人或时间线。
- 发布时仍待处理的线下充值,只有在真实提交人可确认、平台账号已绑定企微且金额/付款凭证等资料完整时,才幂等创建真实企微审批继续处理;其余进入明确迁移异常清单,不自动入账,也不能再调用旧 `offline-pay`
- 上线前必须确认 UR#37 的线下充值场景和模板已发布、平台绑定可用、回调/轮询可达、钱包入账 Worker 和通知 Worker 已部署;条件不满足时不得开放线下创建入口。
- 回滚应用时保留已经创建的充值单、支付单、真实支付事实、企微实例、Outbox、Integration Log、Audit Event、钱包流水和通知。已进入第三方支付或真实企微的业务不能恢复旧本地按钮只能继续安全同步、重试或人工处置。
- 已经实际入账的资金不通过迁移或应用回滚自动反向删除;任何资金冲正必须是后续独立、受审计的业务流程。
### 统一错误与安全响应
- 所有接口使用统一 `{code,msg,data,timestamp}`;分页使用项目公共结构。金额始终使用分,时间使用项目统一时区/RFC3339格式。
- 请求字段、在线/线下金额、支付方式、备注或附件元数据非法使用 `CodeInvalidParam=1001`;未登录使用 `CodeUnauthorized=1004`;企业账号、错误创建角色、越权店铺及无权资源统一使用 `CodeForbidden=1005`
- 相同 `request_id` 的请求指纹不一致使用 `CodeConflict=1007`;状态不允许的业务动作使用 `CodeInvalidStatus=1050`;无可用支付配置使用 `CodeNoPaymentConfig=1175`;企微场景/模板/本人绑定不可用复用 UR#37 的稳定错误语义。
- 微信/支付宝明确预下单失败或恢复失败使用统一、可区分支付方式的支付错误;若现有错误码不足,新增公共错误码而不是在 Handler 拼接底层错误。结果未知返回稳定“支付创建结果确认中/请稍后重试”语义,不透传 SDK、HTTP、数据库或 Redis 错误。
- Handler 参数验证统一返回公共参数错误Application/Domain/Query 不向调用方返回 `fmt.Errorf`。日志、审计和响应不得包含私钥、Secret、商户签名、完整回调报文、对象存储永久凭证或企微敏感标识。
### 前端页面与交互
- 代理资金页保留充值入口。打开时获取可用支付方式,金额输入单位为元并明确最低 100 元、最高 100 万元;只展示后端返回的微信/支付宝选项,无可用方式时禁止提交并显示统一业务提示。
- 用户选择支付方式并点击创建时生成新的 `request_id`。同一次请求超时重试复用原 ID用户再次主动点击“创建支付”生成新 ID和新订单不搜索或复用旧待支付单。
- 创建成功后用 `qr_content` 渲染二维码,展示充值单号、金额、支付方式和本地支付/入账状态。不展示精确过期倒计时,不请求二维码图片,也不显示“取消支付”或调用“重新生成二维码”接口。
- 在线页面可提供“再次创建支付”用户动作,但它本质上重新调用创建接口并形成独立业务单;旧单在充值记录中继续自然收敛。
- 页面可见时每 3 秒读取本地轻量状态,隐藏时暂停、恢复时立即刷新。支付成功但入账处理中/失败时保留明确提示;入账完成后停止轮询、刷新现有钱包资金概况并展示成功。
- 平台线下创建页只展示目标店铺、金额、可选备注和 15 个付款凭证。提交中禁止重复操作;场景、绑定、附件或企微提交前置失败时保留表单内容。
- 线下创建成功进入充值详情,分为业务资料、企微审批和钱包处理结果三个区域。不展示本地确认、拒绝、退回、重提或操作密码输入框。
- 列表和详情按 `approval_source` 展示:`none` 审批列为“-”;`wecom` 展示只读企微状态;`legacy` 展示“历史审批”且无操作按钮。代理界面不展示审批人列或内部意见。
- 加载、空、网络失败、支付方式为空、支付创建失败、支付已成功但入账失败、企微提交中/失败、审批拒绝和异常撤销均有独立展示,不由前端自行推导或改写服务端状态。
## 公共能力发布依赖
- 最终发布票必须明确阻塞于公共基础 12 号票、全局审计 19 号票、公共通知 08 号票、UR#37 企微发布 14 号票和 UR#38 钱包切换 10 号票。
- 充值事件必须登记载荷版本、消费者幂等键、失败明细和通知策略;资金结论以 Domain Ledger 为准Audit Event 记录操作者与前后变化,企微/支付外部事实进入 Integration Log。
- 当前 PRD 尚未拆票;生成 tickets 时必须把上述依赖落到最终发布门禁,不得只写“依赖公共基础”。
## Testing Decisions
### 最高公共测试接缝
- 主要自动化接缝为Fiber 真实路由与认证 → Recharge Application/Domain/Query → GORM → 现有测试 PostgreSQL → Outbox/公开 Worker Handler → 代理主钱包、唯一流水、通知和统一审计。测试只断言公开响应和持久化业务事实,不断言私有函数或目录结构。
- 微信/支付宝网络边界使用可编程 Payment Adapter覆盖预下单、查单、回调确认和异常不在本地或 Agent 自动化中调用真实微信、支付宝。支付协议验签与基础能力优先复用 C 端已经验证的实现,本需求重点验证后台充值分发和业务闭环。
- 企微网络边界复用 UR#37 的可编程 WeCom Adapter并保留真实企微线下充值验收对象存储直接使用现有真实 S3 Provider不建立内存替身。
- 实现时沉淀可复用测试 Harness环境守卫、唯一运行标识、真实认证/Fiber 请求、受控支付与企微 Adapter、Outbox 捕获、公开 Worker 驱动、真实 S3、精确资源台账和失败清理并提供不含敏感信息的 HTTP/curl 冒烟模板。
### 环境隔离
- 本地开发和 Agent 自动化测试固定使用 Redis DB 7已部署测试环境继续使用 Redis DB 6。测试启动前必须核验普通 Redis Client、Asynq Client和 Worker Server 实际 DB任一不是 7 时立即失败。
- 自动化不得向 DB 6 投递任务,不执行 `FLUSHDB`、全前缀删除或清空队列。每次运行生成唯一 `run_id`,只清理本次实际创建的精确 Key和任务事实。
- PostgreSQL 继续使用现有测试数据库,不新建 Agent 专用数据库。所有账号、店铺、钱包、充值、支付、审批、流水和通知夹具使用唯一运行标识,只按实际主键精确清理;禁止 `TRUNCATE`、模糊删除或改写既有业务数据。
- S3 测试对象 Key包含唯一运行标识覆盖真实上传、Head/元数据、读取及精确删除。网络、鉴权或删除失败使测试失败;禁止按目录前缀或 Bucket 级清理。
- 自动化捕获待投递任务后直接调用公开 Worker Handler不通过 `sleep` 等待测试环境 Worker 抢任务。生产仍使用真实 Asynq 投递。
### 在线创建、支付和查单场景
- HTTP 创建覆盖:代理成功、平台/超管在线拒绝、企业拒绝、请求携带 `shop_id` 拒绝、微信/支付宝可用性、配置在列表后失效、金额 9999/10000/100000000/100000001、支付方式非法和支付方式创建后不可切换。
- 幂等覆盖:同账号同 `request_id` 同载荷返回原单,不同载荷冲突,并发重复只有一张充值单和支付单;新 `request_id` 在金额相同且旧单待支付时仍创建新单。
- Adapter 支付链接覆盖微信 v3 H5/v2 MWEB 成功、明确失败、结果未知和支付宝 WAP 本地签名成功/失败;同一请求不得重复创建支付单。
- 响应覆盖原样 `qr_content`、前端可渲染字符串/HTTPS URL、不返回 `expires_at`、支付通道或配置,不创建二维码图片文件。
- 回调覆盖签名/验签失败、订单不存在、业务类型错误、支付方式不符、配置不符、金额不符、第三方交易号冲突、业务关联错误、成功、重复成功和乱序通知。
- 查单覆盖明确待支付、已支付、已关闭/失效、不存在、未知、超时和限流;证明前端状态接口不调用 Adapter查单与回调并发只固化一次支付成功。
- 迟到成功覆盖支付单已失败、充值单已关闭后收到有效成功回调,最终仍只入账一次;多张独立支付单均成功时分别入账。
### 钱包、通知和审计场景
- 两阶段测试证明支付成功事务完成后即使钱包事务失败,支付单仍已支付、充值单为已支付、处理状态失败/重试,不能回到待支付或向渠道返回失败。
- 钱包覆盖正常余额、负余额自然冲减、乐观锁冲突、数据库失败、流水已存在但状态未完成、Worker 崩溃前/后、租约过期、重复 Outbox和两个 Worker 并发;余额、版本、唯一流水和完成审计只变化一次。
- 通知覆盖在线主账号、在线非主账号提交人、线下目标代理主账号、同一接收人身份重合、重复 Worker和通知投递失败每个充值单与接收人最多一条到账通知通知失败不回滚钱包。
- 审计覆盖在线创建、线下创建、明确预下单失败、结果未知、支付确认、第三方状态异常、钱包成功/失败、审批拒绝、撤销和通过后撤销;验证金额、状态前后值和关联 ID完整且不泄漏敏感配置。
### 线下企微场景
- 创建覆盖平台/超级管理员本人绑定成功、未绑定、场景暂停、模板失效、企业/代理拒绝、目标店铺越权、金额 0/1/100000000/100000001、备注边界、附件 0/1/5/6、对象不存在、其他主体对象和元数据不符。
- 事务测试证明充值单、结构化附件、唯一企微实例和提交 Outbox要么全部存在要么全部不存在相同 `request_id` 不产生第二条审批。
- 状态自动化覆盖提交中、审批中、同意、拒绝、撤销、删除、通过后撤销、重复/乱序回调和轮询并发;拒绝后原单不可编辑或重提,新申请产生全新业务事实。
- 入账前撤销验证任务被阻止且充值关闭;入账后撤销验证余额不自动扣回、充值保持完成,并产生严重审计和平台异常通知。
- 真实企微至少使用平台本人绑定身份创建一笔独立线下代充值,上传真实 S3付款凭证完成 `applyevent`、人工同意、加密回调或轮询同步、钱包入账、唯一流水、到账通知和审计核对。回调沿用“企业微信 → 用户中转应用 → 本地服务”原样转发,后端仍完成验签解密。
- 真实企微不要求每次人工制造撤销、删除或通过后撤销,这些异常由可编程 WeCom Adapter 自动化稳定覆盖。
### 查询、权限、迁移和前端验收
- 查询覆盖默认分页、最大页大小、稳定排序、多个筛选 AND组合、代理本店/下级范围、平台范围、企业拒绝,以及列表/详情/支付状态/附件使用同一权限投影。
- 信息投影分别以平台和代理读取同一线下充值:平台在具备业务权限时可见完整审批资料,代理只能看到业务资料和最小审批摘要;任何响应不返回支付配置、通道或内部错误。
- 迁移演练覆盖支付状态盘点、默认值/注释/约束不一致、历史终态 legacy、可迁移待处理线下单、缺少平台绑定或附件的异常清单、重复执行幂等以及旧 `offline-pay``reject``resubmit` 路由确实不存在。
- 前端验收覆盖可用方式加载、每次主动新建、二维码渲染、无倒计时、3 秒本地轮询、页面隐藏暂停、支付成功/入账处理中/失败、余额刷新、线下表单、企微只读详情、审批与到账两类通知、加载/空/失败状态。
- 本地自动化不要求真实微信或支付宝。部署到测试环境后按生效配置手工完成一笔微信 H5/MWEB 和一笔支付宝 WAP 最低金额扫码,验证真实支付 URL、回调/查单、本地支付状态、钱包入账和到账通知;该人工联调不阻塞本地实现完成门禁,但属于上线前验收。
- 完成门禁至少包括目标单元/集成测试、相关包测试、全量 Go 测试、并发/竞态专项、静态检查、迁移演练、OpenAPI 生成校验、真实 S3和真实企微线下充值。新增 Handler 时同步两个接口文档生成入口。
## Out of Scope
- 不建设多支付通道池、通道优先级、权重轮询、自动切换或故障转移。
- 不让前端选择或提交支付通道、支付配置 ID、商户号或目标在线充值店铺。
- 不生成二维码图片,不新增二维码生成、重新生成、主动取消支付或手工关单接口。
- 不向后台在线充值返回 `expires_at`,不展示本地推算的精确倒计时,也不以本地时间直接关闭第三方支付单。
- 不复用相同金额或未过期支付单;也不因新单创建而自动取消旧单。
- 不在本地或 Agent 自动化中调用真实微信、支付宝;真实扫码由部署测试环境人工验收。
- 不接入微信/支付宝自动退款,不因充值状态 5 扩展本期退款流程。
- 不允许代理线下代充值,不允许平台/超级管理员替代理创建在线支付,不允许企业账号访问。
- 不为新充值支持 `bank`,不允许创建后修改金额、目标店铺或支付方式。
- 不保留本地确认入账、拒绝、退回、重提、操作密码或人工重复入账按钮。
- 不新增 `7=已退回`、支付“已关闭”状态或 `wallet_posting_status` 同义字段。
- 不向代理公开企微审批人、内部意见、审批人附件、支付配置或内部异常详情。
- 不借 UR#34 重构 C 端全部支付、全仓钱包、全局支付配置或未触碰的历史订单模块。
## Further Notes
- 当前代码已经有代理充值表、主钱包和流水、统一 `tb_payment`、微信 H5/MWEB/回调基础、支付宝 WAP 支付基础及支付配置,但代理充值创建只支持 `wechat/offline`,未创建支付单或返回付款内容;代理端复用现有网页支付 URL、支付查单补偿和两阶段入账需要补齐。
- 当前支付配置模型把微信提供方和支付宝参数放在同一条全局生效配置中。UR#34 按现状复用,不把它扩成多通道路由系统。
- 当前微信 v3 已有查单/关单能力,微信 v2 已补 MWEB 下单与查单;支付宝 SDK 具备 WAP、TradeQuery 和 TradeClose。支付方式可用性必须以本需求实际需要的 Adapter 能力为准,不能只判断某个字段非空。
- 当前 `tb_payment` 迁移写的是 `14`,运行代码和 DTO使用 `03`;这是实施前必须通过存量核查解决的历史一致性问题,不是新 Agent可以忽略的文档差异。
- 当前旧线下充值使用字符串 Key列表、操作密码和本地 `offline-pay/reject`。新实现以本 PRD 和 UR#37公共企微契约为准,旧实现仅用于迁移事实核对,不代表目标行为。

View File

@@ -1,219 +0,0 @@
# PRDUR#35 退款企微审批与整单终结
Status: ready-for-agent
---
## Problem Statement
当前退款流程把退款申请、平台内审批、退款金额修改、钱包回款、佣金回扣和套餐失效混在同一个旧 Service 中。系统仍提供本地通过、拒绝、退回和重提接口,审批人能够修改实际退款金额;审批通过后又用进程内 Goroutine 分别处理佣金与套餐,失败后既不能可靠恢复,也可能错误地把未完成的处理标记为完成。
现有退款创建接口还要求调用方提交订单实收金额,并允许用单条套餐使用记录限定退款范围。这样会把本应由订单事实决定的数据交给前端,也与本期确认的“金额可以小于实收,但一旦通过就按整张订单终结”相冲突。代理退款查询当前按创建账号隔离,上级代理无法在既有店铺层级数据范围内管理下级退款;平台内部审批资料又缺少按主体投影,存在向代理泄漏审批人、意见或审批附件的风险。
退款涉及真实资金、负余额、已发放佣金、套餐队列、资产状态和外部企微终态。系统必须明确区分申请金额、实际完成金额、企微审批状态和本地业务处理状态,并保证重复回调、重复 Worker、进程中断和局部失败不会造成重复回款、重复扣佣或伪造成功。
## Solution
保留现有退款创建、列表和详情资源,但把新退款申请接入 UR#37 提供的企业微信审批公共能力。创建时后端从订单读取实收金额及相关快照,只接收申请金额、原因、备注和 15 个本地对象存储附件;金额提交后固定,企微审批人只能同意或拒绝。退款单、唯一企微审批实例和提交 Outbox 在同一 PostgreSQL 事务创建,接口立即返回本地退款及“企微提交中”状态。
企微同意后,无论申请金额是否等于订单实收金额,都启动整单退款终结:订单变为已退款,该订单产生的有效套餐全部失效,相关佣金全部失效;已发放佣金从对应佣金钱包全额扣回,余额允许为负。只有代理主钱包支付订单自动按原扣款流水回溯原钱包;微信、支付宝、线下及个人资产钱包订单均由财务先在系统外完成退款,再在企微同意,系统不调用支付渠道退款,也不回充资产钱包。
审批结论和本地业务处理分开保存与展示。企微已经同意后,即使资金、佣金或套餐处理失败,审批状态和退款状态仍保持已通过,失败由独立处理状态、错误摘要和可靠 Worker 重试表达。企微拒绝即终结当前退款;业务人员修正问题后若仍需退款,必须新建退款单,不能编辑或重提原单。
## User Stories
1. 作为代理退款发起人,我希望使用订单事实创建退款,而不需要自行填写订单实收金额或选择某条套餐使用记录。
2. 作为平台退款发起人,我希望填写固定的申请退款金额、原因、备注和业务凭证,并在提交后立即获得本地退款单号。
3. 作为退款发起人,我希望系统在申请金额不大于订单实收金额时允许提交,并明确告诉我本次通过后会整单终结。
4. 作为退款发起人,我希望同一订单存在审批中或业务处理未完成的退款时不能再次创建,避免并发退款。
5. 作为平台账号,我希望通过本人绑定的企微成员发起审批,确保企微中的实际发起身份可追溯。
6. 作为代理账号,我希望无需绑定企微,由配置的固定成员代提交,同时审批表单仍展示我才是真实业务提交人。
7. 作为审批人,我希望在企微看到退款单号、订单实收金额、可退款区间、本次申请金额、原因、备注、附件和真实提交人。
8. 作为审批人,我只能同意或拒绝固定金额,不能在审批时修改退款金额。
9. 作为业务人员,我希望金额填写错误时拒绝当前单并重新创建,而不是改写已经提交的业务事实。
10. 作为财务人员,我希望微信、支付宝、线下及资产钱包订单先在线下完成人工退款,再通过企微表达已确认完成,无需回到系统点击第二次确认。
11. 作为代理主钱包所有者,我希望企微同意后资金准确退回当时真实扣款的钱包,而不是按当前代理关系猜测退款钱包。
12. 作为欠款代理,我希望退款可以自然冲减主钱包负余额,而不是因余额为负而拒绝入账。
13. 作为个人客户,我希望资产钱包支付订单仍可申请退款,但系统不会错误地把资金自动充回资产钱包。
14. 作为佣金归属代理,我希望订单退款时未发放佣金停止发放,已发放佣金被完整扣回,且每笔变化可追溯。
15. 作为佣金归属代理,我接受退款回扣后佣金钱包余额为负,以真实反映已经支取但现应追回的佣金。
16. 作为套餐使用者,我希望退款通过后该订单生成的有效套餐全部失效,主套餐失效时其加油包也一并失效。
17. 作为资产使用者,我希望退款套餐失效后系统尝试激活下一条排队主套餐;没有可激活套餐时停止资产使用。
18. 作为退款发起人,我希望看到申请金额与实际退款金额的区别,避免把审批通过误认为所有本地处理都已完成。
19. 作为退款发起人,我希望企微拒绝后原退款单保持只读,并能从订单重新进入创建流程发起一张新退款。
20. 作为代理管理者,我希望按既有店铺层级和退款查看权限看到本店及有权管理的下级店铺退款,而不是只能看到自己创建的记录。
21. 作为代理查看者,我希望查看退款凭证、真实业务提交人、审批状态和业务处理结果,但看不到平台内部审批人、意见或审批人附件。
22. 作为平台查看者,我希望只有在具备退款业务查看权限时才能读取完整企微审批时间线和附件,企微运营权限不能绕过业务权限。
23. 作为运维人员,我希望审批状态、退款状态和业务处理状态分别展示,能准确识别“企微已通过但本地处理失败”。
24. 作为运维人员,我希望资金、佣金、订单和套餐步骤失败后可以可靠重试,且已经完成的步骤不会重复执行。
25. 作为审计人员,我希望退款、订单、钱包、资金流水、佣金、套餐和企微实例之间可以完整关联,并保留金额与状态的前后事实。
26. 作为审计人员,我希望自动退款、佣金失效、通过后撤销和人工异常处置都有明确原因与操作者,而不是只依赖自由文本备注。
27. 作为系统维护人员我希望企微重复回调、回调与轮询并发、Worker 重投和进程中断都不会重复回款或重复扣佣。
28. 作为系统维护人员,我希望企微撤销、删除和通过后撤销进入明确异常处置,不会自动放行新的退款或自动冲正已经完成的资金。
29. 作为前端开发者,我希望创建、列表和详情返回稳定的退款、审批及处理契约,不需要在页面推断资金是否已经完成。
30. 作为验收人员,我希望实现阶段同时通过可重复的企微 Adapter 自动化和真实企微同意/拒绝链路,而不是把真实企微验证推迟到后续联调。
## Implementation Decisions
### 范围、依赖与领域口径
- UR#35 负责退款申请、退款终态消费、整单退款终结、退款 Query 和前端业务页面;企微连接、身份绑定、模板版本、附件上传、提交、加密回调、轮询和审批详情由 UR#37 的公共能力提供,退款模块不得自行实现第二套企微客户端或审批状态机。
- “申请退款金额”是提交企微的固定金额;“实际退款金额”只表示资金已经实际完成。两者不因为整单终结而自动改成订单实收金额。
- “整单退款终结”表示订单、该订单生成的套餐和该订单佣金资格全部终结,不表示必须按订单全额向客户退款。即使申请金额小于订单实收金额,也不保留差额的第二次退款权利。
- 一张退款单只对应一条企微审批实例。企微拒绝后的再次退款是新的业务事实,必须有新的退款 ID、退款单号、申请资料、提交人快照和企微审批实例。
- 退款创建、企微终态消费、代理钱包回溯、佣金失效和套餐失效属于复杂写用例,采用 Application UseCase、Domain、Repository 与 Infrastructure Adapter 分层;退款列表和详情采用 Query 通道。只迁移完成本需求所需的最小完整退款用例,不主动迁移未触碰的订单、钱包或套餐模块。
### 创建退款契约
- 保留 `POST /api/admin/refunds`。允许超级管理员、平台账号和代理账号发起;企业账号不允许发起。所有主体仍受既有认证、退款业务权限、订单数据范围和越权防护约束。
- 请求只包含:`order_id``requested_refund_amount`;必填 `refund_reason`(最多 1000 字);可选 `remark`(最多 500 字);必填 `attachments`15 项)。
- 每个附件项固定包含 `file_key``file_name``file_size`。附件必须来自当前本地私有对象存储授权范围,后端按现有存储规则校验对象存在、上传归属、文件类型和实际大小;请求中的名称与大小作为申请快照,不能替代对象元数据校验。
- 新请求不再接受 `actual_received_amount``package_usage_id`。订单实收金额、订单号、资产、店铺、买卖方、支付方式和必要的资金关联事实由后端从有权限访问的订单读取并固化快照。
- 申请金额必须满足 `0 < requested_refund_amount <= 订单实收金额`。只允许对已支付且尚未整单退款的订单创建。
- 创建前必须确认 `refund_approval` 场景、当前模板和本次企微发起身份可用。平台/超级管理员使用本人有效企微绑定;代理使用配置的固定代提交成员。任一前置不可用时,在写入退款、审批实例或 Outbox 前失败,前端保留当前表单。
- 同一订单存在提交中、审批中、已通过但业务处理未成功,或尚未完成异常人工处置的退款时拒绝创建。已拒绝退款不再占用活跃名额;已撤销、已删除或通过后撤销不能自动放行新建。
- 退款单、唯一审批实例和 `WeComApprovalSubmissionRequested` Outbox 必须在同一 PostgreSQL 事务创建。创建接口不等待远程企微,成功响应返回退款详情以及 `approval.status=0` 的“提交中”状态。
- 创建并发通过数据库唯一约束、状态条件或等价的持久化业务约束保证同一订单不会产生两张活跃退款Redis 只能作为快速防重,不能成为唯一正确性依据。
### 固定金额与企微表单
- 退款企微模板至少展示:退款单号、订单号、店铺、真实业务提交人、订单实收金额、可退款区间、本次申请退款金额、退款原因、申请备注和申请附件。
- 可退款区间在提交时固定为大于 0 且不超过订单实收金额;本次申请金额只读。企微审批人只能使用模板配置的同意或拒绝动作,不提供金额修改控件。
- 系统不提供本地审批按钮,也不向企微回写或伪造审批结论。认为金额错误时必须拒绝当前退款,再创建新退款。
- 申请附件的本地对象 Key 是权威资料;上传企微的 `media_id` 只是审批副本。审批人后续上传的附件属于企微审批资料,不能替代退款申请附件。
### 三套独立状态
- 退款业务状态为生命周期 `int``1=待审批``2=已通过``3=已拒绝``4=已退回``5=已撤销/审批已删除`。状态 4 只保留历史兼容,新企微审批不再产生“退回”。
- 本地业务处理状态为生命周期 `int``0=未触发``1=处理中``2=处理成功``3=处理失败`。各响应同时返回对应的 `processing_status_name`
- 企微审批状态完全复用 UR#37 公共常量:`0=提交中``1=审批中``2=已通过``3=已驳回``4=已撤销``5=通过后撤销``6=已删除``7=提交失败``8=提交结果未知`。退款模块不得复制或重新编号。
- 三套状态分别返回,禁止用退款状态或处理状态覆盖企微状态。所有状态 DTO 的 description 必须从公共 constants 原文复制,并提供对应中文名称字段。
- 企微驳回把退款状态从待审批条件更新为已拒绝,处理状态保持未触发;拒绝原因和企微时间线来自公共审批快照。
- 企微同意把退款状态条件更新为已通过,并触发可靠的退款终态 Outbox。即使后续本地处理失败退款状态和企微状态也不得降级或回改。
- 企微撤销或删除把退款状态置为 5记录异常原因并进入人工处置不自动执行资金动作也不自动允许新退款。
- 通过后撤销时,资金尚未执行则阻止后续资金任务;资金已执行则不自动冲正,保留实际退款金额,写 `critical` Audit Event 和站内告警,交由人工处理。
### 资金处理与实际退款金额
- `requested_refund_amount` 始终是申请金额。新增 `actual_refund_amount`,只在资金已实际完成时写入本次申请金额;未完成时为 `null`
- 微信、支付宝、线下及其他非代理主钱包支付订单,由财务在系统外完成退款后再同意企微。企微同意被视为财务已经确认完成,终态 Worker 写入 `actual_refund_amount=requested_refund_amount`;系统不调用支付渠道退款 API也不提供本地 `manual-complete` 二次确认。
- 个人客户使用资产钱包支付的订单同样由财务在系统外退款。系统不得调用现有资产钱包自动回充逻辑,不创建资产钱包退款流水;企微同意时记录实际退款金额并继续整单终结。
- 代理主钱包支付订单只按原订单扣款流水定位原主钱包和原资金关系,退回本次申请金额并写唯一退款流水。禁止根据当前店铺上下级关系、当前买卖方或当前钱包猜测退款目标。
- 如果代理订单缺少可核验的原扣款流水,资金步骤失败,`actual_refund_amount` 保持为空,处理状态为失败并记录可运维错误摘要;不得走历史兼容猜测分支。
- 代理主钱包余额增加与钱包版本、唯一退款流水、`actual_refund_amount` 和资金 Audit Event 在同一事务提交。钱包原余额为负时允许正常增加,结果自然冲减欠款。
- 资金已经完成但后续佣金或套餐处理失败时,`actual_refund_amount` 必须保留,不能因整单处理未成功而清空或重复退款。
-`approved_refund_amount` 只保留历史读取和迁移兼容,不再是新创建、企微表单或新公共响应中的业务概念。
### 订单、佣金与套餐整单终结
- 一旦企微同意,本次处理以整张订单为边界。资金步骤完成后,订单支付状态条件更新为已退款;申请金额小于实收金额时也执行同样更新,并禁止该订单再申请差额退款。
- 查找该订单产生的全部佣金记录。处于已冻结、解冻中、尚未发放或待人工修正的记录不移动钱包,直接改为已失效;处于已发放的记录先从对应佣金钱包全额扣回,再改为已失效,钱包允许变为负数。
- 佣金记录增加结构化失效事实:`invalid_reason` 至少支持 `order_refund``manual_resolution`;退款自动失效时保存 `invalid_refund_id``invalidated_at`,人工失效时另存人工操作者。不能只在 `remark` 中描述失效。
- 每条已发放佣金以“退款单 + 佣金记录”为业务防重键。佣金状态、佣金钱包余额和版本、回扣流水及统一 Audit Event 在同一事务提交;命中已完成防重事实时直接返回成功,不重复扣款或重复审计。
- 退款失效后的佣金不得被原有解冻、发放或补算任务重新发放。订单佣金流程的派生结论必须与全部佣金已失效保持一致。
- 失效该订单生成的所有仍有效套餐,不接受 `package_usage_id` 精确失效。命中主套餐时级联失效其加油包,并为套餐保存退款 ID、退款单号及失效时间等退款快照。
- 套餐失效完成后,按现有套餐队列规则尝试激活下一条待生效主套餐;没有下一条主套餐时按公共卡/设备状态写入能力停止资产。该过程复用套餐与卡状态领域能力,不在退款模块复制状态判断。
- 资金、订单、每条佣金和套餐步骤均需可单独识别已完成事实。任何步骤失败都把处理状态置为失败并由可靠 Worker 重试;只有全部必要步骤成功后才把处理状态置为处理成功。
### 可靠执行、幂等与审计
- 不再使用进程内 Goroutine 处理佣金或套餐。企微首次进入终态时由公共审批能力写业务 Outbox退款终态 Worker 使用 Asynq 执行;载荷只传结构化退款标识,不传预序列化字节、附件内容或密钥。
- Worker 通过处理状态、条件更新和有期限租约领取任务。未过期的处理中任务不能被第二个消费者重复执行;租约过期可恢复。失败摘要不得包含数据库连接、对象存储签名 URL、企微密钥或原始第三方响应。
- 退款终态事件、资金回款、佣金回扣、套餐失效和处理成功都必须各自具备持久化业务幂等事实。Redis 锁可以减少并发,但不能代替数据库条件更新、唯一约束和钱包乐观锁。
- 审计使用全局统一 Audit Event不新建退款私有审计表。至少关联退款、订单、审批实例、钱包、钱包流水、佣金记录和套餐使用记录并记录操作来源、真实业务提交人、系统执行身份、失效原因、金额和状态前后值。
- 钱包余额/版本/流水与其资金 Audit Event 同事务;佣金状态/钱包/回扣流水与其 Audit Event 同事务;订单及关键退款状态变更与对应 Audit Event 同事务。重复任务命中已完成事实时不新增第二条等价审计。
- 外部企微调用、附件上传、回调和详情同步使用公共 Integration LogIntegration Log 只记录接口、耗时、企微错误码/摘要和关联标识,不记录 Token、Secret、EncodingAESKey、完整对象 Key、临时 `media_id` 或文件内容。
### 权限、数据范围与信息投影
- 移除当前代理按 `creator` 隔离退款的专属规则。代理主账号及店铺内具备退款查看权限的账号按既有店铺层级数据范围读取本店及可管理下级店铺退款。
- 列表、详情、业务附件下载和导出必须复用同一主体权限投影。资源不存在与无权访问统一返回禁止访问语义,不能借错误差异探测其他店铺退款。
- 代理可见:退款业务资料、申请附件、订单及金额快照、真实业务提交人、企微审批状态和时间、业务处理状态及面向业务的失败提示。
- 代理不可见:企微审批人、内部意见、审批人上传附件、企微内部成员标识、模板内部映射及运维错误详情。
- 平台账号和超级管理员仍必须具备退款业务查看权限,才可读取完整审批人、意见、时间线和审批附件。企微审批运营或异常恢复权限本身不能绕过退款业务数据权限。
- 附件返回受保护的业务附件引用及下载能力,不把对象存储签名 URL 或企微临时 `media_id` 作为永久字段。历史导出或已获得的附件引用也不能绕过当前权限。
### API 与查询契约
- 保留 `POST /api/admin/refunds``GET /api/admin/refunds``GET /api/admin/refunds/{id}`
- 下线并不再注册:`POST /api/admin/refunds/{id}/approve``/reject``/return``/resubmit`,以及任何本地人工退款确认接口。不能保留隐藏兼容入口。
- 列表继续使用 `page``page_size`、退款状态、订单、店铺和资产标识等既有筛选;默认第 1 页、每页 20、最大 100默认按创建时间倒序并以 ID 作为并列排序键。所有筛选按 AND 组合,空参数不改变原查询。
- 创建成功的 `data` 与详情使用同一退款详情结构。列表项固定返回退款 ID/单号、订单与资产快照、店铺、真实业务提交人、订单实收金额、申请金额、实际退款金额、退款状态及名称、企微来源/状态及名称、当前审批人摘要和处理状态及名称。代理投影中的当前审批人摘要固定为空。
- 详情返回完整退款业务资料、申请附件、订单/支付快照、`approval` 对象以及根级处理状态。申请附件项返回 `file_key``file_name``file_size` 和受保护下载能力;不能返回对象存储永久地址或企微 `media_id`
- `approval` 固定包含 `source``approval_instance_id`、可空 `sp_no``status``status_name``template_version`、真实业务提交人、状态更新时间和 `business_process_result`;平台完整投影另包含审批人、意见、审批附件和时间线,代理投影不返回这些内部字段。
- 详情根级处理字段至少包含 `processing_status``processing_status_name`、面向当前主体脱敏后的 `processing_error`、开始时间和完成时间。审批状态、退款状态和处理状态不得合并成单个前端状态。
- 历史本地审批返回 `approval.source=legacy`,不伪造 `sp_no`、审批节点或企微时间线。新企微退款固定返回 `approval.source=wecom`
- 所有接口使用统一 `{code,msg,data,timestamp}` 响应和项目分页结构。错误码固定复用当前公共语义:请求字段、金额或附件元数据非法使用 `CodeInvalidParam=1001`;未登录使用 `CodeUnauthorized=1004`;订单或退款不存在与越权统一使用 `CodeForbidden=1005`;订单并非已支付、已经整单退款或退款状态不允许使用 `CodeInvalidStatus=1050`;活跃退款及并发重复创建使用 `CodeConflict=1007`;对象不存在/类型非法分别使用 `CodeStorageFileNotFound=1093``CodeStorageInvalidFileType=1095`;场景或模板不可用、代理固定成员不可用使用 `CodeServiceUnavailable=2004`。平台本人未绑定使用 `CodeInvalidStatus=1050` 和稳定消息“请先绑定企业微信”前端结合本人绑定查询显示绑定入口。任何错误均不得透传底层企微、数据库、Redis、对象存储或验证器信息。
### 前端页面与交互
- 退款创建表单只展示订单、申请退款金额、必填原因、可选备注和 15 个附件。订单实收金额及“通过后整单终结”的提示由后端订单/退款契约展示,不允许前端提交或覆盖实收金额。
- 提交中禁用重复提交;创建成功后进入退款详情,显示企微“提交中”。场景暂停、平台账号未绑定、代理固定成员不可用或附件校验失败时保留全部表单内容,并展示后端明确原因;平台账号未绑定时可原地进入 UR#37 的本人绑定流程。
- 退款详情分为“退款业务信息”“企微审批信息”“业务处理结果”三个稳定区域。审批与处理状态各自有加载、空、失败和刷新表现。
- 页面不显示本地通过、拒绝、退回、重提、审批金额修改或人工退款确认按钮。企微拒绝后只读展示原因;若仍需退款,从订单重新进入创建页,不在旧退款详情中编辑。
- 对非代理钱包订单明确提示财务必须先在系统外完成退款再同意企微;对代理钱包订单提示同意后系统自动回溯原主钱包。
- 处理失败时代理只看到可行动的业务提示,平台按权限看到脱敏错误摘要和“系统重试中/联系管理员”;不得提供会重复执行资金动作的前端按钮。
- 通过后撤销等高风险异常使用明显告警,并说明资金不会自动冲正。代理与平台页面严格遵守各自审批资料投影。
### 数据迁移、发布与回滚
- 退款记录补充唯一审批实例引用、真实提交人显示快照、结构化申请附件、申请备注、实际退款金额、处理状态、错误摘要、处理开始/完成时间及处理租约/版本等可靠执行字段。数据库不建立外键,也不使用 GORM 关联标签。
- 现有 `actual_received_amount` 继续作为后端生成的订单实收快照;新接口不再接受客户端值。现有 `package_usage_id``approved_refund_amount` 仅保留历史兼容,新退款不写入且整单处理不读取。
- 现有 `remark` 若包含历史审批备注,不直接改写语义;新申请备注使用明确的申请备注字段。新附件使用结构化元数据保存,旧 `refund_voucher_key` 只读兼容,并在对象仍存在时投影为历史业务附件。
- 佣金记录补充结构化失效原因、关联退款、失效时间和必要的人工操作者字段;已发放佣金回扣流水建立退款与佣金记录级唯一业务约束。
- 停机发布前盘点待审批、已通过但旧异步标记未完整、已拒绝、已退回及历史终态退款。历史终态保留为 `legacy`;待审批记录按 UR#37 迁移规则创建真实企微审批,缺少平台绑定、代理固定身份、附件或真实提交人事实的记录进入明确的迁移待处理清单。
- 历史状态 4 保持“已退回”只读,不自动创建新审批或改为拒绝。历史已通过但资金、佣金或套餐事实不一致的记录先进入对账与人工处置,不允许迁移脚本猜测已完成。
- 发布顺序必须先具备 UR#37 公共企微能力、真实模板映射、平台绑定和代理固定成员,再启用退款创建与终态 Worker旧本地审批路由在同一停机窗口移除。
- 应用回滚必须保留已形成的退款、企微实例、Outbox、Integration Log、Audit Event、资金流水和失效事实。已经进入真实企微的退款不得恢复旧本地审批按钮只能继续同步、重试安全的本地步骤或人工处置。
## Testing Decisions
- 最高公共自动化接缝为Fiber HTTP 路由与真实认证 → Refund Application/Domain/Query → GORM → 现有测试 PostgreSQL → Outbox/公开 Worker Handler → 钱包、佣金、套餐和统一审计;企业微信网络边界使用可编程 WeCom Adapter。测试只断言公开响应、数据库业务事实、资金流水、状态和审计不断言私有函数或目录结构。
- 本地开发和 Agent 自动化测试必须使用 Redis DB 7已部署测试环境继续使用 DB 6。测试入口在启动前读取并验证 Redis Client、Asynq Client 与 Worker Server 的实际 DB任一不是 7 就立即失败。禁止向 DB 6 投递任务,禁止对 DB 7 执行 `FLUSHDB`
- PostgreSQL 沿用现有测试库,不新建数据库。每次运行使用唯一标识创建隔离订单、店铺、钱包、佣金、套餐和退款夹具,只按实际创建 ID 精确清理;禁止 `TRUNCATE`、清表、模糊删除或修改既有业务数据。
- 对象存储直接使用现有真实 S3不建立内存替身。测试使用唯一 Key 上传申请附件,验证元数据、下载及企微附件提交,结束时只删除本次创建对象;真实上传、读取或删除失败均使测试失败。
- 自动化测试捕获待投递任务后直接调用公开 Worker Handler不通过 `sleep` 等待后台 Worker。实现需沉淀可复用的环境守卫、唯一夹具、精确清理、真实 S3 和 Worker 驱动 Harness并提供真实 HTTP/curl 冒烟模板curl 不替代 Go 自动化断言。
- 创建 HTTP 测试覆盖三类允许账号、企业账号拒绝、订单越权、订单不存在、未支付/已退款订单、金额为零/负数/超过实收、缺少原因、备注超长、附件数量/元数据/归属错误、场景不可用、平台未绑定、代理固定成员失效和并发重复创建。
- 创建事务测试证明退款、唯一企微实例和提交 Outbox 要么全部成功,要么全部不存在;远程企微尚未响应时接口仍只产生一组本地事实。
- 固定金额测试证明申请金额写入后不会被企微详情、重复回调或终态 Worker 修改;企微表单包含实收金额、可退款区间和申请金额,审批动作不接受金额字段。
- 状态测试覆盖企微提交中、审批中、通过、拒绝、撤销、删除、通过后撤销、提交失败和结果未知,并证明退款状态、企微状态与处理状态独立。历史已退回只读且旧重提路由不存在。
- 非代理钱包测试覆盖微信、支付宝、线下和个人资产钱包:企微通过后记录实际退款金额,不调用任何渠道退款或资产钱包回充,不生成对应自动退款流水,但仍执行订单、佣金和套餐整单终结。
- 代理钱包测试使用真实钱包与版本字段,覆盖按原扣款流水退款、负余额冲减、缺失原扣款流水、重复 Worker、并发 Worker、余额更新后崩溃恢复和唯一退款流水证明系统不按当前代理关系猜测钱包。
- 整单语义测试至少包含“申请金额小于订单实收金额”,验证只退申请金额但订单仍标记已退款、该订单全部有效套餐及佣金均终结,且不能再申请剩余差额。
- 佣金测试覆盖已冻结、解冻中、已发放、已失效和待人工修正;已发放记录全额扣佣金钱包并允许负数,其他未发放状态不动钱包;验证结构化失效字段、唯一回扣流水、同事务 Audit Event 和原发放任务不能复活记录。
- 套餐测试覆盖订单生成的待生效/生效中等有效主套餐、加油包级联、下一主套餐激活、无下一套餐时停止资产、重复处理、处理中崩溃和状态写入失败恢复;不得使用单个 `package_usage_id` 缩小范围。
- 局部失败测试依次制造资金失败、佣金中途失败、套餐失效失败和资产状态失败。资金未完成时实际退款金额为空;资金完成后的下游失败保留实际退款金额;修复后重试只补未完成步骤,最终不产生重复资金或审计。
- 权限测试对同一退款使用本店代理、上级代理、无管理关系代理、具备业务权限平台、仅具备企微运营权限平台和超级管理员读取,验证店铺层级范围及平台业务权限。列表、详情、附件下载和导出必须给出一致投影。
- 信息投影测试证明代理可见业务凭证、真实提交人、审批状态和处理结果,但看不到审批人、内部意见或审批人附件;有退款业务权限的平台可以看到完整审批详情,无业务权限的平台即使有企微运营权限也不能读取。
- 可靠性测试覆盖 Outbox 重投、Asynq 重投、租约过期、回调与轮询并发、重复终态、乱序状态、乐观锁冲突和处理成功后再次消费。每个外部终态只触发一次业务处理,完成步骤不重复。
- 可编程 WeCom Adapter 自动化必须覆盖真实企微难以稳定制造的超时、明确失败、响应丢失、重复/乱序回调、撤销、删除、通过后撤销、并发轮询和限流。加密回调自动化仍从真实 HTTP 回调入口进入,使用测试 Token/AES Key 生成协议密文并验证签名、解密和 CorpID不能绕过协议直接调用内部同步函数。
- 真实企微验收是 UR#35 实现完成门禁,不得推迟到 INT-06。真实参数、模板和密钥只通过环境变量或安全配置提供不写入 Spec、源码、日志或报告回调按“企业微信 → 用户提供的中转应用 → 本地服务”进入,中转应用原样转发企微查询参数与请求体,后端仍执行完整验签、解密和 CorpID 校验。
- 真实企微至少执行两张独立退款:其一由代理创建,使用固定企微成员代提交并人工同意,建议选择代理主钱包订单,同时验证真实附件上传、`applyevent`、加密回调、钱包回溯、佣金、套餐、订单和审计;其二由平台账号创建,使用本人绑定企微身份并人工拒绝,验证轮询兜底、退款终结且不触发资金或套餐处理。
- 真实企微验收采用可复用两阶段流程:阶段一创建唯一隔离夹具并通过真实 HTTP 创建退款,输出运行 ID、退款号与 `sp_no`;人工在企微同意或拒绝;阶段二等待或主动驱动公共同步/轮询和退款 Worker再自动核对数据库、钱包流水、佣金、套餐、审批实例、Audit Event 与幂等结果,并生成不含敏感信息的通过/失败报告。
- 真实企微每次不强制人工制造撤销、删除或通过后撤销,这些异常由可编程 Adapter 自动化覆盖。真实验收必须证明同意、拒绝、真实附件、两类发起身份、真实加密回调和回调缺失时的轮询兜底。
- 前端人工验收覆盖创建表单保留、本人绑定入口、代理代提交、列表和详情三状态分区、代理/平台投影、拒绝后新建、处理失败、通过后撤销高风险告警以及所有加载、空、失败和无权限状态。
- 完成门禁同时要求:相关 Go 自动化、数据库和真实 S3 测试通过;可编程 Adapter 异常矩阵通过;两条真实企微验收通过;迁移演练、旧路由不存在、生成的 OpenAPI、前端接入和数据核对均通过。INT-06 只做跨需求与前后端复验,不能替代本需求真实企微门禁。
## Out of Scope
- 不建设本地审批流、审批任务、审批节点配置、审批按钮或本地“待我审批”。
- 不允许企微审批人修改退款金额,也不保留 `approved_refund_amount` 作为新业务字段。
- 不提供原退款单编辑、退回修改、重提或拒绝后的复用;再次退款必须新建。
- 不调用微信、支付宝或其他支付渠道的自动退款 API。
- 不自动回充个人资产钱包,不把资产钱包退款扩展为资金渠道能力。
- 不提供非代理钱包的本地人工退款确认按钮或 `manual-complete` 接口。
- 不自动冲正通过后撤销前已经完成的资金、佣金或套餐动作。
- 不允许一张订单通过多张退款单拆分退款,也不保留申请金额之外差额的后续退款权利。
- 不向代理公开审批人、内部意见、审批人附件或企微内部标识。
- 不在 UR#35 重复实现 UR#37 的连接配置、账号绑定、模板发布、回调协议、轮询调度或异常恢复后台。
- 不在本需求迁移无关订单、钱包、佣金、套餐或卡状态模块的全部旧代码。
## Further Notes
- 当前代码仍要求前端提交实收金额和可选套餐使用记录,仍注册本地通过、拒绝、退回与重提接口;实现必须以本 Spec 为准删除这些新流程入口,不能把当前行为当成兼容要求。
- 当前代理退款查询按创建账号过滤与已确认的店铺层级查看范围冲突Query 改造必须覆盖列表、详情、附件和导出,不能只改列表。
- 当前代理钱包退款在找不到原扣款流水时会按关系猜测钱包,个人资产钱包会自动回充;两条兼容分支都与本 Spec 冲突,必须从新退款终态用例中移除或隔离。
- 当前佣金和套餐后处理使用进程内 Goroutine并可能在部分失败后写完成布尔值这些布尔值只能用于历史对账不能作为新处理链路的可靠幂等事实。
- 当前套餐失效能力在不传 `package_usage_id` 时已经能够按订单查找目标并级联主套餐加油包,可作为迁移时的行为参考,但必须纳入可靠 Worker、状态条件、审计和下一套餐/资产状态闭环。
- UR#37 必须先提供可调用的公共企微契约UR#44 列表摘要、UR#42 退款附件导出和 UR#57 退款中禁止换货应消费本 Spec 的状态与权限投影,不得自行定义另一套“活跃退款”或审批信息。
- 本需求同时触及企微、资金、佣金、套餐、权限、迁移和真实环境验收,预计超过一个高质量实现上下文。进入实现前应基于本 Spec 评估并拆分窄的端到端 tracer-bullet tickets拆分不能按 Model、Service、Handler 和测试做水平切层。

View File

@@ -1,290 +0,0 @@
# PRDUR#36 批量订购套餐
Status: ready-for-agent
---
## Problem Statement
运营人员需要根据一份离散资产清单批量为卡或设备订购套餐。现有后台单笔下单只能一次处理一个资产,无法提供批次进度、逐行结果、部分成功恢复和稳定幂等;原始需求中把整批理解为单一代理,也无法覆盖同一文件包含多个代理名下资产的真实场景。
本需求需要在不弱化现有单笔订单规则的前提下,增加 CSV 直传、异步解析和逐行下单能力。批次只统一支付方式,不统一代理:系统在处理每一行时根据资产当前归属确定结算代理,再校验该代理的套餐授权、成本价和主钱包。文件结构错误必须整批失败且不产生订单;资产、套餐、归属、重复行和余额等业务错误允许逐行失败。
## Solution
新增“批量订购套餐”入口。操作员在创建批次时选择一个套餐和一种支付方式,前端通过现有对象存储预签名接口把只含资产标识的 CSV 和线下凭证直传真实私有 S3再提交唯一 `request_id``package_id`、整批支付方式和稳定对象 Key 创建任务;请求不包含 `shop_id`,也不上传文件字节。
创建接口完成对象归属、类型和 10MB 大小校验后立即返回任务。Worker 下载完整 CSV先完成文件级校验并持久化逐行明细再严格按 CSV 行号执行。每个有效业务行在独立事务中解析当前结算代理、复用统一套餐可售策略和订单领域,以 `wallet` 扣结算代理主钱包,或以 `offline` 创建已支付订单。任务和明细使用状态条件、处理租约及稳定幂等键保证重复消费不会重复下单或扣款。
## User Stories
1. 作为平台运营人员,我希望上传一份 CSV 为多张卡或设备订购套餐,而不是逐笔创建订单。
2. 作为平台运营人员,我希望同一 CSV 可以包含不同代理名下的资产,不需要预先按代理拆文件或在页面选择代理。
3. 作为平台运营人员,我希望整批明确选择钱包或线下支付,避免一份文件混合不同支付语义。
4. 作为运营人员,我希望 CSV 只填写资产标识,由系统统一识别卡或设备及其支持的标识形式。
5. 作为平台运营人员,我希望文件结构错误整批失败且不产生订单,而单行业务错误不影响其他合规行。
6. 作为平台运营人员,我希望任务刷新后仍能恢复进度,并按行查看结算代理、金额、订单和中文失败原因。
7. 作为财务人员,我希望线下凭证作为批次业务资料永久保留,但本期不把它扩展为财务核销系统。
8. 作为代理,我希望批量钱包订购只扣我的主钱包,并继续遵守我的套餐授权、成本价和信用额度规则。
9. 作为审计人员,我希望每一行都能追溯原始标识、规范资产、结算代理、套餐、金额和最终订单。
10. 作为系统维护人员,我希望重复 HTTP 请求、重复 Worker 消费和进程中断恢复都不会产生重复订单或重复扣款。
## Implementation Decisions
### 范围与领域口径
- “批量订购批次”是一份选择单个套餐并使用统一支付方式的 CSV 订购任务,不绑定单一代理。
- “结算代理”是每一行处理时根据资产当前归属解析出的代理;套餐授权、成本价、钱包、订单买卖方快照和失败原因均以该代理为准。
- `shop_id` 不是创建参数、CSV 字段或前端隐含参数。后端即使收到未知字段也不能据此改变结算代理。
- 批量用例复用 Order 领域和统一 Wallet Domain只迁移完成本用例所需的最小完整复杂写边界列表、任务详情和明细使用 Query 通道。
- 不复制现有巨大订单 Service 的宽松分支。需要把资产、套餐、可售策略、定价、订单、套餐生效和钱包扣款收口为可由单笔后台下单与批量单行命令共同调用的领域/Application 能力。
- 本需求不改变现有单笔后台下单 HTTP 接口,也不把未触碰的订单模块一次性整体迁移。
### 入口与现有认证边界
- 不新增 `order:bulk_purchase` 或其他批量订购权限码,不在后端增加超级管理员、平台、代理或企业类型的显式拦截。
- 页面是否展示入口由前端现有菜单与可见性规则决定;能够通过现有后台认证调用接口的主体,后端视为可以使用该能力。
- 创建、任务详情和明细接口只复用现有后台路由认证及公共数据范围机制,不额外建立任务创建人隔离或新的 RBAC 判断。
- “无新增后端权限拦截”不等于跳过业务规则。每一行仍必须校验资产当前归属、结算代理、所选套餐授权、可售状态、定价和钱包。
- 创建任务成功、任务终态及每个成功订单均写公共审计。审计包含任务号、支付方式、文件安全摘要、凭证数量、汇总、操作者及逐行关联,不记录预签名 URL、鉴权令牌或环境密钥。
### CSV 契约
- 只接受 `.csv`,不接受 `.xlsx``.xls` 或把 Excel 文件改扩展名后的内容。
- 编码固定为 UTF-8可带 UTF-8 BOM换行允许 LF 或 CRLF。
- 前端随版本发布静态模板,后端不增加模板下载接口。建议模板文件名为 `批量订购套餐模板-v1.csv`
- 固定且唯一的表头为单列 `资产标识`。缺列、多列、重复列或未知列均为文件级错误CSV 不包含资产类型、套餐编码或套餐名称。
- 套餐由创建任务请求中的单个 `package_id` 决定,整个批次的每一行都订购该套餐。
- 资产标识去除首尾空白后交给系统统一资产解析能力,解析结果包含资产类型、资产 ID 和规范标识。批量订购不得另写一套卡/设备识别分支或维护自己的标识白名单。
- 当前统一解析能力支持 ICCID、卡 `virtual_no`、MSISDN、设备 `virtual_no`、IMEI 和 SN未来统一解析能力新增或修正标识规则时批量订购自动复用。
- 空资产标识、未命中或无法唯一解析属于行级失败;禁止猜测或自动修复科学计数法、控制字符等被破坏的数据。
- 单个文件最大 10MB数据行最多 1000 行。空文件、只有表头、非法 UTF-8、CSV 引号语法错误、表头错误或第 1001 行出现,均使任务整体失败且不创建任何订单。
- Worker 必须先完成整个文件的结构和行数校验,再开始任何订单写入;不能边解析边下单后才发现文件级错误。
- 文件有效后,每一个数据行都持久化为明细。业务无效行、空字段行和重复行仍占用原始行号并计入失败,确保 `total_count = success_count + fail_count`
### 资产解析与文件内判重
- Worker 通过统一资产解析能力得到唯一资产类型、资产 ID 和规范标识后再判定文件内重复,不能直接按用户填写的字符串判重。
- 明细同时保留用户原始资产标识和统一解析结果;规范标识的选取规则属于统一资产能力,批量订购不自行决定卡或设备的标识优先级。
- 未命中或命中多个资产时该行失败;统一资产解析能力必须保证唯一结果,不能使用 `First` 任取一条。
- 文件内重复键为统一解析得到的 `资产类型 + 资产ID`。整个批次只有一个套餐,因此重复键不再包含套餐。
- 重复键首次出现的行正常进入业务处理,后续行失败并在 `failure_reason` 中指出首次出现的 CSV 行号。
- 本期不把重复行解释为数量;未来需要多份订购时再增加明确数量字段。
- 统一资产解析能力必须提供适用于批量调用的接口,避免逐行跨表查询形成 N+1历史脏数据多命中必须明确失败不能由批量用例任取一条。
### 对象存储与上传归属
- 复用 `POST /api/admin/storage/upload-url`,新增用途 `bulk_purchase`。该用途只生成批量订购目录下的 `.csv` KeyContent-Type 固定为允许的 CSV 类型。
- 线下凭证继续使用 `attachment` 用途;允许项目现有支持的图片或文件类型,数量沿用后台订单凭证上限,当前为 1 至 5 个。
- 前端使用预签名 URL 直接 `PUT` 到当前真实私有 S3业务接口只接收稳定 `file_key``voucher_keys`,不接收 multipart 或字节流。
- 现有存储 Provider 缺少对象元数据和上传主体证明。实现必须补充对象元数据查询能力,至少返回对象是否存在、实际大小和 Content-Type同时保存预签名上传授权记录包含 Key、purpose、申请人账号、声明文件名/类型和签发时间。
- 创建任务时校验 CSV Key 来自 `bulk_purchase` 用途、上传授权属于当前账号、对象真实存在、实际大小不超过 10MB、扩展名和 Content-Type 合法。凭证 Key 也必须属于当前账号的附件上传授权并真实存在。
- 上传授权在任务创建事务中绑定到该任务。同一 Key 可以随相同 `request_id` 的幂等重试返回原任务,但不能被另一个批次或另一个账号再次绑定。
- 预签名 URL 和永久对象 Key 是不同概念。任务只保存稳定私有 Key不保存会过期的上传 URL查询时按现有受权下载机制展示凭证。
- 源 CSV 和凭证按对象存储统一生命周期保留。Worker 下载失败属于任务级基础设施失败;已经确定的订单结果不因后续对象清理失败而回滚,但必须记录告警。
- 自动化测试直接调用真实 S3不实现或保留内存 Provider 替身。测试使用本次运行的唯一对象 Key并在结束时只删除自己创建的对象真实上传、Head、下载或删除失败都必须使相应测试失败。
### 创建任务 API
- `POST /api/admin/bulk-purchases` 使用 JSON 请求:
```json
{
"request_id": "01J...",
"package_id": 1001,
"payment_method": "wallet",
"file_key": "bulk-purchase/2026/07/unique.csv",
"voucher_keys": []
}
```
- `request_id` 必填,最大 64 字符,由前端为一次用户提交生成全局唯一值。`package_id` 必填且大于 0一个任务只接受一个套餐。`payment_method` 只允许复用现有后台订单枚举 `wallet|offline`,不新增 `agent_wallet`
- 创建任务时加载并快照所选套餐的 ID、编码和名称确认套餐存在各结算代理是否拥有授权、价格是否有效及当前是否可售仍在逐行处理时按最新事实校验。
- `wallet``voucher_keys` 必须为空;`offline` 时必须提供 1 至 5 个凭证 Key。一个批次不能混合支付方式。
- 请求 DTO 不声明 `shop_id`,创建接口若检测到显式提交 `shop_id` 必须返回参数错误,不能忽略后让调用方误以为它参与了结算;其他未知字段延续项目统一 JSON 兼容策略,但不参与业务决策。
- `request_id` 建立数据库唯一约束。相同 `request_id`、相同操作者和相同请求指纹重复提交时返回原任务,不重复绑定文件、创建任务或投递消息。
- 相同 `request_id``package_id`、支付方式、文件 Key、凭证 Key 或操作者不同,返回冲突错误,不能静默返回语义不同的旧任务。
- 创建事务保存任务、上传授权绑定和可靠任务事件。Asynq 不可用时不能丢失已提交任务;数据库中的待处理任务/事件是事实源,由投递器重试发送。
- 成功响应至少返回 `task_id``task_no``request_id`、所选套餐快照、`payment_method``status``status_name``created_at`。成功只表示任务已接收,不表示 CSV 已通过或订单已创建。
### 任务与明细查询 API
- `GET /api/admin/bulk-purchases/{task_id}` 返回任务汇总,至少包括:
- 任务 ID、任务号、请求 ID、所选套餐 ID/编码/名称快照、支付方式。
- 状态与中文状态名、源文件名、凭证数量和有权预览所需的附件引用。
- 总行数、已处理数、成功数、失败数、涉及代理数。
- 全部有效成功行金额合计;金额单位固定为分,类型为 `int64`
- 任务级错误码与中文原因、操作者快照、创建/开始/完成时间。
- `GET /api/admin/bulk-purchases/{task_id}/items` 支持 `page``page_size``status``asset_identifier`
- 默认第 1 页、每页 20 条,`page_size` 最大 100。
- 默认按 `row_no ASC`,不允许前端改变业务处理顺序。
- `status` 只允许明细状态枚举;多个筛选参数使用 AND 组合。
- `asset_identifier` 去除首尾空白后,对该任务内的用户原始标识或规范标识做精确匹配,不做跨任务搜索。
- 明细响应至少包括:行号、解析后的资产类型、原始资产标识、解析资产 ID、规范标识、结算代理 ID/名称快照、金额、状态与中文名、订单 ID、失败码、中文失败原因和处理时间套餐信息统一来自任务快照不保存不存在的逐行套餐输入。
- 查询仅返回业务安全信息,不返回 SQL、底层错误、对象存储永久凭证、Redis Key 或其他代理的钱包余额。
- 所有接口使用统一 `{code,msg,data,timestamp}` 响应。参数校验统一返回参数错误;任务不存在返回统一不存在错误;现有后台认证失败沿用公共认证错误;同一请求 ID 的不同载荷、文件已绑定等返回冲突;对象不存在、类型错误、文件过大和存储失败复用或补齐统一存储错误码。
### 数据模型与索引
- 新建独立任务表和逐行明细表,不复用语义不同的导入、导出或设备批量分配表;不建立数据库外键或 GORM 关联标签。
- 任务至少保存:任务号、`request_id`、请求指纹、源文件 Key/名称/类型/大小、所选套餐 ID/编码/名称快照、支付方式、凭证 Key 快照、操作者账号/类型/名称快照、涉及代理数、总数/已处理数/成功数/失败数、成功金额、状态、任务级错误码/原因、任务租约持有者/到期时间、开始/完成时间和公共审计字段。
- 明细至少保存:任务 ID、CSV 行号、统一解析得到的资产类型、原始资产标识、解析资产 ID、规范标识快照、结算代理 ID/名称快照、金额、状态、订单 ID、失败码/原因、幂等键、处理租约和处理时间。
- 上传授权记录需要稳定保存 Key、purpose、申请账号、声明元数据、绑定业务类型/ID和绑定时间以便创建接口证明“属于当前上传主体”不通过可猜测目录前缀代替归属校验。
- 任务号、`request_id` 和上传授权 Key 使用有效记录唯一索引;明细使用 `(task_id,row_no)``idempotency_key` 唯一索引,并为 `(task_id,status,row_no)` 建查询索引。
- `request_id` 是全局唯一;行幂等键固定为 `bulk_purchase:{task_id}:{row_no}`
- 状态类字段使用 `int`,类型/方式类使用 `string`。任务复用全局异步任务状态常量,不定义 Bulk 私有任务状态;逐行明细使用公共行处理状态。支付方式、资产类型、失败码和 Redis Key 生成函数定义到公共常量包,并添加中文注释。
- 不需要迁移或回填历史订单。新表上线前为空;已成功生成的标准订单继续按现有订单事实保留。
### 状态机与任务恢复
- 全局异步任务状态统一为:`1:待处理, 2:处理中, 3:已完成, 4:已失败, 5:已取消`;任务响应必须包含对应 `status_name`
- 文件有效并完成全部行业务处理后,任务固定为 `3:已完成`。全部成功、部分成功或全部业务行失败由 `success_count``fail_count` 表达,不创建“部分成功”状态。
- 文件级校验失败、源对象下载失败或无法恢复的任务级基础设施错误进入 `4:已失败`。文件级失败不得创建订单;是否保留零条明细由错误发生阶段决定,并通过任务错误说明。
- 逐行明细不是独立异步任务,使用公共行处理状态:`1:待处理, 2:处理中, 3:成功, 4:失败`,并返回对应 `status_name`;不为明细增加没有业务语义的取消状态。
- 本期任务没有取消入口,但保留全局任务状态码 `5:已取消`,不把它改作其他含义。
- Worker 使用状态条件和带过期时间的租约领取任务。只有待处理或租约已过期的处理中任务可被领取;终态不可被重新执行。
- 文件校验通过后在开始下单前一次性持久化全部明细。Worker 重启时读取现有明细继续,而不是重新生成不同的行号或幂等键。
- 每个明细也通过条件更新或行锁领取。成功/失败终态不可被另一消费者覆盖;租约过期的处理中明细根据事务事实安全恢复。
- 任务汇总始终从明细表重新聚合,不信任进程内累加值。`processed_count = success_count + fail_count`,终态时等于 `total_count`
- Asynq 载荷只传结构化 `task_id`,不得传预序列化 `[]byte`、CSV 字节、临时路径、凭证内容或认证上下文。
### 严格行序与逐行业务流程
- 文件通过后严格按 `row_no ASC` 串行推进业务结算。可以批量预加载只读数据,但不得并发执行钱包扣款或改变行序结果。
- 每行处理时重新读取资产当前归属,不使用创建任务时的归属快照;结算代理不存在、归属异常或没有有效主钱包时只失败当前行。
- 解析出的结算代理必须拥有对应套餐当前有效授权;金额使用该代理当前授权成本价和现有订单定价规则,不使用 CSV 套餐名称或前端金额。
- 每行复用统一套餐可售策略。批量订购不是个人本人续费入口,不能利用后台或批量身份绕过下架限制;渠道下架套餐固定拒绝。
- 继续执行现有后台订单中适用于该支付方式的资产状态、套餐组合、互斥、使用期、生效、赠送、强充等不变量。批量入口不得复制一套较宽规则。
- `offline` 把整批凭证快照关联到每个成功订单,直接创建符合现有后台线下语义的已支付订单并激活套餐;不扣代理钱包。
- `wallet` 逐行锁定该资产结算代理的有效主钱包,使用统一公式计算可用金额:只有启用信用时才计入有效信用额度。
- `wallet` 不预占整批或某一代理的全部金额。当前行余额不足只失败当前行并继续;后续金额更小的行在当时可用金额足够时仍可成功。因此,同一代理资金不足时 CSV 行序就是订购优先级。
- 单个成功行的事务必须原子提交:订单、订单明细、套餐使用/激活、结算代理与价格快照、钱包余额与版本、钱包真实金额流水、明细成功状态,以及本用例要求的可靠事件/审计。
- 单行事务失败不得留下已扣钱包但无订单、已有订单但明细仍可重复执行,或套餐已生效但订单回滚的中间事实。
- 行级业务失败记录稳定 `failure_code` 和中文原因后继续下一行底层数据库、S3 或 Redis 错误不能原样写给用户。
### 幂等、并发与失败码
- HTTP 幂等由 `request_id` 唯一约束和请求指纹保护;任务投递幂等由任务 ID 和任务状态/租约保护;单行幂等由 `(task_id,row_no)`、稳定幂等键、明细行锁/条件状态和单行事务共同保护。
- 同一任务的两个 Worker、Worker 崩溃后重试以及 Asynq 至少一次投递都不能重复创建订单、扣款、写钱包流水、激活套餐或写成功审计。
- 钱包扣款使用统一 Wallet Domain 的版本/条件更新或等价并发保护,不能只在内存判断余额;并发扣款后总可用金额不得小于 0。
- 与普通订单并发购买同一资产时仍要由 Order Domain 保护套餐和资产不变量,不能只依赖批量任务自身租约。
- 推荐稳定任务失败码包括:`storage_download_failed``invalid_encoding``invalid_header``unknown_column``invalid_csv``empty_file``row_limit_exceeded`
- 推荐稳定行失败码包括:`invalid_asset_identifier``asset_not_found``asset_identifier_ambiguous``duplicate_row``package_not_authorized``package_not_purchasable``settlement_agent_invalid``main_wallet_not_found``insufficient_balance``order_create_failed`
- 失败码供前端稳定展示和筛选,`failure_reason` 使用用户可理解中文并可补充首次重复行号等上下文;不把整个中文文案当作程序判断条件。
### 线下凭证语义
- 线下凭证是整个批次的业务资料快照,所有成功的线下订单都能追溯到该批次及凭证。
- 本期不校验同一凭证是否在其他批次使用,不建立凭证金额与订单金额的自动核销,也不因重复文件内容拒绝批次。
- 凭证可以是图片或文件,保存私有对象 Key页面通过受权下载地址预览或下载不把附件字节写入 CSV、数据库大字段或任务载荷。
- 对象存储中的凭证按现有附件保留策略长期可访问;不能把创建时的短期预签名 URL 当作永久业务地址。
### 前端交互
- 页面采用“参数确认 → 上传 → 处理中 → 结果”四个稳定阶段,可放在批量订购独立页或现有订单页入口。
- 参数阶段选择单个套餐和整批支付方式,不展示代理选择器。`offline` 显示 1 至 5 个凭证上传;`wallet` 不显示或清空凭证。
- CSV 模板是前端静态资源,只有 `资产标识` 一列,说明标识复用系统统一识别能力,当前支持 ICCID、卡虚拟号、MSISDN、设备虚拟号、IMEI 和 SN并明确不接受 Excel。
- 整个 CSV 使用参数阶段选择的套餐,前端不让用户在行内填写或为不同资产选择不同套餐。
- 前端先申请上传 URL并直传真实 S3再以同一个用户动作生成的 `request_id` 创建任务。网络超时重试必须复用原 `request_id`,用户主动新建批次才生成新值。
- 提交前展示所选套餐、支付方式、CSV 文件名和凭证数量,不展示虚构的单一代理或整批钱包余额;提交期间禁止重复点击。
- 创建成功后保存任务 ID并刷新详情。页面刷新、关闭后重开或网络恢复时可以根据任务 ID恢复不依赖持续驻留的轮询内存状态。
- 处理中显示任务号、操作员、总数、已处理数、成功数、失败数、成功金额和涉及代理数。解析尚未完成时允许总数为 0并显示“正在校验文件”。
- 结果页默认筛选失败明细,可切换全部/成功/失败,并按资产标识精确搜索;展示行号、原始资产、规范资产、批次套餐、结算代理、金额、订单和中文原因。
- 任务完成后根据成功数和失败数展示“全部成功/部分成功/全部业务行失败”的结果摘要,不把“部分成功”当成状态码。本期没有单行重试接口;用户复制失败行、修正后重新上传会创建全新任务,旧任务历史不改变。
- 文件级失败展示任务错误和模板修正建议;行级失败展示逐行原因。前端不得自行推断或改写后端失败码。
### 发布、回滚与依赖
- 钱包路径依赖统一 Wallet Domain 已具备信用启用开关、有效信用额度公式、版本并发保护和真实金额流水。若 UR#38 尚未落地,必须先完成这段共享能力,禁止在批量代码中复制旧余额判断。
- 套餐校验依赖统一可售策略能够区分 C 端本人续费与后台/批量入口。若 UR#40 尚未落地,必须先具备该策略,批量不能暂时放宽下架限制。
- 发布包含新表、索引、权限、上传用途、存储元数据能力、API、任务投递器和 Worker。Worker 尚未部署或数据库迁移未完成时不得开放前端入口。
- 上线前验证 PostgreSQL、Redis、Asynq 和真实 S3 配置,生成接口文档,并完成上传、任务、钱包、线下、部分成功和现有后台认证联调。
- 发布窗口内短暂停止新批量任务,先部署兼容迁移和 Worker再部署 API/前端。旧版本不会读取新表,不需要历史回填。
- 回滚时先关闭前端入口和任务创建,等待或人工处置已领取任务,再回滚应用。已经创建的标准订单、钱包流水、套餐使用、任务和审计均作为业务事实保留,不做反向删除。
- 不在仍有待处理或处理中任务时删除新表或上传用途;数据库降级迁移不是常规应用回滚步骤。
## 公共能力发布依赖
- 最终 11 号发布票阻塞于公共基础 12 号票、全局审计 19 号票和 UR#38 钱包切换 10 号票;公共能力未完成不阻止独立切片开发,但阻止生产放量。
- 批量任务必须登记事件类型、载荷版本、Worker 幂等键和逐行失败明细;订单与钱包仍以各自 Domain Ledger 为权威,任务创建、终态及人工操作进入 Audit Event。
## Testing Decisions
### 可复用 Agent 集成测试规范与工具
- 本需求实现时同时沉淀一份“Agent 集成测试规范”,覆盖环境防误连、夹具命名、唯一运行标识、精确清理、失败后清理、敏感信息保护和禁止事项,供后续批量任务复用。
- 提供可复用 Go 测试 Harness至少封装配置守卫、真实 PostgreSQL/Redis/S3 连接、Fiber 请求、测试账号和真实认证令牌、受控任务投递捕获、公开 Worker Handler 驱动、统一响应解码、资源清理台账。
- 为本需求提供 UTF-8、BOM、非法表头、重复资产、多代理、钱包不足和线下凭证等固定 CSV `testdata`,并提供穿过公开边界的完整示例测试。
- Go 集成测试是主要自动化入口;`curl` 只作为已部署环境的真实网络冒烟模板验证登录、预签名上传、PUT、创建、查询和终态不承担并发、幂等和数据库断言。
- 测试公共行为,不直接测试私有函数。创建接口使用 Fiber `app.Test` 穿过真实路由、认证、Handler、Application/Domain、GORM 和统一响应Worker 通过公开任务 Handler 驱动,不依赖私有解析方法。
- 为避免后台 Worker 抢任务,集成测试在应用注入点捕获待投递 `task_id`,然后直接调用公开 Worker Handler不通过 `sleep` 等待异步碰运气。生产仍使用真实 Asynq 投递器。
### Redis 与 Asynq 隔离
- 已部署测试环境固定使用 Redis DB 6本地开发和 Agent 自动化测试固定使用 Redis DB 7。
- 每次自动化测试启动前必须读取实际生效配置,并确认普通 Redis Client、Asynq Client 和 Worker Server 的 DB 都等于 7。任一不是 7 时立即失败,绝不向 DB 6 写普通 Key或投递任务。
- 测试不得执行 `FLUSHDB`、全前缀扫描删除或清空队列。每次运行生成唯一 `run_id`Redis 业务键、任务标识和清理名单只指向本次实际创建的精确 Key。
- 测试结束和失败清理都按资源台账删除精确 Key其他本地进程在 DB 7 的数据不受影响。
### PostgreSQL 共享测试库
- 自动化测试继续使用当前现有测试数据库,不创建新的数据库或专用数据库。
- 每次运行创建带唯一 `run_id` 的平台账号、权限、代理、钱包、资产、套餐、授权和其他夹具,只使用本次创建记录的实际主键执行业务。
- 清理按依赖顺序和实际主键精确删除本次创建的记录;禁止 `TRUNCATE`、整表删除、模糊条件删除、复用后篡改现有业务数据或假设测试库为空。
- 测试开始前记录资源清理台账;任何中途失败仍执行清理,并将未清理的精确 ID 输出为诊断信息但不得输出密码、Token、S3 密钥或完整敏感业务数据。
- 钱包并发测试也在该共享测试库内使用完全独立的本次夹具,不能锁定或扣减已有代理钱包。
### 真实 S3
- 自动化测试直接使用当前可调用的真实 S3 Provider不使用内存替身、临时本地对象模拟或绕过预签名协议。
- 每个测试对象 Key 都包含唯一 `run_id`CSV 和凭证走真实上传;测试覆盖对象 Head/元数据、下载和精确删除。
- 清理只删除本次资源台账中记录的对象 Key禁止删除目录前缀或执行 Bucket 级清理。
- 真实 S3 网络、鉴权、上传、下载、Head 或删除失败均使集成测试失败,便于及早发现环境和 Provider 契约问题。
### 后端场景
- 上传契约覆盖 `bulk_purchase` purpose、`.csv`、实际 Content-Type、10MB 边界、对象不存在、其他账号 Key、其他用途 Key、Key 已绑定、凭证类型和 1/5/6 个凭证。
- CSV 解析覆盖 UTF-8、UTF-8 BOM、LF、CRLF、单列中文固定表头、缺失/额外/重复列、非法引号、空文件、只有表头、1000/1001 行、空字段、控制字符和伪装 Excel。
- 资产解析通过统一公共接缝覆盖卡 ICCID、卡虚拟号、唯一 MSISDN、MSISDN 未命中/多命中、设备虚拟号、IMEI、SN、未命中和历史多命中断言批量用例没有另一套识别规则。
- 判重覆盖同一资产使用不同受支持标识只首行处理、后续指出首行号,以及无法解析资产的行不会误判成同一个空资产。
- 认证测试覆盖现有后台认证有效和失效;断言没有新增 `order:bulk_purchase`、账号类型拦截或任务创建人隔离。
- 请求幂等覆盖相同 `request_id` 同载荷返回原任务、改变 `package_id` 或其他载荷/操作者时冲突、并发创建只有一个任务和一个可靠事件。
- 文件级错误验证任务失败且订单、钱包流水、套餐使用均为零;行业务错误验证部分成功且已成功行不被回滚;全部行业务失败仍是任务已完成且 `success_count=0``fail_count=total_count`
- `wallet` 覆盖多代理各扣自己的主钱包、信用启用/禁用、无主钱包、余额不足、行序优先、前一行不足但后一便宜行成功,以及不产生整批预占。
- `offline` 覆盖凭证必填、成功订单立即支付和激活、不扣钱包、凭证可跨批复用且保留批次关联。
- 套餐规则覆盖创建时套餐不存在、任务套餐快照、各结算代理的授权成本、未授权、禁用、下架不能被批量绕过,以及现有组合/资产状态/强充规则;任何价格都不能由前端或 CSV 提交。
- 重复消费覆盖两个 Worker 同时领取、任务租约过期、单行处理中崩溃、事务提交前/后故障、重复 Asynq 消息,断言订单、扣款、流水和激活各只有一次。
- 查询覆盖默认分页、最大页大小、行号排序、状态和资产标识筛选使用 AND、任务汇总、全局状态名称和失败码。
- 审计覆盖任务创建/终态、逐行订单和钱包事实,并验证日志和响应不包含环境密钥、认证令牌、预签名 URL 或底层错误。
### 前端、联调和完成命令
- 前端验收覆盖入口可见性、单套餐选择、四阶段页面、单列静态 CSV 模板、真实直传、支付方式切换、线下凭证、重复点击、刷新恢复、解析中空进度、计数表达部分成功、失败默认筛选、精确搜索和新任务重试。
- 部署联调使用 `curl` 模板完成真实 HTTP 和真实 S3 PUT但凭据只从环境读取不写入脚本、文档或终端回显验证部署环境明确连接 Redis DB 6。
- 自动化完成门禁至少包括目标单元/集成测试、相关包测试、全量 Go 测试、竞态或并发专项测试、静态检查、OpenAPI 生成校验和数据库迁移验证。
- 新 Handler 必须同步两个接口文档生成入口DTO 枚举描述从公共常量原文复制,所有状态响应包含中文 `status_name`
## Out of Scope
- 不支持 Excel不维护 CSV/Excel 双解析器。
- 不在创建请求或 CSV 中接受 `shop_id`,不让前端选择或推断结算代理。
- 不允许一批混合 `wallet``offline`,也不新增 `agent_wallet` 支付枚举。
- 不在批量订购内维护资产标识白名单或另写卡/设备识别逻辑;不支持模糊搜索或资产号段匹配。
- 不用重复行表达套餐数量,不提供单行重试或修改旧任务接口。
- 不为钱包支付预占整批或某代理全部金额,不因一行余额不足回滚其他成功行。
- 不允许批量订购绕过下架、授权、资产状态、定价、强充或订单领域规则。
- 不对线下凭证做跨批唯一、金额核销或自动财务对账。
- 不新增后端 CSV 模板下载或失败结果文件下载接口。
- 不创建新的 PostgreSQL 测试数据库,不对共享测试库或 Redis DB 7 执行破坏性清理。
- 不为对象存储实现内存替身;自动化和联调均验证当前真实 S3。
- 不新增批量订购后端权限码、账号类型拦截或任务创建人隔离。
- 不重构当前需求未触碰的整个订单、钱包或存储模块。
## Further Notes
- 当前仓库没有批量订购 API、任务模型或 Worker需要作为新用例实现。
- 当前后台订单已使用 `wallet|offline`,批量必须复用这两个常量;部分现有 DTO 对线下凭证和支付枚举的描述不完全一致,实现时以公共常量和本规格为准并同步修正触碰处。
- 当前已有统一 `Asset.Resolve` 和全局资产标识注册表,但 fallback 对历史多命中仍可能任取第一条。本需求必须复用并完善这一个统一解析边界,使单个和批量解析都能返回唯一资产类型与 ID不能在批量模块内再实现一套。
- 当前存储接口只有存在性、上传/下载和预签名能力,没有对象元数据与上传主体归属事实;这两项是安全接收 `file_key` 的必要实现,不得只检查字符串前缀。
- 当前 Redis 普通客户端、Asynq Client 和 Worker Server使用同一个 Redis DB 配置来源,测试守卫必须验证最终生效值为 7而不是只检查某个环境变量字符串。
- 用户已最终确认CSV 只有 `资产标识`,资产类型由统一解析能力识别;创建任务选择单个 `package_id`;后端不新增显性权限拦截;任务状态复用全局五态且部分成功只通过计数表达;自动化测试直接使用真实 S3PostgreSQL 沿用现有测试库。

View File

@@ -1,19 +0,0 @@
# 01 — 统一资产标识的批量唯一解析
**What to build:** 批量订购可以通过系统唯一的资产解析能力,一次解析一组用户输入的资产标识,并稳定得到资产类型、资产 ID 和规范标识。ICCID、卡虚拟号、MSISDN、设备虚拟号、IMEI 和 SN 使用与其他业务相同的注册规则;未命中或历史脏数据导致多命中时明确失败,不能任取一条。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
**架构通道:** Application + Port/Adapter。
**完整业务边界:** 本票深化公共 Asset 解析接缝,使单个和批量调用共享唯一匹配语义与规范标识选择规则。明确不实现批量订购任务、不迁移资产列表或生命周期逻辑、不在批量模块维护第二套标识白名单。
- [ ] 公共解析结果包含原始输入对应的资产类型、资产 ID、规范标识和稳定失败分类并能区分未命中与多命中。
- [ ] ICCID、卡虚拟号、唯一 MSISDN、设备虚拟号、IMEI 和 SN 均通过现有全局标识注册规则解析;新增调用方无需复制识别分支。
- [ ] 同一资产使用不同受支持标识时得到相同资产类型与 ID规范标识由公共能力统一决定。
- [ ] 历史脏数据造成多条候选时返回明确歧义,单个和批量调用均不得使用 `First` 或其他任取逻辑。
- [ ] 批量接口以集合查询或等价批处理方式完成解析,避免按行跨卡表、设备表和标识表形成 N+1。
- [ ] 测试覆盖全部支持标识、首尾空白、未命中、多命中、不同标识指向同一资产及批量查询次数边界。

View File

@@ -1,19 +0,0 @@
# 02 — 交付受控直传与对象归属证明
**What to build:** 操作员可以为批量订购 CSV 和线下凭证申请受控上传地址并直传真实私有 S3业务接口随后能够证明对象由当前账号按声明用途申请、已经真实上传且元数据符合约束。上传授权可原子绑定业务任务已绑定 Key 不能被其他账号或其他批次复用。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为简单写 Application。
**完整业务边界:** 本票收口预签名上传授权、对象 Head 元数据和业务绑定能力,并增加 `bulk_purchase` 受控用途。明确不解析 CSV、不创建批量任务、不改变其他对象存储消费者的业务语义、不实现内存 Provider。
- [ ] `bulk_purchase` 用途只签发批量订购目录下的 `.csv` Key并限制为允许的 CSV Content-Type附件继续复用现有 `attachment` 用途和文件类型规则。
- [ ] 上传授权稳定保存对象 Key、用途、申请账号、声明文件名、声明 Content-Type 和签发时间,不以目录前缀推断上传主体。
- [ ] 存储 Provider 可以查询对象是否存在、实际大小和实际 Content-Type并以统一安全错误区分不存在、类型不符、过大和基础设施失败。
- [ ] 授权绑定使用业务类型、业务 ID 和绑定时间形成持久事实;同一 Key 的并发绑定最终由数据库约束裁决。
- [ ] 相同业务幂等重试可以识别原绑定,其他账号、其他用途或其他批次复用同一 Key 时明确冲突。
- [ ] 真实 S3 集成测试覆盖预签名上传、Head、下载、精确删除、10MB 边界、伪造类型、对象不存在、跨账号、跨用途和重复绑定。

View File

@@ -1,19 +0,0 @@
# 03 — 建立批量任务真实依赖的集成测试 Harness
**What to build:** Agent 和开发者可以通过一个可复用的 Go 集成测试 Harness安全驱动真实 PostgreSQL、Redis DB 7、真实 S3、Fiber 后台认证和公开 Worker Handler。每次运行只创建、记录和清理带唯一运行标识的资源测试失败也不会清空或污染共享环境。
**Blocked by:** `.scratch/ur36-bulk-package-purchase/issues/02-controlled-upload-object-ownership.md` — 02 — 交付受控直传与对象归属证明
**Status:** ready-for-agent
**架构通道:** Infrastructure。
**完整业务边界:** 本票收口本需求及后续同类批量任务需要的安全集成测试基础设施。明确不替换全仓测试框架、不创建新 PostgreSQL 数据库、不使用 S3 内存替身、不通过等待真实后台 Worker 抢任务完成断言。
- [ ] Harness 启动时读取最终生效配置,并确认普通 Redis Client、Asynq Client 和 Worker Server 均使用 DB 7任一不符时在写入前失败。
- [ ] Harness 支持唯一 `run_id`、测试账号与真实认证、Fiber 请求、统一响应解码、真实 S3 上传及公开 Worker Handler 驱动。
- [ ] 待投递任务可在应用公开注入点被受控捕获,并以结构化 `task_id` 调用 Handler不依赖 `sleep` 或私有处理函数。
- [ ] PostgreSQL、Redis 和 S3 资源进入精确清理台账成功和失败清理只操作本次创建的主键、Key 和对象,不执行 TRUNCATE、FLUSHDB、前缀扫描或 Bucket 清理。
- [ ] 清理失败输出安全诊断和未清理资源标识不输出数据库密码、Token、对象存储密钥、预签名 URL 或完整敏感数据。
- [ ] 固定 `testdata` 覆盖 UTF-8、BOM、非法表头、重复资产、多代理、钱包不足和线下凭证并至少提供一个穿过公开 HTTP 与 Worker 边界的示例。

View File

@@ -1,19 +0,0 @@
# 04 — 统一后台与批量入口的套餐可售策略
**What to build:** 后台单笔订购和批量单行订购可以调用同一套餐可售策略,按写入时的资产、结算代理、套餐授权、渠道状态、价格和操作场景得到一致结论。批量或后台主体不能进入个人历史用户的下架续费例外,也不能绕过禁用、未授权或组合限制。
**Blocked by:** None — can start immediately
**Status:** ready-for-agent
**架构通道:** 复杂写前置 Domain Policy。
**完整业务边界:** 本票收口 UR#36 实际触碰的后台套餐购买策略接缝,并与 UR#40 的统一策略语义兼容。明确不实现 C 端下架续费页面、不迁移套餐管理、不迁移未触碰的订单流程、不在 Query 快照上做最终写入判断。
- [ ] 策略输入显式包含操作主体、资产及当前世代、结算代理、套餐、销售渠道和操作场景,不依赖可被遗漏的隐式布尔开关。
- [ ] 套餐全局禁用、渠道下架、授权缺失或失效、价格异常、赠送及既有套餐组合限制均返回稳定业务失败。
- [ ] 后台单笔和批量场景遇到下架套餐固定拒绝,不能使用个人客户的 `renew_only` 历史资格。
- [ ] 结算金额来自结算代理当前有效授权成本和现有定价规则,前端或 CSV 无法提交价格覆盖策略结果。
- [ ] 写命令在事务前后按最新事实重新校验必要不变量,不能把查询接口先前返回的资格当作授权凭证。
- [ ] 测试覆盖平台及代理渠道、启用与禁用、上架与下架、授权删除或失效、价格低于成本、组合限制,以及后台与批量结果一致性。

View File

@@ -1,23 +0,0 @@
# 05 — 提供统一的单资产套餐订购命令
**What to build:** 现有后台单笔入口和批量任务中的一个业务行可以调用同一个单资产套餐订购命令,按资产当前归属确定结算代理,完成套餐可售校验、定价、订单及套餐激活编排。钱包支付通过统一 Wallet Application/Port 接缝完成,线下支付生成符合现有后台语义的已支付订单。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/04-unified-admin-package-purchasability.md` — 04 — 统一后台与批量入口的套餐可售策略
- `.scratch/ur38-agent-main-wallet-credit/issues/05-unified-agent-wallet-debit.md` — 05 — 统一代理订单、代购与钱包支付扣款
**Status:** ready-for-agent
**架构通道:** 主通道为复杂写Application UseCase → Order/Package Domain → Repository/InfrastructureWallet Application/Port 为辅助通道。
**完整业务边界:** 本票迁移后台单资产套餐订购这一最小完整用例,收口资产归属、套餐可售与定价、订单、订单明细、套餐使用及激活编排。钱包领域只通过 UR#38 的稳定接缝协作。明确不迁移其他订单创建、支付、退款、佣金或资产钱包流程。
- [ ] 命令只接收规范资产、套餐、支付方式、凭证快照、操作者和稳定业务幂等标识,不接受可信 `shop_id`、结算价格或套餐可售结论。
- [ ] 每次执行按资产当前归属解析结算代理,并调用统一套餐可售策略重新校验授权、状态、定价及现有订单不变量。
- [ ] `offline` 创建已支付订单、订单明细和套餐使用并完成现有激活语义,不扣代理钱包;凭证关联作为业务快照保留。
- [ ] `wallet` 只调用 UR#38 提供的 Wallet Application/Port 接缝UR#36 不复制信用公式、不直接更新代理钱包模型、不调用旧钱包 Store 扣款条件、不重新实现钱包流水或信用并发规则。
- [ ] 单次成功订购中的订单、套餐使用与激活、结算代理和价格快照、明细关联、审计及可靠事件原子提交;任何失败不留下部分事实。
- [ ] 现有后台单笔 HTTP 契约保持不变并迁移为调用该命令;旧订单 Service 如保留,只能作为本用例的内部迁移门面。
- [ ] 测试覆盖钱包与线下、不同代理归属、未授权、下架、价格异常、资产状态与组合规则、幂等重试及事务回滚。

View File

@@ -1,28 +0,0 @@
# 06 — 幂等创建批量订购任务
**What to build:** 通过现有后台认证的操作员可以提交唯一 `request_id`、单个套餐、统一支付方式、CSV Key 和凭证 Key 创建批量订购任务。接口在返回已接收任务前验证对象归属和真实元数据,并在同一事务保存套餐与操作者快照、上传绑定、请求指纹、任务事实、创建审计和可靠投递事件。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/02-controlled-upload-object-ownership.md` — 02 — 交付受控直传与对象归属证明
- `.scratch/tech-public-foundation/issues/02-transactional-public-outbox-write.md` — 02 — 在业务事务中可靠写入公共 Outbox
- `.scratch/tech-public-foundation/issues/03-outbox-at-least-once-delivery.md` — 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
- `.scratch/tech-public-foundation/issues/05-command-idempotency-contract.md` — 05 — 提供创建命令幂等与并发职责契约
- `.scratch/tech-public-foundation/issues/06-unified-async-task-contract.md` — 06 — 冻结统一异步任务五态和查询契约
- `.scratch/tech-global-audit/issues/01-audit-event-write-loop.md` — 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 复杂写 Application + Port/Adapter。
**完整业务边界:** 本票收口批量订购任务的可靠接收、幂等、上传绑定和投递事实。明确不下载或解析 CSV、不创建逐行明细、不执行订单、不增加批量权限码或任务创建人隔离。
- [ ] 请求只接受 `request_id``package_id``payment_method``file_key``voucher_keys`;显式提交 `shop_id` 返回统一参数错误,其他未知字段不参与业务决策。
- [ ] `wallet` 要求凭证为空;`offline` 要求 1 至 5 个有效附件;套餐存在性和 ID、编码、名称快照在创建时确定。
- [ ] CSV 与凭证必须由当前账号按正确用途申请、真实存在且元数据合法CSV 实际大小不超过 10MB所有 Key 在任务事务内完成绑定。
- [ ] 相同 `request_id`、操作者和规范化请求指纹返回原任务;改变套餐、支付方式、文件、凭证或操作者时返回冲突。
- [ ] 并发首写最终只产生一个任务、一次上传绑定和一个可靠事件Asynq 暂不可用不丢失已提交任务。
- [ ] 任务使用全局五态并返回中文状态名;队列载荷只包含结构化 `task_id`,不传 `[]byte`、文件内容、临时路径或认证上下文。
- [ ] 创建成功写公共审计,记录任务号、支付方式、文件安全摘要和凭证数量,不记录预签名 URL、Token 或环境密钥。
- [ ] 新 Handler 同步注册后台路由并更新两个 OpenAPI 文档生成入口,所有响应使用统一包装和中文错误码语义。

View File

@@ -1,25 +0,0 @@
# 07 — 完整校验 CSV 并固化逐行明细
**What to build:** Worker 可以领取待处理批量任务,下载完整 CSV 后先完成全部文件结构和行数校验,再批量解析资产并一次性持久化所有数据行。文件级错误使任务失败且不产生订单;文件有效时每个原始数据行都有稳定行号、解析结果、失败信息和幂等键,可供后续恢复处理。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/01-batch-unique-asset-resolution.md` — 01 — 统一资产标识的批量唯一解析
- `.scratch/ur36-bulk-package-purchase/issues/03-real-dependency-integration-harness.md` — 03 — 建立批量任务真实依赖的集成测试 Harness
- `.scratch/ur36-bulk-package-purchase/issues/06-idempotent-bulk-purchase-creation.md` — 06 — 幂等创建批量订购任务
**Status:** ready-for-agent
**架构通道:** 主通道为 Application辅助通道为 Infrastructure。
**完整业务边界:** 本票收口任务领取、CSV 文件级校验、公共资产批量解析、文件内判重和明细初始化。明确不执行套餐订购、不扣钱包、不创建订单、不支持 Excel 或单行重试。
- [ ] Worker 仅领取待处理或租约已过期的处理中任务,终态任务不会重新解析;源对象下载失败按安全任务错误进入失败终态。
- [ ] CSV 仅接受 UTF-8 或 UTF-8 BOM、LF 或 CRLF、唯一单列表头 `资产标识`;空文件、只有表头、非法编码、引号语法错误、缺列、多列、重复或未知列均为文件级错误。
- [ ] 实际文件不得超过 10MB数据行不得超过 1000Worker 在任何订单写入前读完整文件并确认第 1001 行不存在。
- [ ] 文件有效后每个数据行都持久化并保留原始行号和原始标识;空值、控制字符、未命中及多命中作为行级失败计数,不被省略。
- [ ] 文件内重复在公共解析成功后按 `资产类型 + 资产ID` 判断;首次出现行继续处理,后续行失败并指出首次 CSV 行号,不同标识指向同一资产也能识别。
- [ ] 明细具有 `(任务, 行号)` 和稳定 `bulk_purchase:{task_id}:{row_no}` 幂等约束,并保存公共行处理状态、租约和安全失败码。
- [ ] 文件验证与明细初始化完成后Worker 重启直接读取已有明细,不重新生成行号或幂等键;汇总从明细事实聚合。
- [ ] 真实 S3 和 PostgreSQL 测试覆盖编码、换行、表头、10MB、1000/1001 行、伪装 Excel、空字段、控制字符、资产歧义和跨标识重复。

View File

@@ -1,24 +0,0 @@
# 08 — 逐行完成线下支付批量订购
**What to build:** 文件校验通过的线下支付任务可以严格按 CSV 行号逐行订购套餐。每行按资产当前归属确定结算代理,调用统一单资产订购命令创建已支付订单并激活套餐;一行业务失败只记录该行中文原因并继续,成功订单都能追溯批次及整批凭证。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/05-single-asset-package-purchase-command.md` — 05 — 提供统一的单资产套餐订购命令
- `.scratch/ur36-bulk-package-purchase/issues/07-validate-csv-persist-items.md` — 07 — 完整校验 CSV 并固化逐行明细
**Status:** ready-for-agent
**架构通道:** 复杂写Application → Order/Package Domain。
**完整业务边界:** 本票收口批量任务的 `offline` 逐行执行、结果记录和任务终态。明确不建设凭证金额核销、跨批唯一、自动财务对账、单行重试或钱包扣款。
- [ ] Worker 仅按 `row_no ASC` 串行领取待处理或过期处理中明细,不并发改变业务顺序,已有成功或失败终态不可被覆盖。
- [ ] 每行重新读取资产当前归属并解析结算代理,任务创建时不存在单一代理快照,也不接受前端提供的 `shop_id`
- [ ] 有效行调用统一单资产套餐订购命令,按结算代理当前授权、价格和套餐规则创建已支付订单并激活套餐,不扣任何代理钱包。
- [ ] 整批凭证 Key 快照关联到每个成功订单和批次审计;凭证可以被其他批次使用,不建立内容或金额唯一性。
- [ ] 资产、归属、授权、下架、价格、组合及订单业务错误写稳定失败码和中文原因后继续下一行,底层错误不原样返回。
- [ ] 成功行的订单、套餐使用与激活、明细成功状态、逐行关联、Audit Event 和 Outbox 原子提交;事务失败不会留下可重复执行的部分事实。
- [ ] 所有业务行处理完后任务进入已完成;全部成功、部分成功或全部业务失败只通过计数表达,`processed_count = success_count + fail_count = total_count`
- [ ] 测试覆盖多代理、凭证 1 个和 5 个、未授权、下架、全部失败、部分成功、重复 Worker 领取和单行事务回滚。

View File

@@ -1,23 +0,0 @@
# 09 — 逐行完成钱包批量订购与崩溃恢复
**What to build:** 钱包支付任务可以严格按 CSV 行号,分别使用每个资产结算代理的主钱包完成订购。同一代理资金不足只失败当前行,后续更便宜的行仍按当时可用金额判断;重复消息、并发 Worker 和进程中断恢复不会重复创建订单、扣款、写流水或激活套餐。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/05-single-asset-package-purchase-command.md` — 05 — 提供统一的单资产套餐订购命令
- `.scratch/ur36-bulk-package-purchase/issues/08-offline-bulk-row-purchase.md` — 08 — 逐行完成线下支付批量订购
**Status:** ready-for-agent
**架构通道:** 复杂写Application → Order/Package Domain辅助调用 Wallet Application/Port。
**完整业务边界:** 本票在已经验证的逐行任务闭环上增加 `wallet` 支付和故障恢复。它通过 UR#36 Ticket 05 间接消费 UR#38 的钱包能力,不直接依赖或重新实现 Wallet Domain。明确不预占整批资金、不并行结算、不迁移其他钱包用例。
- [ ] 每行按当前资产归属定位结算代理,并通过统一单资产订购命令调用 Wallet Application/Port批量代码不读取信用字段计算可用金额不直接更新钱包或创建钱包流水。
- [ ] 严格行序决定同一代理的资金使用优先级,不按代理汇总预占;当前行余额不足只失败该行,后续金额较小且资金足够时可以成功。
- [ ] 任务和明细使用状态条件、处理租约及稳定幂等键领取;只有租约所有者可以提交处理中状态,终态不可重复执行。
- [ ] 两个 Worker 同时领取、重复 Asynq 消息、任务租约过期和行租约过期时,订单、钱包扣款、真实金额流水、套餐激活及成功审计各最多一次。
- [ ] 单行事务提交前崩溃可安全重试;提交后但确认前崩溃可从订单、明细和幂等事实恢复,不得再次扣款。
- [ ] 钱包不足、无有效主钱包、信用关闭或额度不足等稳定业务失败记录中文安全原因数据库、Redis 或底层钱包错误不泄露给前端。
- [ ] 任务终态汇总从明细重新聚合并记录涉及代理数、成功金额、成功数和失败数,同时写任务终态公共审计。
- [ ] 并发与故障测试覆盖多代理各扣主钱包、现金与信用、无钱包、前贵后便宜、普通订单并发、双 Worker、重复消息及事务提交前后故障。

View File

@@ -1,22 +0,0 @@
# 10 — 提供任务汇总与逐行明细查询
**What to build:** 通过现有后台认证的调用方可以查询批量订购任务汇总及逐行结果,在刷新或重新打开页面后恢复进度。接口按固定行号顺序分页,支持状态和资产标识精确筛选,并返回套餐、结算代理、金额、订单及中文失败信息,不暴露存储凭证、钱包余额或底层错误。
**Blocked by:** `.scratch/ur36-bulk-package-purchase/issues/09-wallet-bulk-row-purchase-recovery.md` — 09 — 逐行完成钱包批量订购与崩溃恢复
**Status:** ready-for-agent
**架构通道:** Query。
**完整业务边界:** 本票收口批量订购任务详情和明细读取模型、DTO 与后台路由。明确不建立通用任务中心、不修改任务状态、不增加任务创建人隔离或新的 RBAC、不迁移其他任务查询。
- [ ] 任务详情返回任务号、请求 ID、套餐快照、支付方式、状态与中文名称、源文件摘要、凭证引用、操作者、计数、涉及代理数、成功金额、安全错误和起止时间。
- [ ] 明细查询默认第 1 页、每页 20 条且最大 100始终按 `row_no ASC`;前端不能改变业务处理顺序。
- [ ] `status` 只接受公共行处理状态,`asset_identifier` 去除首尾空白后仅在当前任务内对原始或规范标识精确匹配;多个筛选使用 AND。
- [ ] 每行返回行号、资产类型、原始标识、资产 ID、规范标识、结算代理快照、金额、状态与中文名称、订单、失败码、中文原因和处理时间。
- [ ] 汇总从明细事实计算或校准,始终满足 `processed_count = success_count + fail_count`;解析阶段允许总数为 0部分成功不增加私有状态。
- [ ] 查询只复用现有后台认证与公共数据范围,不新增批量权限码、账号类型拦截或按创建人隔离;无权与不存在遵循项目统一安全语义。
- [ ] 响应不包含 SQL、底层错误、永久对象访问凭证、Redis Key、预签名 URL、其他代理钱包余额或环境秘密。
- [ ] 新 Handler 同步后台路由及两个 OpenAPI 文档生成入口DTO 枚举说明从公共常量原文复制并包含所有 `status_name`
- [ ] HTTP 测试覆盖默认与最大分页、固定排序、状态与标识 AND 筛选、任务汇总、认证失效、跨创建人查询语义和安全字段边界。

View File

@@ -1,27 +0,0 @@
# 11 — 完成真实链路验收、文档与发布门禁
**What to build:** 发布负责人可以通过自动化测试、OpenAPI、静态 CSV 模板和中文联调契约,验证从后台认证、真实 S3 直传、任务创建、可靠投递、Worker 执行到任务查询的完整链路。发布和回滚步骤明确保护已经产生的订单、钱包流水、套餐使用、任务和审计事实。
**Blocked by:**
- `.scratch/ur36-bulk-package-purchase/issues/03-real-dependency-integration-harness.md` — 03 — 建立批量任务真实依赖的集成测试 Harness
- `.scratch/ur36-bulk-package-purchase/issues/10-bulk-purchase-task-queries.md` — 10 — 提供任务汇总与逐行明细查询
- `.scratch/tech-public-foundation/issues/12-foundation-release-gate-and-integration-contract.md` — 12 — 建立公共基础发布门禁和下游接入契约
- `.scratch/tech-global-audit/issues/19-one-time-audit-cutover-gate.md` — 19 — 执行一次性审计切换与停机发布门禁
- `.scratch/ur38-agent-main-wallet-credit/issues/10-credit-wallet-cutover-gate.md` — 10 — 完成信用钱包停机切换与发布门禁
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为 Application 与 Query 验收。
**完整业务边界:** 本票收口 UR#36 的真实依赖验收、接口文档、静态模板、中文功能总结、发布与回滚门禁。本仓库不实现前端页面;只提供前端交互所需的 API、OpenAPI、静态 CSV 模板和联调验收契约,不扩展为全仓测试治理。
- [ ] 完整示例穿过真实后台认证、预签名 URL、真实 S3 PUT 与 Head、创建 API、可靠任务投递、公开 Worker Handler、PostgreSQL 事实和查询 API。
- [ ] 自动化覆盖文件级零订单失败、行业务部分成功、全部行业务失败、多代理线下与钱包、信用与余额不足、严格行序、幂等 HTTP、双 Worker 和崩溃恢复。
- [ ] 审计验证任务创建、任务终态、成功订单和钱包事实可串联且响应、日志、审计和测试输出不包含密钥、Token、预签名 URL 或底层错误。
- [ ] 提供只有 `资产标识` 一列的 UTF-8 静态 CSV 模板并明确支持的标识、1000 行、10MB、不接受 Excel及长数字保护提示。
- [ ] OpenAPI 完整包含上传用途、创建任务、任务详情和明细查询契约;两个文档生成入口一致,生成校验无漂移。
- [ ] 前端交互联调契约覆盖单套餐、钱包或线下支付、1 至 5 个凭证、四阶段页面、重复点击复用 `request_id`、刷新恢复、解析中空进度、部分成功计数、失败默认筛选和精确搜索。
- [ ] 中文功能总结和 README 说明领域边界、接口、失败闭环、真实依赖测试、部署顺序、监控、停用入口及回滚限制。
- [ ] 发布门禁包含目标及相关包测试、全量 Go 测试、并发或竞态专项、静态检查、OpenAPI 生成、迁移升降级验证,以及 PostgreSQL、Redis/Asynq 和真实 S3 配置核验。
- [ ] 回滚先关闭创建入口并处置待处理或处理中任务;已经形成的标准订单、钱包流水、套餐使用、批量任务和审计作为业务事实保留,不执行反向删除。

View File

@@ -1,262 +0,0 @@
# PRDUR#37 企业微信审批公共能力
Status: ready-for-agent
---
## Problem Statement
当前退款和平台员工线下代充值分别维护本地审批动作,系统内存在通过、拒绝、退回、重提和确认入账等接口及页面。审批节点、审批人和业务终态混在各自 Service 中,无法复用企业微信已有的多级、会签、或签、意见、附件和待办能力,也容易在重复回调或异步失败时重复执行资金动作。
系统当前没有企业微信审批的稳定业务场景、模板版本、平台账号绑定、代理代提交身份、审批实例、回调解密、轮询补偿或统一运行查询。若直接在退款和充值代码中分别调用企微,将产生两套 Token、模板控件、回调、状态和幂等实现。
企微发起人还存在两类身份:平台账号发起业务时必须使用本人绑定的企微成员;代理账号不是企微成员,需要使用固定企微账号代提交。但无论实际调用企微的是谁,审批展示、通知、权限和审计中的申请人都必须是本系统真实业务提交人,不能把固定代提交账号误认为代理本人。
企微模板及控件 ID 会随模板编辑变化;远程申请又不能与本地数据库处于同一事务。系统必须解决模板安全切换、提交结果未知、重复回调、轮询并发、终态只处理一次、敏感配置保护和不同主体的信息可见性。
## Solution
建设一套企业微信审批公共能力,第一版固定服务退款审批和平台员工线下充值审批。企业微信负责模板、节点、审批人、会签/或签、通过/拒绝、意见、附件和企微待办;本系统只负责业务快照、真实业务提交人、企微发起身份、审批状态镜像、回调与轮询同步,以及把首次终态可靠交给对应业务用例。
业务代码只引用稳定场景码。模板 ID、控件 ID、模板详情和字段映射形成不可变版本模板维护必须先暂停场景、等待在途提交租约释放再发布新版本并恢复。场景不可用时退款或线下充值创建在写入任何业务单、审批实例和 Outbox 前直接失败。
平台/超级管理员通过官方企业微信 Web 登录二维码绑定本人 `userid`;代理不绑定企微,由部署配置中的固定成员代提交。审批实例同时冻结真实业务提交人快照、实际企微发起身份及其来源。平台完整查看审批资料,代理只能在原业务数据范围内查看最小化审批摘要。
本地业务单与审批实例同事务创建并写提交 Outbox由 Worker 上传附件副本并调用企微。回调和每 2 分钟轮询共同进入同一个状态同步用例,以 `getapprovaldetail` 为权威;首次进入终态时同事务写业务终态 Outbox业务完成标记保证资金动作只执行一次。
## User Stories
1. 作为超级管理员,我希望查看企微连接和代理固定代提交账号是否就绪,以便在开放业务前发现配置问题。
2. 作为超级管理员,我希望按稳定业务场景管理退款与线下充值模板,而不是让业务代码依赖易变的模板 ID。
3. 作为超级管理员,我希望暂停场景后确认没有旧模板提交仍在运行,再去企业微信编辑模板。
4. 作为超级管理员,我希望读取企微模板并可视化映射业务字段,发布前由后端重新验证控件存在、类型和必填项。
5. 作为平台或超级管理员账号,我希望扫码绑定本人企微身份,以便我创建的审批由本人企微账号发起。
6. 作为平台账号,我只能查看、换绑或解绑自己的企微身份,不能查看其他员工的绑定清单或代其修改 `userid`
7. 作为代理账号,我不需要也不能绑定企微;我创建退款时由系统固定账号代提交,但审批中仍显示我是业务提交人。
8. 作为业务提交人,我希望场景暂停、模板失效或企微身份不可用时在提交表单阶段立即得到明确失败,并保留已填写内容。
9. 作为审批人,我希望企微审批表单包含正确的业务编号、金额、原因、附件和真实业务提交人。
10. 作为运营人员,我希望在审批运行页按业务类型、状态、单号和处理结果定位审批,并查看最后一次权威同步的详情。
11. 作为有审批运营权限的平台人员,我希望手动触发一次详情同步,但不能在本系统执行通过、拒绝或退回。
12. 作为异常恢复人员,我希望对企微提交结果未知的记录安全地绑定已存在的 `sp_no`,或在确认企微未创建后重新发送,避免重复审批。
13. 作为真实业务提交人,我希望审批终态产生站内结果通知,不因实际企微发起身份是代理固定账号而把通知发错人。
14. 作为代理,我希望在有权查看的退款详情中看到申请人、审批状态、状态时间和业务处理结果,但看不到平台内部审批人、意见和审批附件。
15. 作为平台或超级管理员,我希望在具备原业务查看权限时查看审批人、意见、时间线和审批附件。
16. 作为财务人员,我希望企微通过与本地资金处理结果分开显示,避免把“审批通过但业务处理失败”误认为已经完成。
17. 作为审计人员,我希望模板发布、场景变更、账号绑定、异常恢复、回调和外部调用均有可追溯记录,且不泄漏密钥或附件内容。
18. 作为系统维护人员,我希望回调、轮询和重复任务共享同一幂等规则,避免一张业务单重复退款或重复入账。
19. 作为退款发起人,我希望企微拒绝后原退款单保持不可变;若纠正问题后仍需退款,我会创建一张全新的退款单和审批。
20. 作为历史数据查看者,我希望发布前已经结束的本地审批仍以只读历史事实展示,而不是伪造企微单号或审批节点。
## Implementation Decisions
### 范围与系统边界
- 第一版稳定场景固定为:
- `refund_approval`:退款审批。
- `offline_recharge_approval`:平台员工线下充值审批。
- 企业微信负责模板编辑、节点、审批人、会签/或签、通过、拒绝、意见、附件和企微待办。本系统不建立本地审批任务、候选审批人、节点配置或审批按钮。
- UR#37 交付公共企微配置、身份、场景/模板、审批实例、提交、回调、轮询、状态同步、运行 Query 和异常恢复能力。退款及线下充值的具体金额和资金终态分别由 UR#35、UR#34 的业务用例消费公共终态事件。
- 旧退款和线下充值本地审批入口不与企微长期并存;停机发布时由对应业务需求下线通过、拒绝、退回、重提、人工确认入账和操作密码入口。
- 在线微信/支付宝代理充值不进入企微审批;换货也不在本需求接入审批。
### 配置、Token 与敏感信息
- 企微连接通过 Viper 和环境变量配置 CorpID、AgentID、AgentSecret、回调 Token、EncodingAESKey、账号绑定回调地址、代理固定代提交 `userid`、请求超时、2 分钟审批轮询和 10 分钟模板验证间隔。
- Secret、Token、EncodingAESKey 和代理固定原始 `userid` 不进入通用配置表,不由后台在线编辑,也不得通过 API、日志、审计或文档返回。
- `GET /api/admin/wecom/status` 只返回配置就绪状态、最近连通时间、最近错误、回调最近成功时间、Token 最近获取时间、代理固定成员是否就绪及成员显示名。
- Access Token 缓存在 RedisTTL 使用企微 `expires_in - 300秒`,通过 Redis 锁避免多进程并发刷新。
- `auth/getuserinfo`、成员读取、模板读取、附件上传、发起审批和审批详情查询都写 Integration Log仅记录接口、耗时、企微错误码/摘要和请求关联信息,不记录令牌、密钥或完整附件。
- 正式接入前必须轮换曾出现在演示材料中的旧企微密钥,新密钥只能经部署配置提供。
### 审批场景与模板版本
- 场景状态是生命周期 `int``0=已暂停, 1=启用, 2=暂停中``暂停中` 由系统维护,前端不能直接写入。
- 模板版本状态是生命周期 `int``0=已停用, 1=启用, 2=失效`
- 场景保存稳定 `scene_code`、中文名、状态、当前模板版本 ID、暂停原因和乐观锁版本不建立数据库外键或 GORM 关联标签。
- 模板版本保存场景码、递增版本号、企微模板 ID、名称、控件映射、模板快照、指纹、验证结果、发布人和发布时间。历史版本不可修改每个场景只能有一个启用版本。
- 模板发布流程为:暂停场景并停止发放新提交租约,等待所有未过期租约释放,读取新模板,完成业务字段到控件的可视化映射,后端重新调用企微校验,事务内停用旧版本、写新版本、切换当前版本并恢复场景。
- 退款稳定业务字段至少包含店铺、退款单号、订单号、资产标识、订单实收金额、可退款区间、固定申请退款金额、退款原因、申请备注、附件和真实业务提交人;除申请备注可为空外均需完成控件映射。金额控件只用于展示,审批人不得修改;线下充值至少包含店铺、充值单号、金额、备注、附件和真实业务提交人。
- 后端必须验证模板可访问、控件存在、控件类型匹配和所有必填业务字段已映射。前端不能提交控件类型或名称作为可信事实,也不直接编辑原始映射 JSON。
- 后台每 10 分钟验证启用模板。模板不可访问或指纹变化时暂停对应场景并告警;系统不能假设能从旧模板 ID 自动发现企微生成的新模板 ID。
- 场景处于暂停中、已暂停或当前模板失效时,创建退款或线下充值必须在任何业务单、审批实例或 Outbox 写入前失败。前端保留表单,恢复后用户重新发起创建请求;不留下待补提的孤儿业务单。
- 暂停前已成功创建的审批继续接收回调、参与轮询和终态处理,不因场景暂停而中止。
### 平台账号绑定与代理固定代提交
- 平台和超级管理员必须通过官方企微 Web 登录二维码绑定本人企微成员。只有已登录、启用的当前账号可为自己创建绑定会话;代理账号不能创建绑定会话。
- 绑定会话使用 Redis随机 `state` 有效 5 分钟且通过原子读取删除保证单次消费;独立 `session_id` 保存目标账号、发起账号、固定后台 Origin、状态和错误有效 10 分钟供原页面查询结果。
- 回调不接受前端传入的账号 ID。后端使用同一 CorpID 和自建应用 Access Token 调用 `auth/getuserinfo`;只返回 `openid` 的非本企业成员不能绑定。
- 账号 ID 与企微 `userid` 都一对一唯一。企微成员已绑定其他账号时拒绝覆盖,必须先由超级管理员强制解绑。换绑仍需本人重新扫码。
- 回调成功页只向服务端配置的后台 Origin 使用 `postMessage` 通知原窗口;通信失败时前端按 `session_id` 轮询兜底。Origin 不接受请求参数覆盖。
- 平台/超级管理员创建审批前,其本人绑定必须启用且成员可用;不满足时创建请求在业务落库前失败,并向前端返回可原地拉起绑定流程的错误状态。
- 代理账号不绑定企微。代理创建退款时使用部署配置中的固定成员代提交;该成员必须属于当前企业、启用并在应用可见范围内。未配置或失效时同样在业务落库前拒绝。
- 后台只展示固定成员是否就绪及显示名,不提供查看或修改原始固定 `userid` 的入口。
- 绑定、换绑和固定代提交成员变更只影响新审批。历史审批保留提交时的真实业务提交人、实际企微发起 `userid` 和身份来源快照。
### 真实业务提交人与身份语义
- `business_submitter` 是在本系统实际创建退款或线下充值的登录账号。它是列表、详情、通知、权限和审计中的“提交人/申请人”。
- `wecom_creator_userid` 是调用企微 `applyevent` 的实际成员。身份来源是方式类 `string``self_binding``agent_proxy`
- 业务创建事务和审批实例同时保存真实提交人的账号 ID、账号名称、角色/店铺显示快照,以及实际企微发起身份及来源;前端提交的申请人字段不可信。
- 企微模板的 `submitter` 必填字段始终写真实业务提交人名称和账号标识。代理审批单不能只显示固定代提交账号。
- Worker 使用审批实例在创建时冻结的企微身份,不在真正发送时按账号当前绑定或当前配置重新选择。
### 审批实例与一单一审批
- 审批实例保存业务类型、业务 ID/编号、场景码、模板版本及映射快照、真实业务提交人快照、实际企微发起身份及来源、`sp_no`、提交业务快照、企微详情/审批人快照、提交错误、租约、同步时间、业务处理结果和乐观锁版本。
- `(biz_type, biz_id)` 唯一,`sp_no` 非空时全局唯一;业务表使用唯一 `approval_instance_id` 指向实例。不使用 `round_no`,不存在从同一业务单推算“当前审批轮次”的逻辑。
- 审批状态是生命周期 `int`
- `0=提交中`
- `1=审批中`
- `2=已通过`
- `3=已驳回`
- `4=已撤销`
- `5=通过后撤销`
- `6=已删除`
- `7=提交失败`
- `8=提交结果未知`
- 各 DTO 的状态 description 必须从公共 constants 原文复制,并返回对应中文 `status_name`;不得在退款、充值或列表模块复制第二套审批枚举。
- 一张退款单只对应一条企微审批。企微拒绝同时终结审批和当前退款单,原退款单不可修改、不可重提。若处理拒绝原因后仍需退款,重新调用创建退款接口,生成新的退款 ID、退款单号、业务快照、提交人快照和审批实例。
- 已拒绝退款不视为活跃退款;新建仍需拒绝同一订单或资产存在其他审批中或业务处理未完成的退款。企微撤销、删除或通过后撤销属于异常处置,不自动作为新建退款的放行条件。
### 异步提交、租约与附件
- 业务单、唯一审批实例和 `WeComApprovalSubmissionRequested` Outbox 在同一 PostgreSQL 事务创建。业务 API 返回本地业务 ID和“提交中”不等待远程企微完成。
- Worker 通过状态和 `submit_lease_until` 条件更新领取短租约;租约覆盖对象存储下载、企微附件上传和 `applyevent`,处理期间续租,并通过 `defer` 释放。提交结果未知也必须先持久化再释放。
- 暂停场景先进入“暂停中”并停止新租约;只有未过期提交租约全部消失后才进入“已暂停”。
- 本地私有对象存储 Key 是附件权威引用。Worker 下载到受控临时文件后调用企微 `media/upload` 获得临时 `media_id`;不得把临时 `media_id`、预签名 URL 或敏感密钥写入业务快照作为永久依据。
- `applyevent` 固定使用企微模板审批人配置,不由本系统提交或计算审批节点。
### 提交结果未知与异常恢复
- 企微明确返回失败且确认未创建审批时,实例进入“提交失败”,修复配置后可对同一审批实例再次执行技术发送;这不是业务重提。
- 建连失败且可证明请求未发送时允许自动重试。
- 请求已发送后超时或连接中断时进入“提交结果未知”,禁止自动再次调用 `applyevent`
- 异常恢复只允许超级管理员,或具备独立“企微审批异常恢复”权限的平台账号执行,并提供两个动作:
- 绑定已有 `sp_no`:先查询企微详情并核对企业、模板版本、实际企微发起身份及来源、业务场景、真实业务提交人字段和业务快照,全部一致才允许绑定。
- 确认企微未创建后重新发送:保留原尝试,在同一审批实例下记录新的技术提交尝试,不创建业务审批轮次。
- 两种恢复都要求填写恢复原因,保存原请求尝试、校验依据、操作人、前后状态和结果的高风险 Audit EventIntegration Log 不被覆盖。
- 企微已经通过、拒绝或进入其他明确终态后,不允许再对原业务单执行提交恢复。退款拒绝后的再次退款只能创建新业务单。
- 管理 API 采用明确动作接口:
- `POST /api/admin/wecom/approvals/{id}/bind-sp-no`,请求包含 `sp_no``reason`
- `POST /api/admin/wecom/approvals/{id}/confirm-not-created-and-resend`,请求包含 `reason`
### 回调、轮询与状态同步
- `GET /api/callback/wecom/approval` 按企微协议完成 URL 校验;`POST /api/callback/wecom/approval` 校验 SHA1 签名、AES-CBC 解密并核对 `receiveID=CorpID`
- 回调只快速保存 Integration Log 和触发同步,然后按企微约定返回成功;回调事件中的状态不是最终业务权威。
- `getapprovaldetail` 是状态和审批详情的权威来源。回调和轮询都调用同一个 `SyncApprovalStatus` 用例。
- 轮询保持每 2 分钟扫描应轮询的审批中实例,单批最多 100 条并按最久未轮询优先。轮询通过租约或条件更新避免多实例重复占用。
- 状态同步保存审批详情、审批人名称快照、意见、附件元数据和时间线,以乐观锁更新状态。状态未变化时不产生第二个业务事件。
- 首次进入终态时,同一事务写对应业务终态 Outbox。业务 Worker 失败可重试,但 `business_processed_at` 非空后禁止再次执行资金动作。
- 审批状态和业务处理状态必须独立展示。审批已通过但退款或入账处理中/失败时,审批状态仍保持已通过。
- 通过后撤销且资金动作已经完成时不自动冲正,记录 `critical` Audit Event、站内告警并进入人工处理尚未执行时阻止后续资金任务。
### 权限与信息可见性
- 连接状态、场景、模板版本、模板读取/发布、暂停/恢复、平台账号绑定列表和强制解绑仅超级管理员访问。
- 平台账号只能查询、重新绑定和解绑本人企微身份;代理无绑定能力。
- 审批运行列表、详情和立即同步仅超级管理员,或具备独立“企微审批运营”权限的平台账号访问。该权限不包含配置、模板、绑定管理或异常恢复。
- 提交未知恢复仅超级管理员,或具备独立“企微审批异常恢复”权限的平台账号访问。
- 代理无企微配置、绑定、审批运行列表/详情及恢复接口权限,只能按现有店铺层级数据范围读取退款业务详情中的审批摘要。
- 代理摘要只包含真实业务提交人、审批状态、状态时间和业务处理结果,以及其原业务权限允许查看的退款资料与业务凭证。代理不得看到审批人、内部意见、审批人上传附件或企微内部标识。
- 平台/超级管理员仍必须具备对应业务查看权限,才可读取完整审批人、意见、时间线和审批附件;企微运营权限本身不能绕过退款/充值数据权限。
- 详情、附件解析、受保护下载和导出复用同一主体权限投影。持有附件引用或历史导出链接不能绕过当前权限。
- 所有越权响应使用统一禁止访问错误,不能区分资源不存在与无权限。
### API 契约
- 连接及场景:
- `GET /api/admin/wecom/status`
- `GET /api/admin/wecom/approval-scenes`
- `PUT /api/admin/wecom/approval-scenes/{scene_code}/status`,请求包含目标 `status`(只接受暂停或启用)与 `reason`
- 模板:
- `POST /api/admin/wecom/approval-templates/inspect`
- `POST /api/admin/wecom/approval-templates/publish`
- `GET /api/admin/wecom/approval-templates?scene_code=`
- 本人绑定:
- `GET /api/admin/wecom/account-binding/me`
- `POST /api/admin/wecom/account-binding/sessions`
- `GET /api/admin/wecom/account-binding/sessions/{session_id}`
- `DELETE /api/admin/wecom/account-binding/me`
- `GET /api/callback/wecom/account-binding?code=&state=`
- 超级管理员绑定管理:
- `GET /api/admin/wecom/account-bindings?page=&page_size=&binding_status=`
- `DELETE /api/admin/wecom/account-bindings/{account_id}`
- 审批运行:
- `GET /api/admin/wecom/approvals?biz_type=&status=&sp_no=&biz_no=&page=&page_size=`
- `GET /api/admin/wecom/approvals/{id}`
- `POST /api/admin/wecom/approvals/{id}/sync`
- 两个提交未知恢复接口见上文。
- 企微审批回调:
- `GET /api/callback/wecom/approval`
- `POST /api/callback/wecom/approval`
- 列表默认每页 20、最大 100查询保持稳定排序审批运行页默认按更新时间倒序并以 ID 作为并列排序键。
- 所有后台接口使用统一 `{code,msg,data,timestamp}` 响应,列表使用项目统一分页结构;参数验证失败返回统一参数错误,不向调用方泄漏底层企微、数据库或验证器错误。
### 前端页面与交互
- `/system/wecom` 仅超级管理员可见,包含连接状态、代理固定成员就绪状态、审批场景、模板版本、可视化控件映射、平台账号绑定清单和异常审批入口;不提供 Secret 或固定 `userid` 编辑。
- 平台/超级管理员个人中心展示本人绑定状态、成员名称、最近验证时间、绑定/换绑/解绑;代理不展示绑定入口。
- 平台/超级管理员创建需审批业务时若未绑定,在当前表单原地提供扫码入口;成功后继续填写。代理固定成员不可用、场景暂停或模板失效时保留表单并展示后端明确原因。
- `/operations/wecom-approvals` 及稳定详情路由用于运行监控,展示业务类型/编号、`sp_no`、审批状态、模板版本、真实业务提交人、实际企微身份来源、更新时间、业务处理结果和异常标识。
- “立即同步”只调用详情同步,不执行审批动作。提交未知记录按恢复权限展示两个高风险动作及二次确认;业务处理失败只展示错误摘要和任务状态。
- 业务详情复用统一只读 `approval` 区块。平台按业务权限显示完整详情;代理使用最小投影。所有页面均覆盖加载、空、失败、无权限、场景维护、绑定过期和高风险异常状态。
- 页面不得出现本地通过、拒绝、退回、审批金额修改或人工确认退款按钮。
### 架构与迁移
- 场景、模板版本、审批状态机、提交租约、身份绑定和终态幂等属于复杂写,采用 `Handler → Application UseCase → Domain → Repository/WeCom Adapter`
- 审批运行列表、详情、业务摘要和权限投影走 Query可直接使用 GORM 做分页、批量关联与 DTO 投影,不经过聚合根。
- 企业微信 Token、身份、模板、媒体、审批和回调加解密统一封装在 WeCom Adapter退款和充值不得各自调用企微 HTTP API。
- 不建立数据库外键,不使用 GORM 关联标签;关联 ID 在 Application/Domain 中显式维护。
- 停机发布时创建场景、模板版本、平台绑定和审批实例数据结构,配置真实企微回调/可信域名验证平台发起人绑定和代理固定成员再发布模板映射、Worker、业务接入和前端。
- 已结束的历史本地审批保留为 `legacy` 只读事实,不伪造企微实例。发布时仍未结束的退款/线下充值按创建人类型形成真实企微审批;缺少平台绑定、固定代理身份或创建人事实的记录进入迁移待处理清单,不能继续使用旧本地审批接口。
- 回滚应用时保留已经形成的企微实例、提交人/身份快照、Integration Log 和 Audit Event。已有企微申请进入运行后不得恢复旧本地审批动作只能继续同步或人工处置。
## 公共能力发布依赖
- 最终 14 号真实企微发布票阻塞于公共基础 12 号票、全局审计 19 号票和公共通知 08 号票;退款业务消费者还必须替换为 UR#35 的具体纵向 Ticket。
- 审批终态事件必须登记版本和消费者幂等键;审批实例是领域事实,企微请求/回调进入 Integration Log提交、人工同步和状态变化进入 Audit Event。
## Testing Decisions
- 主要自动化接缝采用最高公共行为边界Fiber HTTP 路由与认证 → Application/Domain/Query → GORM → 开发 PostgreSQL、Redis 和 Asynq 可控队列;企业微信网络统一替换为可编程 WeCom Adapter。测试不直接断言私有函数或目录结构。
- 回调测试从真实 HTTP 回调入口注入使用测试 Token/AES Key 生成的签名密文,验证 URL 校验、签名错误、解密错误、错误 CorpID、重复事件、快速响应和统一状态同步不绕过回调协议直接调用内部函数。
- 使用现有测试 PostgreSQL 与本地 Redis DB 7 验证迁移、唯一约束、绑定会话、Token 锁、提交租约、轮询领取和幂等;测试启动时必须校验 Redis Client、Asynq Client 和 Worker Server 的实际 DB 均为 7禁止向已部署测试环境使用的 DB 6 投递任务,也不得执行 `FLUSHDB`。测试及日志不得输出任何连接密码或企微密钥。
- WeCom Adapter 契约测试覆盖 Token 获取/刷新、成员身份、模板读取、附件上传、发起审批、详情查询,以及企微非零错误、超时、响应丢失和限流。自动化测试不依赖真实企微网络。
- 场景测试覆盖首次发布、暂停中停止发租约、租约自然释放/续租、已暂停、新版本原子切换、模板不可访问、指纹变化、必填映射缺失、控件类型不匹配和发布失败保持暂停。
- 身份测试覆盖平台本人扫码、代理禁止绑定、非企业成员、过期/重复 `state`、同一成员绑定第二账号、换绑、自助解绑、强制解绑、固定代理成员可用/失效,以及历史实例不随绑定变化。
- 业务创建前置测试证明场景暂停、模板失效、平台未绑定、代理固定身份不可用时,不产生业务单、审批实例或 Outbox前端重试成功后只产生一组事实。
- 提交测试覆盖 Outbox 重投、并发 Worker、租约过期恢复、附件上传部分失败、明确企微失败、确认未发送的连接失败、已发送响应未知和成功返回 `sp_no`
- 异常恢复测试覆盖无权限、错误状态、`sp_no` 不存在、企业/模板/身份/场景/提交人/业务快照任一不匹配、成功绑定、确认未创建后重新发送、重复操作和高风险审计完整性。
- 状态同步测试覆盖审批中、通过、拒绝、撤销、删除、通过后撤销、详情无变化、回调与轮询并发、重复终态以及乐观锁冲突;验证每个业务终态只产生一次 Outbox。
- 轮询测试覆盖 2 分钟资格、每批 100、最久未轮询优先、多实例并发领取、详情失败重排和回调先到后轮询不重复处理。
- 权限测试覆盖超级管理员、无专项权限平台、仅企微运营平台、仅异常恢复平台、同时具备权限平台和代理;证明企微运营不能维护模板或恢复未知,代理不能访问公共企微接口。
- 信息投影测试对同一退款分别以平台和代理身份读取:平台在具备业务权限时可见审批人/意见/审批附件,代理只见最小摘要;无业务权限的平台即使有企微运营权限也不能读取该业务资料或附件。
- 一单一审批测试验证 `(biz_type,biz_id)` 并发唯一性。企微拒绝后原退款详情只读、旧重提接口不存在;再次退款经创建接口生成新 ID、退款单号、提交人快照和独立审批新旧单互不继承审批资料。
- 日志与响应测试验证 Secret、Token、EncodingAESKey、对象 Key、临时 `media_id`、原始固定 `userid` 和完整附件不会出现在 API、日志或审计详情中。
- 停机迁移测试覆盖历史终态标为 `legacy`、待审批按平台/代理身份迁移、缺失绑定进入待处理、重复执行不重复创建实例,以及旧审批路由确实不再注册。
- 前端人工验收覆盖连接配置、模板维护、暂停等待、本人扫码绑定、代理代提交、审批运行筛选、立即同步、两种异常恢复、权限菜单、详情投影、通过后撤销红色告警以及所有加载/空/失败状态。
- 真实企微验收是企微公共能力及首个接入业务的实现完成门禁,不得只推迟到后续联调。至少使用真实模板完成平台本人发起、代理固定账号代发、真实附件、通过/拒绝、加密回调和回调丢失后的轮询兜底;回调可按“企业微信 → 用户提供的中转应用 → 本地服务”原样转发,后端仍完整验签解密。自动化 Adapter 继续覆盖超时、响应未知、重复/乱序、撤销、删除、通过后撤销和限流等难以稳定人工制造的异常。真实参数和验收记录不得泄漏密钥或个人敏感信息。
## Out of Scope
- 不建设本地通用审批流引擎、BPMN、节点设计器、部门模型、直属领导推导、待我审批或本地审批按钮。
- 不让普通运营手工录入平台员工或代理固定账号的企微 `userid`
- 不为代理账号建立企微绑定,也不把固定代提交账号当成真实业务提交人。
- 不在 UR#37 内定义退款金额、代理钱包回溯或线下充值入账的完整业务规则;这些由 UR#35、UR#34 消费公共终态事件实现。
- 不把代理在线微信/支付宝充值或换货接入审批。
- 不调用微信、支付宝等支付渠道自动退款。
- 不在企微审批人端修改退款金额;企微只同意或拒绝提交时固定的业务快照。
- 不允许企微拒绝后的原退款单编辑或重提,不保留 `resubmit` 兼容接口。
- 不因通过后撤销自动冲正已经完成的资金动作。
- 不向代理公开平台内部审批人、意见、审批附件或企微内部标识。
- 不把企微临时 `media_id`、预签名 URL 或外部原始响应当作永久业务资料。
## Further Notes
- 当前代码尚无企微审批公共模块;退款仍注册本地 `approve/reject/return/resubmit`,旧线下充值也保留本地审核路径。实现必须以停机切换后的契约为准,不能把现状默认为最终产品决定。
- 当前退款活跃校验已把待审批和已通过视为活跃;新模型还需覆盖审批通过后的业务处理未完成。已拒绝退款明确不活跃,因此允许同一订单在纠正问题后重新创建新退款单。
- UR#44 列表审批摘要、UR#42 审批附件导出、UR#35 退款终态和 UR#34 线下充值终态都依赖本公共能力它们必须复用统一实例、状态常量、Query、附件权限和 WeCom Adapter不能各自建立替代模型。
- 该需求横跨配置、身份、模板、远程提交、回调、轮询、运行 Query 与安全边界,预计超过一个高质量实现上下文。进入实现前应基于本 Spec 评估并拆分窄的端到端 tracer-bullet tickets而不是按 Model/Service/Handler 水平切层。

View File

@@ -1,18 +0,0 @@
# 01 — 交付企微连接状态与安全 Adapter 闭环
**What to build:** 超级管理员可以查看企微连接、Token 获取和代理固定代提交成员是否就绪;企微 Token 与成员读取通过统一安全 Adapter 执行,每次真实外部尝试均可追踪,但任何接口、日志或审计都不会泄露 Secret、Token 或固定成员原始 `userid`
**Blocked by:** `.scratch/tech-global-audit/issues/02-integration-log-attempt-loop.md` — 02 — 交付可恢复的 Integration Log 尝试闭环
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为 Query。
**完整业务边界:** 本票收口企微部署配置、Access Token 缓存与并发刷新、成员可用性读取、连接状态投影和外部尝试记录。明确不建立审批状态机,不创建审批实例,不提供敏感配置在线编辑,也不迁移退款或充值逻辑。
- [ ] 企微连接使用部署配置提供 CorpID、AgentID、AgentSecret、回调密钥、绑定回调地址、后台固定 Origin、代理固定代提交成员、请求超时和轮询间隔敏感值不进入通用配置表。
- [ ] Access Token 使用 Redis 缓存TTL 为企微 `expires_in` 减 300 秒并通过有期限锁避免多进程并发刷新Redis 异常时不会把空值或过期值伪装为有效 Token。
- [ ] 成员读取可以确认代理固定成员属于当前企业、已启用且在应用可见范围内,只向超级管理员返回就绪状态和显示名,不返回原始 `userid`
- [ ] 连接状态接口仅允许超级管理员访问,返回配置就绪、最近连通、最近安全错误摘要、回调最近成功和 Token 最近获取时间,使用统一响应结构。
- [ ] Token 和成员外部调用写 Integration Log记录操作、耗时、企微错误码和安全关联标识不记录令牌、密钥、完整响应或固定成员原始标识。
- [ ] WeCom Adapter 契约测试覆盖 Token 获取、缓存命中、并发刷新、企微非零错误、超时、成员失效、Redis 锁恢复及敏感信息不泄漏;新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,21 +0,0 @@
# 02 — 完成平台账号本人扫码绑定闭环
**What to build:** 平台账号和超级管理员可以从本人会话发起官方企微 Web 登录扫码,安全完成本人绑定、查看结果、换绑或解绑;超级管理员可以查看平台绑定清单并强制解绑冲突成员,代理账号始终不能创建或管理企微绑定。
**Blocked by:**
- 01 — 交付企微连接状态与安全 Adapter 闭环
- `.scratch/tech-global-audit/issues/01-audit-event-write-loop.md` — 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 复杂写,采用 Application + Domain + Repository/WeCom Adapter绑定清单为辅助 Query。
**完整业务边界:** 本票收口系统账号与企微成员一对一绑定、一次性会话、本人解绑和超级管理员强制解绑。明确不改变账号认证、角色或店铺模型,不允许人工录入 `userid`,不为代理建立绑定。
- [ ] 只有当前已登录且启用的平台账号或超级管理员可以为自己创建绑定会话;随机 `state` 五分钟有效且原子读取删除,独立 `session_id` 十分钟内可查询结果。
- [ ] 回调只从服务端会话确定目标账号,调用 `auth/getuserinfo` 验证企业成员;前端不能传入或覆盖账号 ID、后台 Origin 或企微成员事实。
- [ ] 账号 ID 与企微 `userid` 均保持一对一唯一;非企业成员、过期或重复 state、成员已绑定其他账号均安全拒绝换绑必须由本人重新扫码。
- [ ] 回调成功页只向服务端配置的固定 Origin 发送结果,原页面可以通过 `session_id` 轮询兜底;响应和页面不泄露 Token 或其他账号信息。
- [ ] 平台账号只能查询和解绑本人;超级管理员可分页查看绑定状态并强制解绑,代理访问所有绑定接口均被拒绝。
- [ ] 绑定、换绑、解绑和强制解绑写统一 Audit Event测试覆盖并发唯一性、会话单次消费、权限、停用账号及历史审批身份快照不随绑定变化新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,22 +0,0 @@
# 03 — 发布审批场景与首个模板版本
**What to build:** 超级管理员可以按稳定场景查看退款和线下充值审批配置,读取企微模板并通过可视化字段映射发布首个不可变模板版本;发布前由后端重新核对模板、控件类型和必填映射,业务代码不再依赖易变的模板 ID 或控件 ID。
**Blocked by:**
- 01 — 交付企微连接状态与安全 Adapter 闭环
- `.scratch/tech-public-foundation/issues/01-public-migration-ownership-and-gates.md` — 01 — 建立公共迁移所有权与检查门禁
- `.scratch/tech-global-audit/issues/01-audit-event-write-loop.md` — 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 复杂写,采用 Application + Domain + Repository/WeCom Adapter场景与版本列表为辅助 Query。
**完整业务边界:** 本票收口稳定场景、不可变模板版本、模板读取、字段映射校验和首次发布。明确不实现运行中安全换版,不创建业务审批实例,不接入退款或充值创建。
- [ ] 初始化稳定场景 `refund_approval``offline_recharge_approval`,场景状态固定为 `0=已暂停、1=启用、2=暂停中`,模板版本状态固定为 `0=已停用、1=启用、2=失效`
- [ ] 模板版本保存递增版本号、企微模板标识、名称、控件映射、模板快照、指纹、验证结果和发布快照;历史版本不可修改,每个场景至多一个启用版本。
- [ ] 模板读取接口只接受稳定场景和企微模板标识,向前端返回可映射控件事实;前端不能把控件名称、类型或原始映射 JSON 作为可信发布依据。
- [ ] 发布前重新从企微读取模板,验证可访问性、控件存在、类型匹配及全部必填业务字段映射;退款和线下充值字段集合符合 PRD金额字段只用于展示。
- [ ] 首次发布在单一事务内写不可变版本、设置当前版本并启用场景;失败时不留下半发布版本或错误的当前版本引用。
- [ ] 模板读取、验证和发布写 Integration Log/Audit Event真实 PostgreSQL 与可编程 Adapter 测试覆盖唯一约束、映射缺失、类型错误、指纹和事务回滚;新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,22 +0,0 @@
# 04 — 提供审批创建前置检查与事务接入契约
**What to build:** 退款和线下充值创建用例可以通过稳定公共接缝确认场景、当前模板和实际企微发起身份可用,并在自己的业务事务中原子创建唯一审批实例及提交 Outbox任一前置失败时不会留下业务单、审批实例或待补提事件。
**Blocked by:**
- 02 — 完成平台账号本人扫码绑定闭环
- 03 — 发布审批场景与首个模板版本
- `.scratch/tech-public-foundation/issues/02-transactional-public-outbox-write.md` — 02 — 在业务事务中可靠写入公共 Outbox
**Status:** ready-for-agent
**架构通道:** 主通道为复杂写 Application + Domain辅助通道为 Application Port/Adapter。
**完整业务边界:** 本票收口审批创建前置判定、真实业务提交人和企微发起身份快照、唯一审批实例以及事务内提交事件。明确不创建退款单或充值单,不定义其金额和活跃业务规则,不调用远程企微。
- [ ] 前置检查按稳定场景读取当前启用模板,并拒绝暂停中、已暂停、模板缺失或模板失效;失败发生在调用方写入任何业务事实前。
- [ ] 平台和超级管理员使用本人启用且成员可用的绑定;代理仅可在允许代理发起的退款场景使用部署配置固定成员,线下充值不允许代理发起。
- [ ] 审批实例冻结业务类型、业务 ID/编号、场景、模板版本与映射、业务快照、真实提交人账号/名称/角色/店铺快照、企微发起身份及 `self_binding/agent_proxy` 来源。
- [ ] `(biz_type,biz_id)` 唯一,非空 `sp_no` 全局唯一;状态初始为 `0=提交中`,不使用审批轮次,不建立数据库外键或 GORM 关联标签。
- [ ] 调用方可使用同一 GORM 事务创建业务事实、审批实例和结构化 `WeComApprovalSubmissionRequested` Outbox任一步失败整体回滚事务中不调用 Redis、Asynq、对象存储或企微。
- [ ] 公共状态常量、中文状态名及公开 DTO 描述保持唯一来源;集成测试以代表性测试业务事实验证前置失败零落库、事务回滚、并发唯一性和成功只产生一组审批事实。

View File

@@ -1,22 +0,0 @@
# 05 — 完成附件上传与企微异步提交闭环
**What to build:** 审批提交 Worker 可以从公共 Outbox 可靠领取审批实例,使用冻结的模板、业务快照和企微发起身份上传附件副本并调用企微发起审批;成功后绑定唯一 `sp_no`,明确失败可安全重试,已发送但响应未知时停止自动重发并等待人工恢复。
**Blocked by:**
- 04 — 提供审批创建前置检查与事务接入契约
- `.scratch/tech-public-foundation/issues/03-outbox-at-least-once-delivery.md` — 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
- `.scratch/tech-global-audit/issues/02-integration-log-attempt-loop.md` — 02 — 交付可恢复的 Integration Log 尝试闭环
**Status:** ready-for-agent
**架构通道:** 复杂写 Application + Domain外部调用位于 Infrastructure WeCom Adapter。
**完整业务边界:** 本票收口技术提交尝试、提交租约、附件副本上传、企微表单构造和提交结果分类。明确不处理企微审批终态,不执行退款或充值资金动作,不把临时企微资料变成永久业务事实。
- [ ] Worker 通过审批状态和 `submit_lease_until` 条件领取、续租并在所有退出路径释放租约;并发 Worker 同一时刻只有租约所有者可以提交。
- [ ] 本地私有对象 Key 是申请附件权威引用Worker 使用受控临时文件上传企微,临时 `media_id`、预签名 URL 和文件内容不进入永久业务快照、普通日志或响应。
- [ ] 企微表单严格使用实例冻结的模板映射、真实业务提交人和实际企微发起身份,审批人配置完全使用企微模板,不由系统计算或提交。
- [ ] 成功响应原子绑定唯一 `sp_no` 并进入审批中;企微明确未创建时进入提交失败,可在同一实例上技术重试且保留尝试历史。
- [ ] 可证明未发送的建连失败允许自动重试;请求已发送后超时或连接中断进入提交结果未知,持久化未知事实后禁止自动再次调用 `applyevent`
- [ ] 测试覆盖 Outbox 重投、并发领取、租约过期恢复、部分附件失败、明确失败、未发送失败、响应未知、成功绑定和结构化 Asynq 载荷,且不依赖真实企微网络。

View File

@@ -1,21 +0,0 @@
# 06 — 完成场景安全暂停、模板换版与失效检测
**What to build:** 超级管理员可以把场景安全切换到暂停中,停止新提交租约并等待已有租约释放后完成暂停,再发布新的不可变模板版本并恢复场景;后台定期发现启用模板不可访问或指纹变化时会自动暂停场景并告警。
**Blocked by:**
- 03 — 发布审批场景与首个模板版本
- 05 — 完成附件上传与企微异步提交闭环
**Status:** ready-for-agent
**架构通道:** 复杂写 Application + Domain模板验证调度为辅助 Infrastructure。
**完整业务边界:** 本票收口场景暂停状态机、租约排空、模板原子切换、恢复和定期有效性检查。明确不停止暂停前已经创建的审批同步或终态处理,不自动发现新的企微模板 ID。
- [ ] 状态接口只接受目标已暂停或启用以及必填原因;暂停中由系统维护,前端不能直接写入,非法转换或乐观锁冲突安全失败。
- [ ] 进入暂停中后立即拒绝新业务前置检查和新提交租约,但已有有效租约可以续租直至完成;所有未过期租约消失后才进入已暂停。
- [ ] 新版本发布前重新读取和验证企微模板;事务内停用旧版本、写新版本、切换当前版本并恢复场景,任一步失败保持场景暂停且旧事实不被改写。
- [ ] 后台按十分钟默认间隔验证启用模板;模板不可访问或指纹变化时标记版本失效、暂停对应场景并产生安全告警,不猜测替代模板 ID。
- [ ] 场景暂停和恢复不影响已创建实例继续接收回调、轮询和终态处理;历史实例始终使用创建时冻结的版本和映射。
- [ ] 测试覆盖暂停等待、停止发租约、租约续期/自然释放、首次与再次发布、发布回滚、模板失效、指纹变化、并发状态更新和 Audit Event 完整性。

View File

@@ -1,22 +0,0 @@
# 07 — 接入加密回调与权威详情同步
**What to build:** 企业微信可以完成审批回调 URL 校验并推送加密事件;系统验签、解密和核对企业身份后快速记录回调并触发统一同步,最终始终以 `getapprovaldetail` 保存审批状态、审批人、意见、附件元数据和时间线。
**Blocked by:**
- 05 — 完成附件上传与企微异步提交闭环
- `.scratch/tech-global-audit/issues/02-integration-log-attempt-loop.md` — 02 — 交付可恢复的 Integration Log 尝试闭环
- `.scratch/tech-public-foundation/issues/10-sensitive-route-safe-summaries.md` — 10 — 为敏感接口提供安全摘要策略
**Status:** ready-for-agent
**架构通道:** 主通道为复杂写 Application + Domain回调协议和企微详情位于 Infrastructure Adapter。
**完整业务边界:** 本票收口回调协议、安全入站记录和单实例权威状态同步。明确不实现批量轮询,不消费退款或充值终态,不把回调载荷中的状态当成业务权威。
- [ ] GET 回调按企微协议完成 URL 校验POST 回调校验 SHA1 签名、AES-CBC 解密并核对 `receiveID=CorpID`,错误签名、密文或企业身份安全拒绝。
- [ ] 入站回调先保存脱敏 Integration Log 与幂等标识只记录事件类型、安全资源标识、大小、哈希和结果不记录密文、明文、签名、Nonce 或密钥。
- [ ] 有效回调快速触发统一 `SyncApprovalStatus` 并按企微约定响应;重复事件、乱序事件和同步任务重复投递不直接改写状态。
- [ ] 同步用例调用 `getapprovaldetail` 作为唯一权威,保存审批详情、审批人名称快照、意见、附件元数据、时间线和最后同步时间。
- [ ] 审批实例以乐观锁和合法状态转换更新;详情未变化时只更新必要轮询事实,不产生第二个等价业务事件。
- [ ] 真实 Fiber 回调测试使用测试 Token/AES Key 构造签名密文,覆盖 URL 校验、签名/解密/CorpID 错误、重复事件、快速响应和权威同步;新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,21 +0,0 @@
# 08 — 完成轮询补偿与终态只发布一次
**What to build:** 系统可以每两分钟并发安全地领取最久未同步的审批实例并查询企微权威详情;无论回调、轮询或重复任务谁先到,审批首次进入终态时都只发布一次对应业务终态事件,并对撤销、删除和通过后撤销保留明确异常语义。
**Blocked by:**
- 07 — 接入加密回调与权威详情同步
- `.scratch/tech-public-foundation/issues/03-outbox-at-least-once-delivery.md` — 03 — 完成 Outbox 到 Asynq 的至少一次投递闭环
**Status:** ready-for-agent
**架构通道:** 复杂写 Application + Domain轮询调度和领取为辅助 Infrastructure。
**完整业务边界:** 本票收口轮询资格、并发领取、审批终态状态机及事务内业务事件发布。明确不实现退款退款、充值入账或其他下游消费者,不把审批通过等同于业务处理成功。
- [ ] 轮询默认每两分钟扫描提交中、审批中和需要恢复同步的实例,单批最多 100 条,按最久未轮询优先并以稳定主键打破并列。
- [ ] 多实例通过轮询租约或条件领取避免同一时刻重复占用;详情失败记录安全摘要并重新安排,进程中断后过期租约可恢复。
- [ ] 回调、轮询和手动同步复用同一状态同步用例,审批中、通过、驳回、撤销、通过后撤销、删除及提交异常均遵守公共状态机。
- [ ] 首次进入终态时在保存审批状态的同一 PostgreSQL 事务写稳定业务终态 Outbox状态未变化、重复终态或乐观锁冲突重试不会产生第二个事件。
- [ ] 审批状态与 `business_process_result/business_processed_at` 独立;通过后撤销在业务未执行时阻断后续处理,已执行时不自动冲正并产生 critical Audit Event 与告警事件。
- [ ] 测试覆盖轮询资格、批量上限、稳定排序、多实例领取、回调与轮询并发、重复终态、乱序状态、乐观锁冲突、撤销异常和每个业务终态最多一个 Outbox。

View File

@@ -1,22 +0,0 @@
# 09 — 交付审批运行查询、权限投影与立即同步
**What to build:** 超级管理员或具备企微审批运营权限的平台账号可以筛选审批运行记录、查看其原业务权限允许的完整审批详情并立即同步;代理和无业务权限的平台账号不能借企微运营接口看到审批人、意见、附件或其他业务资料。
**Blocked by:**
- 02 — 完成平台账号本人扫码绑定闭环
- 03 — 发布审批场景与首个模板版本
- 07 — 接入加密回调与权威详情同步
**Status:** ready-for-agent
**架构通道:** 主通道为 Query立即同步为辅助复杂写 Application。
**完整业务边界:** 本票收口审批运行列表、详情、专项权限、原业务权限复核和主体信息投影。明确不提供本地通过、驳回、退回或资金处理动作,不迁移退款/充值自己的列表和详情 Query。
- [ ] 运行列表支持业务类型、审批状态、`sp_no`、业务单号、业务处理结果和分页过滤,默认每页 20、最大 100按更新时间倒序并以 ID 稳定排序。
- [ ] 列表和详情返回公共状态及中文名称、模板版本、真实业务提交人、企微身份来源、同步时间、业务处理结果和异常标识,审批与业务处理状态独立展示。
- [ ] 只有超级管理员或具备独立企微审批运营权限的平台账号可访问运行接口;该权限不授予场景、模板、绑定管理或异常恢复能力。
- [ ] 完整详情必须再次通过对应退款或充值业务权限 Adapter无权、资源不存在和引用失效使用统一禁止访问语义企微运营权限不能绕过业务数据范围。
- [ ] 平台完整投影可以包含审批人、意见、时间线和审批附件元数据;供业务详情复用的代理投影只包含真实提交人、审批状态/时间和业务处理结果,不返回企微内部标识。
- [ ] 立即同步只调用统一权威详情同步不执行审批动作Query 和 Fiber 集成测试覆盖角色权限、业务权限、分页排序、无 N+1、附件权限和越权一致性新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,23 +0,0 @@
# 10 — 完成提交结果未知的高风险恢复闭环
**What to build:** 超级管理员或具备企微审批异常恢复权限的平台账号可以为提交结果未知的实例安全绑定已存在的 `sp_no`,或在人工确认企微未创建后重新发送同一审批;系统完整保留恢复原因、校验依据、前后状态和每次技术尝试,避免重复企微审批。
**Blocked by:**
- 05 — 完成附件上传与企微异步提交闭环
- 07 — 接入加密回调与权威详情同步
- 09 — 交付审批运行查询、权限投影与立即同步
- `.scratch/tech-global-audit/issues/01-audit-event-write-loop.md` — 01 — 交付不可变 Audit Event 写入闭环
**Status:** ready-for-agent
**架构通道:** 复杂写 Application + Domain企微核对位于 Infrastructure Adapter。
**完整业务边界:** 本票收口提交结果未知的两个明确恢复动作和高风险审计。明确不允许编辑业务申请、不创建业务审批轮次、不恢复已经进入明确终态的审批,退款驳回后的再次申请仍属于 UR#35 新业务单。
- [ ] 两个恢复接口只允许超级管理员或具备独立异常恢复权限的平台账号调用,请求必须包含非空恢复原因;企微运营权限本身不能执行恢复。
- [ ] 绑定已有 `sp_no` 前查询企微详情并核对企业、模板版本、实际企微发起身份及来源、业务场景、真实业务提交人字段和提交业务快照,任一不匹配均拒绝绑定。
- [ ] 成功绑定使用非空 `sp_no` 全局唯一约束和预期状态条件更新,随后进入统一权威同步;重复调用不能覆盖其他实例或制造第二次终态。
- [ ] 确认未创建后重新发送保留原尝试,并在同一审批实例记录新的技术提交尝试;不创建新业务 ID、审批实例或审批轮次。
- [ ] 已经进入通过、驳回、撤销、通过后撤销、删除等明确终态的实例禁止恢复;并发恢复、重复操作和状态变化时安全失败。
- [ ] 两种恢复均写高风险 Audit Event 并保留 Integration Log测试覆盖无权限、非法状态、`sp_no` 不存在、逐项快照不匹配、成功绑定、重新发送、重复并发和审计事务完整性;新增 Handler 同步路由、RouteSpec 和两个 OpenAPI 文档生成器。

View File

@@ -1,21 +0,0 @@
# 11 — 向真实业务提交人发送审批结果通知
**What to build:** 企微审批首次进入终态后,系统可以通过公共通知能力可靠通知真实业务提交人;代理由固定企微成员代提交时仍只把代理本人视为申请人,通过后撤销等高风险异常可按规则通知申请人和当前财务角色账号。
**Blocked by:**
- 08 — 完成轮询补偿与终态只发布一次
- `.scratch/tech-inapp-notifications/issues/08-notification-release-gate.md` — 08 — 完成公共通知发布门禁与下游接入契约
**Status:** ready-for-agent
**架构通道:** Application + Port/Adapter复用公共 Outbox 和通知消费者。
**完整业务边界:** 本票收口企微审批结果通知事件、接收人语义、受控模板与目标引用。明确不复制通知模型、Worker、未读接口或前端消息中心不发送企微待办不承担退款到账通知。
- [ ] 通过、驳回、撤销、删除和通过后撤销使用受控通知类型、稳定事件 ID、审批实例/业务引用和最小结构化模板字段。
- [ ] 普通审批结果接收人固定为审批实例冻结的真实业务提交人账号 ID绝不使用实际企微发起 `userid` 或代理固定代提交账号替代。
- [ ] 通过后撤销且业务已经处理时,除申请人外按公共通知接缝解析当前可用财务角色账号;同一账号多重命中只生成一条通知。
- [ ] 重复回调、轮询、Outbox 和通知 Worker 消费不会生成重复通知;账号已停用或删除时遵循公共通知无接收人语义,不无限重试。
- [ ] 通知只保存受控审批或来源业务引用,目标解析时重新验证当前权限,不保存任意 URL、审批附件、意见或企微内部身份。
- [ ] 端到端测试覆盖平台本人发起、代理代提交、通过/驳回、通过后撤销、多接收人去重、停用提交人、重复事件和受控目标权限变化。

View File

@@ -1,25 +0,0 @@
# 12 — 交付历史审批与待处理数据迁移接缝
**What to build:** 发布人员可以幂等盘点退款和线下充值的历史本地审批,把已结束记录保留为只读 `legacy` 事实,并对仍待处理记录执行真实提交人、企微身份、模板和业务资料预检,输出可迁移项与明确待人工处理清单。
**Blocked by:**
- 02 — 完成平台账号本人扫码绑定闭环
- 03 — 发布审批场景与首个模板版本
- 04 — 提供审批创建前置检查与事务接入契约
- 05 — 完成附件上传与企微异步提交闭环
- 06 — 完成场景安全暂停、模板换版与失效检测
- 07 — 接入加密回调与权威详情同步
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure辅助通道为 Application。
**完整业务边界:** 本票收口公共审批迁移分类、预检、幂等实例创建接缝和回滚边界。明确不猜测退款或充值业务事实,不修改其金额、资金或处理状态;各业务表字段回填和旧路由下线分别由 UR#35、UR#34 的纵向切片负责。
- [ ] 历史已结束的本地审批只投影为 `approval_source=legacy`,保留原业务事实,不创建虚假 `sp_no`、企微审批人、意见、附件或时间线。
- [ ] 待处理记录按业务类型核对真实创建人、账号类型、本人绑定或固定代理身份、当前场景/模板以及业务提供的必需快照和附件事实。
- [ ] 满足条件的记录可通过公共事务接缝幂等创建唯一企微审批实例及提交事件;重复执行不会生成第二个实例、`sp_no` 或 Outbox。
- [ ] 缺少平台绑定、代理固定身份、真实提交人、模板、金额或附件等事实时进入结构化待处理清单,不继续调用旧审批动作,也不由迁移脚本猜测或补造。
- [ ] 应用回滚保留已经形成的企微实例、身份/模板快照、Outbox、Integration Log 和 Audit Event真实企微审批开始后不得恢复旧本地审批按钮。
- [ ] 迁移测试覆盖历史终态、平台/代理待处理、缺失事实、重复执行、并发唯一性、停止条件、安全输出和只清理本次隔离夹具。

View File

@@ -1,23 +0,0 @@
# 13 — 冻结企微后台与个人中心前端契约
**What to build:** 前后端可以依据稳定契约实现 `/system/wecom`、个人中心企微绑定、审批运行列表/详情和异常恢复交互;契约明确每种角色可见入口、服务端状态、加载与失败表现、高风险确认和禁止出现的本地审批动作。
**Blocked by:**
- 02 — 完成平台账号本人扫码绑定闭环
- 03 — 发布审批场景与首个模板版本
- 09 — 交付审批运行查询、权限投影与立即同步
- 10 — 完成提交结果未知的高风险恢复闭环
**Status:** ready-for-agent
**架构通道:** 跨仓前端契约与验收,后端接口分别保持 Query 或 Application 原通道。
**完整业务边界:** 本票收口 OpenAPI、页面状态、权限菜单、交互和人工验收包。当前仓库不包含前端源码因此明确不伪装为前端实现不修改退款或充值业务页面后续前端仓按本契约实施。
- [ ] `/system/wecom` 仅超级管理员可见,展示连接状态、代理固定成员、场景、模板版本/映射、平台绑定清单和异常入口,不提供 Secret 或固定 `userid` 编辑。
- [ ] 平台和超级管理员个人中心展示本人绑定状态、成员名称、最近验证时间及绑定/换绑/解绑;代理不展示绑定入口,扫码失败可通过会话轮询恢复。
- [ ] `/operations/wecom-approvals` 只对超级管理员或具备运营权限的平台账号显示,覆盖筛选、分页、详情、立即同步和业务处理结果;无业务权限时不显示敏感资料。
- [ ] 提交结果未知仅对具备恢复权限的主体展示两个高风险动作,均要求原因和二次确认;页面明确区分绑定已有单号与确认未创建后重发。
- [ ] 所有页面覆盖加载、真实空态、筛选空态、403、网络失败、场景暂停中、模板失效、绑定过期、提交失败、结果未知及通过后撤销告警。
- [ ] 页面和 OpenAPI 不出现本地通过、拒绝、退回、重提、审批金额修改或人工确认资金按钮完成中文验收说明、OpenAPI 校验和接口文档索引更新。

View File

@@ -1,36 +0,0 @@
# 14 — 以退款场景完成真实企微发布门禁
**What to build:** 发布负责人可以使用退款审批场景完成公共企微能力的真实端到端验收:平台本人或代理固定成员发起包含真实附件和真实提交人的审批,人工同意或拒绝,经加密回调或轮询同步到本地,并验证重复事件不会产生重复退款终态事件;验收结果形成不含敏感信息的发布与回滚报告。
**Blocked by:**
- 01 — 交付企微连接状态与安全 Adapter 闭环
- 02 — 完成平台账号本人扫码绑定闭环
- 03 — 发布审批场景与首个模板版本
- 04 — 提供审批创建前置检查与事务接入契约
- 05 — 完成附件上传与企微异步提交闭环
- 06 — 完成场景安全暂停、模板换版与失效检测
- 07 — 接入加密回调与权威详情同步
- 08 — 完成轮询补偿与终态只发布一次
- 09 — 交付审批运行查询、权限投影与立即同步
- 10 — 完成提交结果未知的高风险恢复闭环
- 11 — 向真实业务提交人发送审批结果通知
- 12 — 交付历史审批与待处理数据迁移接缝
- 13 — 冻结企微后台与个人中心前端契约
- `.scratch/tech-public-foundation/issues/12-foundation-release-gate-and-integration-contract.md` — 12 — 建立公共基础发布门禁和下游接入契约
- `.scratch/tech-global-audit/issues/19-one-time-audit-cutover-gate.md` — 19 — 执行一次性审计切换与停机发布门禁
- `.scratch/tech-inapp-notifications/issues/08-notification-release-gate.md` — 08 — 完成公共通知发布门禁与下游接入契约
- UR#35 退款企微审批的“退款创建接入公共审批并消费终态事件”纵向 TicketUR#35 拆票后必须在本票实施前替换为其具体文件引用
**Status:** ready-for-agent
**架构通道:** 主通道为 Infrastructure 与端到端发布验收,辅助通道为跨 PRD Application 契约。
**完整业务边界:** 本票收口 UR#37 公共能力与首个退款业务切片的真实企微联合门禁。公共能力只验证审批实例、提交、同步和终态事件的正确性;退款金额、资金、佣金、套餐终结和业务幂等仍由 UR#35 对应纵向 Ticket 收口,不在本票重复实现。
- [ ] 使用真实退款模板和唯一隔离夹具,通过真实 HTTP 退款创建链路分别验证平台本人绑定发起与代理固定成员代发,企微表单正确展示真实业务提交人、退款快照和真实附件。
- [ ] 人工完成至少一次同意和一次拒绝,验证加密回调可以原样转发并完整验签解密;丢弃一次回调后由两分钟轮询恢复到同一权威结果。
- [ ] 重复回调、重复轮询和重复任务不会产生第二个审批终态 Outbox退款终态消费者的资金正确性与幂等结论引用 UR#35 具体 Ticket 的验收结果。
- [ ] 自动化 Adapter 继续覆盖超时、响应未知、重复/乱序、撤销、删除、通过后撤销和限流等不适合稳定人工制造的异常。
- [ ] 发布检查确认真实密钥已轮换且仅由部署配置提供,回调/可信域名、模板映射、平台绑定、代理固定成员、Worker、监控和通知均就绪任一门禁失败不得开放退款创建。
- [ ] 验收报告只包含运行 ID、业务安全标识、状态、计数和时间不包含密钥、Token、原始 `userid`、对象 Key、临时 `media_id` 或个人敏感信息;回滚说明保留已形成的全部审批事实并采用向前修复。

Some files were not shown because too many files have changed in this diff Show More