## Context Phase 4 完成了订单和支付流程,现在需要实现佣金计算。当终端用户购买套餐支付成功后,系统自动计算各级代理的佣金并入账。 **佣金来源**: 1. **成本价差收入**:每笔订单必触发,售价 - 成本价 = 代理收入 2. **一次性佣金**:满足触发条件时发放一次,金额从梯度配置获取 **当前 CommissionRecord 模型过于复杂**(包含冻结/解冻字段),需要简化。 ## Goals / Non-Goals **Goals:** - 简化 CommissionRecord 模型 - 实现成本价差收入计算(每笔订单) - 实现一次性佣金触发(充值阈值) - 佣金直接入账到店铺钱包 - 提供佣金记录查询和统计 **Non-Goals:** - 不实现冻结/解冻机制 - 不实现长期佣金(号卡专用) - 不实现梯度佣金统计(本期只做配置,统计后续优化) - 不实现佣金审批流程 ## Decisions ### 1. CommissionRecord 模型简化 **决策**:删除冻结相关字段,新增来源和关联字段 ```go // 简化后的 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. 成本价差收入计算 **决策**:各级代理按自己的成本价差计算 **计算规则**: - 终端销售代理:收入 = 售价 - 自己的成本价 - 中间层级代理:收入 = 下级的成本价 - 自己的成本价 ```go 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. 一次性佣金触发 **决策**:两种触发类型,每张卡/设备只触发一次 ```go // 触发类型 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. 钱包入账 **决策**:直接入账,无冻结期 ```go 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 1. **梯度佣金何时统计?** - 当前设计:本期只做配置,不做实际统计 - 待确认:是否需要定时任务统计并发放梯度奖励? 2. **累计充值是否包含当前订单?** - 当前设计:先更新累计充值,再检查触发条件 - 待确认:是否正确? 3. **一次性佣金发放给谁?** - 当前设计:发放给卡/设备的直接归属店铺 - 待确认:是否需要多级分佣?