feat(退款分佣): 佣金回溯明细替换全额失效并补齐读侧与导出
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m26s

用 PRD 2.14 语义整体替换退款佣金「整单全额失效」实现:原佣金保持已发放不变,
回溯事实落在新表 tb_commission_clawback_record 的负数、不可提现明细上。

- 新增成对迁移 000220 建 tb_commission_clawback_record,唯一约束
  (refund_id, original_commission_id) 为权威幂等键,附店铺+时间/原佣金/订单索引。
- 回溯用例(internal/service/refund/clawback.go):准入仅由退款申请状态、审批异常
  标记与退款方式决定;金额按分整数计算,分母取冻结实收(缺失回落审批尝试)、
  分子原路取渠道成功金额,乘法用 math/big 中间量,舍入差自末条起向前补差;
  终态判据要求订单佣金已离开待计算且不存在 status IN (1,2,99) 的记录。
- 三层幂等:唯一约束兜底、佣金行行锁 + 钱包乐观锁、commission_deducted 仅作投影
  并带 WHERE commission_deducted = false 条件置位;闭合三结果为已回溯、无需回溯、
  审批异常转人工。
- 事务内顺序固定:锁提现申请行 → 锁尝试行 → 解冻冻结 → 置驳回 → 插回溯明细 →
  扣 balance(允许为负)→ 写负数流水 → 审计;删除旧全额失效写入与其两个审计调用点,
  refund.invalidate_commission 仅保留常量与注册供历史审计读取。
- 读侧:佣金明细列表 status 筛选透传,两表 UNION ALL 合并分页并以 source ASC 作
  末位次序键;新增佣金明细详情接口并同步路由与 OpenAPI 装配。
- 导出:新增 commission_record 场景(白名单、exporter 注册、DTO oneof、DataSource
  与列定义),粒度为佣金记录,原佣金与回溯各一行,金额保持分且可为负。
- 新增退款佣金回溯周期补偿任务(@every 1m / MaxRetry(3) / Timeout(10m) /
  Unique(10m),独立队列),保留启动时补偿扫描,判据与既有实现一致。

Refs: AUG26-012
This commit is contained in:
2026-09-14 13:40:34 +08:00
parent 67893617fe
commit 1aa4eacee2
30 changed files with 1867 additions and 399 deletions

View File

