# 订单激活幂等性修复 - 设计

## 目标

1. 同一订单被重复支付/重复回调/重复请求时，只会激活一次套餐使用记录。
2. 重复请求返回"幂等成功"（不报错，不重复激活）。
3. 避免因并发导致的重复插入。

## 方案

### 1) 以状态机作为幂等门闸

将订单支付状态从 `pending` 变更为 `paid` 时使用条件更新：

- `UPDATE tb_order SET payment_status=paid,... WHERE id=? AND payment_status=pending`
- 若 `RowsAffected == 0`：
  - 视为订单已被处理（可能已支付/已取消/已退款）
  - 对"已支付"场景直接返回成功（幂等成功）
  - 对"非待支付且非已支付"场景返回对应业务错误（例如已取消不允许支付）

这样可以确保并发下只有一个请求能"拿到激活资格"。

### 2) 激活逻辑只在首次成功支付后执行

`activatePackage` 只在上述条件更新成功后执行；并确保激活过程内的数据读取使用同一个事务 `tx`（避免出现读取不一致或部分写入）。

### 3) 防御性约束（可选但推荐）

为 `tb_package_usage` 增加唯一约束（示例）：
- 同一订单下，同一 `package_id` 只能有一条 usage
- 以 `order_id + package_id` 为主（按当前业务：一个订单对应一个资源，且一次购买不应重复同套餐）

如果未来允许同订单同套餐多份购买，则需要同时引入 `quantity` 或 usage 的明细拆分策略，再调整唯一约束。

## 错误处理规范

### 支付状态常量

使用 `internal/model/order.go` 中已定义的常量：

```go
model.PaymentStatusPending   = 1 // 待支付
model.PaymentStatusPaid      = 2 // 已支付
model.PaymentStatusCancelled = 3 // 已取消
model.PaymentStatusRefunded  = 4 // 已退款
```

### 错误码定义

使用 `pkg/errors/` 中已有的错误码：

| 场景 | 错误码 | 错误消息 |
|------|--------|---------|
| 幂等成功（订单已支付） | 0 | 订单已支付（幂等成功） |
| 订单已取消 | 1050 (CodeInvalidStatus) | 订单已取消，无法支付 |
| 订单已退款 | 1050 (CodeInvalidStatus) | 订单已退款，无法支付 |

**示例代码**：

```go
// 条件更新支付状态
result := tx.Model(&model.Order{}).
    Where("id = ? AND payment_status = ?", orderID, model.PaymentStatusPending).
    Updates(map[string]interface{}{
        "payment_status": model.PaymentStatusPaid,
        "paid_at":        time.Now(),
    })

if result.Error != nil {
    return errors.Wrap(errors.CodeDatabaseError, result.Error, "更新订单支付状态失败")
}

// 检查是否更新成功
if result.RowsAffected == 0 {
    // 重新查询订单状态
    var order model.Order
    if err := tx.First(&order, orderID).Error; err != nil {
        return errors.Wrap(errors.CodeDatabaseError, err, "查询订单失败")
    }
    
    // 根据当前状态返回对应错误
    switch order.PaymentStatus {
    case model.PaymentStatusPaid:
        // 幂等成功：订单已支付，直接返回 nil（不重复激活）
        return nil
    case model.PaymentStatusCancelled:
        return errors.New(errors.CodeInvalidStatus, "订单已取消，无法支付")
    case model.PaymentStatusRefunded:
        return errors.New(errors.CodeInvalidStatus, "订单已退款，无法支付")
    default:
        return errors.New(errors.CodeInvalidStatus, "订单状态异常")
    }
}

// 只有首次支付成功才执行激活
return s.activatePackage(ctx, tx, order)
```

## 验收标准

- 重复调用钱包支付/支付回调接口，不会重复生成 `tb_package_usage` 记录。
- 幂等重复请求返回成功（错误码 0）。
- 已取消/已退款订单返回明确业务错误（错误码 1050）。
- 新增测试通过。

