All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
- 新增套餐系列管理 (CRUD + 状态切换) - 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态) - 清理 8 个废弃分佣模型及对应数据库表 - Package 模型新增建议成本价、建议售价、上架状态字段 - 完整的 Store/Service/Handler 三层实现 - 包含单元测试和集成测试 - 归档 add-package-module change - 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
7.3 KiB
7.3 KiB
Context
Phase 4 完成了订单和支付流程,现在需要实现佣金计算。当终端用户购买套餐支付成功后,系统自动计算各级代理的佣金并入账。
佣金来源:
- 成本价差收入:每笔订单必触发,售价 - 成本价 = 代理收入
- 一次性佣金:满足触发条件时发放一次,金额从梯度配置获取
当前 CommissionRecord 模型过于复杂(包含冻结/解冻字段),需要简化。
Goals / Non-Goals
Goals:
- 简化 CommissionRecord 模型
- 实现成本价差收入计算(每笔订单)
- 实现一次性佣金触发(充值阈值)
- 佣金直接入账到店铺钱包
- 提供佣金记录查询和统计
Non-Goals:
- 不实现冻结/解冻机制
- 不实现长期佣金(号卡专用)
- 不实现梯度佣金统计(本期只做配置,统计后续优化)
- 不实现佣金审批流程
Decisions
1. CommissionRecord 模型简化
决策:删除冻结相关字段,新增来源和关联字段
// 简化后的 CommissionRecord
type CommissionRecord struct {
gorm.Model
BaseModel
ShopID uint // 店铺ID(佣金归属)
OrderID uint // 关联订单ID
IotCardID uint // 关联卡ID(可空)
DeviceID uint // 关联设备ID(可空)
CommissionSource string // 佣金来源: cost_diff-成本价差 one_time-一次性佣金 tier_bonus-梯度奖励
Amount int64 // 佣金金额(分)
BalanceAfter int64 // 入账后钱包余额(分)
Status int // 状态: 1-已入账 2-已失效
ReleasedAt *time.Time // 入账时间
Remark string // 备注
}
删除字段:
agent_id(改用 shop_id)rule_id(不再关联复杂规则)commission_type(改用 commission_source)unfrozen_at、冻结相关状态
2. 佣金计算流程
决策:订单支付成功后异步计算
订单支付成功
↓
发送异步任务 (Asynq)
↓
佣金计算任务执行:
1. 获取订单信息
2. 遍历代理层级(从销售店铺到顶级)
3. 每级计算成本价差收入
4. 检查一次性佣金触发条件
5. 创建 CommissionRecord
6. 更新店铺钱包余额
7. 更新订单 commission_status
3. 成本价差收入计算
决策:各级代理按自己的成本价差计算
计算规则:
- 终端销售代理:收入 = 售价 - 自己的成本价
- 中间层级代理:收入 = 下级的成本价 - 自己的成本价
func CalculateCostDiffCommission(order *Order) []CommissionRecord {
var records []CommissionRecord
// 获取销售店铺(终端销售的代理)
sellerShop := GetShop(order.SellerShopID)
sellerCostPrice := GetCostPrice(sellerShop.ID, order.PackageID)
// 终端销售代理的收入 = 售价 - 成本价
sellerProfit := order.TotalAmount - sellerCostPrice
if sellerProfit > 0 {
records = append(records, CommissionRecord{
ShopID: sellerShop.ID,
OrderID: order.ID,
CommissionSource: "cost_diff",
Amount: sellerProfit,
})
}
// 遍历上级代理链
childCostPrice := sellerCostPrice
currentShop := GetShop(sellerShop.ParentID)
for currentShop != nil {
// 获取当前店铺的成本价
myCostPrice := GetCostPrice(currentShop.ID, order.PackageID)
// 收入 = 下级成本价 - 自己成本价
profit := childCostPrice - myCostPrice
if profit > 0 {
records = append(records, CommissionRecord{
ShopID: currentShop.ID,
OrderID: order.ID,
CommissionSource: "cost_diff",
Amount: profit,
})
}
// 移动到上级
childCostPrice = myCostPrice
currentShop = GetShop(currentShop.ParentID)
}
return records
}
4. 一次性佣金触发
决策:两种触发类型,每张卡/设备只触发一次
// 触发类型 A:一次性充值 ≥ 阈值
func CheckOneTimeRecharge(order *Order, threshold int64) bool {
return order.TotalAmount >= threshold
}
// 触发类型 B:累计充值 ≥ 阈值
func CheckAccumulatedRecharge(card *IotCard, threshold int64) bool {
return card.AccumulatedRecharge >= threshold
}
// 检查并发放一次性佣金
func TriggerOneTimeCommission(order *Order, card *IotCard) {
if card.FirstCommissionPaid {
return // 已发放过
}
// 获取配置的触发条件和金额
tier := GetCommissionTier(card.SeriesAllocationID)
// 检查触发条件
triggered := false
switch tier.TriggerType {
case "one_time_recharge":
triggered = CheckOneTimeRecharge(order, tier.ThresholdValue)
case "accumulated_recharge":
triggered = CheckAccumulatedRecharge(card, tier.ThresholdValue)
}
if triggered {
// 发放佣金
CreateCommissionRecord(...)
// 标记已发放
card.FirstCommissionPaid = true
UpdateCard(card)
}
}
5. 钱包入账
决策:直接入账,无冻结期
func CreditCommission(record *CommissionRecord) error {
return Transaction(func(tx *gorm.DB) error {
// 1. 获取店铺钱包
wallet := GetWallet("shop", record.ShopID)
// 2. 增加余额
wallet.Balance += record.Amount
UpdateWallet(wallet)
// 3. 记录余额
record.BalanceAfter = wallet.Balance
record.Status = 1 // 已入账
record.ReleasedAt = time.Now()
UpdateCommissionRecord(record)
// 4. 创建钱包交易记录
CreateWalletTransaction(...)
return nil
})
}
6. API 设计
# 佣金记录查询
GET /api/admin/commission-records 佣金记录列表
GET /api/admin/commission-records/:id 佣金记录详情
# 佣金统计
GET /api/admin/commission-stats 佣金统计(总收入、各来源占比)
GET /api/admin/commission-stats/daily 每日佣金统计
Risks / Trade-offs
风险 1:异步计算失败
风险:佣金计算任务失败导致佣金未发放
缓解:
- Asynq 自动重试机制
- 记录任务执行日志
- 提供手动触发补偿接口
风险 2:并发更新钱包余额
风险:多笔佣金同时入账导致余额计算错误
缓解:
- 使用数据库事务
- 钱包更新使用乐观锁或悲观锁
风险 3:代理层级变更
风险:订单支付后代理层级变更,佣金计算基于哪个时间点?
缓解:
- 佣金计算基于订单支付时的代理关系
- 订单中可记录销售店铺ID快照
Open Questions
-
梯度佣金何时统计?
- 当前设计:本期只做配置,不做实际统计
- 待确认:是否需要定时任务统计并发放梯度奖励?
-
累计充值是否包含当前订单?
- 当前设计:先更新累计充值,再检查触发条件
- 待确认:是否正确?
-
一次性佣金发放给谁?
- 当前设计:发放给卡/设备的直接归属店铺
- 待确认:是否需要多级分佣?