Files
junhong_cmp_fiber/openspec/changes/add-order-payment/design.md
huang 79c061b6fa
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 5m24s
feat: 实现套餐管理模块,包含套餐系列、双状态管理、废弃模型清理
- 新增套餐系列管理 (CRUD + 状态切换)
- 新增套餐管理 (CRUD + 启用/禁用 + 上架/下架双状态)
- 清理 8 个废弃分佣模型及对应数据库表
- Package 模型新增建议成本价、建议售价、上架状态字段
- 完整的 Store/Service/Handler 三层实现
- 包含单元测试和集成测试
- 归档 add-package-module change
- 新增多个 OpenSpec changes (订单支付、店铺套餐分配、一次性分佣、卡设备系列绑定)
2026-01-27 19:55:47 +08:00

6.7 KiB
Raw Blame History

Context

Phase 3 完成了卡/设备的套餐系列关联,现在需要实现订单和支付流程。核心是"强充"机制:用户必须通过购买套餐来充值,不能直接给钱包充值。这确保每笔资金流入都有对应的套餐购买记录。

三类买家

  1. 个人客户:通过 H5/小程序购买,使用卡/设备钱包或第三方支付
  2. 代理商:通过后台购买,使用店铺钱包
  3. 企业客户:后台直接分配套餐,不走订单流程(本期不做)

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
代理为店铺购买:支付金额 = 代理的成本价(用于囤货/测试)

理由

  • 简化首期实现,所有终端用户统一售价
  • 代理的利润 = suggested_retail_price - 成本价
  • 后续如需支持代理自定义售价,可扩展 ShopPackageAllocation 增加 retail_price 字段

非首期功能

  • 代理自定义售价
  • 促销折扣价

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)
        
        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)
    }
}

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. 代理为终端用户购买?

    • 当前设计:代理只能为自己店铺购买
    • 待确认:是否需要代理帮终端用户购买的场景?