@@ -1,23 +1,130 @@
## MODIFIED Requirements
### Requirement: 佣金异常状态可见
系统 SHALL 将佣金记录保持为已冻结、解冻中、已发放、已失效、回溯或待人工修正;链路断裂的记录进入待人工修正而不是静默计入可提现余额。回溯记录 MUST 为负数且不可提现MUST NOT 计入可提现余额或提高可提现额度。
#### Scenario: 佣金链路断裂
- **GIVEN** 佣金记录无法关联完成后续发放所需事实
- **WHEN** 系统处理该记录
- **THEN** 记录保持待人工修正状态且不增加可提现余额
#### Scenario: 回溯记录不计入可提现余额
- **GIVEN** 代理店铺存在已发放佣金及其回溯记录
- **WHEN** 查询佣金明细并按可提现余额判定提现资格
- **THEN** 回溯记录以「回溯」状态与负数金额可见,且不增加该店铺的可提现余额
### Requirement: 退款佣金回扣可靠完成
系统 SHALL 在退款申请已通过时持久化佣金回溯请求;回溯请求的投递或处理异常不得静默遗留,且退款单在全部应有回溯明细生成并完成对应钱包流水前不得标记为已回溯。原佣金记录 MUST NOT 因回溯改变状态、金额、佣金来源或发放时间。
#### Scenario: 已退款订单佣金回扣失败后恢复
- **WHEN** 已退款订单的佣金回溯首次处理失败或进程中断
- **THEN** 退款单保持回溯未完成状态并保留可重试事实,后续成功处理后原佣金仍为已发放、回溯明细与佣金钱包负数流水均已生成且退款单标记为已完成
#### Scenario: 订单佣金未终态
- **WHEN** 退款申请已通过但原订单佣金仍未进入终态
- **THEN** 系统不写回溯明细、不标记回溯完成,并保留可重试事实等待终态
#### Scenario: 终态确无佣金
- **WHEN** 原订单佣金已进入终态且确认无佣金
- **THEN** 系统标记该退款单无需回溯并写入「无需回溯」审计,且不产生任何钱包变动
#### Scenario: 审批异常转人工不回溯
- **WHEN** 退款申请存在审批异常标记(企业微信通过后撤销)
- **THEN** 系统不生成回溯明细与钱包变动,标记该退款单的回溯后处理已闭合并写入转人工审计
### Requirement: 退款后处理可补偿
系统 SHALL 对已通过但回溯未完成或资产未完成后处理的退款单提供幂等补偿;重复补偿不得重复生成回溯明细、重复扣减佣金钱包、重复写负数流水或重复处理资产。
#### Scenario: 遗留退款单补偿
- **WHEN** 补偿流程发现已通过且回溯完成事实缺失的退款单
- **THEN** 系统恢复该退款单的唯一后处理请求,并在全部应有回溯明细生成后更新其完成事实
#### Scenario: 周期性补偿
- **WHEN** 补偿扫描按既有周期任务形态执行且存在回溯后处理未闭合的退款单
- **THEN** 系统按固定周期重复补偿直至完成事实落库或该退款单转人工,且不重复产生任何资金事实
## ADDED Requirements
### Requirement: 套餐退款佣金回溯
系统 SHALL 在套餐退款后保留原佣金不变,并创建关联原佣金记录和退款单的负数、不可提现回溯明细,冻结原订单号及原佣金关键字段。同一退款业务必须幂等;若佣金计算未终态则等待终态后生成,确认无佣金才标记无需回溯。换货不在本期范围。
部分退款按本次退款金额与订单冻结实收金额比例,对每条原佣金按分向下取整;最后一条补足舍入差,累计回溯不得超过原佣金。生成前系统 MUST 拒绝并释放待审核提现,再生成回溯明细和钱包扣款流水;佣金钱包允许负余额
系统 SHALL 在退款申请已通过后生成关联原佣金记录与退款单的负数、不可提现回溯明细,并保留原佣金记录不变。回溯明细 MUST 冻结原订单号、原佣金标识、负数金额、不可提现标识、回溯后佣金钱包实际余额与生成时间。换货不在本期范围
回溯准入 MUST 由退款申请状态与方式得出:仅退款申请已通过时可生成,原路退款还须渠道明确成功。待审批、原路处理中、渠道明确失败与企业微信通过后撤销 MUST NOT 生成回溯明细;渠道明确失败可修改材料后重提,重提后按最终成功金额生成一次。同一退款业务 MUST 幂等幂等依据为「一次退款对应一次原佣金」的唯一事实MUST NOT 依赖退款单上的佣金回扣标记。
原订单佣金未进入终态时系统 MUST 等待终态后再生成MUST NOT 提前判定为无需回溯;确认无佣金时才标记无需回溯。
部分退款按本次成功退款金额与本次退款冻结实收金额的比例对每条原佣金等比例回溯。金额 MUST 以分整数精确计算MUST NOT 溢出或引入浮点误差;每条按分向下取整,舍入差自稳定顺序(原佣金标识升序)末条起向前补足,每条不超过该条剩余可回溯余额;累计回溯 MUST NOT 超过各原佣金的可回溯余额。冻结实收金额缺失或非正时系统 MUST 记录可恢复失败且不落库、不改变余额MUST NOT 以订单标价或申请金额替代。
生成回溯前系统 MUST 先拒绝并释放待审核提现的冻结余额再生成回溯明细与钱包扣款流水佣金钱包余额允许为负MUST NOT 因余额不足而跳过或拒绝回溯。
#### Scenario: 部分退款舍入
- **WHEN** 一笔部分退款关联多条原佣金且比例计算产生分级舍入差
- **THEN** 系统按各条向下取整并仅在最后一条补差,回溯总额等于应回溯额且不超过各原佣金可回溯余额
- **THEN** 系统按各条向下取整并自末条起向前补差,回溯总额等于应回溯额且不超过各原佣金可回溯余额
#### Scenario: 全额回溯
- **WHEN** 退款金额与冻结实收金额相等
- **THEN** 系统按各原佣金的剩余可回溯金额回溯,回溯总额等于各原佣金金额之和
#### Scenario: 冻结实收金额非正
- **WHEN** 本次退款冻结实收金额缺失或非正
- **THEN** 系统记录可恢复失败,不写回溯明细、不改变佣金钱包余额,且不以订单标价替代计算
#### Scenario: 原路渠道失败重提后按最终金额回溯
- **GIVEN** 一笔原路退款曾在渠道明确失败并可重提
- **WHEN** 该退款重提后最终渠道明确成功
- **THEN** 系统仅在该退款申请已通过时按其最终成功金额生成一次回溯明细,失败阶段不产生任何回溯事实
#### Scenario: 重复退款消费
- **WHEN** 同一退款完成事件被重复消费
- **THEN** 系统不重复生成回溯明细、钱包扣款或提现释放事实
#### Scenario: 回溯后佣金钱包负余额
- **GIVEN** 店铺佣金钱包余额不足以覆盖本次回溯金额
- **WHEN** 系统生成回溯明细
- **THEN** 钱包余额允许为负并记录回溯后实际余额,且不因余额不足而跳过或拒绝回溯
#### Scenario: 原佣金保持不变
- **WHEN** 一笔已发放佣金被回溯
- **THEN** 该原佣金记录的状态、金额、佣金来源与发放时间均不变,可提现余额按回溯金额减少
### Requirement: 回溯明细关联查询与导出
系统 SHALL 在佣金明细中分别展示原发放佣金和回溯扣款记录,并允许从任一记录查询其关联的退款单、原佣金或全部回溯明细。回溯记录必须显示负数金额、不可提现标识、来源退款单号、原佣金记录号、生成时间和回溯后佣金钱包实际余额。佣金明细及导出 MUST 使用既有佣金数据范围:代理仅可读取自身及其既有可见范围内的事实,平台/超级管理员遵循既有范围;无权记录不得通过关联 ID、汇总或导出泄露。
导出应冻结筛选条件、操作者和可见范围;原佣金与回溯记录均作为独立行导出,回溯后余额为对应钱包变动提交后的实际余额,可为负数
系统 SHALL 在佣金明细中分别展示原发放佣金和回溯扣款记录,并允许从任一记录查询其关联的退款单、原佣金或全部回溯明细。回溯记录必须显示负数金额、不可提现标识、来源退款单号、原佣金记录号、生成时间和回溯后佣金钱包实际余额。原佣金与回溯记录 MUST 合并为同一列表的同一分页与同一排序口径MUST NOT 因来源表不同而丢失或重复任一条事实
佣金明细及导出 MUST 使用既有佣金数据范围:代理仅可读取自身及其既有可见范围内的事实,平台与超级管理员遵循既有范围;无权记录不得通过关联 ID、汇总或导出泄露存在性。
佣金明细导出 MUST 使用佣金记录粒度,原佣金与回溯记录各占一行,并冻结创建时筛选条件、操作者与可见范围。回溯后余额为对应钱包变动提交后的实际余额,可为负数;金额保持分,展示层转元 MUST NOT 改变负数或余额事实。
#### Scenario: 代理查询越权回溯记录
- **WHEN** 代理使用回溯记录 ID、原佣金 ID 或退款单号查询其数据范围外的回溯关系
- **THEN** 系统按既有数据范围返回不存在或空结果,不泄露关联事实
#### Scenario: 重复退款消费
- **WHEN** 同一退款完成事件被重复消费
- **THEN** 系统不重复生成回溯明细、钱包扣款或提现释放事实
#### Scenario: 原佣金与回溯合并分页
- **GIVEN** 同一店铺同时存在原佣金记录与回溯记录
- **WHEN** 查询佣金明细列表并翻页
- **THEN** 两类记录按同一排序口径出现在同一结果集内,任一条不缺失也不重复
#### Scenario: 回溯记录导出
- **WHEN** 导出含回溯记录的佣金明细
- **THEN** 原佣金与回溯记录各占一行,回溯行金额为负数、可提现标识为不可提现、含关联佣金明细与退款单标识,且回溯后余额为负数时原样导出

