feat: 归档佣金计算触发和快照变更,同步规范文档
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m40s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m40s
- 归档 OpenSpec 变更到 archive 目录 - 创建 2 个新的主规范文件:commission-trigger 和 order-commission-snapshot - 实现订单佣金快照字段和支付自动触发 - 确保事务一致性,所有佣金操作在同一事务内完成 - 提取成本价计算为公共工具函数
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
schema: spec-driven
|
||||
created: 2026-01-29
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
# 佣金计算链路修复 - 设计
|
||||
|
||||
## 目标
|
||||
|
||||
1. 订单创建时就写入后续佣金计算所需的关键字段快照。
|
||||
2. 订单支付成功后(首次成功支付)自动 enqueue 佣金计算异步任务。
|
||||
3. 佣金计算具备可重复执行的幂等语义(订单佣金已计算则跳过)。
|
||||
|
||||
## 关键字段来源(你已确认)
|
||||
|
||||
以购买校验结果为准:
|
||||
- `allocation`:来自 `PurchaseValidationResult.Allocation`
|
||||
- `SeriesID`:`allocation.SeriesID`
|
||||
- `SellerShopID`:`allocation.ShopID`(该分配记录对应的“售卖/收益归属店铺”)
|
||||
- `SellerCostPrice`:根据 allocation 的基础返佣规则从订单金额推导(与成本价差计算一致的口径)
|
||||
|
||||
说明:这里的 SellerCostPrice 是为了支持“成本价差佣金”计算,作为链路上的稳定快照,避免后续配置变更影响历史订单。
|
||||
|
||||
## 支付成功后触发佣金计算
|
||||
|
||||
- 触发点:订单支付成功且为首次成功支付(由“订单激活幂等提案”提供门闸)。
|
||||
- 触发动作:enqueue `commission:calculate`,payload 为 `order_id`。
|
||||
- 入队失败策略:
|
||||
- 不回滚支付成功(避免影响主链路)
|
||||
- 保持 `commission_status = pending`,允许后续重试(例如后台补偿任务/人工触发/定时任务扫描)
|
||||
|
||||
## 事务一致性(可选)
|
||||
|
||||
当前佣金计算服务使用了 `Transaction` 包裹,但内部 Store 若未使用同一个 `tx`,一致性会被破坏。
|
||||
|
||||
推荐方案之一:
|
||||
- 为各 Store 增加 `WithDB(tx)` 或在方法中接收 `db *gorm.DB` 参数,确保写入走同一个事务 `tx`。
|
||||
|
||||
## 验收标准
|
||||
|
||||
- 创建订单后,订单表中 `series_id/seller_shop_id/seller_cost_price` 等字段正确写入。
|
||||
- 首次支付成功后,会 enqueue 佣金计算任务(可通过日志/测试验证)。
|
||||
- 佣金任务重复执行不重复发放(已计算则跳过)。
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
# 佣金计算链路修复:支付后自动入队 + 订单佣金字段快照
|
||||
|
||||
## Why
|
||||
|
||||
你确认的目标:**订单支付成功后自动触发佣金计算(异步任务)**,且佣金计算所需的关键字段应当来源于“购买校验结果”。
|
||||
|
||||
当前实现存在以下风险:
|
||||
- 佣金计算依赖订单字段(如 `series_id/seller_shop_id/seller_cost_price`),但订单创建时未填充,可能导致计算错误或空指针风险。
|
||||
- 佣金计算任务已定义,但缺少稳定触发入口,导致支付后佣金不计算或需要人工补偿。
|
||||
|
||||
## What Changes
|
||||
|
||||
- **订单创建时写入佣金快照字段**:基于购买校验结果(allocation/series)填充订单的 `SeriesID/SellerShopID/SellerCostPrice` 等字段,确保后续计算稳定。
|
||||
- **支付成功后自动入队佣金计算任务**:在订单从待支付变为已支付的“首次成功支付”场景,enqueue `commission:calculate` 异步任务,执行佣金计算。
|
||||
- **计算事务一致性(可选但推荐)**:调整佣金计算服务的事务使用方式,确保“佣金记录 + 钱包入账 + 订单佣金状态更新”具备一致性。
|
||||
- **补充测试**:新增/完善测试,避免回归。
|
||||
|
||||
## Impact
|
||||
|
||||
涉及模块(预期):
|
||||
- 订单创建:`internal/service/order/service.go`
|
||||
- 异步任务:`internal/task/commission_calculation.go`(触发入口)与队列注入
|
||||
- 佣金计算:`internal/service/commission_calculation/service.go`
|
||||
- 测试:`internal/service/order/service_test.go`、`internal/task/*` 或集成测试
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# 支付后自动触发佣金计算
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 支付成功后自动入队佣金计算任务
|
||||
|
||||
系统 SHALL 在订单首次支付成功时自动 enqueue 佣金计算异步任务(`commission:calculate`),确保佣金及时发放。
|
||||
|
||||
**触发条件**:
|
||||
- 订单从"待支付"变为"已支付"(首次成功支付)
|
||||
- 订单 `commission_status` 为 `pending`(未计算)
|
||||
|
||||
**任务参数**:
|
||||
- 任务类型:`commission:calculate`
|
||||
- Payload:`{"order_id": <订单ID>}`
|
||||
|
||||
#### Scenario: 首次支付成功触发计算
|
||||
|
||||
- **WHEN** 订单支付成功,订单状态从"待支付"变为"已支付"
|
||||
- **THEN** 系统自动 enqueue `commission:calculate` 任务,payload 包含订单 ID
|
||||
- **AND** 订单 `commission_status` 保持为 `pending`(任务执行后才更新为 `calculated`)
|
||||
|
||||
#### Scenario: 重复支付不重复触发
|
||||
|
||||
- **WHEN** 订单已经是"已支付"状态,再次收到支付成功通知(幂等场景)
|
||||
- **THEN** 系统不重复 enqueue 佣金计算任务
|
||||
- **AND** 日志记录"订单已支付,跳过重复入队"
|
||||
|
||||
#### Scenario: 已计算佣金的订单不触发
|
||||
|
||||
- **WHEN** 订单 `commission_status` 为 `calculated`(已计算)
|
||||
- **THEN** 系统跳过入队操作
|
||||
- **AND** 日志记录"订单佣金已计算,跳过入队"
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 入队失败不影响支付主链路
|
||||
|
||||
系统 SHALL 确保佣金任务入队失败时不回滚订单支付成功状态,保障主业务链路稳定。
|
||||
|
||||
**失败处理策略**:
|
||||
- 入队失败时记录 ERROR 级别日志(包含订单 ID、失败原因)
|
||||
- 订单状态保持为"已支付",不回滚
|
||||
- 订单 `commission_status` 保持为 `pending`,允许后续补偿
|
||||
|
||||
**补偿机制**:
|
||||
- 后台补偿任务扫描 `commission_status=pending` 且已支付的订单
|
||||
- 人工触发佣金计算(后台接口)
|
||||
- 定时任务重试入队(可选)
|
||||
|
||||
#### Scenario: 入队失败记录日志
|
||||
|
||||
- **WHEN** 佣金任务入队失败(队列服务不可用或网络超时)
|
||||
- **THEN** 系统记录 ERROR 日志,包含订单 ID、失败原因、队列配置信息
|
||||
- **AND** 订单支付状态保持为"已支付",不回滚
|
||||
|
||||
#### Scenario: 失败后允许补偿
|
||||
|
||||
- **WHEN** 后台补偿任务扫描到 `commission_status=pending` 且 `payment_status=paid` 的订单
|
||||
- **THEN** 系统可重新 enqueue 佣金计算任务或直接执行计算
|
||||
- **AND** 避免佣金永久丢失
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 佣金计算任务幂等性
|
||||
|
||||
系统 SHALL 确保佣金计算任务可重复执行,不重复发放佣金。
|
||||
|
||||
**幂等检查**:
|
||||
- 任务执行前检查订单 `commission_status`
|
||||
- 如果已为 `calculated`,跳过计算并返回成功
|
||||
|
||||
**状态更新**:
|
||||
- 计算完成后将订单 `commission_status` 更新为 `calculated`
|
||||
- 状态更新与佣金记录创建在同一事务中
|
||||
|
||||
#### Scenario: 任务重复执行跳过计算
|
||||
|
||||
- **WHEN** 佣金计算任务执行时,订单 `commission_status` 已为 `calculated`
|
||||
- **THEN** 系统跳过佣金计算和钱包入账操作
|
||||
- **AND** 任务返回成功(避免 Asynq 重试)
|
||||
- **AND** 日志记录"订单佣金已计算,跳过执行"
|
||||
|
||||
#### Scenario: 并发任务只有一个成功
|
||||
|
||||
- **WHEN** 同一订单的佣金计算任务被重复入队,两个 worker 并发执行
|
||||
- **THEN** 第一个任务成功完成计算并更新状态为 `calculated`
|
||||
- **AND** 第二个任务检查到状态已为 `calculated`,跳过计算
|
||||
|
||||
#### Scenario: 任务失败可安全重试
|
||||
|
||||
- **WHEN** 佣金计算任务执行失败(数据库异常、钱包服务不可用)
|
||||
- **THEN** Asynq 自动重试任务
|
||||
- **AND** 重试时幂等检查确保不重复发放佣金
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 队列客户端依赖注入
|
||||
|
||||
系统 SHALL 通过依赖注入方式将队列客户端注入到订单服务,遵循现有 bootstrap 架构。
|
||||
|
||||
**注入位置**:
|
||||
- `internal/service/order/service.go` 的 `Service` 结构体
|
||||
- 添加 `queueClient *asynq.Client` 字段
|
||||
|
||||
**注入方式**:
|
||||
- 在 `internal/bootstrap/services.go` 中初始化订单服务时传入队列客户端
|
||||
- 队列客户端在 `bootstrap.Bootstrap()` 中统一创建
|
||||
|
||||
#### Scenario: 订单服务接收队列客户端
|
||||
|
||||
- **WHEN** 系统启动时执行 `bootstrap.Bootstrap()`
|
||||
- **THEN** 订单服务(`order.Service`)通过构造函数接收队列客户端实例
|
||||
- **AND** 队列客户端可在服务内部调用 `Enqueue()` 方法
|
||||
|
||||
#### Scenario: 支付成功时调用队列客户端
|
||||
|
||||
- **WHEN** 订单支付成功,订单服务执行入队操作
|
||||
- **THEN** 系统通过注入的队列客户端调用 `Enqueue("commission:calculate", payload)`
|
||||
- **AND** 不在服务内部直接创建队列客户端(遵循依赖注入原则)
|
||||
|
||||
---
|
||||
@@ -0,0 +1,60 @@
|
||||
# 订单佣金快照字段
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 订单创建时填充佣金快照字段
|
||||
|
||||
系统 SHALL 在订单创建时填充佣金计算所需的关键字段快照,确保后续佣金计算不受配置变更影响。
|
||||
|
||||
**快照字段**:
|
||||
- `series_id`:套餐系列 ID
|
||||
- `seller_shop_id`:售卖/收益归属店铺 ID
|
||||
- `seller_cost_price`:卖家成本价(用于成本价差佣金计算)
|
||||
|
||||
**字段来源**:基于购买校验结果(`PurchaseValidationResult`)
|
||||
- `series_id` ← `allocation.SeriesID`
|
||||
- `seller_shop_id` ← `allocation.ShopID`
|
||||
- `seller_cost_price` ← 根据 allocation 的基础返佣规则从订单金额推导
|
||||
|
||||
#### Scenario: 订单创建时写入佣金快照
|
||||
|
||||
- **WHEN** 用户购买套餐,订单创建成功,购买校验返回 `allocation` 数据
|
||||
- **THEN** 订单表中 `series_id`、`seller_shop_id`、`seller_cost_price` 字段已正确填充
|
||||
- **AND** 字段值来源于购买校验结果,而非订单提交参数
|
||||
|
||||
#### Scenario: 缺少 allocation 数据时的处理
|
||||
|
||||
- **WHEN** 订单创建时购买校验结果中缺少 `allocation` 数据
|
||||
- **THEN** 系统记录警告日志,订单佣金快照字段保持 NULL 或默认值
|
||||
- **AND** 订单 `commission_status` 标记为 `pending`(待计算),允许后续补偿
|
||||
|
||||
#### Scenario: 后续佣金计算使用快照字段
|
||||
|
||||
- **WHEN** 佣金计算任务执行时读取订单数据
|
||||
- **THEN** 系统使用订单表中的快照字段(`series_id`、`seller_shop_id`、`seller_cost_price`)
|
||||
- **AND** 不再实时查询套餐配置或返佣规则,避免配置变更影响历史订单
|
||||
|
||||
---
|
||||
|
||||
### Requirement: 成本价推导方法复用
|
||||
|
||||
系统 SHALL 提供统一的成本价推导方法,确保订单创建和佣金计算使用相同的计算口径。
|
||||
|
||||
**方法职责**:
|
||||
- 输入:订单金额、allocation 数据(包含返佣规则)
|
||||
- 输出:卖家成本价(seller_cost_price)
|
||||
- 逻辑:与"成本价差佣金"计算保持一致
|
||||
|
||||
#### Scenario: 订单创建时调用成本价推导
|
||||
|
||||
- **WHEN** 订单创建服务填充 `seller_cost_price` 字段
|
||||
- **THEN** 系统调用统一的成本价推导方法,基于订单金额和 allocation 数据计算
|
||||
- **AND** 推导结果写入订单表 `seller_cost_price` 字段
|
||||
|
||||
#### Scenario: 佣金计算时复用相同逻辑
|
||||
|
||||
- **WHEN** 佣金计算服务执行成本价差计算
|
||||
- **THEN** 系统使用订单快照中的 `seller_cost_price`(已在创建时推导)
|
||||
- **AND** 避免重复推导,确保计算口径一致
|
||||
|
||||
---
|
||||
@@ -0,0 +1,24 @@
|
||||
# 佣金计算链路修复 - 实现任务
|
||||
|
||||
## 1. 订单佣金字段快照
|
||||
|
||||
- [x] 1.1 在 `internal/service/order/service.go` 创建订单时,从购买校验结果填充 `SeriesID/SellerShopID/SellerCostPrice`
|
||||
- [x] 1.2 补充/复用"成本价推导"工具方法,确保口径与佣金计算一致
|
||||
|
||||
## 2. 支付成功后自动入队
|
||||
|
||||
- [x] 2.1 在首次支付成功路径 enqueue `commission:calculate`(payload: order_id)
|
||||
- [x] 2.2 注入队列客户端到订单服务(遵循现有 bootstrap 依赖注入方式)
|
||||
- [x] 2.3 明确入队失败策略:记录日志,订单保持 `commission_status=pending` 可重试
|
||||
|
||||
## 3. 佣金计算一致性与健壮性(可选但推荐)
|
||||
|
||||
- [x] 3.1 调整 `internal/service/commission_calculation/service.go`,确保事务内对佣金记录/钱包/订单状态更新使用同一 `tx`
|
||||
- [x] 3.2 增加必要的空值保护:缺少关键字段时返回业务错误而非 panic
|
||||
|
||||
## 4. 测试与验证
|
||||
|
||||
- [x] 4.1 新增单元测试:订单创建后佣金快照字段写入正确
|
||||
- [x] 4.2 新增单元/集成测试:支付成功后会 enqueue 佣金计算任务(可通过可注入的队列 client 验证)
|
||||
- [x] 4.3 运行 `go test ./...` 确保通过
|
||||
|
||||
Reference in New Issue
Block a user