## 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. **代理为终端用户购买？**
   - 当前设计：代理只能为自己店铺购买
   - 待确认：是否需要代理帮终端用户购买的场景？
