# 订单激活幂等性修复 - 设计 ## 目标 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)。 - 新增测试通过。