## Context Phase 3 完成了卡/设备的套餐系列关联,现在需要实现订单和支付流程。核心是"强充"机制:用户必须通过购买套餐来充值,不能直接给钱包充值。这确保每笔资金流入都有对应的套餐购买记录。 **三类买家**: 1. 个人客户:通过 H5/小程序购买,使用卡/设备钱包或第三方支付 2. 代理商:通过后台购买,使用店铺钱包 3. 企业客户:后台直接分配套餐,不走订单流程(本期不做) ## Goals / Non-Goals **Goals:** - 设计订单和订单明细模型 - 实现套餐购买订单创建流程 - 实现钱包支付和第三方支付回调 - 验证购买权限(卡/设备的套餐系列关联) - 套餐生效后更新流量额度 **Non-Goals:** - 不实现企业客户的套餐分配(后台直接操作) - 不实现第三方支付发起(仅处理回调) - 不实现佣金计算(Phase 5) - 不实现退款流程 ## Decisions ### 1. 订单模型设计 **决策**:Order + OrderItem 两级结构 ```go // Order 订单模型 type Order struct { gorm.Model BaseModel OrderNo string // 订单号(唯一) OrderType string // 订单类型: single_card-单卡购买 device-设备购买 BuyerType string // 买家类型: personal-个人客户 agent-代理商 BuyerID uint // 买家ID(个人客户ID或店铺ID) IotCardID uint // IoT卡ID(单卡购买时) DeviceID uint // 设备ID(设备购买时) TotalAmount int64 // 订单总金额(分) PaymentMethod string // 支付方式: wallet-钱包 wechat-微信 alipay-支付宝 PaymentStatus int // 支付状态: 1-待支付 2-已支付 3-已取消 4-已退款 PaidAt *time.Time // 支付时间 CommissionStatus int // 佣金状态: 1-待计算 2-已计算 } // OrderItem 订单明细模型 type OrderItem struct { gorm.Model BaseModel OrderID uint // 订单ID PackageID uint // 套餐ID PackageName string // 套餐名称(快照) Quantity int // 数量(通常为1) UnitPrice int64 // 单价(分) Amount int64 // 小计(分) } ``` **理由**: - 支持一个订单购买多个套餐(虽然初期可能只买一个) - 快照套餐名称,避免套餐修改影响历史订单 - 佣金状态用于 Phase 5 的异步佣金计算 ### 2. 订单号生成规则 **决策**:时间戳 + 随机数 ``` 格式:ORD{YYYYMMDDHHMMSS}{6位随机数} 示例:ORD20260127143052123456 ``` **理由**: - 可读性好,包含时间信息 - 随机数避免并发冲突 - 长度固定,便于存储和展示 ### 3. 购买价格确定 **决策**:使用 Package.suggested_retail_price 作为统一售价 ``` 个人客户购买:支付金额 = Package.suggested_retail_price 代理为店铺购买:支付金额 = 代理的成本价(用于囤货/测试) ``` **理由**: - 简化首期实现,所有终端用户统一售价 - 代理的利润来自返佣(基础返佣 + 一次性佣金) - 后续如需支持代理自定义售价,可扩展 ShopPackageAllocation 增加 retail_price 字段 **非首期功能**: - 代理自定义售价 - 促销折扣价 --- ### 3.1 佣金配置版本快照 **决策**:订单创建时快照当时的佣金配置版本 **新增字段**: ```go type Order struct { // ... 现有字段 // 🆕 佣金配置版本快照 CommissionConfigVersion int `gorm:"column:commission_config_version;comment:佣金配置版本"` } ``` **理由**: - 佣金配置可能随时调整(基础返佣、一次性佣金等) - 订单创建时锁定配置版本,确保历史订单的佣金计算依据可追溯 - 使用 ShopSeriesAllocationConfig 表查询特定版本的配置 **查询示例**: ```go // 订单创建时 config := allocationConfigStore.GetEffective(allocationID, time.Now()) order.CommissionConfigVersion = config.Version // 佣金计算时 config := allocationConfigStore.GetByVersion(allocationID, order.CommissionConfigVersion) // 使用 config 中的返佣配置计算佣金 ``` --- ### 4. 购买权限验证 **决策**:多层验证 ```go func ValidatePurchase(card/device, packageID) error { // 1. 获取卡/设备的 series_allocation_id allocationID := card.SeriesAllocationID if allocationID == 0 { return "该卡未关联套餐系列" } // 2. 获取套餐信息 pkg := GetPackage(packageID) // 3. 验证套餐属于该系列 allocation := GetAllocation(allocationID) if pkg.SeriesID != allocation.SeriesID { return "该套餐不在可购买范围内" } // 4. 验证套餐状态 if pkg.Status != 1 || pkg.ShelfStatus != 1 { return "该套餐已下架" } return nil } ``` ### 5. 支付流程 **决策**:同步钱包支付 + 异步第三方支付 ``` 钱包支付流程: 1. 创建订单(待支付) 2. 检查钱包余额 3. 扣减钱包余额(事务) 4. 更新订单状态(已支付) 5. 套餐生效 6. 触发佣金计算(异步) 第三方支付流程: 1. 创建订单(待支付) 2. 返回订单信息,前端发起支付 3. 支付回调更新订单状态 4. 套餐生效 5. 触发佣金计算(异步) ``` ### 6. 套餐生效逻辑 **决策**:创建 PackageUsage 记录 + 更新销售统计 ```go func ActivatePackage(order *Order) { for _, item := range order.Items { pkg := GetPackage(item.PackageID) // 1. 创建套餐使用记录 usage := &PackageUsage{ OrderID: order.ID, PackageID: item.PackageID, UsageType: order.OrderType, // single_card 或 device IotCardID: order.IotCardID, DeviceID: order.DeviceID, DataLimitMB: pkg.DataAmountMB, ActivatedAt: time.Now(), ExpiresAt: time.Now().AddDate(0, pkg.DurationMonths, 0), Status: 1, // 生效中 } CreatePackageUsage(usage) // 2. 🆕 更新销售统计(用于一次性佣金的梯度判断) allocationID := GetAllocationIDByPackage(order, item.PackageID) if allocationID > 0 { commissionStatsService.UpdateStats(ctx, allocationID, "all_time", 1, item.Amount) } } } ``` **关键说明**: - 套餐生效后,更新 ShopSeriesCommissionStats 表 - 统计维度:allocationID(该系列分配的累计销量和销售额) - 统计类型:"all_time" 表示永久累计(不按周期重置) - 一次性佣金的梯度判断依赖此统计数据 ### 7. API 设计 ``` # 订单管理(后台) POST /api/admin/orders 代理创建订单 GET /api/admin/orders 订单列表 GET /api/admin/orders/:id 订单详情 POST /api/admin/orders/:id/cancel 取消订单 # 订单操作(H5/个人客户) POST /api/h5/orders 个人客户创建订单 GET /api/h5/orders 我的订单列表 GET /api/h5/orders/:id 订单详情 POST /api/h5/orders/:id/pay 钱包支付 # 支付回调 POST /api/callback/wechat-pay 微信支付回调 POST /api/callback/alipay 支付宝回调 ``` ## Risks / Trade-offs ### 风险 1:并发支付 **风险**:同一订单被重复支付 **缓解**: - 支付前检查订单状态 - 使用数据库乐观锁或 Redis 分布式锁 - 支付回调幂等处理 ### 风险 2:套餐生效失败 **风险**:支付成功但套餐生效失败 **缓解**: - 使用事务保证支付和套餐生效原子性 - 失败时自动退款或人工处理 - 记录详细日志便于排查 ### 风险 3:价格不一致 **风险**:下单时和支付时套餐价格变化 **缓解**: - 订单中存储下单时的价格快照 - 支付时使用订单金额,不重新查询套餐价格 ## Open Questions 1. **订单超时取消?** - 当前设计:不自动取消 - 待确认:是否需要定时任务取消超时未支付订单? 2. **部分支付?** - 当前设计:不支持 - 待确认:是否需要支持钱包余额不足时组合支付? 3. **代理为终端用户购买?** - 当前设计:代理只能为自己店铺购买 - 待确认:是否需要代理帮终端用户购买的场景?