This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,32 @@
|
||||
## Context
|
||||
|
||||
现有佣金提现有申请和钱包事实;本 Change 以资格及审批实例补齐其前置条件,不以外部线下打款作为完成条件。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 分销码由店铺唯一约束保护;扫码注册与审批结果使用稳定审批实例幂等消费。
|
||||
- 资格申请与附件版本分表/快照,替换、作废和代理停用以状态失效,不覆盖历史审批。
|
||||
- 提现创建在事务内锁定佣金钱包并写冻结和审批提交;通过仅一次扣减/确认,驳回仅一次释放。
|
||||
|
||||
## 业务动作契约
|
||||
|
||||
### 分销码与扫码注册
|
||||
|
||||
- 代理店铺创建事务生成全局唯一、不可修改的随机 `distribution_code`;二维码只包含 H5 注册入口及该码。唯一冲突重试生成,不允许人工指定或编辑。
|
||||
- `POST /api/c/v1/agent-distribution-registrations`:提交 `distribution_code`、短信已验证手机号、密码及既有注册必填资料。服务锁定上级店铺,校验其代理启用;失效码/停用代理统一返回“分销码不可用”。成功仅创建待审批代理/店铺和企业微信审批实例,不建立下级归属。
|
||||
- 企业微信通过消费者幂等启用新代理/店铺,并在同一事务写直接上级店铺和上级当前业务员快照;驳回不启用。重复回调不重复创建层级或账号。
|
||||
|
||||
### 提现资格资料
|
||||
|
||||
- `POST /shops/:shop_id/withdrawal-qualifications`:仅本人代理店铺。请求合同、法人身份证正反面附件;企业必须提供统一社会信用代码,个人必须提供法人身份证号;可选营业执照、门头照;企业可选发票,发票抬头和统一社会信用代码必须与合同主体一致。创建新资料版本并提交企业微信,替换合同或身份证立即使旧有效资格失效。
|
||||
- 超级管理员作废资格必须填写原因;代理停用自动失效全部有效资格。合同与身份证审批通过后长期有效,直至替换、作废或停用;历史版本、附件、审批实例永不覆盖。
|
||||
|
||||
### 提现申请与审批
|
||||
|
||||
- 既有 `POST /shops/:shop_id/withdrawal-requests` 增加资格有效校验。锁定该店铺佣金钱包后冻结可提现余额、申请金额、手续费/实际到账金额、收款信息和可选发票快照;余额不足、资格无效或非本人代理均不创建申请或冻结。
|
||||
- 企业微信通过时以申请/审批实例条件更新一次确认到账;驳回时仅一次释放冻结。代理可在资格仍有效时修改被驳回申请的金额、收款信息和申请级发票并创建新审批实例;资料资格不因提现驳回失效。
|
||||
- 新业务禁用本地 `approve/reject` 终审;提交/回调未知复用既有审批恢复,不允许通过重提制造第二笔冻结。每次资格、冻结、审批终态和重提均记录审计。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移及唯一/状态索引;隔离库验证码唯一和停用、资格失效、提现冻结/重提、重复审批回调及 up/down/up。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-008`。
|
||||
|
||||
## Why
|
||||
|
||||
代理下级归属和提现资料/资金缺少企业微信终审及失效边界,无法可靠追溯。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 代理店铺唯一分销码和审批后下级注册。
|
||||
- 合同、法人身份证为核心的提现资格审批与失效。
|
||||
- 提现余额/资料快照、企微终审和驳回释放。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `agent-distribution-withdrawal`: 分销注册、资料资格与提现审批。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响代理店铺、H5 注册、附件、佣金钱包、企业微信审批和 Schema。
|
||||
@@ -0,0 +1,26 @@
|
||||
## Purpose
|
||||
|
||||
使代理下级注册和佣金提现均以企业微信审批、资格有效性和不可变业务快照为准,避免代理层级或资金事实因资料变更、重复回调而漂移。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 分销码与下级代理注册
|
||||
系统 SHALL 在每个代理店铺创建时生成全局唯一、不可修改的随机分销码;二维码仅编码 H5 注册入口和该码。代理扫码后以手机号短信验证、设置密码并创建待企业微信审批的下级代理和店铺;仅审批通过时启用,并将新店铺设为分销码所属店铺的直接下级,复制上级当时业务员为初始业务员。代理停用后其码立即不可注册,既有下级和佣金关系不级联变更。
|
||||
|
||||
#### Scenario: 停用代理码注册
|
||||
- **WHEN** 客户使用已停用代理所属店铺的分销码注册
|
||||
- **THEN** 系统拒绝创建下级代理或店铺
|
||||
|
||||
### Requirement: 提现资料资格
|
||||
代理首次提现前 SHALL 提交企业微信资料资格申请;合同和法人身份证正反面必填,企业代理填写统一社会信用代码、个人代理填写法人身份证号。合同资格主体必须填写统一社会信用代码或身份证号;营业执照、门头照可选,发票仅企业可选且其抬头/统一社会信用代码必须与合同主体一致。审批通过且资料未过期才有效;合同和身份证通过后长期有效,直到代理替换资料、超级管理员作废或代理停用。
|
||||
|
||||
#### Scenario: 资料替换后提现
|
||||
- **WHEN** 有效资格的代理替换合同或法人身份证资料
|
||||
- **THEN** 原资格失效,代理必须重新审批通过后才可提现
|
||||
|
||||
### Requirement: 提现冻结与企业微信终审
|
||||
提现申请 SHALL 冻结可提现余额、金额、手续费、收款信息和可选发票快照,并创建企业微信审批。本地不得人工通过或驳回;企业微信通过即视为已到账,驳回时释放本申请冻结余额。驳回后代理可修改金额、收款信息和本次发票重新提交,每次新建审批实例;资料资格保持有效。发票为申请级材料,若上传必须按当时有效合同主体校验并冻结。
|
||||
|
||||
#### Scenario: 提现审批驳回
|
||||
- **WHEN** 企业微信最终驳回一笔提现申请
|
||||
- **THEN** 系统仅一次释放其冻结佣金余额,保留审批快照,并允许在资格仍有效时修改后重提
|
||||
@@ -0,0 +1,13 @@
|
||||
## 1. 分销与资格
|
||||
- [ ] 1.1 追踪代理/店铺创建、H5 短信注册、佣金提现、附件、企微审批和钱包冻结链路。
|
||||
- [ ] 1.2 新增分销码、资格申请/资料版本、提现审批快照的成对迁移、模型、状态与唯一约束。
|
||||
- [ ] 1.3 实现分销注册、停用门禁、审批后下级归属/业务员快照及审计。
|
||||
- [ ] 1.4 实现资格提交、主体/附件/发票校验、失效和企微回调。
|
||||
|
||||
## 2. 提现
|
||||
- [ ] 2.1 实现有效资格校验、钱包锁定冻结、申请快照、企微终审、驳回释放和新实例重提。
|
||||
- [ ] 2.2 注册后台/H5 路由及 OpenAPI,保障附件和数据范围。
|
||||
|
||||
## 3. 验证
|
||||
- [ ] 3.1 隔离库验证迁移、分销码停用、资格替换、冻结/释放和重复回调。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-agent-distribution-withdrawal-qualification --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,26 @@
|
||||
## Decisions
|
||||
|
||||
- 线上充值复用支付单和实际商户路由,成功消费者以支付单/钱包流水唯一约束入账。
|
||||
- 线下申请保存不可变金额和付款证据快照;审批回调在主钱包锁事务中条件入账。
|
||||
- 线上未知结果和线下审批未知均保持在途,复用既有查询/恢复,不把重试当作新入账。
|
||||
|
||||
## 配置与充值动作契约
|
||||
|
||||
### 允许方式与查询
|
||||
|
||||
- `GET /agent-self-recharge-payment-methods`:代理、平台用户仅返回“全局允许方式 ∩ 当前启用商户池可用方式”的有序 `wechat`/`alipay` 列表;不得返回商户身份、凭证或全局允许范围。交集为空返回空列表。
|
||||
- `PUT /agent-self-recharge-payment-methods`:仅超级管理员,保存 `wechat_only`、`alipay_only` 或 `wechat_and_alipay`;记录操作者、前后值和时间。修改不更新任何已有充值/支付单的支付方式、商户 ID 或快照。
|
||||
|
||||
### 自身店铺充值
|
||||
|
||||
- `POST /agent-recharges` 的代理在线分支强制 `shop_id` 为空且认证店铺为已启用代理自身店铺;传入下级/其他店铺返回无权。线上请求 `amount`、`payment_method`、`request_id`,其中方式必须在当前交集内;以 `request_id` 与调用者/店铺唯一复用既有支付创建结果,失败预下单不入账。
|
||||
- 在线创建在事务内选择实际商户、冻结支付/商户快照并创建充值记录;渠道成功消费者锁定支付、充值和主钱包,以支付 ID/钱包流水唯一约束一次入账。失败、关闭、退款或未知状态不加余额;未知结果由既有查单/回调恢复,禁止客户端重试直接增加余额。
|
||||
|
||||
### 线下转账
|
||||
|
||||
- 线下请求必须包含 `amount`(正分)、`payer_name`、`transferred_at`(带时区时间)、`transfer_channel_or_bank`、`transaction_no`、至少一个 `payment_voucher_key` 和可选备注;创建时冻结全部字段、状态为待企业微信审批,不增加主钱包。
|
||||
- 企业微信通过消费者锁定申请、审批实例和主钱包,条件更新一次增加余额和钱包流水;驳回、撤回、关闭不入账。仅未成功申请可修改上述材料并创建新审批实例重提;未知审批保持在途。平台/超级管理员查询和处理一律先应用既有店铺数据范围,审计不记录凭证正文。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增线下充值申请、附件快照、审批关联及唯一约束的成对迁移;验证权限、回调重放、未知、驳回重提和 up/down/up。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-017`。
|
||||
|
||||
## Why
|
||||
|
||||
代理自助充值需同时覆盖线上支付和可审计的线下转账,且不能将付款成功与钱包入账混淆。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增代理自身店铺线上微信/支付宝充值。
|
||||
- 新增带凭证的线下转账申请及企业微信终审。
|
||||
- 固化支付回调、审批回调和钱包入账幂等。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `agent-self-recharge-payment`: 代理自助充值支付方式。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响代理主钱包、支付商户、企业微信审批、附件、审计和 Schema。
|
||||
@@ -0,0 +1,18 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理自助充值支付方式
|
||||
超级管理员 SHALL 维护代理在线自充允许方式:仅微信、仅支付宝或微信和支付宝;代理实际可用线上方式为该允许范围与当前可用对应商户池方式的交集,交集为空时拒绝创建线上充值单。平台用户和代理仅可查询实际可用方式,不得查看或修改允许范围。配置变更只影响后续新单,已创建未支付充值单保留其支付方式及商户快照。系统 SHALL 允许已启用代理在其自身店铺充值入口选择实际可用的线上微信、线上支付宝或线下转账;不得为下级店铺代充。线上方式创建支付单并按实际商户路由,渠道成功回调幂等增加该代理主钱包余额;失败、关闭或未知不得增加余额,未知结果通过既有支付查询/回调恢复,不允许重复支付单入账。
|
||||
|
||||
线下转账申请必须填写转账金额、付款人、转账时间、银行/支付渠道、流水号和凭证附件;创建后状态为待企业微信审批,不立即入账。企业微信通过时在事务内锁定申请并仅一次增加主钱包余额;驳回、关闭或撤回不入账,代理可修改未成功申请后以新审批实例重提。充值金额以分保存,展示元时两位小数;平台/超级管理员仅可查看和处理其既有数据范围。
|
||||
|
||||
#### Scenario: 配置与商户池交集为空
|
||||
- **WHEN** 超级管理员允许一种线上方式,但该方式没有可用商户池成员
|
||||
- **THEN** 代理可用线上方式列表不含该方式,创建该方式充值单被拒绝,已创建未支付单不受影响
|
||||
|
||||
#### Scenario: 重复线上成功回调
|
||||
- **WHEN** 同一线上充值支付成功回调被重复投递
|
||||
- **THEN** 系统只增加一次代理主钱包余额并保留幂等支付事实
|
||||
|
||||
#### Scenario: 线下申请审批驳回
|
||||
- **WHEN** 企业微信驳回线下转账充值申请
|
||||
- **THEN** 系统不增加钱包余额,并允许代理修改申请后创建新审批实例重提
|
||||
@@ -0,0 +1,8 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 代理自充支付方式
|
||||
系统 SHALL 使代理在线自充可用支付方式等于超级管理员维护的允许范围与当前启用商户池支付方式的交集;交集为空时拒绝创建新充值单。配置变更仅影响后续订单,既有未支付订单保留其支付方式和商户快照。
|
||||
|
||||
#### Scenario: 规则命中
|
||||
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||
@@ -0,0 +1,9 @@
|
||||
## 1. 充值实现
|
||||
- [ ] 1.1 追踪代理钱包、线上支付、商户路由、线下附件和企业微信审批链路。
|
||||
- [ ] 1.2 新增线下申请/快照/审批关联的成对迁移、模型、状态和幂等约束。
|
||||
- [ ] 1.3 实现自身店铺门禁、线上支付创建/成功入账恢复、线下申请/企微终审/重提和钱包事务审计。
|
||||
- [ ] 1.4 注册路由、OpenAPI及代理/平台查询数据范围。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 隔离库验证支付方式、越权代充、重复回调、未知恢复、线下驳回重提和 up/down/up。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-agent-self-recharge-payment-methods --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
20
openspec/changes/add-asset-package-hierarchy/design.md
Normal file
20
openspec/changes/add-asset-package-hierarchy/design.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## Context
|
||||
|
||||
套餐使用记录已有 `master_usage_id`,不能为展示重复存储关系。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 查询批量读取资产全部套餐使用,按 `master_usage_id` 内存分组并稳定排序。
|
||||
- 不改变套餐状态、金额或生命周期;缺失主记录只形成读模型异常项。
|
||||
|
||||
## 查询与响应契约
|
||||
|
||||
- 后台资产详情及 H5 `GET /api/c/v1/asset/package-history` 保持既有资产权限、分页和套餐字段;不新增写接口、迁移、关联表或状态变更。查询需在资产范围内一次读取该资产全部相关 `PackageUsage`,不能按每个主套餐逐条查询加油包。
|
||||
- 响应主项包含原套餐使用字段、`children` 加油包数组和 `expand_by_default`;主项 `master_usage_id` 必须为 `null`。子项保留自身 `package_usage_id`、`master_usage_id`、状态、购买创建时间、生效时间和原历史字段。
|
||||
- 以 `master_usage_id IS NULL` 的记录为主套餐;关联存在的加油包嵌入对应主项。已生效加油包先按生效时间正序,待生效加油包后按购买创建时间正序;排序字段相同再按 `package_usage_id` 正序,保证后台和 H5 一致。
|
||||
- 主套餐只要有一个关联子项即 `expand_by_default=true`;无子项为 `false`。主套餐、子项失效、过期、用尽、退款或历史状态均不得删除或改写层级。
|
||||
- 加油包的 `master_usage_id` 指向物理不存在记录时,返回顶层异常项:保留原字段、`relationship_status=master_missing`、`relationship_status_name=关联主套餐缺失`、`children=[]`、`expand_by_default=false`;不得猜测替代主套餐或丢弃该项。
|
||||
|
||||
## Verification
|
||||
|
||||
验证多主套餐、待生效/失效加油包、缺失主记录、后台/H5 数据范围和分页。
|
||||
24
openspec/changes/add-asset-package-hierarchy/proposal.md
Normal file
24
openspec/changes/add-asset-package-hierarchy/proposal.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-013`。
|
||||
|
||||
## Why
|
||||
|
||||
资产套餐历史平铺展示,无法识别主套餐与其加油包的真实关联。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在后台和 H5 按既有套餐使用关联投影主套餐—加油包层级。
|
||||
- 保留失效历史;主套餐物理缺失作为可观察异常。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- 无。
|
||||
|
||||
### Modified Capabilities
|
||||
- `package-lifecycle`: 套餐历史层级展示。
|
||||
|
||||
## Impact
|
||||
|
||||
影响套餐使用查询、后台资产详情、H5 和 OpenAPI。
|
||||
@@ -0,0 +1,16 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 资产套餐层级投影
|
||||
系统 SHALL 在后台资产详情和 H5 资产套餐历史中,直接以既有 `PackageUsage.master_usage_id` 将主套餐和关联加油包投影为层级结构,不新增关联表。响应中每个主套餐必须返回 `expand_by_default`:存在至少一个关联加油包时为 `true`,否则为 `false`;后台与 H5 使用同一规则。加油包按生效时间正序;待生效加油包按购买创建时间正序并排在已生效包之后。主套餐或加油包失效、过期、用尽时仍保留层级;仅关联主套餐物理缺失时作为“关联主套餐缺失”异常独立项返回。
|
||||
|
||||
#### Scenario: 已失效加油包
|
||||
- **WHEN** 某加油包及其主套餐已失效但主套餐记录仍存在
|
||||
- **THEN** 系统仍将加油包嵌入该主套餐层级,不因状态失效拆散关系
|
||||
|
||||
#### Scenario: 主套餐含加油包
|
||||
- **WHEN** 主套餐关联至少一条加油包使用记录
|
||||
- **THEN** 后台和 H5 返回该主套餐时均将 `expand_by_default` 设为 `true`
|
||||
|
||||
#### Scenario: 主套餐物理缺失
|
||||
- **WHEN** 加油包关联的主套餐使用记录不存在
|
||||
- **THEN** 系统返回带“关联主套餐缺失”标识的异常独立项
|
||||
7
openspec/changes/add-asset-package-hierarchy/tasks.md
Normal file
7
openspec/changes/add-asset-package-hierarchy/tasks.md
Normal file
@@ -0,0 +1,7 @@
|
||||
## 1. 查询契约
|
||||
- [ ] 1.1 追踪后台资产详情、H5 套餐历史、`PackageUsage` 和 `master_usage_id` 查询链路。
|
||||
- [ ] 1.2 实现批量层级投影、稳定排序和缺失主套餐异常项;更新 DTO、路由说明和 OpenAPI。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 验证多层级、失效保留、待生效排序、缺失主记录及后台/H5 数据范围。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-asset-package-hierarchy --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
31
openspec/changes/add-asset-wallet-auto-renewal/design.md
Normal file
31
openspec/changes/add-asset-wallet-auto-renewal/design.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## Context
|
||||
|
||||
套餐续购、资产钱包和复机已有独立事实;自动续费仅编排这些既有能力,不能把复机失败当作资金失败。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 保存全局配置和按资产/日期的尝试记录,用唯一约束保证每日一次。
|
||||
- Worker 按资产锁重读资格、当前售价和余额,以既有订单/钱包事务完成续购;人工订单通过同一锁优先。
|
||||
- 成功后以可靠任务调用复机,单独记录结果;通知使用既有事件去重。
|
||||
|
||||
## 配置、扫描与执行契约
|
||||
|
||||
### 配置维护
|
||||
|
||||
- `GET /asset-auto-renewal-config` 与 `PUT /asset-auto-renewal-config` 仅超级管理员、平台用户。配置为单例:`enabled`、`scope`(`all_main_packages`/`specified_main_packages`)、`package_ids`(指定范围时非空且只能是可售主套餐)、`days_before_expiry`(正整数)。保存时记录操作者、前后快照和时间;代理、企业、个人客户无读取或修改入口。
|
||||
- 配置变更只影响后续扫描,已产生的尝试记录不重算;关闭开关后 Worker 不创建新尝试或订单。
|
||||
|
||||
### 每日扫描与尝试
|
||||
|
||||
- Worker 在上海自然日按资产扫描,先用 `(asset_id, attempt_date)` 唯一记录占位,确保每资产每天至多一次尝试。仅选择存在当前有效主套餐、套餐未到期、最终到期时间进入 `days_before_expiry` 窗口、套餐在配置范围且资产钱包正常的资产;加油包、已到期套餐、流量阈值事件均不是触发源。
|
||||
- Worker 对每项候选锁定资产、当前主套餐、资产钱包和当日尝试,再次读取配置、到期时间、当前可售续费价与人工订单。人工成功续购已产生时,标记跳过且不扣款;余额不足或套餐不可续费时记录失败原因、不建订单、不扣款。
|
||||
|
||||
### 续费、通知与复机
|
||||
|
||||
- 合格项复用既有资产钱包订单/套餐生效事务,以执行时当前续费价扣同一资产钱包可用余额,创建同套餐商品续购订单、钱包流水和套餐使用事实;任一步失败整体回滚资金、订单和套餐,并写失败尝试。成功后当日不再处理该资产。
|
||||
- 余额不足、不可续费、订单失败和复机失败均使用客户/业务员/日期/原因类型幂等键投递最多一条通知;接收人只在事件创建时解析,业务员不存在时不阻断续费或尝试记录。
|
||||
- 续费成功后仅当资产处于可恢复停机且运营商状态不是风险停机或已销户时投递可靠复机任务。复机成功更新既有状态;失败/未知保存执行结果并走既有恢复,不回滚钱包扣款、订单、套餐生效或续费成功事实。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移;隔离库验证范围、窗口、每日去重、价格、余额、手动并发、复机和 up/down/up。
|
||||
25
openspec/changes/add-asset-wallet-auto-renewal/proposal.md
Normal file
25
openspec/changes/add-asset-wallet-auto-renewal/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-010`。
|
||||
|
||||
## Why
|
||||
|
||||
客户容易遗漏套餐续费;资产钱包余额充足时应在到期前自动续购,但不得与手动购买、停复机或钱包资金事实混淆。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增全局自动续费范围和最终到期前天数配置。
|
||||
- 每资产每天一次从同资产钱包按当前续费价续购。
|
||||
- 手动优先,失败通知,成功后条件复机且复机失败不回滚续费。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `asset-auto-renewal`: 资产钱包自动续费。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响套餐、资产钱包、订单、任务、运营商复机、通知和 Schema。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Purpose
|
||||
|
||||
在套餐最终到期前的受控窗口内,仅以同一资产钱包余额自动续购当前有效主套餐,并使扣款、套餐生效和复机失败具有明确且可恢复的边界。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 自动续费配置、权限与频率
|
||||
仅超级管理员和平台用户 SHALL 查看或修改全局自动续费开关、适用全部或指定主套餐及统一到期前 N 天;每次新增、修改、启用、停用必须记录操作者、修改前后值和时间。系统 SHALL 对已保存的有效配置执行续费:仅对当前有效主套餐、尚未到期、进入最终到期前窗口的资产处理;不得按流量阈值触发。每项资产每天最多尝试一次,成功后停止,套餐到期后不再自动尝试。
|
||||
|
||||
续费 MUST 仅扣该资产钱包可用余额,续购同一套餐商品,价格取执行时当前渠道可售续费价。余额不足或套餐不可续费时,不创建订单、不扣款,并向当前个人客户和资产所属店铺当时有效业务员各创建每日至多一条通知。
|
||||
|
||||
#### Scenario: 无权限修改配置
|
||||
- **WHEN** 代理、企业或个人客户请求修改自动续费配置
|
||||
- **THEN** 系统拒绝请求且不改变配置或产生执行任务
|
||||
|
||||
#### Scenario: 窗口内余额不足
|
||||
- **WHEN** 合格资产进入自动续费窗口但资产钱包余额不足
|
||||
- **THEN** 系统记录当日尝试失败且不扣款,并向当前客户和有效业务员各投递一次通知
|
||||
|
||||
### Requirement: 并发、成功与复机
|
||||
手动续购 SHALL 优先于自动续费。自动任务必须锁定并重读资产、套餐和钱包;发现人工已成功续购时跳过,避免重复扣款。成功续费后,仅当资产为可恢复停机且运营商状态不是风险停机或已销户时,系统调用既有复机;复机失败不得回滚已成功的订单、套餐或钱包扣款,必须保存失败结果并通知客户和业务员。
|
||||
|
||||
#### Scenario: 手动续购并发成功
|
||||
- **WHEN** 自动任务锁定后发现同一资产已由人工成功续购
|
||||
- **THEN** 自动任务不创建第二笔订单、不扣款,并结束本次尝试
|
||||
9
openspec/changes/add-asset-wallet-auto-renewal/tasks.md
Normal file
9
openspec/changes/add-asset-wallet-auto-renewal/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1. 配置与执行
|
||||
- [ ] 1.1 追踪套餐最终到期、续购价格、资产钱包、手动订单、停复机和通知链路。
|
||||
- [ ] 1.2 新增配置、每日尝试/结果的成对迁移、模型、唯一约束和管理接口。
|
||||
- [ ] 1.3 实现每日扫描、资格判断、资产锁、当前价格订单与同钱包扣款,保证手动优先。
|
||||
|
||||
## 2. 副作用与验证
|
||||
- [ ] 2.1 实现余额/不可续费通知、成功后的条件复机、复机失败记录和通知;不得回滚续费。
|
||||
- [ ] 2.2 更新路由/OpenAPI。
|
||||
- [ ] 2.3 隔离库验证窗口、每日一次、并发、资金、通知、复机及 up/down/up;运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-asset-wallet-auto-renewal --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,30 @@
|
||||
## Why
|
||||
|
||||
生产环境在异常重启恢复后,Worker 对审计与外部交互记录执行的大量读写和留存扫描使 PostgreSQL 出现严重磁盘 I/O 等待。维护者需要能在不修改代码的情况下临时停止新增统一审计记录和外部交互记录,并停止其归档、留存任务,以保护核心业务数据库可用性。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增默认启用的运行时配置开关,分别控制统一审计事件及资源快照、外部交互日志的新增持久化;关闭后不再向 `tb_audit_event`、`tb_audit_event_resource`、`tb_integration_log` 写入新记录。
|
||||
- 关闭记录开关时,业务操作、外部调用、回调处理和既有领域状态处理继续执行;审计调查和外部交互日志查询仅返回已有历史记录。
|
||||
- **BREAKING** 关闭开关期间不产生审计事实、资源快照或外部交互记录,也不建立新的审计与外部交互关联;依赖 Integration Log 作为内部重试或幂等辅助信息的调用必须在不落库时保持既有业务正确性。
|
||||
- 新增默认启用的 Worker 开关,关闭后不注册、不调度且不消费 Audit 日归档、Integration Log 日归档和日志日留存任务;已入队的对应任务必须安全跳过,不扫描或修改三张表。
|
||||
- 保持现有归档和留存开关的语义不变;新的任务总开关独立于“是否物理清理”的开关。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `observability-write-controls`: 审计、外部交互记录和其后台归档/留存任务的运行时启停控制。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `operations-audit`: 允许维护者在运行时关闭新增审计事实及审计相关后台任务,并定义关闭期间的查询与任务行为。
|
||||
- `external-integration`: 允许维护者在运行时关闭新增外部交互日志及其归档/留存任务,并定义外部调用与回调的降级边界。
|
||||
|
||||
## Impact
|
||||
|
||||
- 配置:`pkg/config` 默认配置、环境变量映射和生产运行说明。
|
||||
- 运行装配:`cmd/api`、`cmd/worker`、`internal/bootstrap`、任务注册与处理。
|
||||
- 基础设施:统一审计 Writer、Integration Log Repository 及其调用方的无记录降级路径。
|
||||
- 任务:Audit 日归档、Integration 日归档、日志日留存任务及其 Asynq 队列处理。
|
||||
- 文档:OpenSpec 契约、生产运维说明与配置说明。
|
||||
@@ -0,0 +1,21 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 审计与外部交互记录运行时写入开关
|
||||
系统 SHALL 提供默认启用、可由配置文件及环境变量覆盖的独立运行时开关,分别控制统一审计事件/资源快照和外部交互日志的新建持久化。关闭审计开关时,不得向 `tb_audit_event` 或 `tb_audit_event_resource` 写入新记录;关闭外部交互开关时,不得向 `tb_integration_log` 写入新记录。已有历史记录仍可按既有权限查询。
|
||||
|
||||
关闭任一开关不得阻断业务状态变更、外部调用、回调处理、钱包/订单等领域事实或已有幂等语义;不得将 Integration Log 是否落库作为业务正确性前提。开关重新启用后只记录后续操作,不回填关闭期间事实。
|
||||
|
||||
#### Scenario: 关闭审计写入
|
||||
- **WHEN** 审计写入开关关闭且业务操作成功执行
|
||||
- **THEN** 业务操作正常完成,系统不创建审计事件或资源快照
|
||||
|
||||
#### Scenario: 关闭外部交互日志写入
|
||||
- **WHEN** 外部调用或回调在外部交互日志开关关闭期间执行
|
||||
- **THEN** 系统维持既有调用、回调和幂等业务结果,且不创建新的外部交互日志
|
||||
|
||||
### Requirement: 审计与外部交互后台任务总开关
|
||||
系统 SHALL 提供默认启用的 Worker 总开关,分别控制审计日归档、外部交互日志日归档和日志日留存任务。关闭时 Worker 不注册、不调度且不消费对应任务;已经入队的任务被消费时必须安全跳过,不扫描、归档、删除或修改三张目标表。该总开关独立于既有物理清理配置。
|
||||
|
||||
#### Scenario: 已入队任务在关闭后执行
|
||||
- **WHEN** 相关归档或留存任务已入队且对应 Worker 开关后来关闭
|
||||
- **THEN** 任务安全跳过,目标表不发生扫描或写入
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,29 @@
|
||||
## Context
|
||||
|
||||
通道累计流量和停复机已有外部调用链;本能力增加本地停机锁及可靠任务,不改变套餐预警。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 通道配置和卡周期锁分表;锁以通道、卡、周期唯一。
|
||||
- 达量事务写锁和 Outbox 停机任务;新周期扫描按锁、主套餐和其他锁决定仅解锁或复机。
|
||||
|
||||
## 配置、锁与任务契约
|
||||
|
||||
### 通道配置
|
||||
|
||||
- 扩展既有运营商通道创建/编辑 DTO:`traffic_threshold_enabled`、`traffic_threshold_value`、`traffic_threshold_unit`、`traffic_period_type`、`traffic_period_start_day`、`traffic_threshold_status`。仅超级管理员、平台用户可读写;阈值必须为正数,单位只能是系统支持的 MB/GB,周期起始日为 1~28,所有时间边界按上海时区计算。
|
||||
- 启用时完整校验字段;停用时停止后续达量判断但不删除当前周期锁或历史任务结果。新增、更新、启用、停用均写通道 ID、前后字段、操作者和时间审计;不向代理、企业、个人客户暴露配置字段。
|
||||
|
||||
### 达量停机
|
||||
|
||||
- 运营商流量同步/周期扫描按卡当前所属通道及上海时区周期边界读取运营商回传累计流量,换算至配置单位;不使用套餐真流量预警数据。达到或超过阈值时,事务中以 `(channel_id, card_id, period_start)` 唯一键创建通道阈值停机锁和 Outbox 停机任务。
|
||||
- 唯一冲突表示该周期已处理;重复同步、并发扫描或任务重放不得创建第二把锁或重复发起停机。停机调用失败/未知保留锁、任务结果与安全失败原因,复用既有外部调用恢复;锁存在时所有人工或自动复机入口先拒绝。
|
||||
|
||||
### 新周期解锁与复机
|
||||
|
||||
- 周期转换任务锁定上周期仍有效的通道锁,解除其通道阈值限制;随后重新检查当前有效主套餐、风险停机、销户和其他停机锁。仅全部条件允许时写可靠复机任务;任一条件不满足时只解锁,不调用运营商。
|
||||
- 复机任务成功记录结果;失败/未知保留可恢复执行结果,不重建通道锁或改变套餐状态。通道停用、卡换通道或删除配置均不得使历史锁/任务失去审计关联。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移;隔离库验证周期边界、重复达量、停机失败、复机条件和 up/down/up。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-011`。
|
||||
|
||||
## Why
|
||||
|
||||
运营商通道需按其计费周期流量自动停复机,不能与套餐预警混用。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增通道阈值、周期和停机锁。
|
||||
- 达量可靠停机,新周期按套餐和其他锁条件复机。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `carrier-channel-traffic-threshold`: 通道流量阈值控制。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响通道、卡状态、可靠任务、外部运营商调用和 Schema。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Purpose
|
||||
|
||||
按运营商通道自身计费周期和累计流量控制停复机,独立于套餐真流量预警。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 通道阈值配置、权限与审计
|
||||
仅超级管理员和平台用户 SHALL 在运营商通道新建或编辑时配置流量阈值开关、阈值数值、流量单位、统计周期和生效状态;阈值数值必须为正,单位必须为系统支持单位,统计周期必须能换算为明确起止边界。新增、修改、启用、停用均必须记录操作者、修改前后字段和时间。代理、企业和个人客户不得读取或修改通道阈值配置。
|
||||
|
||||
#### Scenario: 越权修改通道阈值
|
||||
- **WHEN** 非超级管理员、非平台用户请求创建、编辑或启停通道阈值
|
||||
- **THEN** 系统拒绝请求,不修改配置且不产生审计成功事实
|
||||
|
||||
### Requirement: 通道阈值停机与周期恢复
|
||||
系统 SHALL 为每个已启用运营商通道按其配置统计周期判断运营商回传的每张卡当前周期累计流量;达量即写通道阈值停机锁、创建可靠停机任务并调用运营商停机。持锁卡在当前周期内 MUST 拒绝复机;调用失败或未知保留任务结果并按既有恢复机制处理。
|
||||
|
||||
新周期开始时,系统 SHALL 对仍持锁卡解除通道锁;仅存在有效主套餐且不存在风险停机、销户或其他停机锁时调用自动复机,不符合条件不得调用复机。复机失败记录结果并可靠处理。
|
||||
|
||||
#### Scenario: 达量后人工复机
|
||||
- **WHEN** 当前计费周期内持有通道阈值停机锁的卡请求复机
|
||||
- **THEN** 系统拒绝复机且保留该锁
|
||||
|
||||
#### Scenario: 新周期仍有其他停机锁
|
||||
- **WHEN** 新周期开始的持锁卡没有风险停机但存在其他停机锁
|
||||
- **THEN** 系统解除通道阈值锁但不调用运营商复机
|
||||
@@ -0,0 +1,8 @@
|
||||
## 1. 阈值控制
|
||||
- [ ] 1.1 追踪通道流量、卡状态、停复机锁、运营商任务与恢复链路。
|
||||
- [ ] 1.2 新增通道配置、周期停机锁、任务结果的成对迁移、模型、索引和管理接口。
|
||||
- [ ] 1.3 实现达量写锁/可靠停机、周期扫描解锁/条件复机及幂等恢复。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 隔离库验证周期、达量、拒绝复机、其他锁、失败重试和 up/down/up。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-carrier-channel-traffic-thresholds --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
30
openspec/changes/add-commission-clawback-records/design.md
Normal file
30
openspec/changes/add-commission-clawback-records/design.md
Normal file
@@ -0,0 +1,30 @@
|
||||
## Context
|
||||
|
||||
退款和佣金终态可能异步到达,回溯必须等待原佣金事实且不能修改其历史记录。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 回溯表以退款+原佣金唯一,保存负数快照;退款消费者等待佣金终态再可靠重试。
|
||||
- 在钱包事务内先处理待审提现释放,再插入回溯和扣款流水;唯一约束保证重放安全。
|
||||
|
||||
## 生成、资金与读侧契约
|
||||
|
||||
### 退款事件消费
|
||||
|
||||
- 退款完成可靠事件以 `refund_id`、`order_id` 进入回溯用例;锁定退款、订单和佣金终态。原订单佣金未终态时不写“无需回溯”,仅保留可重试事件;终态无佣金时写退款已处理且无需回溯的审计;换货事件不进入本用例。
|
||||
- 查询原订单全部可回溯佣金,按稳定顺序计算。全额退款回溯每条剩余可回溯金额;部分退款以 `refund_amount / frozen_actual_paid_amount` 计算,每条向下取整,最后一条仅补足总额舍入差且不得超过该条剩余可回溯余额。冻结实收金额缺失或非正时记录可恢复失败,不以订单标价替代。
|
||||
- 新表以 `(refund_id, original_commission_id)` 唯一,保存负数金额、原佣金/订单/退款快照、不可提现标识、生成时间;唯一冲突视为已生成,禁止第二次扣款。
|
||||
|
||||
### 钱包与提现原子边界
|
||||
|
||||
- 在同一钱包事务内,先锁定代理佣金钱包和所有待审核/审批中的提现申请;拒绝这些申请、释放其冻结余额并保存“退款回溯优先”原因,然后插入所有回溯明细和负数佣金钱包流水。允许钱包余额低于零。
|
||||
- 原佣金记录、历史发放金额和已提现完成事实不更新、不删除;回溯是独立负数事实。事务任一步失败时不释放提现、不写部分回溯或部分流水,可靠事件保留重试。
|
||||
|
||||
### 查询与导出
|
||||
|
||||
- 扩展佣金明细列表/详情:原佣金返回 `clawback_records` 摘要,回溯明细返回 `original_commission_id`、退款单号、负数金额、不可提现、回溯后实际钱包余额和生成时间。关联查询先应用既有佣金数据范围,再按关联 ID 查询;越权不泄露存在性。
|
||||
- 导出每条原佣金和回溯明细各一行,冻结筛选、操作者、可见范围和生成时间;金额保持分,展示层转换元不得改变负数或余额事实。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移;隔离库验证全额/部分、舍入、佣金延迟、重复事件、提现释放、负余额及 up/down/up。
|
||||
25
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
25
openspec/changes/add-commission-clawback-records/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-012`。
|
||||
|
||||
## Why
|
||||
|
||||
套餐退款后佣金需以独立负数事实回溯,并与提现冻结和钱包余额一致。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增不可提现负数回溯明细及原佣金/退款关联。
|
||||
- 按冻结实收比例、舍入和幂等规则扣回佣金。
|
||||
- 回溯前释放待审提现,允许佣金钱包负余额。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- 无。
|
||||
|
||||
### Modified Capabilities
|
||||
- `agent-funds-commission`: 退款佣金回溯。
|
||||
|
||||
## Impact
|
||||
|
||||
影响退款事件、佣金明细、钱包、提现和 Schema。
|
||||
@@ -0,0 +1,23 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐退款佣金回溯
|
||||
系统 SHALL 在套餐退款后保留原佣金不变,并创建关联原佣金记录和退款单的负数、不可提现回溯明细,冻结原订单号及原佣金关键字段。同一退款业务必须幂等;若佣金计算未终态则等待终态后生成,确认无佣金才标记无需回溯。换货不在本期范围。
|
||||
|
||||
部分退款按本次退款金额与订单冻结实收金额比例,对每条原佣金按分向下取整;最后一条补足舍入差,累计回溯不得超过原佣金。生成前系统 MUST 拒绝并释放待审核提现,再生成回溯明细和钱包扣款流水;佣金钱包允许负余额。
|
||||
|
||||
#### Scenario: 部分退款舍入
|
||||
- **WHEN** 一笔部分退款关联多条原佣金且比例计算产生分级舍入差
|
||||
- **THEN** 系统按各条向下取整并仅在最后一条补差,回溯总额等于应回溯额且不超过各原佣金可回溯余额
|
||||
|
||||
### Requirement: 回溯明细关联查询与导出
|
||||
系统 SHALL 在佣金明细中分别展示原发放佣金和回溯扣款记录,并允许从任一记录查询其关联的退款单、原佣金或全部回溯明细。回溯记录必须显示负数金额、不可提现标识、来源退款单号、原佣金记录号、生成时间和回溯后佣金钱包实际余额。佣金明细及导出 MUST 使用既有佣金数据范围:代理仅可读取自身及其既有可见范围内的事实,平台/超级管理员遵循既有范围;无权记录不得通过关联 ID、汇总或导出泄露。
|
||||
|
||||
导出应冻结筛选条件、操作者和可见范围;原佣金与回溯记录均作为独立行导出,回溯后余额为对应钱包变动提交后的实际余额,可为负数。
|
||||
|
||||
#### Scenario: 代理查询越权回溯记录
|
||||
- **WHEN** 代理使用回溯记录 ID、原佣金 ID 或退款单号查询其数据范围外的回溯关系
|
||||
- **THEN** 系统按既有数据范围返回不存在或空结果,不泄露关联事实
|
||||
|
||||
#### Scenario: 重复退款消费
|
||||
- **WHEN** 同一退款完成事件被重复消费
|
||||
- **THEN** 系统不重复生成回溯明细、钱包扣款或提现释放事实
|
||||
@@ -0,0 +1,8 @@
|
||||
## 1. 回溯实现
|
||||
- [ ] 1.1 追踪退款完成、佣金计算终态、提现冻结和佣金钱包链路。
|
||||
- [ ] 1.2 新增回溯明细、退款/原佣金唯一约束、状态/索引的成对迁移和 DTO。
|
||||
- [ ] 1.3 实现比例分摊、最后一条舍入补差、终态等待、待审提现释放、负余额扣款及幂等消费者。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 隔离库验证全额/部分、重复消费、佣金延迟、负余额和 up/down/up。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-commission-clawback-records --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
94
openspec/changes/add-employee-collection-bills/design.md
Normal file
94
openspec/changes/add-employee-collection-bills/design.md
Normal file
@@ -0,0 +1,94 @@
|
||||
## Context
|
||||
|
||||
见 `proposal.md` 与 `specs/employee-collection-bill/spec.md`。现有后台套餐订单、代理充值、企业微信审批和审计已有各自业务事实,但没有将“后台账号代客户经办后的公司应收”作为独立对象保存。现有 `tb_agent_recharge_record` 已有金额、支付凭证和审批实例关联;套餐订单已有 `actual_paid_amount`。这些来源只能提供已确定的金额和关联键,不能被新功能改写。
|
||||
|
||||
本设计只处理上线后事件。线下付款并非本系统支付渠道事实,企业微信审批人员以第三方记录核验,因此本地只留存申请人声明、附件、冻结快照和企业微信最终结果。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 让账单、申请和分摊形成可并发保护、可重提、可审计的本地财务事实。
|
||||
- 使企业微信是唯一终审来源,同时沿用已有可靠提交、回调和轮询恢复机制。
|
||||
- 在订单/充值入账、退款和核销之间建立明确且幂等的关联。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不建设公司收款账户目录,不接入或改造 OCR 契约,不校验同一外部付款在不同申请间的累计分摊。
|
||||
- 不回填历史业务,不允许“其他”来源手工建账,不处理平台代理 C 端资产钱包充值。
|
||||
- 不以账单功能重构订单、代理充值或企业微信通用审批模块。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 账单、申请、分摊分表保存
|
||||
|
||||
建立员工代收款账单、核销申请、核销分摊和线下收款方式字典四类事实。账单绑定唯一来源业务;申请绑定一次外部付款和一个企业微信审批实例;分摊连接申请与账单并冻结账单来源摘要。附件复用现有对象存储键/附件模式,审批快照复用既有通用审批上下文能力。
|
||||
|
||||
不把多笔付款、账单状态或附件塞入来源订单 JSON:来源订单既有生命周期不等于员工欠款,且一笔付款对多账单是独立关系。
|
||||
|
||||
### 2. 金额统一使用分,分摊在锁定账单中预占
|
||||
|
||||
账单应收、已核销、预占和申请付款金额均用 `int64` 分。提交/重提在同一 GORM 事务中按账单 ID 升序 `FOR UPDATE` 锁定,重新计算“应收金额 - 已通过分摊 - 其他审批中分摊”,再写申请与分摊。企业微信通过消费同样锁定申请和相关账单,使用审批实例唯一关联/状态条件更新保证至多入账一次。
|
||||
|
||||
不使用乐观展示余额或仅在回调时校验;那会让并发审批中申请超额占用同一账单。
|
||||
|
||||
### 3. 企业微信审批作为唯一状态推进器
|
||||
|
||||
申请提交事务只写本地申请、冻结快照、审批实例及可靠提交请求。审批回调和既有兜底查询都进入同一个幂等消费用例:通过才计入账单已核销,驳回才释放预占。提交失败或渠道未知保持在途,禁止本地财务人工改审批结果。
|
||||
|
||||
已驳回“重提”保留原申请主键,但新增一次审批实例及当次不可变快照;已通过分摊永不更新。这样列表可以按申请聚合,审计仍能回放每次审批。
|
||||
|
||||
### 4. 来源事件采用幂等 Outbox/消费者
|
||||
|
||||
线下套餐订单创建成功和代理充值审批入账完成后,在各自成功事务中写唯一来源事件或直接以唯一来源约束创建账单;选择以现有 Outbox 可用模式为准。消费者以来源类型+来源 ID 唯一约束去重。订单创建不得等待企业微信或外部付款;代理充值必须以“已通过且已入账”这一既有终态作为来源。
|
||||
|
||||
### 5. 退款只自动影响未存在已通过分摊的账单
|
||||
|
||||
退款处理在退款成功业务事务中查找来源账单并锁定。无已通过分摊时写冲销/关闭事实及更新金额;有已通过分摊时不动账,只写可追溯关联提示。这避免已由企业微信核验的员工欠款被退款回调静默重建或冲销。
|
||||
|
||||
## 业务动作契约
|
||||
|
||||
以下是本 Change 新增后台动作的已确认设计;路径遵循既有 `/api/admin` 路由约定,所有金额字段均为 `int64` 分,所有成功响应使用既有 `pkg/response` 包装。
|
||||
|
||||
### 收款方式字典维护
|
||||
|
||||
- `POST /employee-collection-payment-methods`:仅超级管理员。请求包含 `code`(1~64 字符、全局唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。创建后返回字典 ID、字段值和创建时间。
|
||||
- `PUT /employee-collection-payment-methods/:id`:仅超级管理员;不得修改已引用项的 `code`,可修改名称、排序、启停和备注。不存在返回既有“资源不存在”错误;重复编码返回稳定“收款方式编码已存在”错误。
|
||||
- `DELETE /employee-collection-payment-methods/:id`:仅未被申请引用的项可物理删除;已引用返回“收款方式已被引用,只能停用”。每个成功写操作记录操作者和前后快照。
|
||||
|
||||
### 账单查询与关闭
|
||||
|
||||
- `GET /employee-collection-bills`:员工强制加 `debtor_account_id=当前账号`;财务、超级管理员按既有数据范围过滤。支持来源类型、来源单号、账单状态、欠款人、客户/店铺、创建时间范围筛选和分页。每行返回账单 ID、来源摘要、欠款人快照、应收、已核销、预占、剩余、状态和创建时间。
|
||||
- `GET /employee-collection-bills/:id`:在同一数据范围校验后返回账单、来源摘要、退款冲销、分摊、申请与审批历史;附件只返回既有授权下载所需的安全引用,不返回对象存储敏感内容。
|
||||
- `POST /employee-collection-bills/:id/close`:仅超级管理员;请求 `reason` 必填、最长 500 字符。事务中锁定账单,存在审批中申请返回“账单存在审批中核销申请,不能关闭”;已关闭返回既有状态冲突;成功时仅作废未核销余额并写关闭审计。
|
||||
|
||||
### 核销申请创建、修改与重提
|
||||
|
||||
- `POST /employee-collection-applications`:员工为本人可见账单创建,超级管理员可代办但请求必须附 `acting_reason`(1~500 字符)。请求包含 `payment_method_id`、`paid_amount`(正分)、`payer_name`、`paid_at`(带时区 RFC3339 时间)、`external_transaction_no`、`payment_voucher_keys`(1~5 个既有附件键)、`remark`、`allocations[]`;每个分摊包含 `bill_id` 和正的 `amount`。
|
||||
- 服务按账单 ID 升序锁定,校验字典启用、账单可见且未关闭、分摊不超过该账单 `应收-已核销-其他审批中预占`、分摊总额不超过 `paid_amount`。成功返回申请 ID、状态 `审批中`、审批实例 ID、冻结快照与各分摊;任一校验失败时不保存申请、分摊或预占。
|
||||
- `PUT /employee-collection-applications/:id`:仅申请人或代办超级管理员,且仅已驳回申请可修改;入参同创建。事务释放旧驳回版本无预占事实,重新锁定和校验账单,保存新的不可变材料快照并创建新的企业微信审批实例。已通过、审批中、已撤销/关闭状态返回状态冲突。
|
||||
|
||||
### 企业微信审批结果消费
|
||||
|
||||
- 企业微信回调和既有状态恢复任务均按审批实例 ID 进入同一应用用例,不提供后台“通过/驳回”接口。
|
||||
- 最终通过:锁定申请及按 ID 升序的全部账单;仅当申请仍为审批中时,将每笔分摊从预占转入已核销,重新计算账单 `待核销/部分核销/已核销` 状态,标记申请已通过,并记录审批结果。重复或乱序的同一终态不重复增加已核销金额。
|
||||
- 最终驳回:仅当申请仍为审批中时释放全部预占,标记已驳回并保存审批意见;重复回调不重复释放。提交失败、回调延迟和未知结果维持在途,由既有查询恢复任务确认,不得人工改写终态。
|
||||
|
||||
### 来源建账与退款冲销
|
||||
|
||||
- 后台线下套餐订单成功提交后,以 `order.id`、`operator_account_id`、`operator_account_type=platform` 和非空 `actual_paid_amount` 判定建账;来源唯一键为 `order:{id}`。重复订单事务、可靠事件重放或消费者重试均返回同一账单,不重复建账。
|
||||
- 代理线下充值仅在既有审批最终通过且钱包入账完成后,以 `agent_recharge.id` 为唯一来源建账;线上充值、审批未通过或未完成入账不建账。
|
||||
- 套餐退款成功时锁定来源账单:无已通过分摊的全额退款关闭账单;无已通过分摊的部分退款冲减应收;存在已通过分摊时只新增退款关联提示,不修改应收、已核销或员工欠款。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [企业微信回调重复、乱序或未知] → 以审批实例、申请状态和分摊状态条件更新幂等消费,复用渠道查询恢复。
|
||||
- [外部付款敏感信息泄露] → 附件使用对象键和既有授权访问;日志/审计只记录脱敏摘要与业务 ID。
|
||||
- [来源事件与账单创建不一致] → 在来源成功事务写可靠事件,消费者以唯一来源约束重放。
|
||||
- [超额核销] → 提交、重提和审批通过均锁定账单并校验预占余额。
|
||||
- [退款与核销并发] → 退款和审批消费按相同账单锁顺序串行,已通过分摊优先保留。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建字典、账单、申请、分摊、审批快照/冲销关联所需表、唯一约束和查询索引;不修改既有迁移。
|
||||
2. 先部署可读新表和来源事件的兼容代码,再启用账单生产与核销入口;上线时间作为历史切割点写受控配置或迁移基准。
|
||||
3. 在隔离环境验证上线前订单/充值不建账、重复事件不重复建账、并发预占、通过/驳回/重提、退款联动及迁移 up/down/up。
|
||||
4. 回滚时先停止新入口和事件消费;已有账单事实保留,只有维护者确认未产生不可逆业务数据时才执行 down。
|
||||
31
openspec/changes/add-employee-collection-bills/proposal.md
Normal file
31
openspec/changes/add-employee-collection-bills/proposal.md
Normal file
@@ -0,0 +1,31 @@
|
||||
## Why
|
||||
|
||||
平台代理或无代理归属的 C 端客户以线下方式购买套餐、或代理线下充值预存款时,实际经办后台账号形成公司应收欠款。当前系统只有订单或充值审批,无法将这笔欠款、客户外部付款凭证、企业微信核验和最终核销结果形成独立、可分摊且可审计的闭环。
|
||||
|
||||
本 Change 落实讨论稿 AUG26-001:只覆盖功能上线后的新增业务,不回填任何历史账单,避免把历史支付事实以推测方式写入新财务账。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增员工代收款账单:后台线下套餐订单创建成功后,或代理线下预存款/主钱包充值经企业微信审批通过并完成入账后,按确定金额为实际经办账号创建唯一账单。
|
||||
- 新增核销申请和账单分摊:员工按一笔外部付款创建一张申请,可选择多张账单并填写各自分摊金额;同一账单可由多笔已通过申请分次核销。
|
||||
- 新增固定分类的线下收款方式字典;申请冻结字典名称、外部付款、附件、账单分摊和审批快照。OCR 仅可预填流水号和付款金额,人工确认值才是业务事实。
|
||||
- 核销申请仅由企业微信最终通过或驳回驱动;提交失败、回调延迟或结果未知复用既有审批查询/恢复闭环,禁止本地人工绕过终审。
|
||||
- 新增账单、核销申请、分摊、附件与审批记录的权限受控查询;超级管理员关闭未结清账单、来源订单退款时的账单冲销均保留审计。
|
||||
- **BREAKING**:会生成员工账单的后台线下套餐订单不再在创建时强制上传付款凭证;凭证和外部付款信息改为核销申请必填。赠送套餐等不生成账单的既有线下订单继续保持原凭证要求。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `employee-collection-bill`: 员工代收款账单、线下收款方式、分摊核销申请、企业微信审批闭环、退款冲销和财务查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。本 Change 通过新能力监听并引用既有订单和代理充值的已确定业务事实,不重写其既有主规格。
|
||||
|
||||
## Impact
|
||||
|
||||
- 数据:新增账单、收款方式字典、核销申请、分摊和审批快照等表;订单/充值来源只保存可追溯关联,不回填历史记录。
|
||||
- 写侧:后台线下套餐下单、代理线下充值入账、核销提交/重提/关闭、企业微信审批结果消费、套餐退款。
|
||||
- 读取:财务账单与申请列表、详情、导出;员工仅看本人,财务/超级管理员按既有数据范围看全部。
|
||||
- 依赖:复用既有企业微信通用审批实例、可靠提交和状态恢复机制;OCR 与外部付款渠道不在本 Change 新建契约。
|
||||
@@ -0,0 +1,83 @@
|
||||
## Purpose
|
||||
|
||||
为后台账号代客户经办的线下套餐购买和代理预存款充值建立独立的员工应收、外部付款核验和企业微信终审闭环;该能力只保存可追溯的本地业务事实,不推测或回填历史第三方付款。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 员工代收款账单来源、金额与上线边界
|
||||
系统 SHALL 仅为功能上线后新发生的下列业务创建员工代收款账单,并以实际发起该业务的后台账号作为不可修改的欠款人:
|
||||
|
||||
- 平台业务员或超级管理员创建的、会生成账单的后台线下套餐订单,账单金额取订单 `actual_paid_amount`;代理代购和无代理归属自营 C 端均适用。该订单创建成功后仍按既有规则立即激活。
|
||||
- 代理线下预存款/主钱包充值在企业微信最终通过且完成入账后,账单金额取对应充值记录 `amount`。
|
||||
|
||||
客户自行线上支付、平台代理 C 端客户充值资产钱包、订单失败或取消、赠送套餐等不产生员工账单的既有线下订单,以及“其他”手工来源 MUST NOT 创建账单。系统 MUST 为同一来源业务建立至多一张账单,并保存来源类型、来源 ID、来源单号、客户/店铺快照、欠款人、应收金额和创建时间;上线前业务不回填、不补建。
|
||||
|
||||
#### Scenario: 后台线下套餐订单产生账单
|
||||
- **WHEN** 平台业务员或超级管理员成功创建一个需要生成账单的后台线下套餐订单
|
||||
- **THEN** 系统以该操作账号为欠款人、以订单 `actual_paid_amount` 为应收金额创建唯一待核销账单,且订单无需因未上传付款凭证而阻断
|
||||
|
||||
#### Scenario: 审批入账的代理充值产生账单
|
||||
- **WHEN** 功能上线后代理线下预存款/主钱包充值经企业微信最终通过并完成入账
|
||||
- **THEN** 系统以实际发起充值的后台账号和充值 `amount` 创建唯一待核销账单
|
||||
|
||||
#### Scenario: 重复来源或历史业务不产生重复账单
|
||||
- **WHEN** 同一来源业务被重复处理、重复回调,或业务发生在功能上线前
|
||||
- **THEN** 系统至多保留一张来源关联账单,且不补建上线前账单
|
||||
|
||||
### Requirement: 账单余额、状态与关闭
|
||||
账单 SHALL 独立维护 `待核销`、`部分核销`、`已核销`、`已关闭` 状态及应收金额、已核销金额、审批中预占金额和剩余可核销金额。只有企业微信最终通过的分摊增加已核销金额;审批中的分摊预占剩余可核销金额,防止并发申请超额核销。账单已核销金额等于应收金额时 MUST 为已核销;关闭账单只作废当时未核销余额,已核销金额必须保留。
|
||||
|
||||
欠款人离职、禁用或变更组织后,账单欠款人身份和既有账单范围 MUST 保持不变。员工仅可查询本人账单和申请;财务与超级管理员可按既有数据范围查询;仅超级管理员可代办创建、修改或重提申请,且必须记录实际代办人和原因。
|
||||
|
||||
#### Scenario: 部分核销后仍可继续核销
|
||||
- **WHEN** 一张账单存在企业微信已通过但未结清的分摊
|
||||
- **THEN** 系统增加已核销金额、将账单标记为部分核销,并仅允许新的分摊使用未被已通过或审批中分摊占用的余额
|
||||
|
||||
#### Scenario: 关闭未结清账单
|
||||
- **WHEN** 超级管理员对待核销、部分核销或已驳回关联申请的账单填写关闭原因并执行关闭,且账单不存在审批中申请
|
||||
- **THEN** 系统作废未核销余额、将账单标记为已关闭、保留已核销金额和操作审计
|
||||
|
||||
#### Scenario: 审批中账单不可关闭
|
||||
- **WHEN** 超级管理员尝试关闭存在审批中核销申请的账单
|
||||
- **THEN** 系统拒绝关闭,账单金额和状态不变
|
||||
|
||||
### Requirement: 外部付款核销申请与分摊
|
||||
员工 SHALL 按一笔外部付款创建一张核销申请。申请 MUST 选择一个启用的线下收款方式字典项,并保存其稳定编码和名称快照;必须保存经人工确认的付款金额、付款方、付款时间、外部交易流水号、至少一个支付凭证和可选其他凭证、备注及一个或多个账单分摊。申请人只能通过勾选可见账单创建分摊,系统带出只读来源订单、客户和资产信息。
|
||||
|
||||
系统 SHALL 按账单产生时间从早到晚用本次付款金额预填分摊,最后一张填入剩余金额;申请人可修改各分摊金额。单笔分摊 MUST 大于零且不得超过该账单可核销余额;分摊总额 MUST 不超过本次人工确认付款金额。同一外部付款可被多个核销申请引用,本期 MUST NOT 对跨申请累计分摊金额实施系统防重或金额上限校验。
|
||||
|
||||
#### Scenario: 一笔付款分摊多张账单
|
||||
- **WHEN** 员工选择多张可见账单并提交一笔外部付款的核销申请
|
||||
- **THEN** 系统按账单时间预填分摊、校验每张账单可核销余额和申请总额,并为该申请创建唯一企业微信审批实例
|
||||
|
||||
#### Scenario: 账单并发申请预占
|
||||
- **WHEN** 两个核销申请并发选择同一账单的剩余余额
|
||||
- **THEN** 系统至多接受不超过该账单未核销余额的审批中和已通过分摊,其余申请返回余额不足且不创建超额分摊
|
||||
|
||||
### Requirement: 核销申请审批、重提与幂等
|
||||
核销申请状态 SHALL 为 `审批中`、`已通过`、`已驳回`、`已撤销/已关闭`,且不得以申请状态覆盖账单核销状态。提交或重提时系统 MUST 冻结当次收款方式、外部付款、附件、备注、账单分摊及审批材料快照,并创建新的企业微信审批实例。
|
||||
|
||||
企业微信最终通过时,系统 MUST 幂等地将申请标记为已通过、将各分摊写入账单已核销金额并释放其预占;最终驳回时 MUST 标记申请已驳回、释放全部预占且保留审批意见。已驳回申请可修改全部申请内容后重提,历史审批实例、材料和结果不得覆盖;已通过分摊不可修改。企业微信提交失败、回调延迟或结果未知时申请保持在途,系统 MUST 使用既有查询/恢复机制确认渠道结果,且不得由本地人工通过或拒绝绕过企业微信。
|
||||
|
||||
#### Scenario: 企业微信通过核销申请
|
||||
- **WHEN** 企业微信对含多笔分摊的核销申请返回最终通过,且该结果首次被消费
|
||||
- **THEN** 系统仅一次更新申请、各账单已核销金额和状态,并保留审批实例及冻结快照
|
||||
|
||||
#### Scenario: 企业微信驳回后重提
|
||||
- **WHEN** 企业微信最终驳回核销申请
|
||||
- **THEN** 系统释放预占、保留驳回实例和意见;员工或有代办权限的超级管理员修改申请后重提时创建新的审批实例
|
||||
|
||||
### Requirement: 收款方式字典、退款联动与可追溯性
|
||||
系统 SHALL 提供唯一固定分类的线下收款方式字典。超级管理员可维护名称、稳定编码、排序、启停和备注;已被业务引用的字典项 MUST NOT 被物理删除,只能停用,且历史申请继续显示冻结名称。
|
||||
|
||||
来源套餐订单全额退款且账单从未存在已通过分摊时,系统 MUST 自动关闭账单并记录“来源订单全额退款”;部分退款且从未存在已通过分摊时,系统 MUST 按退款金额冲减账单应收金额并保留来源订单退款冲销记录。账单存在任一已通过分摊时,系统 MUST NOT 自动冲销,仅在账单详情提示来源订单退款。退款不恢复已核销账单的员工欠款。
|
||||
|
||||
账单、申请、分摊、附件、审批实例、字典快照、关闭和退款冲销 MUST 可按权限查询并记录操作审计;审计和日志不得保存完整支付凭证敏感内容。OCR 若可用仅用于预填,必须允许申请人或审核人更正,且识别值不是资金事实。
|
||||
|
||||
#### Scenario: 来源订单部分退款且未核销
|
||||
- **WHEN** 来源套餐订单部分退款,且其账单不存在任何已通过分摊
|
||||
- **THEN** 系统按退款金额冲减账单应收金额,保留退款冲销关联,并重新计算账单状态和可核销余额
|
||||
|
||||
#### Scenario: 被引用字典项停用
|
||||
- **WHEN** 超级管理员停用已被核销申请引用的线下收款方式
|
||||
- **THEN** 新申请不可选择该方式,历史申请仍展示其冻结名称和稳定编码
|
||||
27
openspec/changes/add-employee-collection-bills/tasks.md
Normal file
27
openspec/changes/add-employee-collection-bills/tasks.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## 1. 账单数据与基础契约
|
||||
|
||||
- [ ] 1.1 盘点现有订单、代理充值、通用企业微信审批、附件、Outbox 和审计模型,确定来源事件、附件键和审批快照的复用点;不得复制敏感付款内容。
|
||||
- [ ] 1.2 新增成对迁移及 GORM 模型:线下收款方式字典、员工代收款账单、核销申请、申请—账单分摊、退款冲销/审批快照关联;为来源唯一性、审批实例唯一性、账单查询和分摊锁定建立约束/索引。
|
||||
- [ ] 1.3 定义金额分、账单状态、申请状态、来源类型和稳定错误码;实现中文名称、DTO 枚举说明及金额/附件/分摊校验。
|
||||
- [ ] 1.4 实现超级管理员维护线下收款方式字典的新增、编辑、启停和受引用不可删除规则,并写配置审计。
|
||||
|
||||
## 2. 来源建账与账单读取
|
||||
|
||||
- [ ] 2.1 在后台线下套餐订单成功路径识别应建账场景,以实际操作后台账号和 `actual_paid_amount` 可靠、幂等地创建账单;调整仅该场景的创建时付款凭证要求,保留赠送等非建账订单的既有要求。
|
||||
- [ ] 2.2 在代理线下预存款/主钱包充值企业微信通过且完成入账路径可靠、幂等地创建账单,金额取充值 `amount`;线上充值和未入账审批不得建账。
|
||||
- [ ] 2.3 实现账单列表、详情和统计 Query:按来源、状态、时间、员工、客户筛选,员工仅见本人,财务/超级管理员按数据范围见全部;返回应收、已核销、预占和未核销金额及审批中标识。
|
||||
- [ ] 2.4 实现账单关闭用例:仅超级管理员、仅允许无审批中申请的未结清账单、必须填写原因,并在事务内保存状态变化与成功审计。
|
||||
|
||||
## 3. 核销申请、审批与退款联动
|
||||
|
||||
- [ ] 3.1 实现核销申请创建和已驳回重提:锁定选中账单、按时间预填、校验付款金额与分摊、预占余额、冻结收款方式/外部付款/附件/账单摘要,并创建新的企业微信审批实例和可靠提交请求。
|
||||
- [ ] 3.2 接入企业微信最终通过、驳回、提交失败和状态查询恢复:通过时一次性增加已核销并释放预占,驳回时释放预占;用审批实例、状态条件更新和账单锁保证重复/乱序回调不重复核销。
|
||||
- [ ] 3.3 实现代办权限、申请/分摊/审批历史查询及附件授权访问;超级管理员代办创建、修改或重提时强制记录实际代办人和原因。
|
||||
- [ ] 3.4 在套餐退款成功处理链路实现账单冲销:无已通过分摊的全额退款自动关闭、部分退款冲减应收;存在已通过分摊时只保留退款关联提示,不恢复欠款。
|
||||
- [ ] 3.5 为建账、申请提交/重提、审批通过/驳回、账单关闭和退款冲销补齐事务内审计;检查日志、错误和导出不暴露附件内容、完整交易敏感体或 OCR 原始结果。
|
||||
|
||||
## 4. 路由、文档与验证
|
||||
|
||||
- [ ] 4.1 注册账单、申请、字典和导出所需路由及 RouteSpec,补齐 `internal/bootstrap`、`cmd/api/docs.go` 和 `cmd/gendocs/main.go` 装配;Handler 使用 `pkg/response` 和稳定错误。
|
||||
- [ ] 4.2 在隔离数据库按显式 `DB_*` 和 `scripts/migrate.sh` 验证迁移 up/down/up;人工核对上线前来源不建账、来源幂等、并发预占、审批重放、驳回重提、关闭限制及退款三类联动。
|
||||
- [ ] 4.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-employee-collection-bills --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,83 @@
|
||||
## Context
|
||||
|
||||
见 `proposal.md`。现有换货单以 `migrate_data` 和 `migration_completed` 两个布尔字段记录意图和成功结果;`Service.Complete` 在同一数据库事务中完成资产归属、客户绑定、资产状态和可选业务数据迁移。迁移报错会回滚整个事务,现有失败审计不保存可供列表查询的迁移失败状态。
|
||||
|
||||
现有迁移函数已在同一事务中处理钱包余额、套餐使用记录、累计字段和资产标签。物流换货的发货后状态允许再次确认完成;直接换货创建即在同一事务完成,创建失败时不持久化换货单。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 为已持久化换货单提供稳定、可查询的迁移状态和安全失败原因。
|
||||
- 保持业务数据迁移和换货完成的原子性,并让物流换货的失败可由超级管理员或平台用户重试。
|
||||
- 兼容既有响应字段和历史换货数据。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不改变直接换货创建失败即整体回滚、无换货单留存的现有行为。
|
||||
- 不修改迁移项目、增加迁移明细表、迁移手机号—资产关联,或改变资产归属和个人客户—资产绑定的既有换货动作。
|
||||
- 不新增列表筛选、导出或路由。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 1. 使用状态字段取代布尔字段推断
|
||||
|
||||
在 `tb_exchange_order` 新增非空 `migration_status` 和非空 `migration_failure_reason`。状态使用字符串 `not_migrated`、`pending`、`migrated`、`failed`,中文名称仅在应用层投影;失败原因最长 500 字符且为空表示无失败原因。
|
||||
|
||||
新字段表达完整结果,保留 `migrate_data`、`migration_completed` 和 `migration_balance` 供兼容客户端及既有业务使用。新建/发货时按是否选择迁移写入 `not_migrated` 或 `pending`;成功完成写入 `migrated` 并清空失败原因。
|
||||
|
||||
备选方案是在现有两个布尔字段上叠加前端规则。放弃原因是无法表达失败和失败原因,且容易把待迁移与失败混淆。
|
||||
|
||||
### 2. 主事务回滚后以短事务落失败状态与审计
|
||||
|
||||
业务数据迁移仍与换货完成共享原有 GORM 事务。任何一步失败均回滚资产状态、归属、客户绑定、钱包、套餐和标签的本次修改,确保重试从完整且未部分迁移的事实开始。
|
||||
|
||||
外层识别到迁移失败后,另开短事务,以换货单仍处于可确认完成状态为条件更新 `migration_status=failed` 与经安全截断的失败原因,并写入对应失败审计。失败状态的持久化不得与已回滚的业务数据迁移共用事务。
|
||||
|
||||
备选方案是让迁移失败提交部分换货结果。放弃原因是会产生无法可靠补偿的钱包和套餐事实,且违背本期整套迁移原子执行的产品边界。
|
||||
|
||||
### 3. 只为失败的物流换货增加受限重试
|
||||
|
||||
保持既有确认完成入口。换货单为物流流程、业务状态仍为已发货待确认且迁移状态为 `failed` 时,仅超级管理员或平台用户可再次确认;用例在事务内重新锁定并验证状态,然后从钱包余额开始重新执行全部迁移。未失败的换货沿用现有可确认权限和状态门禁。
|
||||
|
||||
直接换货继续创建即完成;其迁移失败会回滚整笔创建,不留换货单或失败状态,避免为单一失败路径引入新的直接换货中间状态与重试接口。
|
||||
|
||||
### 4. 一次成对迁移完成历史映射
|
||||
|
||||
新增一对当前根迁移,不修改历史迁移。迁移新增列后,以既有字段回填:`migrate_data=false` 映射为 `not_migrated`;`migrate_data=true AND migration_completed=true` 映射为 `migrated`;其余 `migrate_data=true` 映射为 `pending`。历史记录的失败原因置空。
|
||||
|
||||
不增加索引:本期没有迁移状态筛选或后台批处理查询,现有列表分页读取已直接投影换货单字段。
|
||||
|
||||
## 行为与数据契约
|
||||
|
||||
### 数据投影与历史映射
|
||||
|
||||
- `tb_exchange_order` 新增 `migration_status varchar(20) NOT NULL`、`migration_failure_reason varchar(500) NOT NULL DEFAULT ''`;DTO 列表与详情新增 `migration_status`、`migration_status_name`,并仅在状态为 `failed` 时返回 `migration_failure_reason`。
|
||||
- 上线迁移将 `migrate_data=false` 映射 `not_migrated`,`migrate_data=true AND migration_completed=true` 映射 `migrated`,其余已存在 `migrate_data=true` 映射 `pending`;不推断历史失败原因。
|
||||
|
||||
### 创建、发货与确认完成
|
||||
|
||||
- `POST /exchanges`:沿用现有创建入参和权限。物流单创建时按 `migrate_data` 初始化 `not_migrated` 或 `pending`;直接换货在同一创建事务中执行完成和可选迁移,任一步失败则整个创建回滚,不返回换货单或 `failed` 状态。
|
||||
- `POST /exchanges/:id/ship`:沿用既有物流状态机和发货字段;不改变迁移状态,选择迁移的单仍为 `pending`。
|
||||
- `POST /exchanges/:id/complete`:先在同一事务锁定换货单并验证既有“已发货待确认”状态和数据范围。`not_migrated` 只执行固有资产归属及个人客户绑定;`pending` 执行钱包余额、有效套餐使用、累计充值、资产标签的完整迁移及固有动作。成功时写 `migrated`、清空失败原因、写完成时间和既有成功审计。
|
||||
|
||||
### 失败与受限重试
|
||||
|
||||
- 当 `pending` 迁移任一步失败时,主事务必须回滚资产归属、个人客户绑定、钱包、套餐、累计字段、标签和完成状态;外层另开短事务,条件为换货单仍是可确认完成状态,写 `failed`、安全截断至 500 字符的失败原因及失败审计。
|
||||
- 对 `migration_status=failed` 的物流单,`POST /exchanges/:id/complete` 仅超级管理员或平台用户可重试;锁定后从钱包余额开始重跑全部迁移,禁止仅重试某一子项。非平台账号返回无权,非失败单沿用既有完成状态门禁,不将完成接口变成通用重复执行入口。
|
||||
- 手机号—资产关联永不在上述动作中读取、复制或删除;新资产后续按自身 H5 手机号绑定规则处理。
|
||||
|
||||
### 读取行为
|
||||
|
||||
- `GET /exchanges` 与 `GET /exchanges/:id` 沿用既有换货数据范围,返回新状态字段;旧 `migrate_data`、`migration_completed`、`migration_balance` 保持原响应兼容,但调用方不得再以其组合判断迁移结果。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [失败原因可能包含底层敏感或不稳定信息] → 使用稳定错误的安全摘要并限制长度,禁止直接返回数据库、外部服务或敏感载荷。
|
||||
- [失败状态更新与失败审计二次事务异常] → 复用既有换货失败审计的次级故障记录方式;状态更新与成功必达审计同事务,更新失败时返回原失败并保留诊断。
|
||||
- [并发确认造成重复迁移] → 重用换货单 `FOR UPDATE` 锁与预期业务状态更新;只有仍为 `failed` 的失败重试可以进入受限路径。
|
||||
- [旧客户端只读取布尔字段] → 保持原字段及其成功语义,新字段只增不删。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 在隔离数据库执行新迁移,核对历史映射、非空约束和 down 后 Schema。
|
||||
2. 发布同时包含迁移、写侧状态转换、列表/详情 DTO 投影和审计更新的版本。
|
||||
3. 发生应用回滚时,先回滚应用至仍兼容新增列的版本;仅在确认没有依赖新状态的数据或功能后执行 down 迁移。
|
||||
@@ -0,0 +1,28 @@
|
||||
## Why
|
||||
|
||||
现有换货单只以“是否要求迁移”和“是否完成迁移”两个布尔值表达迁移结果;迁移失败会回滚,后台无法在列表和详情中区分未迁移、待迁移、已迁移及迁移失败,也无法获知失败原因或在修复后重试。
|
||||
|
||||
本轮 AUG26-005 已收口迁移范围和失败处理,需让换货运营能准确判断业务数据迁移结果并追溯异常。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 将换货业务数据迁移结果统一为不迁移、待迁移、已迁移、迁移失败四个可观察状态,并保留最近一次失败原因。
|
||||
- 在换货完成的迁移失败场景中保留换货单可完成状态及失败事实;超级管理员或平台用户修复条件后可再次确认完成,并重新原子执行完整迁移。
|
||||
- 在换货列表和详情返回迁移状态及失败原因(仅迁移失败时),替代前端对现有布尔字段的推断。
|
||||
- 将迁移范围明确限定为资产钱包余额、有效套餐使用记录、累计充值字段和资产标签;资产归属、个人客户—资产绑定及手机号—资产关联不属于该迁移范围。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- 无。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `order-refund-exchange`: 明确换货业务数据迁移的状态、失败恢复、范围和列表/详情可见性。
|
||||
|
||||
## Impact
|
||||
|
||||
- 影响 `internal/model/exchange_order.go`、换货完成写用例、换货列表 Query、换货 DTO 及既有换货审计。
|
||||
- 需要新增成对数据库迁移,以保存迁移状态和最近失败原因;不修改既有迁移。
|
||||
- 既有换货列表与详情接口将新增/明确迁移状态字段,前端应改按状态展示。
|
||||
@@ -0,0 +1,41 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 换货业务数据迁移状态与失败恢复
|
||||
系统 SHALL 为每张已持久化的物流换货单返回业务数据迁移状态 `not_migrated`(不迁移)、`pending`(待迁移)、`migrated`(已迁移)或 `failed`(迁移失败),以及对应的中文状态名称。未选择业务数据迁移的换货单状态 MUST 为 `not_migrated`;选择迁移但尚未成功完成的换货单状态 MUST 为 `pending`;完整迁移成功后状态 MUST 为 `migrated`;迁移执行失败后状态 MUST 为 `failed`,并保存最近一次可安全展示的失败原因。
|
||||
|
||||
换货列表和详情 SHALL 返回迁移状态及中文名称;仅当状态为 `failed` 时返回最近一次失败原因。既有 `migrate_data`、`migration_completed` 和迁移余额字段 SHALL 保持兼容,但客户端不得再通过它们推断迁移结果。直接换货创建失败继续按既有原子性整体回滚,不产生可查询的失败换货单。
|
||||
|
||||
#### Scenario: 不迁移的换货单
|
||||
- **WHEN** 创建或发货时未选择业务数据迁移
|
||||
- **THEN** 换货列表和详情返回 `not_migrated` 及“不迁移”,且不返回迁移失败原因
|
||||
|
||||
#### Scenario: 待迁移的换货单
|
||||
- **WHEN** 换货单已选择业务数据迁移但尚未成功完成换货
|
||||
- **THEN** 换货列表和详情返回 `pending` 及“待迁移”
|
||||
|
||||
#### Scenario: 成功完成业务数据迁移
|
||||
- **WHEN** 换货完成时全部业务数据迁移成功
|
||||
- **THEN** 系统原子完成换货及业务数据迁移,列表和详情返回 `migrated` 及“已迁移”,并清除最近一次失败原因
|
||||
|
||||
#### Scenario: 迁移失败后保留可恢复事实
|
||||
- **WHEN** 换货完成时任一业务数据迁移步骤失败
|
||||
- **THEN** 系统不得提交本次换货完成及任何部分迁移结果,换货单保持可确认完成状态,返回迁移失败,并在独立持久化事实中将迁移状态更新为 `failed` 和最近一次失败原因
|
||||
|
||||
#### Scenario: 管理员重试失败迁移
|
||||
- **WHEN** 超级管理员或平台用户对处于可确认完成状态且迁移状态为 `failed` 的换货单再次确认完成
|
||||
- **THEN** 系统重新原子执行完整业务数据迁移;成功后将状态更新为 `migrated`,再次失败则保留 `failed` 并覆盖为最近一次失败原因
|
||||
|
||||
#### Scenario: 非平台账号重试失败迁移
|
||||
- **WHEN** 非超级管理员且非平台用户尝试再次确认迁移状态为 `failed` 的换货单
|
||||
- **THEN** 系统拒绝该操作,换货单及迁移状态不变
|
||||
|
||||
### Requirement: 换货业务数据迁移范围
|
||||
系统 SHALL 仅在选择业务数据迁移的换货完成中迁移旧资产的钱包余额、有效套餐使用记录、累计充值字段和资产标签。资产归属与个人客户—资产绑定 SHALL 继续作为换货完成固有动作,不受业务数据迁移选项控制;手机号—资产关联 MUST NOT 随换货或业务数据迁移转移,新资产首次访问时按其适用的手机号绑定规则处理。
|
||||
|
||||
#### Scenario: 选择业务数据迁移完成换货
|
||||
- **WHEN** 换货单选择业务数据迁移并成功确认完成
|
||||
- **THEN** 系统迁移钱包余额、有效套餐使用记录、累计充值字段和资产标签,且不迁移手机号—资产关联
|
||||
|
||||
#### Scenario: 不选择业务数据迁移完成换货
|
||||
- **WHEN** 换货单未选择业务数据迁移并确认完成
|
||||
- **THEN** 系统仍完成资产归属与个人客户—资产绑定的固有换货动作,但不迁移钱包余额、套餐使用记录、累计充值字段或资产标签
|
||||
18
openspec/changes/add-exchange-data-migration-status/tasks.md
Normal file
18
openspec/changes/add-exchange-data-migration-status/tasks.md
Normal file
@@ -0,0 +1,18 @@
|
||||
## 1. 数据契约
|
||||
|
||||
- [ ] 1.1 新增一对当前根迁移,为 `tb_exchange_order` 添加迁移状态和失败原因字段,并按既有迁移布尔字段回填历史记录。
|
||||
- [ ] 1.2 在换货模型和常量中定义四种迁移状态及中文名称,保留现有布尔字段的兼容语义。
|
||||
- [ ] 1.3 扩展换货列表、详情 DTO 及两个读侧投影,返回迁移状态、中文名称及仅失败时的安全失败原因。
|
||||
|
||||
## 2. 换货完成与恢复
|
||||
|
||||
- [ ] 2.1 在创建、发货和成功完成的写路径维护不迁移、待迁移和已迁移状态,并在成功后清除失败原因。
|
||||
- [ ] 2.2 保持完整换货和业务数据迁移在同一 GORM 事务;迁移失败时回滚全部业务修改,再以条件短事务保存物流换货单的失败状态、经安全处理的失败原因和审计事实。
|
||||
- [ ] 2.3 限制迁移失败的物流换货重试仅由超级管理员或平台用户发起;重试须锁定换货单、重新执行全套迁移并防止并发重复完成。
|
||||
- [ ] 2.4 保持直接换货失败时整体回滚且不持久化失败换货单,确认迁移范围不包含手机号—资产关联。
|
||||
|
||||
## 3. 文档与验证
|
||||
|
||||
- [ ] 3.1 更新换货接口 OpenAPI 描述并运行 `go run cmd/gendocs/main.go`,核对状态枚举及失败原因的响应契约。
|
||||
- [ ] 3.2 在隔离数据库按 `scripts/migrate.sh` 使用显式 `DB_*` 参数验证新迁移 up/down/up、历史状态映射及回滚后的 Schema。
|
||||
- [ ] 3.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`openspec validate add-exchange-data-migration-status --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
21
openspec/changes/add-export-time-filter-standards/design.md
Normal file
21
openspec/changes/add-export-time-filter-standards/design.md
Normal file
@@ -0,0 +1,21 @@
|
||||
## Decisions
|
||||
|
||||
- 共享导出筛选解析器返回 UTC 边界和冻结筛选快照,查询不在导出 Worker 中重新解释日期。
|
||||
- 导出任务保存授权范围快照而非执行时重新计算;列表/导出复用同一 Query 条件构造。
|
||||
|
||||
## 参数、查询与导出契约
|
||||
|
||||
### 统一时间解析
|
||||
|
||||
- 新增共享解析器,输入可选 `start_time`、`end_time` 字符串,必须以 RFC3339 秒级且带显式时区解析为瞬时 UTC 值;拒绝无时区、毫秒精度、非法日期和 `start_time > end_time`。两端均存在时 Query 使用 `time >= start_time AND time <= end_time`;单端只应用对应边界。
|
||||
- IoT/设备任务、换货、分配、订单、代理充值、佣金、提现统一绑定该参数并按各自创建/申请时间过滤;授权记录按授权发生时间;临期列表按当前有效主套餐最终到期时间。DTO、OpenAPI 和导出筛选名均固定为 `start_time`/`end_time`,不再接受模块私有日期字段作为新契约。
|
||||
|
||||
### 导出任务快照
|
||||
|
||||
- 创建临期、佣金明细、达量预警导出时,先复用页面 Query 构造器解析全部筛选和时间边界,再保存规范化过滤器、操作者 ID、创建时可见店铺/资产范围、时区、创建时间和口径版本。Worker 只读取该快照,不重新从请求、当前角色或当前页面解析筛选。
|
||||
- 临期导出以资产为粒度,选择当前有效主套餐最终到期时间和剩余天数;加油包不单独生成行。佣金导出以钱包变动明细为粒度,保存每次变动提交后的实际余额,允许回溯负数。预警导出以预警记录为粒度,套餐/流量/阈值/到期字段读触发快照,店铺/业务员/用户组可按执行时当前归属补全,但必须同时落在创建时冻结范围。
|
||||
- 导出完成记录结果文件、行数、完成时间和失败安全摘要;任何权限变化、筛选条件变化或后台归属变化不得扩大已创建任务的数据集。失败重试继续使用原快照,不创建第二份不同口径文件。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
为任务快照新增成对迁移;验证时区边界、空边界、非法/超长区间、权限变化及 up/down/up。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-014`。
|
||||
|
||||
## Why
|
||||
|
||||
后台导出和列表可能使用不同时间口径或在异步执行时漂移权限范围。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 统一日期/时间解析、左闭右开区间和最大范围校验。
|
||||
- 导出冻结列表筛选、时区和数据范围。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `export-time-filter`: 导出时间筛选标准。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响后台导出任务、查询 DTO、权限快照和 OpenAPI。
|
||||
@@ -0,0 +1,17 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 统一时间筛选参数与字段
|
||||
系统 SHALL 对 IoT/设备任务、换货、分配、订单、代理充值、佣金、提现及其导出统一使用可选 `start_time`、`end_time` 参数。参数必须为带时区的 RFC3339 秒级时间,区间为**闭区间**;任一端可缺省。上述业务按创建时间或申请时间筛选;授权记录按授权发生时间筛选;临期列表按当前生效主套餐最终到期时间筛选。格式非法或开始时间晚于结束时间时拒绝请求。
|
||||
|
||||
#### Scenario: 两端均传入
|
||||
- **WHEN** 请求携带合法的 `start_time` 与 `end_time`
|
||||
- **THEN** 系统仅返回权威时间大于等于开始时间且小于等于结束时间的记录
|
||||
|
||||
### Requirement: 三类异步导出及冻结口径
|
||||
临期列表、佣金明细和套餐流量达量预警 SHALL 复用既有异步导出任务,并在创建时冻结全部页面筛选条件、操作者和可见店铺范围。临期导出一行对应一项资产,仅取当前生效主套餐最终到期时间和剩余天数,加油包不得单独成行。预警导出一行对应一条预警记录,套餐、用量、总量、阈值和到期时间使用触发快照,店铺、业务员和用户组在执行时按当前归属补充。佣金明细必须导出每次佣金钱包变动后的实际余额,回溯记录可为负数。
|
||||
|
||||
异步执行不得重新解释时间、扩大创建时店铺范围或遗漏页面筛选;文件结果只含创建时有权读取的事实。
|
||||
|
||||
#### Scenario: 预警归属在导出前变更
|
||||
- **WHEN** 预警记录创建后资产所属店铺或业务员变更,再执行已创建导出任务
|
||||
- **THEN** 套餐及流量字段仍使用触发快照,店铺、业务员和用户组使用执行时当前归属,且不得超出任务创建时冻结的可见店铺范围
|
||||
@@ -0,0 +1,12 @@
|
||||
## Purpose
|
||||
|
||||
为 2026 年 8 月迭代提供独立、可验证的 统一时间筛选与导出快照 行为契约,避免与既有模块的兼容行为混淆。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 统一时间筛选与导出快照
|
||||
系统 SHALL 对受影响列表使用可单端省略的 `start_time` 和 `end_time` RFC3339 秒级闭区间,并按规定业务时间筛选。异步导出必须冻结创建时筛选条件、操作者和可见店铺范围,且按各业务规定使用触发快照或执行时归属。
|
||||
|
||||
#### Scenario: 规则命中
|
||||
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||
@@ -0,0 +1,8 @@
|
||||
## 1. 统一筛选
|
||||
- [ ] 1.1 清点本期后台导出/列表入口及现有时间字段和权限 Query。
|
||||
- [ ] 1.2 实现上海时区日期解析、左闭右开区间、最大范围校验和筛选快照。
|
||||
- [ ] 1.3 改造导出任务以冻结范围并复用列表 Query;更新 DTO/OpenAPI。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 验证日期边界、时间格式、空范围、超限、权限变更和导出/列表一致性。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-export-time-filter-standards --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,33 @@
|
||||
## Context
|
||||
|
||||
个人客户通知已有隔离、已读和投递能力,物流换货已有状态机。弹窗配置不能替代通知事实,风险换卡不能另建待处理记录。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新增运营弹窗配置及版本/投放去重事实;候选查询在同一资产上下文计算风险优先级和配置匹配,创建/复用个人通知。
|
||||
- 风险地址提交以客户+旧资产唯一约束和事务创建物流换货单,地址写入换货单而不单独建表。
|
||||
- 频率去重使用客户、配置版本、资产/日期键;通知内容冻结在投放时,已读复用现有服务。
|
||||
- 后台仅管理配置,不得写任意 URL;H5 只接收受控目标类型。
|
||||
|
||||
## 后台与 H5 动作契约
|
||||
|
||||
### 运营弹窗配置
|
||||
|
||||
- `POST /h5-popup-configurations`:仅超级管理员、平台用户。请求 `title`(1~100 字符)、`content`(1~2000 字符)、`starts_at`、`ends_at`、`enabled`、`priority`、`pages`(首页/资产详情/套餐购买/资产钱包充值)、可选店铺/设备类型/卡类型集合、`frequency`(`once`/`daily`)和可选 `action_type`(`package_purchase`/`asset_wallet_recharge`)。结束时间不得早于开始时间;不接受 URL、前端路由或任意动作参数。
|
||||
- `PUT /h5-popup-configurations/:id` 更新时递增配置版本;旧版本通知不改写。`POST /:id/enable`、`/disable` 仅影响后续候选;全部成功写操作记录操作者、前后值、版本和时间。
|
||||
|
||||
### H5 候选查询与风险换卡
|
||||
|
||||
- `GET /api/c/v1/popup-candidates`:当前个人客户必须提交 `page` 和当前资产标识;首页也必须先由客户选定当前资产。服务校验该资产属于当前客户或其既有授权范围,否则按资源不可见返回。
|
||||
- 查询先判断广电卡、运营商扩展状态风险停机、无活动物流换货单、未提交风险地址和“客户+资产+上海自然日”未展示;命中时创建/复用风险通知并只返回风险换卡候选。关闭或稍后处理只调用既有通知已读,不修改风险资格,次日允许再次投放。
|
||||
- 未命中风险时,按当前时间、启用状态、页面、店铺/设备类型/卡类型范围和频率匹配运营配置;同维度多值取任一命中,无配置即全量。只返回优先级最高一条,同优先级取最近更新时间;以客户、配置版本、资产、日期/一次性键创建或复用通知。
|
||||
|
||||
### 风险地址提交与通知读取
|
||||
|
||||
- `POST /api/c/v1/risk-exchanges/:asset_id/address`:当前个人客户提交 `recipient_name`、`recipient_phone`、`recipient_address`;均必填且沿用既有换货地址字段长度校验。事务中锁定客户和旧资产,复核风险资格,以客户+旧资产唯一约束创建物流换货单,`migrate_data=false`;重复提交返回首次创建的换货单与首次地址,禁止覆盖。
|
||||
- 弹窗通知内容、配置版本、资产和受控动作在投放时冻结并写个人站内通知,保留 90 天。候选查询不标记已读;关闭、点击受控操作、进入通知详情仅通过既有 `PUT /api/c/v1/notifications/:id/read` 幂等标记当前客户自己的通知。
|
||||
- 通知受控操作只返回类型与资产关联,不返回 URL;前端按白名单映射页面。客户读取他人通知或不属于其资产的风险换卡均按既有隔离规则不可见。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移和索引;隔离库验证风险条件、每日限制、地址幂等、优先级、版本重投、范围匹配、通知隔离及 up/down/up。
|
||||
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-007`。
|
||||
|
||||
## Why
|
||||
|
||||
风险停机客户需在 H5 自助留下换卡地址,运营也需可控的资产定向弹窗;两者都必须保留投放事实且不预生成无访问客户的通知。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增风险换卡候选与地址提交,幂等创建物流换货单。
|
||||
- 新增运营弹窗配置、范围/优先级/频率/版本投放和受控动作。
|
||||
- 复用个人客户通知保存快照、已读和 90 天历史。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `h5-popup-notification`: H5 风险换卡和运营弹窗。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响 H5、个人通知、资产/换货查询、后台配置、审计和 Schema。
|
||||
@@ -0,0 +1,30 @@
|
||||
## Purpose
|
||||
|
||||
在客户实际访问 H5 时,按当前资产事实投放风险换卡或运营弹窗,并将投放内容作为个人客户通知留存,避免预生成通知或开放任意跳转链接。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 风险换卡候选与地址提交
|
||||
系统 SHALL 仅在当前 H5 客户访问的资产为广电卡、运营商扩展状态为风险停机、且不存在待填写信息、待发货、已发货待确认或已完成物流换货单时返回风险换卡弹窗。同一客户同一资产每天至多展示一次;稍后处理仅抑制当天,次日仍可命中。风险换卡优先级固定高于运营弹窗。
|
||||
|
||||
客户提交收货人姓名、收货手机号和完整地址文本后,系统 MUST 幂等创建关联旧资产的物流换货单;首次地址锁定,客户不得修改。自动换货单不预设业务数据迁移,发货选择新资产时仍由后台按既有换货流程决定。风险条件不再成立或地址已提交后停止新投放,已投放通知保留 90 天。
|
||||
|
||||
#### Scenario: 重复提交风险地址
|
||||
- **WHEN** 客户对同一风险资产重复提交收货地址
|
||||
- **THEN** 系统保留首次地址和唯一物流换货单,不创建第二张换货单
|
||||
|
||||
### Requirement: 运营弹窗实时匹配
|
||||
系统 SHALL 允许超级管理员和平台用户管理全局运营弹窗的标题、内容、有效期、启停、优先级、店铺/设备类型/卡类型范围、四种页面(首页、资产详情、套餐购买、资产钱包充值)、频率和一个可选受控操作。范围同一维度多选为任一匹配,未配置范围即全量;H5 请求必须携带当前页面资产标识,首页使用当前选中资产。操作仅可为套餐购买或资产钱包充值,不得配置任意 URL。
|
||||
|
||||
运营弹窗仅在客户请求候选时实时匹配并创建或复用通知;每客户每配置支持仅一次或每天一次。候选只返回优先级最高一条,同优先级取最近更新时间最新;配置修改形成新版本,既有通知保留快照,修改后的仅一次配置可向原命中客户重新投放。配置到期/停用停止新投放,历史通知保留 90 天。
|
||||
|
||||
#### Scenario: 风险与运营候选同时命中
|
||||
- **WHEN** 当前资产同时满足风险换卡和多个运营弹窗条件
|
||||
- **THEN** 系统仅返回风险换卡候选,并保持通知未读
|
||||
|
||||
### Requirement: 通知留存与已读
|
||||
弹窗投放 SHALL 复用个人客户站内通知,保存投放时内容、配置版本、资产和受控操作快照,并同时出现在通知列表。创建或返回候选不得自动已读;客户关闭、点击操作或进入通知详情后通过既有已读接口幂等标记已读。通知读取必须维持个人客户隔离。
|
||||
|
||||
#### Scenario: 关闭弹窗
|
||||
- **WHEN** 当前个人客户关闭其未读弹窗
|
||||
- **THEN** 系统仅标记该客户该通知已读,不影响其他客户或未来符合条件的投放
|
||||
13
openspec/changes/add-h5-risk-exchange-notifications/tasks.md
Normal file
13
openspec/changes/add-h5-risk-exchange-notifications/tasks.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## 1. 数据与配置
|
||||
- [ ] 1.1 追踪个人通知、资产风险状态、物流换货、H5 认证和既有已读链路。
|
||||
- [ ] 1.2 新增弹窗配置、版本/投放去重所需成对迁移、模型、范围和索引。
|
||||
- [ ] 1.3 实现管理端配置 CRUD、启停、受控页面/动作/频率校验和审计。
|
||||
|
||||
## 2. H5 行为
|
||||
- [ ] 2.1 实现携带资产标识的候选接口:风险条件、每日限制、运营范围/优先级/版本匹配和个人通知创建/复用。
|
||||
- [ ] 2.2 实现风险地址提交、唯一物流换货单创建、首次地址锁定及数据迁移默认边界。
|
||||
- [ ] 2.3 接入既有个人通知列表和已读,注册路由/OpenAPI。
|
||||
|
||||
## 3. 验证
|
||||
- [ ] 3.1 隔离库验证风险/运营优先级、频率、范围、版本、地址重复提交、通知隔离及迁移 up/down/up。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-h5-risk-exchange-notifications --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
2
openspec/changes/add-operations-reports/.openspec.yaml
Normal file
2
openspec/changes/add-operations-reports/.openspec.yaml
Normal file
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
27
openspec/changes/add-operations-reports/design.md
Normal file
27
openspec/changes/add-operations-reports/design.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Decisions
|
||||
|
||||
- 只读 Query 分别以卡首次激活成功事实和续购套餐实际生效事实为权威源;不以当前订单/资产状态反推历史。
|
||||
- 激活按卡去重,续费同时计算订单计数和资产去重计数;金额始终聚合分。
|
||||
- 先施加请求人数据范围,再关联设备、套餐、用户组、店铺、业务员维度;缺失维度用占位值保留事实。
|
||||
- 导出复用同一 Query 并保存筛选、范围、时区和口径版本快照。
|
||||
|
||||
## 查询与导出契约
|
||||
|
||||
### 激活情况统计
|
||||
|
||||
- `GET /operations-reports/activations`:仅超级管理员、平台用户;请求 `start_time`、`end_time` 必填,上海时区左闭右开,及可选 `device_type`、`package_id`、`business_user_group_id`、`shop_id`、`business_owner_account_id`、`group_by`。先应用既有资产/店铺范围,再以卡 `activated_at` 的首次成功激活事实过滤和聚合;同一卡在区间内至多贡献 1。
|
||||
- 响应返回统计边界、分组维度代码/名称、`activation_count`;关联的设备、套餐、用户组、店铺或业务员物理缺失时返回固定“未知/已删除”占位,不用当前资产状态、退款、换货或取消结果排除历史激活。
|
||||
|
||||
### 套餐续费情况统计
|
||||
|
||||
- `GET /operations-reports/package-renewals`:筛选与分组维度同激活报表。权威事实是续购订单支付成功且对应主套餐使用记录实际生效;新购、加油包、失败/关闭支付、仅支付成功未生效均排除。
|
||||
- 每个分组返回 `renewal_order_count`、`renewal_asset_count`(按资产去重)、`received_renewal_amount`(分)。同一资产多笔生效续购增加订单数和金额但只增加一次资产数;金额从订单冻结实收金额读取,禁止由套餐当前售价反算。
|
||||
|
||||
### 权限与导出
|
||||
|
||||
- 不在调用者数据范围内的 `shop_id`、资产或维度筛选返回既有无权/空集合语义,不返回越权聚合。时间缺失、格式无效、开始不早于结束或不支持的 `group_by` 返回稳定参数错误,不执行聚合。
|
||||
- `POST /operations-reports/activations/export` 与 `/package-renewals/export` 创建异步任务,保存请求人、报表类型、规范化筛选、上海时区、口径版本、授权范围和创建时间。Worker 复用相同 Query,生成的列与页面指标一致,并记录文件、行数、完成时间或安全失败摘要;后续角色/店铺变化不得扩大范围。
|
||||
|
||||
## Verification
|
||||
|
||||
验证跨日边界、首次激活去重、退款后激活保留、续购未生效排除、多次续费、维度缺失、权限和导出一致性。
|
||||
25
openspec/changes/add-operations-reports/proposal.md
Normal file
25
openspec/changes/add-operations-reports/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-015`。
|
||||
|
||||
## Why
|
||||
|
||||
首期运营分析需有统一、可导出的激活和套餐续费统计,避免把订单、退款、钱包等不同行为混成一个泛化报表。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增按首次激活成功时间统计的激活情况统计表。
|
||||
- 新增按套餐续购实际生效时间统计的套餐续费情况统计表。
|
||||
- 固化设备、套餐、用户组、店铺、业务员维度、金额/数量口径、权限与导出快照。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `operations-report`: 激活与套餐续费运营报表。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响资产/卡激活、套餐使用、订单、店铺/业务员维度、导出和数据权限查询。
|
||||
@@ -0,0 +1,22 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 激活情况统计报表
|
||||
系统 SHALL 提供激活情况统计表,按上海时区、卡实际首次激活成功时间统计。查询必须支持时间范围及设备类型、套餐、用户组、店铺、业务员维度筛选和分组;返回激活数量、各维度名称及“未知/已删除”历史维度占位值。取消、退款、换货或当前资产状态变化不得改写已发生的首次激活事实;同一卡仅计一次首次成功激活。
|
||||
|
||||
#### Scenario: 已激活资产后续退款
|
||||
- **WHEN** 卡在统计区间内首次激活成功,后续套餐退款或资产状态变化
|
||||
- **THEN** 系统仍按首次激活时间计入激活数量,不重复或撤销该历史激活事实
|
||||
|
||||
### Requirement: 套餐续费情况统计报表
|
||||
系统 SHALL 提供套餐续费情况统计表,按上海时区、套餐续购订单支付成功并使套餐续期生效的时间统计。查询支持时间范围及设备类型、套餐、用户组、店铺、业务员维度筛选和分组;返回续费订单数、续费资产数、实收续费金额(分)和各维度名称。新购、加油包购买、失败/关闭支付和未生效续购不计入续费;同一资产在区间内多次成功续费按订单数累计,资产数按资产去重。
|
||||
|
||||
#### Scenario: 续购支付成功但套餐未生效
|
||||
- **WHEN** 续购支付成功但套餐生效事务尚未完成或最终失败
|
||||
- **THEN** 系统不将该订单计入套餐续费统计
|
||||
|
||||
### Requirement: 报表权限、导出与口径快照
|
||||
仅超级管理员和平台用户 SHALL 查询或导出报表,并按既有数据范围过滤店铺、代理和资产。时间范围必填、按左闭右开区间解释;导出冻结请求人、筛选条件、时区、统计口径版本和授权范围,记录操作人、导出时间、筛选条件及导出结果。导出列与页面对应指标一致,异步执行不得因后续权限变化扩大数据范围。
|
||||
|
||||
#### Scenario: 无权范围筛选
|
||||
- **WHEN** 请求人筛选其无权访问的店铺
|
||||
- **THEN** 系统拒绝请求或按既有范围规则不返回该店铺聚合值,不泄露任何统计结果
|
||||
@@ -0,0 +1,12 @@
|
||||
## Purpose
|
||||
|
||||
为 2026 年 8 月迭代提供独立、可验证的 激活与套餐续费日报 行为契约,避免与既有模块的兼容行为混淆。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 激活与套餐续费日报
|
||||
系统 SHALL 自功能上线后每日生成激活和套餐续费稳定日报快照,不回填上线前历史。查询支持日/月趋势、单一业务维度分组及异步导出,并按已确认的采购、激活、在网、活跃和续费率口径计算。
|
||||
|
||||
#### Scenario: 规则命中
|
||||
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||
9
openspec/changes/add-operations-reports/tasks.md
Normal file
9
openspec/changes/add-operations-reports/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1. 两张报表
|
||||
- [ ] 1.1 确认首次激活成功和续购实际生效的权威表、终态/时间字段及维度关联。
|
||||
- [ ] 1.2 实现激活情况统计:时间范围、五类维度、卡去重、历史维度占位和数据范围。
|
||||
- [ ] 1.3 实现套餐续费统计:生效门槛、订单数/资产数/实收金额和数据范围。
|
||||
- [ ] 1.4 实现同 Query 导出及筛选、时区、口径、权限快照和操作审计;更新路由/OpenAPI。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 验证首次激活、多次续费、退款、未生效续购、跨日、维度缺失、权限和导出一致性。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-operations-reports --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
39
openspec/changes/add-package-real-usage-alerts/design.md
Normal file
39
openspec/changes/add-package-real-usage-alerts/design.md
Normal file
@@ -0,0 +1,39 @@
|
||||
## Context
|
||||
|
||||
套餐商品已有 `real_data_mb`,套餐使用记录承载真实用量;现有通知以事件投递并按接收人隔离。预警必须是套餐级观测,不能复用运营商通道阈值停机锁。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新增套餐规则和预警表;规则以套餐唯一,预警以套餐使用记录+阈值快照唯一,保存触发时流量、资产/卡/设备、套餐和店铺/业务员快照。
|
||||
- 扫描查询当前有效使用记录,按资产汇总真实用量与额度,并选出主套餐规则;用唯一约束和事务创建预警及通知事件,扫描可安全重跑。
|
||||
- 不向未来业务员补发:接收人仅在预警创建事务中解析。读侧以资产范围过滤,导出复用现有快照任务。
|
||||
|
||||
## 管理、扫描与查询动作契约
|
||||
|
||||
### 规则维护
|
||||
|
||||
- `POST /package-traffic-alert-rules`:仅超级管理员、平台用户;请求 `package_id`、`threshold_percent`(大于等于 1、小于等于 100,允许小数)、`enabled`、`remark`(最多 500 字符)。套餐必须存在且 `real_data_mb > 0`;同套餐已有规则返回“套餐已存在真流量预警规则”。
|
||||
- `PUT /package-traffic-alert-rules/:id`:允许修改阈值、启停、备注;不修改已产生预警快照。停用后扫描不建新预警;启用或降低阈值后不主动回填,仅由下一次扫描按当前有效套餐判断。
|
||||
- `GET /package-traffic-alert-rules` 返回套餐、真流量总额度、阈值、启用状态、备注和更新时间。所有成功写操作记录操作者、前后值和时间。
|
||||
|
||||
### 扫描与预警创建
|
||||
|
||||
- Worker 只读取当前有效套餐使用记录;按同一资产聚合这些记录的真实已用量与套餐 `real_data_mb`,不读取虚流量、展示流量或运营商通道累计值。主套餐不存在有效规则、总额度不大于零或比例未达阈值时跳过。
|
||||
- 对命中主套餐规则的套餐使用记录,在事务中写预警唯一键 `(package_usage_id, threshold_percent_snapshot)`,同时冻结套餐、资产、卡、当前设备、店铺、业务员、真实用量、额度、比例、阈值和触发时间。唯一冲突视为已处理,不重复投递通知。
|
||||
- 创建时仅解析当前资产所属店铺的有效业务员;存在时在同一可靠事件链创建一条站内通知,保存预警 ID 作为幂等键;不存在时只保存预警。通知失败进入既有可靠投递恢复,不能删除预警或重新计算快照。
|
||||
|
||||
### 列表、详情与导出
|
||||
|
||||
- `GET /package-traffic-alerts`:仅超级管理员、平台用户,先应用既有资产数据范围;支持套餐、店铺、业务员、资产/卡标识、阈值、触发时间和通知投递状态筛选、分页。返回冻结快照与通知结果,当前归属变化不得改写预警事实。
|
||||
- `GET /package-traffic-alerts/:id`:同一数据范围校验后返回完整预警快照和通知投递历史;越权与不存在统一按既有资源不可见处理。
|
||||
- `POST /package-traffic-alerts/export`:复用异步导出;创建时冻结操作者、筛选、时间范围和可见资产范围。每行对应一条预警,执行时不得扩大范围或重算已冻结流量字段。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 流量数据延迟 → 下次扫描补建,不回写已冻结预警。
|
||||
- 多卡设备 → 以使用记录资产归属聚合,不从当前设备反推流量。
|
||||
- 扫描并发 → 唯一索引处理同一命中重复创建。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移和索引;隔离环境验证规则启停/降阈值、有效套餐汇总、去重、无业务员、权限导出和 up/down/up。
|
||||
27
openspec/changes/add-package-real-usage-alerts/proposal.md
Normal file
27
openspec/changes/add-package-real-usage-alerts/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-004`。
|
||||
|
||||
## Why
|
||||
|
||||
运营需要在套餐真实流量达量时通知资产所属店铺的有效业务员;现有套餐和通知能力没有规则、去重预警事实或可导出的触发快照。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 为每个套餐商品维护至多一条 1%~100% 真流量预警规则;规则变更只影响后续扫描,既有预警冻结快照。
|
||||
- 扫描同一资产全部当前有效套餐的真流量汇总,以实际消耗流量的套餐记录关联资产和主套餐规则判断达量。
|
||||
- 为同一套餐使用记录和阈值仅建一条预警,通知当时有效业务员,并提供权限受控列表与异步导出。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `package-traffic-alert`: 真流量规则、预警事实、通知和导出。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。既有套餐状态与通知接收人隔离规则保持不变。
|
||||
|
||||
## Impact
|
||||
|
||||
影响套餐配置、套餐使用/流量扫描任务、资产投影、通知事件、导出、审计和新增 Schema。
|
||||
@@ -0,0 +1,32 @@
|
||||
## Purpose
|
||||
|
||||
按套餐真实流量和当前有效套餐事实生成一次性达量预警,向资产所属店铺当时有效业务员投递可追溯通知,而不将通道级停复机控制或虚流量混入套餐预警。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 真流量预警规则
|
||||
系统 SHALL 为每个套餐商品维护至多一条当前真流量预警规则,阈值为 1% 至 100% 的小数百分比。规则启用、修改或降低阈值只影响后续扫描;既有预警 MUST 保留触发时的套餐、阈值、流量和资产快照。规则停用后停止创建新预警;重新启用或降低阈值后,下次扫描发现已有有效套餐达量时必须补建符合条件的预警。
|
||||
|
||||
#### Scenario: 降低阈值后补建
|
||||
- **WHEN** 管理员降低一个启用规则的阈值,下一次扫描发现其有效套餐已达到新阈值
|
||||
- **THEN** 系统创建预警并冻结新阈值,不修改既有预警快照
|
||||
|
||||
### Requirement: 汇总口径、去重与通知
|
||||
系统 SHALL 以同一资产全部当前有效套餐的 `真流量使用量 / 真流量总额度` 汇总比例判断达量,并使用该资产主套餐的规则。实际消耗流量的套餐使用记录所关联资产是权威归属;插拔卡时预警同时展示卡和当前关联设备,但 MUST NOT 汇总多张卡。虚流量、已失效/过期/非当前有效套餐不得计入。
|
||||
|
||||
同一套餐使用记录和同一命中阈值 MUST 至多创建一条预警。预警创建时仅向资产所属店铺当时有效业务员创建站内通知;无有效业务员时仍保留预警事实但不补发给未来新增业务员。通知和预警均须幂等,重复扫描不得重复创建。
|
||||
|
||||
#### Scenario: 多个有效套餐共同达量
|
||||
- **WHEN** 某资产的多个当前有效套餐真流量汇总达到其主套餐规则阈值
|
||||
- **THEN** 系统为命中套餐使用记录创建唯一预警,并只向扫描时该资产所属店铺的有效业务员投递通知
|
||||
|
||||
#### Scenario: 重复扫描
|
||||
- **WHEN** 相同套餐使用记录和相同阈值被重复扫描命中
|
||||
- **THEN** 系统保留原预警和通知,不创建重复记录
|
||||
|
||||
### Requirement: 预警查询与导出
|
||||
超级管理员和平台用户 SHALL 在既有资产数据范围内查询和导出预警;列表和导出返回资产、卡、当前设备、套餐使用记录、真流量用量/额度/比例、阈值快照、触发时间及通知投递结果。导出必须使用既有异步任务并冻结创建时操作者、筛选条件和可见范围。
|
||||
|
||||
#### Scenario: 受限导出
|
||||
- **WHEN** 平台用户在其资产数据范围内创建预警导出
|
||||
- **THEN** 导出仅包含创建时可见预警,即使任务执行期间店铺归属发生变化
|
||||
16
openspec/changes/add-package-real-usage-alerts/tasks.md
Normal file
16
openspec/changes/add-package-real-usage-alerts/tasks.md
Normal file
@@ -0,0 +1,16 @@
|
||||
## 1. 数据与规则
|
||||
|
||||
- [ ] 1.1 追踪套餐使用有效态、真流量字段、资产/卡/设备关联、有效业务员、通知事件及异步导出调用链。
|
||||
- [ ] 1.2 新增成对迁移、模型和约束:套餐唯一规则、预警快照、使用记录+阈值唯一去重、查询/导出索引。
|
||||
- [ ] 1.3 实现规则 CRUD、1%~100% 校验、启停和审计;更新套餐管理 OpenAPI。
|
||||
|
||||
## 2. 扫描与读侧
|
||||
|
||||
- [ ] 2.1 实现可重跑扫描:按资产汇总当前有效套餐真流量、选择主套餐规则、排除虚流量/失效记录,并原子创建预警与通知事件。
|
||||
- [ ] 2.2 接入既有任务调度和通知投递,确保无有效业务员仍建预警、重复扫描不重复通知。
|
||||
- [ ] 2.3 实现受资产数据范围保护的预警列表/详情和异步导出,冻结导出筛选与可见范围。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- [ ] 3.1 在隔离数据库验证迁移 up/down/up、规则变化补建、有效套餐汇总、重复扫描、插拔卡展示、通知接收人和导出权限。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-package-real-usage-alerts --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
58
openspec/changes/add-payment-merchant-pools/design.md
Normal file
@@ -0,0 +1,58 @@
|
||||
## Context
|
||||
|
||||
现有 `tb_wechat_config` 同时承担支付渠道配置;订单、充值和 `tb_payment` 通过 `payment_config_id` 供回调加载创建时配置。`tb_payment` 已有非敏感 `merchant_identity` 快照,但不足以区分商户池、服务商和完整路由。新模型必须让新单和旧单分流,不能把历史订单指向迁移后新商户。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 独立实体与不可变路由快照
|
||||
新增商户、商户池、池成员、微信授权配置及金额/笔数统计事实;支付单扩展实际商户和商户池 ID 及非敏感快照。商户凭证只留在商户表受控字段,支付单/审计不复制。被引用后锁定商户支付方式、服务商与身份,避免历史验签与退款语义漂移。
|
||||
|
||||
### 路由和统计在支付创建/成功边界闭合
|
||||
支付创建在事务内读取唯一启用池、按方式选择成员并冻结路由;无成员/池失败即返回。金额/笔数统计只在现有“支付成功首次生效”路径按实际商户条件递增,以支付 ID 唯一约束避免重复回调累计;不在预下单预占。时间方式由当前时间和受控起点计算,不写逐次路由日志。
|
||||
|
||||
### 新旧配置双读切换
|
||||
新支付单具有 merchant ID 时,支付加载、回调验签、查询和退款均从商户加载服务商凭证;merchant ID 为空的历史单保持现有 `payment_config_id` 路径。迁移先复制生效配置中完整凭证,再发布新支付路径;不可将现有记录批量回填为新商户,因为其实际历史身份无法保证一致。
|
||||
|
||||
### 敏感配置边界
|
||||
后台管理响应按 PRD 向两类已认证管理角色完整返回凭证;所有 logger、错误、审计 payload、支付单快照和导出只允许写 ID、名称和脱敏/非敏感身份。更新凭证后使现有配置加载缓存失效;历史回调读取最新有效凭证以支持轮换。
|
||||
|
||||
## 管理与支付动作契约
|
||||
|
||||
以下路径为本 Change 新增后台管理契约;所有管理写操作仅限超级管理员和平台用户,金额均为分,敏感凭证只在已认证管理请求的写入/详情响应中传输,绝不进入支付快照、审计、日志或导出。
|
||||
|
||||
### 商户
|
||||
|
||||
- `POST /payment-merchants`:请求 `name`、`payment_method`(`wechat`/`alipay`)、`provider_type`、`merchant_identity`、`credentials`、`enabled`、`remark`。同一支付方式下身份标识不得重复;凭证缺失或与服务商类型不匹配时拒绝。
|
||||
- `PUT /payment-merchants/:id`:未被任何支付单引用时可修改全部字段;被引用后仅可改名称、凭证、启停、备注,修改支付方式、服务商类型或商户身份返回“已被支付单引用,不能修改收款身份”。
|
||||
- `DELETE /payment-merchants/:id`:被引用或仍属于任一商户池时拒绝;删除前必须显式二次确认。停用不影响已冻结该商户的查单、验签和退款。
|
||||
|
||||
### 商户池
|
||||
|
||||
- `POST /payment-merchant-pools`:请求 `name`、`payment_method`、`enabled`、`strategy`(金额/笔数/时间)、策略参数和有序 `member_ids`。成员均须存在、启用、与池支付方式一致且不重复;同一支付方式最多一个启用池,冲突返回“该支付方式已有启用商户池”。
|
||||
- `PUT /payment-merchant-pools/:id`:修改阈值只保留当前统计;修改金额/笔数统计周期、时间周期、起始时间或每轮成员排序时开启新统计周期;自然周期仅调整排序时保留未移除成员累计。成员移除后不再选择,但历史支付快照不改写。
|
||||
- `POST /payment-merchant-pools/:id/enable` 与 `/disable`:启用时再次校验唯一启用池和可用成员;停用后新支付创建明确失败,不回退综合支付配置。
|
||||
|
||||
### 微信授权配置
|
||||
|
||||
- `GET /wechat-authorizations`:最多返回一个启用配置;仅管理角色可读取。`PUT /wechat-authorizations/current` 创建或更新唯一配置,写入公众号 H5/JSSDK、小程序登录及支付 AppID 所需字段。
|
||||
- 启用第二个配置返回“平台已有启用微信授权配置”;停用后 C 端微信登录/OpenID/微信支付 AppID 不得静默回退其他支付商户或旧配置。
|
||||
|
||||
### 新旧支付分流
|
||||
|
||||
- C 端套餐订单、资产钱包充值、代理在线预存款创建支付时,在同一事务内读取对应支付方式唯一启用池并冻结 `merchant_id`、`merchant_pool_id`、非敏感收款身份与轮询快照;无可用成员返回“暂无可用商户”。
|
||||
- `merchant_id` 非空的支付,在回调、查单、退款时加载该商户当前凭证;为空的历史支付仅按既有 `payment_config_id` 处理。不得按当前启用池为历史单推断商户。
|
||||
- 首次支付成功消费者以支付 ID 唯一记账金额/笔数统计;重复回调不重复累计。预下单、失败、关闭和退款均不变更统计。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 并发成功回调超过阈值:这是“只统计成功、不预占”的明确结果,下一次选路才跳过。
|
||||
- 商户停用后的退款:停用不阻断历史退款,实际调用仍由凭证/渠道结果决定。
|
||||
- 迁移缺失凭证:不造空商户池,受影响新支付明确失败。
|
||||
- 旧回调误入新路径:以支付单 merchant ID 为唯一分流条件,严禁按当前启用池推断。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建商户/池/成员/微信授权表、唯一启用约束、支付单路由列和成功统计索引。
|
||||
2. 在迁移事务中从唯一生效综合配置复制完整凭证并创建单成员池;全过程禁止日志输出密钥/证书。
|
||||
3. 隔离库验证微信直连、富友、支付宝、空配置、停用历史商户、回调兼容、三种轮询及 up/down/up。
|
||||
4. 回滚前停止新支付创建;有新路由单时仅回退应用流量,不执行会破坏新支付事实的 down。
|
||||
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
27
openspec/changes/add-payment-merchant-pools/proposal.md
Normal file
@@ -0,0 +1,27 @@
|
||||
## Why
|
||||
|
||||
当前所有线上支付依赖一份综合支付配置,无法按支付方式在多个实际收款商户之间受控轮询;创建支付后的商户事实也不足以让停用商户的历史回调、查单和原路退款继续安全执行。
|
||||
|
||||
本 Change 落实 AUG26-002:将收款商户、支付路由和 C 端微信授权分离,并以商户池快照取代新业务对旧综合支付配置的依赖。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增独立商户管理:微信商户与支付宝商户分别建档;商户保存支付能力和敏感凭证,但历史支付单仅保存非敏感身份快照。
|
||||
- 新增每种支付方式至多一个启用商户池,支持按成功收款金额、成功笔数或时间周期轮询;新支付仅从命中池选择实际商户。
|
||||
- 新增全局唯一启用的微信授权配置,专供 C 端公众号 H5/JSSDK、小程序登录、OpenID 和支付 AppID;它不是微信收款商户。
|
||||
- 新建 C 端套餐购买、资产钱包充值、代理在线预存款充值按商户池路由;后台线下/钱包支付不经过商户池。无可用商户时失败,不得回退旧配置或自动换商户重试。
|
||||
- 从当前生效综合支付配置一次性复制完整凭证形成新商户、单成员商户池及微信授权配置;新支付切换后,旧配置和历史订单仅继续处理其自身回调、查询和退款。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `merchant-payment-routing`: 商户、商户池、微信授权配置、轮询、历史商户快照和迁移切换行为。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。该能力向既有支付创建、回调和退款调用链提供已选商户事实,不改变其资金幂等不变量。
|
||||
|
||||
## Impact
|
||||
|
||||
影响支付配置模型和后台接口、`tb_payment`/订单/充值支付关联、微信/支付宝/富友支付加载和回调、支付与退款审计、敏感信息访问及新增成对迁移。
|
||||
@@ -0,0 +1,44 @@
|
||||
## Purpose
|
||||
|
||||
管理实际收款商户、商户池轮询和全局微信授权配置,使新线上支付的收款身份可冻结、历史支付可继续使用其原商户,并避免凭证泄露或无配置时静默回退。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 商户与微信授权配置管理
|
||||
系统 SHALL 将实际收款商户与微信授权配置分离。一个商户 MUST 仅对应 `wechat` 或 `alipay` 一种支付方式,并保存名称、支付方式、服务商类型、商户号或应用标识、敏感凭证、状态和备注;微信直连与富友均为微信支付商户。平台最多存在一个启用的微信授权配置,该配置保存 C 端公众号 H5/JSSDK、小程序登录所需参数,C 端微信登录、OpenID 和微信支付 AppID MUST 只读取该配置。
|
||||
|
||||
超级管理员和平台用户可创建、编辑、启用、停用商户、商户池和微信授权配置,其他角色无管理入口。被支付单引用的商户 MUST NOT 删除且其支付方式、服务商类型、商户号/应用标识不得修改;未被引用商户仅可移出所有商户池并经二次确认删除。停用只影响新支付单,历史支付的回调、查单和原路退款仍使用该商户当前凭证。管理 API 可向上述已认证管理角色返回完整凭证,但日志、审计快照、错误和普通业务响应 MUST NOT 保存或返回敏感凭证。
|
||||
|
||||
#### Scenario: 受引用商户停用
|
||||
- **WHEN** 管理员停用已被支付单命中的商户
|
||||
- **THEN** 新支付单不再选择该商户,已命中支付单的回调、查询和原路退款仍按该商户处理
|
||||
|
||||
#### Scenario: 非管理角色读取配置
|
||||
- **WHEN** 不具备超级管理员或平台用户身份的账号请求商户或微信授权配置
|
||||
- **THEN** 系统拒绝访问且不返回任何凭证或身份字段
|
||||
|
||||
### Requirement: 商户池唯一性与轮询配置
|
||||
系统 SHALL 为每种支付方式最多启用一个商户池;停用历史池可保留,但不得同时启用多个同支付方式池。商户池成员支付方式 MUST 与池一致,成员按明确顺序排列;金额/笔数方式必须配置 `每轮累计`、`自然日累计` 或 `自然月累计` 统计周期,时间方式必须配置最小为 1 分钟的数值、单位和起始时间。
|
||||
|
||||
金额和笔数轮询只统计已确认支付成功结果,不在预下单时预占,也不因退款回冲。达到阈值的成员在当前周期跳过;所有成员达到阈值时新支付失败。时间轮询自起始时间按固定时段和成员顺序选择,成员停用即时跳下一个可用成员但不重置时段。预下单失败不得自动切换或重试,失败单不计入统计;客户再次发起时重新选择。修改阈值保留当前统计,修改统计周期、金额/笔数方式、时间周期、起始时间或每轮排序按 PRD 规则开启新周期;自然周期排序调整保留未移除成员累计。
|
||||
|
||||
#### Scenario: 并发预下单未预占额度
|
||||
- **WHEN** 多个客户并发创建金额或笔数轮询支付单且当前成员尚未达到阈值
|
||||
- **THEN** 系统可使这些支付单均命中当前成员,只有后续确认成功的支付才计入累计,已创建支付单不因轮询切换改挂商户
|
||||
|
||||
#### Scenario: 当期没有可用商户
|
||||
- **WHEN** 启用商户池中不存在启用且未达阈值的成员,或商户池已停用
|
||||
- **THEN** 系统拒绝创建新支付单并提示暂无可用商户,不回退到旧综合支付配置
|
||||
|
||||
### Requirement: 新支付商户快照与历史兼容
|
||||
C 端套餐购买、C 端资产钱包充值及代理在线预存款充值 SHALL 按支付方式通过对应启用商户池选择实际商户;后台线下订单和钱包余额支付 MUST NOT 经过商户池。每笔通过商户池创建的支付单 MUST 保存商户 ID、商户名称/支付方式/服务商类型/商户号或应用标识快照、商户池 ID/名称快照及轮询方式快照,但不得复制敏感凭证。支付、回调验签、查单和原路退款读取该实际商户当前凭证;商户退款能力只由服务商类型和退款必需凭证完整性决定,不提供人工开关。
|
||||
|
||||
上线迁移 MUST 从当前生效综合支付配置复制完整凭证:创建全局微信授权配置、微信/支付宝商户和各自单成员启用池。凭证不完整的方式不建池;迁移仅在数据库复制敏感数据。新订单必须只走商户池;旧配置和其历史订单不改写,继续服务历史回调、查询和退款。
|
||||
|
||||
#### Scenario: 新支付冻结实际商户
|
||||
- **WHEN** 客户以微信或支付宝创建覆盖范围内的新线上支付单
|
||||
- **THEN** 系统选择并冻结一个实际商户和商户池路由快照,并使用该商户的服务商凭证发起支付
|
||||
|
||||
#### Scenario: 旧支付单回调
|
||||
- **WHEN** 商户池切换后收到未带新商户快照的历史支付单回调
|
||||
- **THEN** 系统按既有综合支付配置兼容处理该历史单,不将其改挂到任何新商户
|
||||
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
20
openspec/changes/add-payment-merchant-pools/tasks.md
Normal file
@@ -0,0 +1,20 @@
|
||||
## 1. 数据与配置管理
|
||||
|
||||
- [ ] 1.1 追踪 `tb_wechat_config`、订单/充值、`tb_payment`、支付加载器和三类回调的现有 `payment_config_id` 读写链路,列出新旧分流点和敏感字段清单。
|
||||
- [ ] 1.2 新增成对迁移:商户、商户池、成员、微信授权配置、成功累计/路由快照所需表列、唯一启用/成员支付方式/历史引用约束和查询索引;不得修改既有迁移。
|
||||
- [ ] 1.3 实现商户、商户池及微信授权配置模型、管理 Query/Handler/RouteSpec:管理角色权限、启停、成员排序、受引用字段锁定、移出后确认删除及无敏感审计。
|
||||
- [ ] 1.4 实现迁移时从当前生效综合支付配置复制完整微信授权、微信/支付宝商户和单成员池;不完整方式不创建池,迁移日志不得含凭证。
|
||||
|
||||
## 2. 商户池选择与支付链路
|
||||
|
||||
- [ ] 2.1 实现金额、笔数、时间轮询选择器及池配置变更重置规则;使用事务/受控查询保证唯一启用池和成员状态一致。
|
||||
- [ ] 2.2 在 C 端套餐支付、资产钱包充值、代理在线充值的创建路径接入选择器,保存商户/池/方式非敏感快照;无可用商户和预下单失败不回退、不换商户。
|
||||
- [ ] 2.3 在支付成功首次生效路径按支付 ID 幂等累计金额/笔数;退款不得回冲,失败/重复回调不得计入。
|
||||
- [ ] 2.4 改造支付加载、微信/支付宝/富友回调、查单和原路退款:新单读取实际商户,历史 merchant ID 为空的单继续读取旧 `payment_config_id`;停用商户仍可处理历史单。
|
||||
- [ ] 2.5 清理日志、错误、审计和普通 DTO 中的敏感凭证,管理 API 仅向超级管理员/平台用户按 PRD 返回完整配置并使凭证更新刷新加载缓存。
|
||||
|
||||
## 3. 文档与验证
|
||||
|
||||
- [ ] 3.1 更新支付、商户和微信授权管理接口 OpenAPI,并运行 `go run cmd/gendocs/main.go`。
|
||||
- [ ] 3.2 在隔离数据库验证迁移 up/down/up、配置迁移、三类新支付、缺失配置、三种轮询、并发成功累计、商户停用历史回调/退款及旧单兼容。
|
||||
- [ ] 3.3 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`openspec validate add-payment-merchant-pools --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
32
openspec/changes/add-phone-asset-associations/design.md
Normal file
32
openspec/changes/add-phone-asset-associations/design.md
Normal file
@@ -0,0 +1,32 @@
|
||||
## Context
|
||||
|
||||
现有 H5 已有手机号绑定与全局开关;新关系不能由后台或换货推测建立。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新表以手机号、资产和有效状态保存关系,并对当前有效关系实施十项计数与唯一约束。
|
||||
- 验证/换绑在事务内锁定相关手机号关系;换绑先检查总数再整体迁移。
|
||||
- 批量导入复用逐行任务,读取按资产数据范围,日志仅保留脱敏手机号。
|
||||
|
||||
## H5 与后台动作契约
|
||||
|
||||
### H5 建联与换绑
|
||||
|
||||
- 既有登录签发 token 时,按全局强制绑定开关及当前访问资产查询有效关联;开关开启且当前手机号未关联该资产时,响应 `need_bind_phone=true`,并限制依赖该资产的业务入口直至验证成功。开关关闭时不创建新关联,也不删除历史关联。
|
||||
- 既有 `bind_phone` 短信场景验证成功后,事务中锁定手机号和资产有效关系;同一手机号—资产已有效关联则幂等成功,否则先计算该手机号有效资产数。达到十项返回“该手机号最多关联10项有效资产”,不写关联或手机号变更。
|
||||
- 既有 `change_phone_old`、`change_phone_new` 两个验证码均验证成功后,事务锁定旧、新手机号关系;计算新号码当前有效关系数加旧号码待迁移有效关系数,超过十项则整体失败。通过时把旧号码全部有效关系原子失效/迁移至新号码,并记录旧、新号码脱敏审计;任一步失败不变更任一关系。
|
||||
|
||||
### 后台查看与解除
|
||||
|
||||
- `GET /phone-asset-associations`:仅超级管理员、平台用户,先按资产数据范围过滤;支持资产标识、手机号(仅权限内完整值)、关联状态、创建时间筛选,返回资产、手机号、建立时间、建立来源固定为 `h5_sms_verification` 和状态。
|
||||
- `DELETE /phone-asset-associations/:id`:请求必须含 `reason`(1~500 字符)和二次确认;锁定指定有效关联后复核资产数据范围,标记失效并审计操作者、资产、脱敏手机号、原因和时间。后台没有创建/补录接口。
|
||||
- `POST /phone-asset-associations/batch-unbind`:请求去重的资产集合、原因、二次确认;每个资产独立解除其全部有效关系,返回成功数、失败数与逐资产结果。越权、资产不存在和已无有效关系对调用方使用统一失败文案。
|
||||
- Excel 解绑复用既有异步导入:每行按资产标识处理,独立授权与事务,任务持久化行号、结果和失败原因;一行失败不得回滚已成功行。
|
||||
|
||||
### 边界
|
||||
|
||||
- 换货、资产导入、后台资产编辑和个人客户主手机号历史记录均不得创建、推断、复制或迁移该关系。新换货资产在首次 H5 访问时才依当前开关走验证。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移,不回填;隔离库验证开关、上限、换绑、权限解绑、换货边界及 up/down/up。
|
||||
25
openspec/changes/add-phone-asset-associations/proposal.md
Normal file
25
openspec/changes/add-phone-asset-associations/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-009`。
|
||||
|
||||
## Why
|
||||
|
||||
现有全局绑定不能保留手机号与资产的验证关系,也无法安全支持按资产解除。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增 H5 验证建立的手机号—资产关联及十项上限。
|
||||
- 新增客户原子换绑和后台受权限控制的单项/批量/Excel 解绑。
|
||||
- 保持全局强制绑定,换货不迁移关系,不回填历史。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `phone-asset-association`: 已验证手机号与资产关联。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响 H5 认证、资产查询、短信、导入、审计和 Schema。
|
||||
@@ -0,0 +1,21 @@
|
||||
## Purpose
|
||||
|
||||
保存仅由 H5 短信验证建立的手机号—资产当前有效关系,使全局强制绑定能逐资产执行,同时提供受资产数据范围控制的后台查看与解除能力。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: H5 验证建立关联与数量上限
|
||||
系统 SHALL 保留既有全局 H5 强制绑定开关。开关开启时,客户首次登录一项未关联当前手机号的资产必须完成短信验证后建立关系;已关联资产不重复验证。开关关闭时登录不要求验证且不新增关系,已有关系保留。关联只能由 H5 验证建立,单手机号最多关联十项当前有效资产;上线不回填历史客户—资产关系。
|
||||
|
||||
#### Scenario: 第十一项资产验证
|
||||
- **WHEN** 已关联十项有效资产的手机号验证另一项资产
|
||||
- **THEN** 系统拒绝本次新关联,已有十项关系不变
|
||||
|
||||
### Requirement: 换绑、查看和解绑
|
||||
客户更换手机号时 MUST 同时验证旧、新号码,并原子将旧手机号全部有效资产关系迁至新号码;新号码现有关联数加迁移数超过十项时整次失败。后台仅超级管理员和平台用户可在资产数据范围内查看完整关联手机号、单项解绑、勾选批量解绑及 Excel 解绑;后台不得补录。
|
||||
|
||||
单项解绑必须指定一条资产—手机号关系。批量和 Excel 按资产解除该资产全部当前有效手机号关系,必须二次确认、填写原因、逐条审计;逐资产独立执行,返回成功数、失败数和统一越权失败文案。换货不得迁移该关系,新资产首次访问仍按全局开关验证。
|
||||
|
||||
#### Scenario: 换绑超过上限
|
||||
- **WHEN** 客户换绑后新手机号关联总数将超过十项
|
||||
- **THEN** 系统不迁移任何关系且旧、新手机号关系均保持原状
|
||||
12
openspec/changes/add-phone-asset-associations/tasks.md
Normal file
12
openspec/changes/add-phone-asset-associations/tasks.md
Normal file
@@ -0,0 +1,12 @@
|
||||
## 1. 关联与 H5
|
||||
- [ ] 1.1 追踪 H5 登录/短信验证、客户换绑、资产数据范围和换货调用链。
|
||||
- [ ] 1.2 新增关联表、有效关系唯一/计数索引的成对迁移、模型和审计常量。
|
||||
- [ ] 1.3 实现全局开关下的验证建联、十项限制和原子换绑。
|
||||
|
||||
## 2. 后台解绑
|
||||
- [ ] 2.1 实现资产详情/列表完整手机号投影、单项解绑、二次确认批量和逐行 Excel 解绑及统一越权结果。
|
||||
- [ ] 2.2 保持换货不迁移,补齐路由和 OpenAPI。
|
||||
|
||||
## 3. 验证
|
||||
- [ ] 3.1 隔离库验证上限、换绑回滚、权限、批量部分成功、换货边界和 up/down/up。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-phone-asset-associations --strict` 与 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
13
openspec/changes/add-priority-polling-queue/design.md
Normal file
13
openspec/changes/add-priority-polling-queue/design.md
Normal file
@@ -0,0 +1,13 @@
|
||||
## Context
|
||||
|
||||
优先轮询是普通轮询之上的高优先级调度,不是另一套轮询业务逻辑。自动套餐事件、无有效套餐、普通轮询异常补偿和受控人工触发均可入队;优先与普通轮询共享同一卡互斥和既有并发边界。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 队列表保存卡、活动状态、合并来源、触发类型、来源订单/套餐使用、尝试次数和结果;同一卡活动项唯一。
|
||||
- 在订单/套餐生效、无有效套餐识别、普通轮询异常后通过可靠事件入队;具备既有手动轮询权限的后台账号可在数据范围内通过 `POST /priority-polling-queue` 提交 `asset_identifier` 与必填 `reason`。消费者按来源事实幂等合并,不能由支付回调直接重复写队列。
|
||||
- Worker 用行锁领取优先项,复用普通轮询既有并发上限、外部调用保护、套餐/流量/状态同步和失败重试;普通轮询查询活动项排除对应卡。完成或最终失败出队并写触发来源、操作来源、次数、时间和结果审计。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移、活动项唯一索引和来源索引;隔离库验证三种自动触发、回调重放、同卡合并、普通互斥、失败重试和 up/down/up。
|
||||
24
openspec/changes/add-priority-polling-queue/proposal.md
Normal file
24
openspec/changes/add-priority-polling-queue/proposal.md
Normal file
@@ -0,0 +1,24 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-016`。
|
||||
|
||||
## Why
|
||||
|
||||
紧急卡状态查询与普通轮询竞争,无法保证处理顺序或追踪加急事实。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增受限管理的卡轮询优先队列。
|
||||
- Worker 优先领取队列项,并以唯一活动项和锁避免同卡并发。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `priority-polling-queue`: 卡轮询加急队列。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
影响后台卡管理、异步轮询、运营商调用、审计和 Schema。
|
||||
@@ -0,0 +1,8 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 优先轮询队列
|
||||
系统 SHALL 在不改变普通轮询内容、并发上限和失败重试的前提下,为指定业务场景创建高优先级轮询任务。同一资产未完成优先任务必须合并触发来源、次数和最近时间,而不得重复执行同一轮轮询。
|
||||
|
||||
#### Scenario: 规则命中
|
||||
- **WHEN** 业务请求或任务满足本需求定义的前置条件
|
||||
- **THEN** 系统按上述规则完成处理、保留可追溯事实,并拒绝与状态、权限或幂等约束冲突的重复操作
|
||||
@@ -0,0 +1,21 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 套餐生命周期自动优先入队
|
||||
系统 SHALL 在下列业务事实提交成功后,将关联资产对应卡自动加入优先轮询队列:无有效套餐、新购套餐首次成为有效套餐、已过期套餐完成续购、流量用尽后购买的加油包完成生效,及普通轮询出现既有异常补偿条件。具备既有轮询手动触发权限的后台账号也可为其数据范围内资产手动入队并必须填写原因。入队必须等待订单、支付、套餐使用记录提交成功;失败或取消订单不得入队。队列项记录触发类型(新购、过期续购、加油包)、来源订单/套餐使用记录、触发时间及执行结果。
|
||||
|
||||
同一卡只能有一条活动优先项。多个触发同时到达时,系统合并来源事实、保留最近触发时间,且最多执行一次未完成优先轮询;不得因重复支付回调、任务重放或相同订单重试重复入队。手动入队不允许修改调度优先级或绕过既有并发上限;它与自动/异常触发合并到同一卡活动项。
|
||||
|
||||
#### Scenario: 人工与异常补偿触发合并
|
||||
- **WHEN** 同一卡已有自动入队活动项,管理员手动触发或普通轮询异常补偿再次触发
|
||||
- **THEN** 系统仅追加触发来源、次数和最近时间,不创建第二项或重复调用轮询
|
||||
|
||||
#### Scenario: 重复支付成功回调
|
||||
- **WHEN** 已生效套餐订单的支付成功回调重复投递
|
||||
- **THEN** 系统仅创建或更新一条关联该卡的活动优先队列项
|
||||
|
||||
### Requirement: 优先领取、执行与出队审计
|
||||
Worker SHALL 先领取活动优先项,再执行既有套餐、流量和卡状态轮询;普通轮询不得与已领取优先项并发。领取时使用行锁跳过已锁项,同一卡由队列状态互斥。轮询成功后标记完成并出队;可恢复失败按既有策略重试,超过策略标记失败并保留安全失败原因。入队、领取、每次失败、重试、完成和失败出队均记录触发来源、操作来源、时间和结果;仅具有既有轮询任务权限的后台账号可查询记录。
|
||||
|
||||
#### Scenario: 普通轮询遇到优先项
|
||||
- **WHEN** 普通轮询准备处理一张存在活动或已领取优先项的卡
|
||||
- **THEN** 普通轮询跳过该卡,直到优先项完成或失败出队
|
||||
9
openspec/changes/add-priority-polling-queue/tasks.md
Normal file
9
openspec/changes/add-priority-polling-queue/tasks.md
Normal file
@@ -0,0 +1,9 @@
|
||||
## 1. 自动入队与执行
|
||||
- [ ] 1.1 追踪新购生效、过期续购生效、流量用尽加油包生效、可靠事件、普通轮询和运营商调用链路。
|
||||
- [ ] 1.2 新增队列、来源事实和执行审计的成对迁移、活动项唯一索引、模型与查询权限。
|
||||
- [ ] 1.3 在三种套餐生命周期成功事件后发布可靠入队事件;实现同卡合并、回调/任务重放幂等和禁止人工入队。
|
||||
- [ ] 1.4 实现锁定领取、普通轮询排除、套餐/流量/状态优先轮询、失败恢复和出队审计。
|
||||
|
||||
## 2. 验证
|
||||
- [ ] 2.1 验证三种触发、失败订单不入队、重复回调、并发合并、普通互斥、失败重试和权限。
|
||||
- [ ] 2.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-priority-polling-queue --strict` 和 `openspec doctor --json`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
@@ -0,0 +1,38 @@
|
||||
## Context
|
||||
|
||||
退款服务已有申请、企业微信审批和钱包审计路径,但现有状态/人工入口不足以表达渠道退款未知结果。支付商户池 Change 完成后,新支付单可读取冻结实际商户;历史单继续走旧支付配置。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 退款单新增方式、冻结实收金额、套餐使用/材料快照、渠道退款状态/流水/安全失败原因和审批实例历史;金额均为分。
|
||||
- 创建、审批消费和渠道执行使用同一退款单锁及条件状态更新。一笔订单的活动退款唯一约束防止并发重复;渠道请求使用稳定请求标识,结果未知进入恢复任务而非重发。
|
||||
- 企业微信通过事务内完成套餐失效与钱包退款准备;原路渠道调用使用可靠事件,成功回写退款终态。客户收款信息退款以企微通过终态完成。
|
||||
- 移除/拒绝本地人工终审语义,保留历史兼容读取;未成功重提始终冻结新实例快照。
|
||||
|
||||
## 行为契约
|
||||
|
||||
### 退款申请与重提
|
||||
|
||||
- `POST /refunds`:调用者必须在订单数据范围内。服务锁定订单和既有活动退款,读取订单或原成功支付记录的权威实收金额;缺失、非正或已存在审批中/原路处理中/原路失败申请时拒绝。请求包含退款原因、退款方式、退款金额、客户收款信息及附件(仅客户收款信息方式);金额为正分且不超过冻结实收金额。
|
||||
- 服务按实际支付方式生成可选方式:微信/支付宝线上支付为原路或客户收款信息,资产钱包/代理主钱包仅原钱包,后台线下/员工代收仅客户收款信息;不匹配的方式返回“该订单不支持此退款方式”。客户收款信息与至少一个凭证附件必须同时存在,且不读取员工收款方式字典。
|
||||
- `PUT /refunds/:id` 或既有重提入口只允许驳回、关闭或渠道明确失败的未成功申请;重新锁定订单和申请,保存新的原因、方式、金额、收款信息、附件和套餐使用快照,创建新的企业微信审批实例。提交失败或审批未知不是可重提状态;已成功、审批中、原路处理中返回状态冲突。
|
||||
|
||||
### 企业微信终审与权益处理
|
||||
|
||||
- 回调和既有审批恢复任务以审批实例 ID 进入同一幂等用例;移除新业务的本地人工通过、拒绝和退回终审路径,历史接口仅保留兼容读取或明确拒绝。
|
||||
- 首次最终通过时锁定退款和订单,校验批准金额不超过冻结实收金额;在事务中标记审批通过、使关联套餐失效、接续下一套餐、评估停机并建立退款执行事实。重复/乱序回调不得再次失效套餐或启动第二次渠道退款。
|
||||
- 客户收款信息方式在企业微信通过时标记退款成功;原钱包方式沿用原钱包退款事务;原路方式只写待执行可靠事件,不能在审批事务中假定渠道已成功。
|
||||
|
||||
### 原路执行与恢复
|
||||
|
||||
- 原路执行消费者在调用前锁定退款,验证原支付单、实际收款商户、渠道流水、可退金额和当前商户退款凭证。新支付按冻结 `merchant_id` 加载商户当前凭证,历史支付按 `payment_config_id`;商户停用不阻断历史校验。
|
||||
- 以退款 ID/稳定渠道请求号至多提交一次可确认请求。渠道明确成功时保存渠道退款流水并转退款成功;超时、未知、凭证失效、余额不足、拒绝均写安全原因并保持处理中或失败恢复状态,不得标记成功或盲目再次调用。
|
||||
- 原路失败后若改为客户收款信息退款,必须修改申请材料并走新企业微信审批;审批通过后撤销只记录审批异常,不恢复套餐权益、不取消已提交渠道退款且不自动重提。
|
||||
|
||||
### 读取与审计
|
||||
|
||||
- 退款列表、详情和导出返回冻结实收金额、方式、申请/渠道状态、失败安全摘要、审批实例和渠道流水,并按既有订单数据范围过滤。审计记录申请、重提、审批终态、权益处理、渠道调用和恢复,但不得记录凭证内容、完整收款文本或商户密钥。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
新增成对迁移和活动退款约束,先部署兼容读写与恢复消费者,再关闭人工审批入口;隔离库验证方式矩阵、重复回调、渠道未知、失败重提、权益时点和 up/down/up。
|
||||
@@ -0,0 +1,28 @@
|
||||
## Scope
|
||||
|
||||
- 迭代编号:`AUG26-006`。
|
||||
|
||||
## Why
|
||||
|
||||
现有退款以本地审批状态处理,不能按来源实际支付事实决定退款方式、冻结实收金额,或在企业微信通过后用原实际收款商户可靠执行原路退款。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 退款申请冻结来源订单、权威实收金额、唯一套餐使用情况、退款金额/原因、方式和当次审批材料;实收金额不可由提交人修改。
|
||||
- 建立线上支付、资产钱包、代理主钱包和后台线下订单的退款方式矩阵,并在创建、提交和执行前重复校验。
|
||||
- 企业微信是唯一审批终审:通过即按既有规则失效套餐;客户收款信息退款即完成,原路退款须待渠道明确成功才完成。
|
||||
- 原路退款固定使用原支付单实际商户和渠道流水;超时/未知/失败保留可恢复状态,不重复退款;未成功申请可按规则修改并新建审批实例重提。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- 无。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- `order-refund-exchange`: 退款申请、状态机、方式矩阵和套餐联动。
|
||||
|
||||
## Impact
|
||||
|
||||
影响退款模型/接口、企业微信审批、支付商户与渠道退款、资产/代理钱包、员工账单和佣金回溯;需新增成对迁移并淘汰本地人工终审入口。
|
||||
@@ -0,0 +1,32 @@
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 退款实收金额与方式矩阵
|
||||
系统 SHALL 从来源订单或原成功支付记录带出并冻结权威实收金额,提交人不得填写或修改;无法确定时拒绝申请。退款金额和企业微信授权金额不得超过该冻结额,且一笔订单最多一张最终成功退款申请,成功后不得再申请。
|
||||
|
||||
线上微信/支付宝套餐订单仅可选原路退款或客户收款信息退款;资产钱包支付仅自动退回原资产钱包;代理预存款/主钱包支付仅自动退回原代理钱包;后台线下/员工代收套餐订单仅可选客户收款信息退款。代理充值预存款业务单不在本期退款范围。客户收款信息退款必须包含客户收款信息自由文本和客户凭证附件,且不得复用公司收款方式字典。
|
||||
|
||||
#### Scenario: 无权威实收金额
|
||||
- **WHEN** 来源订单无法取得权威实收金额
|
||||
- **THEN** 系统拒绝创建退款申请,不允许提交人以自填金额替代
|
||||
|
||||
#### Scenario: 钱包订单申请退款
|
||||
- **WHEN** 已支付套餐订单的实际支付方式为资产钱包或代理主钱包
|
||||
- **THEN** 系统只提供退回对应原钱包方式,不展示原路或客户收款信息退款
|
||||
|
||||
### Requirement: 企业微信审批和未成功重提
|
||||
退款申请 SHALL 保存退款原因、冻结实收金额、唯一关联套餐及其使用情况、方式、金额和当次材料快照。超级管理员、平台用户和代理可在各自订单数据范围内创建、修改并重提未成功申请;企业微信是唯一终审,本地不得人工通过、拒绝或退回。
|
||||
|
||||
同一订单同时至多存在一张审批中、原路处理中或原路失败申请。企业微信驳回、申请关闭或渠道明确失败后可修改未成功申请的金额、原因、方式、收款信息和附件并重提;每次必须新建审批实例及快照。提交失败或审批结果未知保持在途,使用既有查询/恢复闭环,不得另建或重提。企业微信通过后撤销不回滚套餐失效或已启动退款,标记审批异常并禁止自动重提。
|
||||
|
||||
#### Scenario: 审批通过前结果未知
|
||||
- **WHEN** 企业微信提交成功性或最终结果暂时未知
|
||||
- **THEN** 申请保持在途且订单不得创建第二张活动申请,系统通过既有恢复机制确认结果
|
||||
|
||||
### Requirement: 原路退款执行与权益时点
|
||||
对线上订单,系统在创建、提交及企业微信通过后的执行前均 SHALL 校验原支付单、实际收款商户、渠道流水、可退金额及商户退款能力/凭证;商户停用不得阻断历史单校验。条件不满足时禁用原路并说明原因,只允许客户收款信息退款。
|
||||
|
||||
企业微信最终通过即按既有规则使关联套餐失效、接续下一套餐并评估停机;客户收款信息退款同时标记退款成功,不等待线下付款。原路退款必须以冻结金额调用原实际收款商户;仅渠道明确成功后标记成功并保存渠道退款流水。超时、未知、凭证失效、余额不足或渠道拒绝时保留处理中/失败与安全原因,不标记成功或重复调用;需改客户收款信息退款时必须修改后重新审批。
|
||||
|
||||
#### Scenario: 原路渠道调用未知
|
||||
- **WHEN** 企业微信已通过的原路退款调用超时且无法确认渠道结果
|
||||
- **THEN** 退款保持原路处理中或失败恢复状态,套餐权益不恢复,系统不得再次盲目提交退款
|
||||
@@ -0,0 +1,15 @@
|
||||
## 1. 退款契约与数据
|
||||
|
||||
- [ ] 1.1 追踪退款、订单/支付、钱包、套餐、企微审批和商户退款调用链;新增成对迁移、模型、状态/方式常量、实收/材料/渠道结果快照及活动申请唯一约束。
|
||||
- [ ] 1.2 实现退款可选方式和权威实收金额投影,创建/提交时冻结金额、套餐使用情况、原因和材料;更新 DTO/OpenAPI。
|
||||
|
||||
## 2. 审批、资金与恢复
|
||||
|
||||
- [ ] 2.1 将退款终审切换为企业微信回调/查询恢复,移除新业务的本地人工通过/拒绝/退回;实现未成功申请的新实例重提。
|
||||
- [ ] 2.2 在企微通过事务内执行套餐失效/接续及原钱包退款;客户收款信息退款直接完成。
|
||||
- [ ] 2.3 实现原实际商户、渠道流水和退款能力三次校验、可靠原路退款调用、幂等回写与未知/失败恢复;联动员工账单冲销和佣金回溯入口。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- [ ] 3.1 在隔离库验证每种来源方式、实收上限、活动申请互斥、审批重放/未知、渠道失败重提、商户停用历史退款和套餐权益时点。
|
||||
- [ ] 3.2 运行 `gofmt -w`、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-refund-methods-and-original-route-refunds --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-08-31
|
||||
42
openspec/changes/add-shop-salesperson-groups/design.md
Normal file
42
openspec/changes/add-shop-salesperson-groups/design.md
Normal file
@@ -0,0 +1,42 @@
|
||||
## Context
|
||||
|
||||
现有店铺已存在负责人候选和数据范围能力;用户组是新的业务分类,不能复用 RBAC 角色或代理店铺层级。推导关系必须保持实时,避免负责人改组后大量回写店铺造成不一致。
|
||||
|
||||
## Decisions
|
||||
|
||||
- 新增业务用户组表和平台用户—组关联(用户唯一)表;组编码唯一且不可改,停用不删除既有成员关联。
|
||||
- 店铺不保存组 ID。列表/详情以店铺负责人关联平台用户,再左连接用户组得到组及停用状态;按组筛选同样使用该关系。
|
||||
- 勾选批量交接采用一次事务:先按操作者数据范围锁定/校验全量店铺及目标用户,再统一更新和写审计。任何校验失败不写入。
|
||||
- Excel 使用既有异步导入模式逐行事务;每行在数据范围内查询,统一拒绝文案不区分无权与不存在,并持久化任务明细。
|
||||
- 用户组成员批量设置直接替换关联;不引入组管理员、层级、额外权限或数据范围计算。
|
||||
|
||||
## 管理动作契约
|
||||
|
||||
### 用户组及成员
|
||||
|
||||
- `POST /business-user-groups`:超级管理员、平台用户提交 `code`(1~64 字符,未删除组内唯一)、`name`(1~100 字符)、`sort`(非负整数)、`enabled`、`remark`(最多 500 字符)。成功返回组 ID 与字段;重复编码返回“业务用户组编码已存在”。
|
||||
- `PUT /business-user-groups/:id`:允许更新名称、排序、启停、备注;`code` 永不允许修改。不存在/已删除返回既有资源不存在。
|
||||
- `DELETE /business-user-groups/:id`:请求须带二次确认;存在成员时返回“用户组仍有成员,只能停用或先移走成员”,不物理删除。
|
||||
- `PUT /business-user-groups/:id/members`:请求 `account_ids` 非空数组;所有账号必须是启用平台用户且目标组启用。事务内替换每个账号旧组关系,任一账号无效则全量回滚。`DELETE /business-user-groups/members` 使用同一校验清空指定账号归属。成功操作写成员前后审计。
|
||||
|
||||
### 店铺负责人批量交接
|
||||
|
||||
- `PUT /shops/business-owner/batch`:请求 `shop_ids`(非空、去重)及 `business_owner_account_id`(有效平台业务员)或显式 `null`(清空)。先按操作者数据范围锁定并校验所有店铺,再统一更新 `tb_shop.business_owner_account_id` 并逐店写审计;任何目标无权、不存在、已删除或负责人无效时,返回统一失败且整批无写入。
|
||||
- Excel 导入使用既有异步导入任务;每行提供店铺标识及负责人账号标识或清空标识。每行独立授权、存在性、负责人有效性校验和事务更新;结果保存行号、成功/失败、失败原因、变更前后负责人及汇总。无权和不存在对调用方使用同一错误文案。
|
||||
|
||||
### 读侧投影
|
||||
|
||||
- 扩展既有店铺列表、详情、筛选与导出:返回 `business_owner_account_id`、负责人名称、`business_user_group_id`、组编码、组名称、组启用状态;组字段从当前负责人—成员关系实时左连接。
|
||||
- 组筛选只匹配当前负责人所属组;负责人为空或无成员关系时归入“未分组”。历史店铺不回填;负责人改组/停用后下一次读立即反映变化。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- 实时 join 增加列表复杂度 → 为负责人和成员关联建立查询索引,不以冗余字段换一致性风险。
|
||||
- 负责人/分组并发更新 → 店铺交接和用户改组均使用事务与受影响行检查;读取接受当前已提交快照。
|
||||
- 导入部分成功 → 明确为逐行语义,任务明细是唯一结果来源。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 新增成对迁移创建用户组、成员关联和导入/查询索引,不回填历史组归属。
|
||||
2. 部署读侧空组兼容,再启用维护、批量和导入入口。
|
||||
3. 隔离库验证成员唯一、停用保留、实时推导、批量原子失败、导入逐行结果及迁移 up/down/up。
|
||||
25
openspec/changes/add-shop-salesperson-groups/proposal.md
Normal file
25
openspec/changes/add-shop-salesperson-groups/proposal.md
Normal file
@@ -0,0 +1,25 @@
|
||||
## Why
|
||||
|
||||
店铺负责人只能逐店维护,平台用户也没有稳定的业务分类;店铺按组统计、筛选和批量交接缺少统一、可追溯口径。
|
||||
|
||||
本 Change 落实 AUG26-003:业务用户组只描述平台用户的业务分类,不改变角色权限、数据范围或代理店铺分组;店铺所属组始终由当前负责人实时推导。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增业务用户组(名称、不可变唯一编码、排序、启停、备注)及平台用户单组归属。
|
||||
- 新增店铺负责人批量设置/清空和 Excel 导入;勾选操作全量校验且原子,导入逐行独立执行并返回明细。
|
||||
- 店铺列表、详情和筛选显示实时推导的负责人业务用户组;停用组保留成员和展示,不影响登录、权限、数据范围或负责人。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
|
||||
- `business-user-group`: 用户组生命周期、成员归属、店铺负责人交接及推导查询。
|
||||
|
||||
### Modified Capabilities
|
||||
|
||||
- 无。现有身份权限和数据范围拒绝行为保持不变。
|
||||
|
||||
## Impact
|
||||
|
||||
影响平台用户、店铺列表/详情、导入任务、数据范围校验、审计、DTO/OpenAPI 和新增 Schema。
|
||||
@@ -0,0 +1,38 @@
|
||||
## Purpose
|
||||
|
||||
以不改变既有角色和数据范围的方式标记平台用户业务分类,并将店铺负责人和业务用户组的批量维护、推导展示与审计定义为一致的可观察行为。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 业务用户组生命周期与成员归属
|
||||
系统 SHALL 允许超级管理员和平台用户维护业务用户组的名称、创建时必填且在未删除组内唯一的稳定编码、排序、启用状态和备注;编码创建后 MUST NOT 修改。用户组不得设置上级、层级或组管理员。仅无成员用户组可由超级管理员或平台用户二次确认删除;有成员时只能停用或先移走成员。
|
||||
|
||||
每个启用平台用户最多属于一个启用业务用户组。超级管理员和平台用户可选择多个平台用户,批量设置至一个启用组或批量清空归属;设置直接替换原归属,停用组不得作为目标。业务用户组 MUST NOT 改变后台角色、登录、权限、数据范围或店铺具体负责人归属。停用后不得新增成员,已有成员关系保留并显示已停用,管理员仍可将成员改组或清空。
|
||||
|
||||
#### Scenario: 批量替换平台用户分组
|
||||
- **WHEN** 管理员选择多个启用平台用户并指定一个启用业务用户组
|
||||
- **THEN** 系统将每个目标用户的原分组直接替换为目标组,不改变其角色、数据范围和登录状态
|
||||
|
||||
#### Scenario: 停用含成员用户组
|
||||
- **WHEN** 管理员停用仍含平台用户成员的业务用户组
|
||||
- **THEN** 系统保留成员关系并标记组已停用,拒绝新增成员但允许后续改组或清空成员
|
||||
|
||||
### Requirement: 店铺负责人和所属组实时推导
|
||||
店铺 SHALL 以当前绑定的平台业务员作为负责人。店铺所属业务用户组 MUST 实时由该负责人的当前用户组推导,不得把组 ID 冗余写入店铺;负责人变更、负责人改组或组停用后,店铺列表、详情和筛选的结果立即按新关系变化。店铺无负责人、负责人无分组时所属组为空;负责人所属组停用时仍返回该组并明确其已停用。
|
||||
|
||||
#### Scenario: 负责人改组改变店铺展示
|
||||
- **WHEN** 某平台业务员的业务用户组被替换或清空
|
||||
- **THEN** 该业务员当前负责的所有店铺在列表、详情和按组筛选中即时呈现新的组或空组,无需更新店铺记录
|
||||
|
||||
### Requirement: 店铺负责人批量交接与导入
|
||||
超级管理员和平台用户 SHALL 仅在其店铺数据权限内勾选多家店铺,批量设置为一个有效平台业务员或批量清空负责人。勾选批量操作 MUST 在提交前校验全部目标店铺均存在、未删除且可管理,并校验目标业务员有效;任一项失败时整批不修改并返回统一失败结果。成功时必须为每家店铺记录负责人前后值、操作者、时间和入口审计。
|
||||
|
||||
Excel 导入 MUST 按每行店铺标识独立校验和执行:有效行成功更新,无权限、店铺不存在/已删除或目标业务员无效行失败;任务返回成功数、失败数和逐行失败原因。导入不得因一行失败回滚其他已成功行,并必须记录每行实际变更审计。
|
||||
|
||||
#### Scenario: 勾选批量包含越权店铺
|
||||
- **WHEN** 管理员提交的店铺集合中任一店铺不在其数据范围、已删除或不存在
|
||||
- **THEN** 系统不修改集合中任何店铺负责人,并返回统一失败结果且不泄露越权店铺存在性
|
||||
|
||||
#### Scenario: 导入包含有效和无效行
|
||||
- **WHEN** 店铺负责人 Excel 导入同时包含可管理店铺和无权或无效店铺
|
||||
- **THEN** 系统更新每个有效行、保留失败行原值,并返回逐行结果及成功/失败汇总
|
||||
17
openspec/changes/add-shop-salesperson-groups/tasks.md
Normal file
17
openspec/changes/add-shop-salesperson-groups/tasks.md
Normal file
@@ -0,0 +1,17 @@
|
||||
## 1. 用户组与查询
|
||||
|
||||
- [ ] 1.1 追踪平台用户、店铺负责人、数据范围、现有批量导入和审计调用链,确认负责人字段及“有效平台业务员”的既有判定。
|
||||
- [ ] 1.2 新增成对迁移、模型和常量:业务用户组、平台用户唯一组关联、编码唯一/启停约束及负责人—成员推导查询索引。
|
||||
- [ ] 1.3 实现用户组 CRUD、启停、无成员二次确认删除、平台用户批量设置/清空及审计;禁止修改编码、向停用组新增成员和改变既有 RBAC。
|
||||
- [ ] 1.4 扩展店铺列表、详情与筛选 Query/DTO/OpenAPI,实时返回负责人组、编码和停用标识,不将组写入店铺。
|
||||
|
||||
## 2. 店铺负责人批量维护
|
||||
|
||||
- [ ] 2.1 实现勾选店铺批量设置/清空:先以操作者数据范围校验所有店铺和目标业务员,在单事务中更新全部店铺并记录逐店审计;任一项失败整批不改。
|
||||
- [ ] 2.2 接入既有异步 Excel 导入任务,逐行校验店铺标识、数据范围和目标业务员,保存成功/失败明细、汇总和实际审计;越权使用统一失败文案。
|
||||
- [ ] 2.3 注册后台路由和权限,更新 `cmd/api/docs.go` 与 `cmd/gendocs/main.go`;Handler 使用全局错误处理和 `pkg/response`。
|
||||
|
||||
## 3. 验证
|
||||
|
||||
- [ ] 3.1 在隔离数据库验证迁移 up/down/up、编码/成员唯一、组停用、负责人改组实时展示、批量原子失败、导入混合结果和数据范围拒绝。
|
||||
- [ ] 3.2 运行 `gofmt -w`(变更 Go 文件)、`go build ./cmd/api ./cmd/worker`、`go run cmd/gendocs/main.go`、`openspec validate add-shop-salesperson-groups --strict`、`openspec doctor --json` 和 `./scripts/context-health.sh`;自动化测试按项目决策为 N/A。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-01
|
||||
@@ -0,0 +1,57 @@
|
||||
## Context
|
||||
|
||||
见 proposal.md。现有 `scripts/migration/` 已使用 Python 加载 YAML/CSV 并生成由维护者以 `psql -1` 执行的 SQL;店铺导入也采用该模式。CSV 固定为 988 条合法且唯一记录,其中 8 条引用同 CSV 内的上级代理。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 复用现有迁移目录的 Python、YAML、CSV 和 SQL 审核模式。
|
||||
- 在 SQL 写入前发现本地输入错误,并在 SQL 事务起点阻断目标库冲突。
|
||||
- 生成稳定编号、父级优先的店铺、初始账号、角色、双钱包和成功审计 SQL。
|
||||
- 让生成器和执行过程都不直接写入目标库。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不新增 HTTP API、Go CLI、Application 用例、数据表、迁移或运行配置字段。
|
||||
- 不修改已有店铺、账号、角色或钱包;目标冲突一律阻断本批。
|
||||
- 不导入套餐、资产、余额、佣金历史或 CSV 备注以外的业务事实。
|
||||
- 不自动执行 SQL、发布生产或代替维护者审核。
|
||||
|
||||
## Decisions
|
||||
|
||||
### Python 生成 SQL,维护者手工单事务执行
|
||||
新增 `scripts/migration/import_shops.py` 及最小 `lib/shop_*` 模块,沿用 `migrate_assets.py` 的参数、配置目录和输出目录约定。生成器只读取本地 CSV 与 YAML,输出 `shop_bulk_import_<batch>.sql`、结果 CSV、错误 CSV 和摘要;不连接目标 PostgreSQL。
|
||||
|
||||
生成器不新增数据库配置。目标库依赖角色、操作者、业务员和冲突的校验被写入 SQL 的开头,在任何业务 INSERT 之前执行。维护者仅能在审核后以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行,事务中的任意 `RAISE EXCEPTION` 或 INSERT 失败均回滚整批。
|
||||
|
||||
备选的 Go CLI 会偏离现有迁移执行模式;Python 直连 PostgreSQL 会扩大凭据与写入边界,均不采用。
|
||||
|
||||
### 本地输入预检与稳定排序
|
||||
CSV 读取器要求“奇成代理名称、店铺名(新卡管)、用户名、联系方式、业务员、是否有上级代理、上级代理名称”列,清理空白后校验必填项、11 位 ASCII 手机号、店铺名/用户名/手机号同批唯一性以及上级代理唯一解析。店铺编号为 `<prefix>-<四位序号>`,序号按原始 CSV 数据行从 1 开始。
|
||||
|
||||
结果 SQL 的创建顺序为无上级记录按 CSV 顺序在前、子记录在其父记录之后;父级可以出现在 CSV 的后续行。循环引用或无法唯一解析的父级属于本地错误,阻止 SQL 生成。
|
||||
|
||||
### SQL 直接构造完整初始事实与审计
|
||||
SQL Builder 使用字符串安全转义、`DO` 守卫、`INSERT ... SELECT` 和 `RETURNING`/CTE 关联新 ID。它创建 `tb_shop`、`tb_account`、`tb_account_role`、`tb_shop_role`、两条 `tb_agent_wallet`,并按当前 `tb_audit_event` 与 `tb_audit_event_resource` 表结构、动作注册事实生成成功审计和业务员归属审计。
|
||||
|
||||
默认角色必须是启用客户角色;每个映射业务员必须是启用平台账号;迁移操作者必须是启用超级管理员。店铺编号、用户名和手机号的有效记录冲突由 SQL 守卫按源 CSV 行号报错。SQL 必须在 `BEGIN` 后首先执行所有守卫,再进行任何业务写入。
|
||||
|
||||
### 初始密码哈希写入 SQL
|
||||
Python 使用新增的 `bcrypt` 依赖,以 `adm@` 加每条手机号后四位在内存中构造初始密码并生成 bcrypt 哈希。哈希直接作为 `tb_account.password` 的 SQL 值写入;这是经确认允许保存的受控 SQL 产物。明文密码不得出现在控制台、结果 CSV、错误 CSV、摘要或日志。
|
||||
|
||||
### 审核产物和 Git 忽略
|
||||
结果 CSV 记录源行号、店铺编号、店铺名称、层级、上级解析和业务员映射;错误 CSV 记录源行号和原因;摘要记录输入数、计划数、父子关系数、错误数及待执行 SQL 路径。实际 YAML、生成 SQL 和所有审核产物均受 `scripts/migration/.gitignore` 保护。
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [SQL 绕过 Go Application 语义] → Builder 以现有 `CreateService`、模型、迁移和审计 Writer 的当前事实为基准,隔离库核对所有初始事实与审计数量。
|
||||
- [bcrypt 哈希位于 SQL 文件] → SQL 仅保存在 Git 忽略的受控目录;不得复制到日志、文档或结果产物。
|
||||
- [预生成到执行之间目标库变化] → SQL 在同一事务的第一阶段重新校验所有可变目标事实。
|
||||
- [长事务处理 988 家店铺] → 事务不包含网络 I/O;如锁等待不可接受,停止执行并调整批次设计,不降级为部分提交。
|
||||
- [CSV 后续插行改变后续编号] → 审核时将输入 CSV 与同批审核产物一起留存;重跑同一文件保持编号稳定。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 维护者将审核通过的 CSV 和实际 `shop_bulk_import.yaml` 放到受控本地目录,运行 Python 生成器并审核 SQL、结果、错误和摘要。
|
||||
2. 在隔离环境以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行 SQL,核对店铺、账号、角色、钱包和审计数量,以及冲突和中途失败时的整批回滚。
|
||||
3. 维护者按相同流程在生产环境手工执行;Agent 不连接生产主机或执行写入。
|
||||
4. SQL 执行失败由单一事务自动回滚;成功后的业务回退不由工具自动执行,需由维护者另行制定删除或禁用方案。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
现有店铺创建入口一次只能创建一家店铺,而奇成代理商清单已整理为 988 家合法且唯一的店铺数据。手工创建会遗漏账号、角色、钱包、上下级关系或审计事实,且无法安全重跑。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 新增受控的 Python 店铺导入 SQL 生成器:读取本地 CSV 和本地导入配置,先执行全量本地预检,再生成单个可审核的 PostgreSQL 事务 SQL。
|
||||
- 生成的 SQL 在写入前校验目标库角色、业务员、店铺编号、用户名和手机号冲突;通过后创建店铺主账号、账号角色、店铺默认角色、主钱包、佣金钱包和成功审计事实。
|
||||
- 支持父店铺优先、业务员账号映射、稳定店铺编码和 `adm@` 加手机号后四位的初始密码规则;Python 生成 bcrypt 哈希并写入受 Git 忽略保护的 SQL 文件。
|
||||
- 输出不含明文密码的逐行结果清单、错误清单和摘要;维护者审核后以 `psql -1` 手工执行生成的 SQL,任一校验或写入失败均回滚整批。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `shop-bulk-import`: 受控生成可审核、可回滚的店铺批量导入 SQL。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- 在 `scripts/migration/` 新增 Python 入口和最小公共模块,复用现有 YAML、CSV、SQL 生成器的目录与执行模式。
|
||||
- 新增 Python `bcrypt` 依赖、本地导入配置示例和受 Git 忽略的 SQL/审核产物。
|
||||
- 不新增 HTTP API、Go CLI、数据库 Schema 或运行配置字段;最终 SQL 由维护者在隔离环境和生产环境手工执行。
|
||||
@@ -0,0 +1,52 @@
|
||||
## Purpose
|
||||
|
||||
定义受控离线迁移工具从已核对的代理商 CSV 生成店铺初始事实 SQL,使维护者可以在执行前审核输入解析、目标库守卫和逐行计划,并以单事务手工执行。
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 导入输入必须在生成 SQL 前完成全量预检
|
||||
系统 SHALL 在生成 SQL 前校验 CSV 必备列、店铺名称/初始用户名/初始手机号同批唯一性、11 位手机号格式、上下级代理引用、业务员映射、默认角色、迁移操作者和稳定生成的店铺编号。上级代理必须按 CSV 的“奇成代理名称”唯一解析。
|
||||
|
||||
#### Scenario: 输入或映射错误被发现
|
||||
- **WHEN** 工具发现任一 CSV 行、配置项或同批关系错误
|
||||
- **THEN** 工具输出带 CSV 行号和原因的错误清单,不生成可执行导入 SQL
|
||||
|
||||
#### Scenario: 上级代理按 CSV 标识解析
|
||||
- **WHEN** 子店铺的上级代理名称在同一 CSV 中唯一对应一个代理记录
|
||||
- **THEN** 工具将该代理记录排在子店铺之前,并在结果清单中记录上级解析结果
|
||||
|
||||
### Requirement: 生成的 SQL 必须在写入前校验目标库状态
|
||||
系统 SHALL 在单事务 SQL 的任何店铺、账号、角色、钱包或审计写入前,校验配置指定的默认角色为启用客户角色、业务员映射为启用平台账号、迁移操作者为可用超级管理员,且目标库不存在同店铺编号、用户名或手机号的有效记录。
|
||||
|
||||
#### Scenario: 目标库冲突被发现
|
||||
- **WHEN** 维护者执行的 SQL 发现角色、账号映射或任一目标记录冲突
|
||||
- **THEN** SQL 以包含对应 CSV 行号和原因的错误终止,且不写入本批店铺、账号、角色、钱包或成功审计事实
|
||||
|
||||
### Requirement: 批量导入 SQL 必须创建完整的店铺初始事实
|
||||
系统 SHALL 为每个通过预检和目标库守卫的 CSV 记录创建启用的店铺、一个启用的店铺主账号、该账号和店铺的默认客户角色关联、主钱包和佣金钱包,并按配置写入平台业务员归属和成功审计。店铺默认角色必须是配置指定的启用客户角色。
|
||||
|
||||
#### Scenario: 成功导入店铺
|
||||
- **WHEN** 维护者以单事务执行通过审核的导入 SQL
|
||||
- **THEN** 每家店铺具有稳定生成的店铺编号、正确的上级层级和业务员归属,并拥有一个主账号、默认角色关联、主钱包、佣金钱包及成功创建审计
|
||||
|
||||
#### Scenario: SQL 中保存初始账号密码哈希
|
||||
- **WHEN** 工具根据配置的初始密码规则生成店铺主账号 SQL
|
||||
- **THEN** SQL 将该账号的 bcrypt 密码哈希写入数据库,结果清单、错误清单、摘要和控制台输出不包含初始密码明文
|
||||
|
||||
### Requirement: 导入执行必须由维护者审核后以单事务手工确认
|
||||
系统 SHALL 只生成 SQL 和审核产物,不直接连接或写入目标 PostgreSQL。维护者 SHALL 在审核通过后以 PostgreSQL 单事务模式执行生成的 SQL;任一记录创建失败时回滚整批。
|
||||
|
||||
#### Scenario: 仅运行生成器
|
||||
- **WHEN** 操作者运行 Python 导入生成器
|
||||
- **THEN** 工具仅生成审核产物和 SQL,不写入目标 PostgreSQL
|
||||
|
||||
#### Scenario: SQL 执行期间发生创建失败
|
||||
- **WHEN** 维护者以单事务模式执行 SQL,且任一记录无法创建
|
||||
- **THEN** PostgreSQL 回滚整批,且本批次不保留任何店铺、账号、角色关联、钱包或成功创建审计事实
|
||||
|
||||
### Requirement: 导入结果必须可审核和重跑定位
|
||||
系统 SHALL 为每次 SQL 生成输出不含明文密码的结果清单与摘要,至少包含源 CSV 行号、稳定店铺编号、店铺名称、层级、上级解析结果、业务员映射结果、计划状态和成功/失败统计。生成的 SQL、结果清单、错误清单和摘要 SHALL 使用同一批次标识以便审核和定位。
|
||||
|
||||
#### Scenario: 生成完成后审核
|
||||
- **WHEN** 工具成功完成 SQL 生成
|
||||
- **THEN** 操作者可通过结果清单核对每条源记录及其生成的店铺编号、层级和业务员归属,并通过摘要确认总数和待执行 SQL 文件
|
||||
@@ -0,0 +1,22 @@
|
||||
## 1. 导入契约与本地配置
|
||||
|
||||
- [x] 1.1 新增店铺批量导入的版本化配置示例和 Git 忽略规则,配置项覆盖店铺编号前缀、默认角色、操作者、初始密码规则及业务员账号映射。
|
||||
- [x] 1.2 在 `scripts/migration/` 实现 Python 配置和 CSV 加载,校验必备列、必填字段、11 位手机号、同批唯一性、父代理唯一解析、稳定店铺编号及父级优先排序。
|
||||
- [x] 1.3 增加 Python `bcrypt` 依赖,并实现不含明文密码的错误清单、结果清单和摘要输出。
|
||||
|
||||
## 2. 单事务 SQL 生成
|
||||
|
||||
- [x] 2.1 实现店铺、主账号、账号角色、店铺角色、双钱包和 bcrypt 密码哈希的 SQL 构造;SQL 按父店铺优先顺序创建并写入操作者和业务员归属。
|
||||
- [x] 2.2 在 SQL 事务起点校验默认客户角色、迁移超级管理员、业务员平台账号及目标库店铺编号/用户名/手机号冲突;错误必须包含 CSV 行号并在写入前中止。
|
||||
- [x] 2.3 按当前审计表结构和动作注册事实生成店铺创建、业务员归属的成功审计 SQL,并确保任一 SQL 失败回滚整批。
|
||||
|
||||
## 3. 迁移入口与运行说明
|
||||
|
||||
- [x] 3.1 新增 `scripts/migration/import_shops.py`,提供 CSV、配置和输出目录参数,只生成 SQL 与审核产物且不连接或写入目标 PostgreSQL。
|
||||
- [x] 3.2 补充 `scripts/migration/README.md` 的准备、SQL 生成、审核、隔离环境 `psql -1` 执行、生产人工执行和回退说明。
|
||||
|
||||
## 4. 验证
|
||||
|
||||
- [x] 4.1 使用最终 CSV 和不含生产凭据的本地配置运行生成器,核对 988 条计划记录、8 条父子关系、SQL 含 bcrypt 哈希且结果/错误/摘要/控制台不含初始密码明文。
|
||||
- [ ] 4.2 在隔离数据库以 `psql -1 -v ON_ERROR_STOP=1 -f` 执行 SQL,核对店铺、主账号、角色关联、双钱包及成功审计数量,并验证冲突或中途失败时整批回滚。
|
||||
- [ ] 4.3 运行 Python 语法检查、生成器 dry-run、`openspec validate --all` 和 `./scripts/context-health.sh`。
|
||||
@@ -0,0 +1,2 @@
|
||||
schema: spec-driven
|
||||
created: 2026-09-02
|
||||
@@ -0,0 +1,51 @@
|
||||
## Context
|
||||
|
||||
现有 step2 依据旧套餐的类型、状态、生效时间和到期时间分类生命周期;分类后只要同一资产有多条 `active` 就阻断。奇成数据中的少量续费记录缺少生效时间且仍标为正常,无法从通用字段推断先后关系。
|
||||
|
||||
## Goals / Non-Goals
|
||||
|
||||
**Goals:**
|
||||
- 用映射配置表达经运营确认的逐卡裁决。
|
||||
- 在生成 SQL 前验证裁决完整性,确保不静默丢失或重复迁移套餐。
|
||||
- 将覆盖后的状态写入现有审核产物,保持可追溯性。
|
||||
|
||||
**Non-Goals:**
|
||||
- 不修改奇成老库记录。
|
||||
- 不依据到期时间、创建时间或套餐时长引入全局自动裁决规则。
|
||||
- 不改变未覆盖资产的现有分类和阻断逻辑。
|
||||
|
||||
## Decisions
|
||||
|
||||
### 使用 ICCID 与生命周期 ID 的显式覆盖
|
||||
在 `mapping.yaml` 增加 `package_lifecycle_overrides`。每项包含完整 ICCID、一个 `active_life_id`、可选的 `pending_life_ids` 与 `skipped_life_ids`。
|
||||
|
||||
生命周期 ID 是老库主键,避免到期时间存在 UTC/本地时区展示差异或同日期重复时的歧义。完整 ICCID 与 step2 的资产键一致。
|
||||
|
||||
未采用“最早到期记录为当前”的规则:已确认的第三张异常卡表明未来记录并不必然均应作为待生效迁移。
|
||||
|
||||
### 覆盖在冲突检测前重分类
|
||||
先获取老库原始生命周期及既有通用分类,再按 ICCID 应用覆盖,最后执行现有映射、唯一 active 校验与 SQL 生成。覆盖只改变配置明确列出的记录状态;同卡其他通用分类为 `active` 或 `pending` 的正式套餐必须也被覆盖明确裁决,否则报配置错误并阻断该资产。
|
||||
|
||||
### 配置与运行时双重校验
|
||||
加载时校验字段格式、同一覆盖内 ID 不重复且状态集合不重叠。读取老库后校验每个被引用 ID 都属于该 ICCID 的 `tbl_card_life` 记录、active 恰好一个、覆盖未遗漏其他原本可迁移记录。错误沿用 step2 的错误 CSV,且不生成该资产套餐 SQL。
|
||||
|
||||
### 本批已确认裁决
|
||||
实现时将下列裁决写入迁移配置:
|
||||
|
||||
| ICCID | active | pending | skipped |
|
||||
|---|---|---|---|
|
||||
| `89860624630055027529` | `DD74BA52EC8242FF94815532DC19389B` | `AB77C853EEF444E2AF20A8475F61CFA7` | 无 |
|
||||
| `89860624630055035589` | `4ACDBC19027A4E90BF500F515093E0A3` | `2D247EF7877C4CE18E74EF9B139B7FCD` | 无 |
|
||||
| `89860624590009246403` | `186B9062F3904AACB4A231041B6B5E98` | 无 | `EB006800000D49EB9A18024922657680` |
|
||||
|
||||
## Risks / Trade-offs
|
||||
|
||||
- [运营裁决填写错误] → 覆盖按老库主键校验,并在审核 CSV 输出覆盖后的每条记录状态。
|
||||
- [老库后续新增套餐记录] → 完整性校验拒绝遗漏的可迁移记录,要求重新确认配置。
|
||||
- [覆盖配置被误用于常规数据] → 不提供全局或通配符规则,覆盖仅作用于单一完整 ICCID。
|
||||
|
||||
## Migration Plan
|
||||
|
||||
1. 发布包含覆盖能力与上述三项配置的迁移脚本。
|
||||
2. 在隔离环境重新生成 step2,核对三张卡各一条 active,前两张各一条 pending,第三张无额外套餐。
|
||||
3. 线上仅执行经审核的生成 SQL;若需回退,移除对应覆盖后重新生成,不写老库。
|
||||
@@ -0,0 +1,24 @@
|
||||
## Why
|
||||
|
||||
奇成 `tbl_card_life` 的少量卡会同时存在多条 `status=1`、未来到期且缺少生效时间的正式套餐记录。迁移脚本无法安全判断当前套餐,因而阻断这些资产的套餐迁移;直接以到期时间做全局裁决会误判其他历史数据。
|
||||
|
||||
## What Changes
|
||||
|
||||
- 在奇成迁移配置中增加按 ICCID 和旧套餐生命周期记录 ID 指定 `active`、`pending`、`skipped` 状态的覆盖能力。
|
||||
- 在检测多个当前生效正式套餐前应用覆盖;覆盖外继续维持现有严格阻断行为。
|
||||
- 对覆盖的完整性和引用的生命周期记录进行校验,并在审核产物中保留实际裁决结果。
|
||||
|
||||
## Capabilities
|
||||
|
||||
### New Capabilities
|
||||
- `qicheng-migration-package-lifecycle-overrides`: 为异常旧套餐生命周期提供可审计、逐卡的迁移状态覆盖。
|
||||
|
||||
### Modified Capabilities
|
||||
- 无。
|
||||
|
||||
## Impact
|
||||
|
||||
- `scripts/migration/config/mapping.yaml`
|
||||
- `scripts/migration/lib/mapping_loader.py`
|
||||
- `scripts/migration/lib/sql_builder.py`
|
||||
- 奇成迁移 step2 生成的 SQL 与审核 CSV;不写入或修改奇成老库。
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user