All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m36s
- 新增订单管理、支付回调、购买验证等核心服务 - 实现订单、订单项目的数据存储层和 API 接口 - 添加订单数据库迁移和 DTO 定义 - 更新 API 文档和路由配置 - 同步 3 个新规范到主规范库(订单管理、订单支付、套餐购买验证) - 完成 OpenSpec 变更归档 Ultraworked with Sisyphus
8.2 KiB
8.2 KiB
Context
Phase 3 完成了卡/设备的套餐系列关联,现在需要实现订单和支付流程。核心是"强充"机制:用户必须通过购买套餐来充值,不能直接给钱包充值。这确保每笔资金流入都有对应的套餐购买记录。
三类买家:
- 个人客户:通过 H5/小程序购买,使用卡/设备钱包或第三方支付
- 代理商:通过后台购买,使用店铺钱包
- 企业客户:后台直接分配套餐,不走订单流程(本期不做)
Goals / Non-Goals
Goals:
- 设计订单和订单明细模型
- 实现套餐购买订单创建流程
- 实现钱包支付和第三方支付回调
- 验证购买权限(卡/设备的套餐系列关联)
- 套餐生效后更新流量额度
Non-Goals:
- 不实现企业客户的套餐分配(后台直接操作)
- 不实现第三方支付发起(仅处理回调)
- 不实现佣金计算(Phase 5)
- 不实现退款流程
Decisions
1. 订单模型设计
决策:Order + OrderItem 两级结构
// 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 佣金配置版本快照
决策:订单创建时快照当时的佣金配置版本
新增字段:
type Order struct {
// ... 现有字段
// 🆕 佣金配置版本快照
CommissionConfigVersion int `gorm:"column:commission_config_version;comment:佣金配置版本"`
}
理由:
- 佣金配置可能随时调整(基础返佣、一次性佣金等)
- 订单创建时锁定配置版本,确保历史订单的佣金计算依据可追溯
- 使用 ShopSeriesAllocationConfig 表查询特定版本的配置
查询示例:
// 订单创建时
config := allocationConfigStore.GetEffective(allocationID, time.Now())
order.CommissionConfigVersion = config.Version
// 佣金计算时
config := allocationConfigStore.GetByVersion(allocationID, order.CommissionConfigVersion)
// 使用 config 中的返佣配置计算佣金
4. 购买权限验证
决策:多层验证
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 记录 + 更新销售统计
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
-
订单超时取消?
- 当前设计:不自动取消
- 待确认:是否需要定时任务取消超时未支付订单?
-
部分支付?
- 当前设计:不支持
- 待确认:是否需要支持钱包余额不足时组合支付?
-
代理为终端用户购买?
- 当前设计:代理只能为自己店铺购买
- 待确认:是否需要代理帮终端用户购买的场景?