View File

@@ -0,0 +1,27 @@
## MODIFIED Requirements
### Requirement: 退款终态事实与失败分类
退款申请 SHALL 保存结构化失败分类、渠道退款状态、渠道退款流水与渠道退款请求号,并在列表、详情和导出中返回冻结实收金额、方式、申请状态、渠道退款状态、失败安全摘要、审批尝试历史和渠道流水,按既有订单数据范围过滤。失败分类 MUST 为稳定枚举,至少覆盖:渠道明确拒绝、渠道凭证失效、渠道余额不足、超时或结果未知、企业微信驳回或关闭、企业微信通过后撤销,以及本地原支付事实不可用。渠道凭证失效与本地原支付事实不可用 MUST 为两个并列分类、语义不得合并:前者指该商户退款必需凭证缺失或失效,后者指本地原支付单、实际收款商户、原渠道流水或可退金额校验不通过。每个分类 MUST 显式标记其是否属于「明确失败」:明确失败表示该次退款尝试已终结且不可自动恢复,非明确失败表示仍在途、可自动恢复或需人工处理。该标记 MUST 仅用于判定尝试终结性与人工处置MUST NOT 作为佣金回溯的准入条件。审计 SHALL 记录申请、重提、审批终态、权益处理、渠道调用与恢复,且不得记录凭证内容、完整收款文本或商户密钥。
为后续佣金回溯能力提供稳定事实,退款终态 SHALL 可按退款单与订单定位,并提供:成功退款金额、冻结实收金额与终态时点。佣金回溯准入 MUST 由退款申请状态、审批异常标记与退款方式得出MUST NOT 依据失败分类标记、退款原因文本或新增的独立完成事件键:仅退款申请已通过且不存在审批异常标记时可进入回溯判定,原路退款还须渠道明确成功;待审批、原路处理中、渠道明确失败(可修改材料后重提)与企业微信通过后撤销均不得回溯。系统 MUST 保留既有退款佣金回扣事件键的兼容语义;佣金回溯的幂等键为一次退款一次回溯,不依赖退款单上的佣金回扣标记。
#### Scenario: 渠道失败分类可查询
- **WHEN** 原路退款因渠道余额不足失败
- **THEN** 退款详情返回该失败分类与安全摘要,且不返回任何凭证内容或完整收款文本
#### Scenario: 审计不含敏感内容
- **WHEN** 渠道退款调用或恢复完成后写入审计
- **THEN** 审计只记录业务标识、金额、状态与脱敏摘要,不记录商户密钥或凭证原文
#### Scenario: 回溯准入仅取决于退款申请状态
- **WHEN** 退款申请处于待审批、原路处理中、渠道明确失败或企业微信通过后撤销
- **THEN** 系统不生成任何佣金回溯事实;仅当退款申请已通过(原路退款还须渠道明确成功)时才生成
#### Scenario: 失败分类不决定回溯准入
- **WHEN** 一次退款尝试带有明确失败分类,但该退款申请尚未处于已通过状态
- **THEN** 系统不生成佣金回溯事实,该分类只用于判定尝试终结性与人工处置