## Context

### 当前问题分析

当前代理在后台创建订单时存在三层架构缺陷：

**第一层：Handler 层缺少参数验证**
- `internal/handler/admin/order.go:27-29` 只调用了 `c.BodyParser(&req)`，没有调用 `middleware.ValidateStruct(&req)`
- 导致 DTO 中定义的 `validate:"required,oneof=wallet offline"` 规则完全失效
- 用户可以传入任何 `payment_method` 值（如 `wechat`、`alipay`），绕过验证

**第二层：Handler 层权限检查不完整**
- `internal/handler/admin/order.go:35-43` 只检查了 `offline` 和 `wallet` 两种支付方式的权限
- `else` 分支没有任何检查，导致其他支付方式绕过权限校验
- 前端如果没有传 `payment_method`（空字符串或未定义），也会进入 `else` 分支

**第三层：Service 层后台和 H5 端共用方法**
- `internal/service/order/service.go:88-318` 的 `Create()` 方法同时服务后台和 H5 端
- `Create()` 方法的 `else` 分支（第 310-317 行）是为 H5 端在线支付（wechat/alipay）准备的，创建待支付订单
- 后台误用这个分支会导致创建待支付订单，但后台没有支付界面，订单永远无法完成

### 现有代码结构

```
admin/order.Create() ─┐
                      ├─→ service.Create(buyerType, buyerID) ← 同一个方法
h5/order.Create() ────┘
                          ├─ if offline → 平台代购（激活）
                          ├─ else if wallet → 钱包支付（扣款+激活）
                          └─ else → 创建待支付订单 ← 后台误用这里
```

### 业务约束

- **后台订单创建**：代理/平台账号使用，仅支持 wallet/offline 支付，立即扣款或激活，不允许待支付状态
- **H5 端订单创建**：C 端用户使用，支持 wallet/wechat/alipay 支付，支持待支付状态（等待用户支付）

---

## Goals / Non-Goals

**Goals:**

1. **修复 Handler 层参数验证**：确保 DTO 的 `validate` 规则生效
2. **修复 Handler 层权限检查**：完整覆盖所有支付方式的权限校验
3. **拆分 Service 方法**：后台和 H5 端使用独立的 Service 方法，避免逻辑混淆
4. **后台订单一步到位**：wallet 支付立即扣款，offline 支付立即激活，不创建待支付订单
5. **保持 H5 端行为不变**：不影响 H5 端订单创建流程
6. **向后兼容**：保留回滚方案，避免破坏现有功能

**Non-Goals:**

1. 修改订单数据模型（无需新增字段）
2. 修改支付方式枚举（保持现有 wallet/wechat/alipay/offline）
3. 修改 H5 端订单创建逻辑（只拆分，不改逻辑）
4. 重构整个订单系统（仅针对性修复代理钱包订单问题）

---

## Decisions

### Decision 1: Service 方法拆分策略

**选择**: 拆分 `OrderService.Create()` 为两个独立方法

**方案对比**:

| 方案 | 优点 | 缺点 |
|------|------|------|
| **方案 A：在现有 `Create()` 中添加 `isAdminContext` 参数** | 改动最小，无 breaking change | 逻辑更复杂，if-else 嵌套增加，难以维护 |
| **方案 B：拆分为 `CreateAdminOrder()` 和 `CreateH5Order()`** ✅ | 逻辑清晰，职责分离，易于测试和维护 | breaking change，需要修改 Handler 层调用 |
| **方案 C：保留 `Create()` 方法，内部调用不同的私有方法** | 对外 API 不变 | 隐藏了后台和 H5 的差异，仍然可能误用 |

**选择方案 B** 的理由：
- 后台和 H5 端的订单创建逻辑差异巨大（后台立即扣款 vs H5 待支付），应该使用不同的方法
- 明确的方法名（`CreateAdminOrder` vs `CreateH5Order`）可以防止误用
- 未来如果需要进一步修改后台订单逻辑，不会影响 H5 端
- breaking change 影响范围可控（只有 2 个 Handler 文件）

**实现细节**:

```go
// CreateAdminOrder 后台订单创建（仅支持 wallet/offline，立即扣款或激活）
func (s *Service) CreateAdminOrder(ctx context.Context, req *dto.CreateAdminOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
    // 1. 验证购买合法性（复用现有逻辑）
    validationResult, err := s.validatePurchase(ctx, req.OrderType, req.IotCardID, req.DeviceID, req.PackageIDs)

    // 2. 幂等性检查（复用现有逻辑）
    existingOrderID, err := s.checkOrderIdempotency(...)

    // 3. 根据支付方式路由
    if req.PaymentMethod == model.PaymentMethodOffline {
        // 平台代购：创建订单并立即激活套餐
        return s.createOrderWithActivation(ctx, order, items)
    } else if req.PaymentMethod == model.PaymentMethodWallet {
        // 钱包支付：检查余额 → 扣款 → 创建已支付订单 → 激活套餐
        return s.createOrderWithWalletPayment(ctx, order, items, operatorShopID, buyerShopID)
    } else {
        // 不应该到这里（DTO 验证已拒绝其他支付方式）
        return nil, errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
    }
}

// CreateH5Order H5 端订单创建（支持 wallet/wechat/alipay，支持待支付状态）
func (s *Service) CreateH5Order(ctx context.Context, req *dto.CreateOrderRequest, buyerType string, buyerID uint) (*dto.OrderResponse, error) {
    // 保留原 Create() 方法的完整逻辑
    // ...
}
```

**迁移策略**:
1. 新增 `CreateAdminOrder()` 和 `CreateH5Order()` 方法
2. 将原 `Create()` 方法重命名为 `CreateLegacy()`（保留作为回滚方案）
3. 修改 Handler 层调用新方法
4. 测试验证后删除 `CreateLegacy()` 方法

---

### Decision 2: DTO 设计

**选择**: 创建新的 `CreateAdminOrderRequest` DTO，保留现有 `CreateOrderRequest`

**理由**:
- 后台和 H5 端的参数验证规则不同（`oneof=wallet offline` vs `oneof=wallet wechat alipay`）
- 使用不同的 DTO 可以在类型层面保证后台不会传入非法支付方式
- 符合"类型安全"原则，编译期就能发现错误

**DTO 定义**:

```go
// CreateAdminOrderRequest 后台订单创建请求（仅允许 wallet/offline）
type CreateAdminOrderRequest struct {
    OrderType     string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
    IotCardID     *uint  `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
    DeviceID      *uint  `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
    PackageIDs    []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
    PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet offline" required:"true" description:"支付方式 (wallet:钱包支付, offline:线下支付)"`
}

// CreateOrderRequest H5 端订单创建请求（保持不变）
type CreateOrderRequest struct {
    OrderType     string `json:"order_type" validate:"required,oneof=single_card device" required:"true" description:"订单类型 (single_card:单卡购买, device:设备购买)"`
    IotCardID     *uint  `json:"iot_card_id" validate:"required_if=OrderType single_card" description:"IoT卡ID(单卡购买时必填)"`
    DeviceID      *uint  `json:"device_id" validate:"required_if=OrderType device" description:"设备ID(设备购买时必填)"`
    PackageIDs    []uint `json:"package_ids" validate:"required,min=1,max=10,dive,min=1" required:"true" minItems:"1" maxItems:"10" description:"套餐ID列表"`
    PaymentMethod string `json:"payment_method" validate:"required,oneof=wallet wechat alipay" required:"true" description:"支付方式 (wallet:钱包支付, wechat:微信支付, alipay:支付宝支付)"`
}
```

**替代方案及为何不选**:
- ~~使用同一个 DTO，在 Handler 层校验支付方式~~：无法利用类型系统，容易漏掉校验
- ~~使用 interface{} 类型~~：失去类型安全，违反 Go 最佳实践

---

### Decision 3: Handler 层参数验证和权限检查

**选择**: 在 Handler 层完整实现参数验证和权限检查

**修复点 1：添加参数验证**

```go
func (h *OrderHandler) Create(c *fiber.Ctx) error {
    var req dto.CreateAdminOrderRequest
    if err := c.BodyParser(&req); err != nil {
        return errors.New(errors.CodeInvalidParam, "请求参数解析失败")
    }

    // ← 添加验证（关键！）
    if err := middleware.ValidateStruct(&req); err != nil {
        return errors.New(errors.CodeInvalidParam)
    }

    // ... 后续逻辑
}
```

**修复点 2：完整的权限检查**

```go
// 检查支付方式权限
if req.PaymentMethod == model.PaymentMethodOffline {
    if userType != constants.UserTypeSuperAdmin && userType != constants.UserTypePlatform {
        return errors.New(errors.CodeForbidden, "只有平台可以使用线下支付")
    }
} else if req.PaymentMethod == model.PaymentMethodWallet {
    if userType != constants.UserTypeAgent && userType != constants.UserTypePlatform && userType != constants.UserTypeSuperAdmin {
        return errors.New(errors.CodeForbidden, "无权创建订单")
    }
} else {
    // ← 添加兜底检查（防御性编程）
    return errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")
}
```

**理由**:
- 参数验证是 Handler 层的职责（参考 CLAUDE.md 架构规范）
- 兜底检查可以防止未来新增支付方式时漏掉权限校验
- 多层防护：DTO 验证（第一道防线） + Handler 检查（第二道防线） + Service 检查（第三道防线）

---

### Decision 4: 错误处理策略

**选择**: 使用统一错误码，添加新错误码（如需要）

**新增错误码**（如果需要）:

```go
// pkg/errors/errors.go
const (
    // ... 现有错误码
    CodeInvalidPaymentMethodForAdmin = 40008 // 后台不支持的支付方式
)
```

**错误返回规范**:
- Handler 层参数验证失败：`errors.New(errors.CodeInvalidParam)`（不泄露细节）
- Service 层钱包余额不足：`errors.New(errors.CodeInsufficientBalance, "余额不足")`
- Service 层支付方式非法：`errors.New(errors.CodeInvalidParam, "后台仅支持钱包支付或线下支付")`

**理由**:
- 遵循项目错误处理规范（参考 CLAUDE.md）
- Handler 层不直接返回底层错误，防止信息泄露

---

### Decision 5: 向后兼容和回滚策略

**选择**: 保留原 `Create()` 方法作为 `CreateLegacy()`，灰度发布

**迁移步骤**:
1. **Phase 1**: 新增 `CreateAdminOrder()` 和 `CreateH5Order()` 方法
2. **Phase 2**: 将原 `Create()` 重命名为 `CreateLegacy()`（保留，暂不调用）
3. **Phase 3**: 修改 Handler 层调用新方法
4. **Phase 4**: 测试环境验证
5. **Phase 5**: 生产环境灰度发布（先 1% 流量，观察 1 天，再逐步放量）
6. **Phase 6**: 全量上线后观察 1 周，无问题后删除 `CreateLegacy()` 方法

**回滚方案**:
- 如果出现问题，立即修改 Handler 层调用 `CreateLegacy()` 方法
- 重新部署后端（5 分钟内可完成）
- 前端无需回滚（因为 DTO 结构兼容）

**理由**:
- breaking change 风险高，需要谨慎迁移
- 保留 `CreateLegacy()` 方法可以快速回滚，避免长时间故障

---

## Risks / Trade-offs

### Risk 1: 前端未同步修改导致参数缺失

**风险**: 前端没有传 `payment_method` 参数，后端拒绝请求

**影响**: 高。后台订单创建功能完全不可用

**缓解措施**:
1. 后端先部署（兼容旧前端，如果 `payment_method` 为空，默认使用 `wallet`）
2. 前端修改后再部署
3. 灰度发布，先在测试环境验证前后端联调

**检测方式**:
- 监控错误日志中 `CodeInvalidParam` 错误的数量
- 如果错误量激增，立即回滚

---

### Risk 2: Service 方法拆分导致代码重复

**风险**: `CreateAdminOrder()` 和 `CreateH5Order()` 有大量重复代码

**影响**: 中。增加维护成本

**缓解措施**:
1. 提取公共逻辑为私有方法（如 `validatePurchase()`、`checkOrderIdempotency()`、`buildOrderItems()`）
2. `CreateAdminOrder()` 和 `CreateH5Order()` 只保留差异化逻辑

**示例**:
```go
func (s *Service) CreateAdminOrder(...) (*dto.OrderResponse, error) {
    // 1. 公共逻辑（提取为私有方法）
    validationResult, err := s.validatePurchase(...)
    existingOrderID, err := s.checkOrderIdempotency(...)

    // 2. 差异化逻辑（后台独有）
    if req.PaymentMethod == model.PaymentMethodWallet {
        return s.createOrderWithWalletPayment(...)
    }
}
```

---

### Risk 3: 灰度发布失败导致部分用户无法下单

**风险**: 灰度发布过程中，部分用户使用新代码，部分用户使用旧代码，行为不一致

**影响**: 中。用户体验不一致

**缓解措施**:
1. 灰度发布按 **用户 ID** 维度（而不是随机流量），确保同一用户始终使用同一版本
2. 灰度期间密切监控错误日志和用户反馈
3. 设置灰度开关（环境变量或配置文件），可以快速回滚到旧代码

---

### Trade-off: 代码拆分 vs 逻辑复用

**选择**: 优先代码拆分（清晰的职责划分），接受一定的代码重复

**理由**:
- 后台和 H5 端的订单创建逻辑差异巨大，强行复用会导致 if-else 嵌套过深，难以维护
- "一点点代码重复" 优于 "过度抽象"（遵循 Go 惯用模式）
- 未来如果需要修改后台订单逻辑，不会影响 H5 端

---

### Trade-off: breaking change vs 保持现状

**选择**: 接受 breaking change，彻底解决问题

**理由**:
- 当前问题严重（代理可以创建永远无法完成的订单），必须彻底修复
- breaking change 影响范围可控（只有 2 个 Handler 文件）
- 保留回滚方案，风险可控

---

## Migration Plan

### Phase 1: 后端开发和测试（1 天）

1. 创建新 DTO：`CreateAdminOrderRequest`
2. 新增 Service 方法：`CreateAdminOrder()` 和 `CreateH5Order()`
3. 重命名原 `Create()` 为 `CreateLegacy()`
4. 修改 Handler 层调用新方法
5. 单元测试（覆盖率 ≥ 90%）

### Phase 2: 前端开发（0.5 天）

1. 后台订单创建界面添加 `payment_method` 下拉框（wallet/offline）
2. 修改请求参数，使用新 DTO

### Phase 3: 联调测试（0.5 天）

1. 测试环境部署后端
2. 测试环境部署前端
3. 完整测试所有场景：
   - 代理钱包支付（余额充足）
   - 代理钱包支付（余额不足）
   - 平台线下支付
   - H5 端订单创建（回归测试）

### Phase 4: 灰度发布（3 天）

1. **Day 1**: 生产环境部署后端，灰度 1% 流量
2. **Day 2**: 观察错误日志，无问题则扩大到 10%
3. **Day 3**: 扩大到 50%，无问题则全量

### Phase 5: 清理（1 天）

1. 观察 1 周，无问题后删除 `CreateLegacy()` 方法
2. 更新文档（API 文档、技术文档）

**总时长**: 约 6 天

---

### Rollback Strategy

**触发条件**:
- 错误率超过 1%
- 用户反馈无法下单
- 前端未同步上线导致大量参数缺失错误

**回滚步骤**:
1. 立即修改 Handler 层调用 `CreateLegacy()` 方法
2. 重新部署后端（5 分钟）
3. 验证功能恢复
4. 排查问题，修复后重新上线

---

## Open Questions

1. **前端是否已经传递 `payment_method` 参数？**
   - 需要确认前端当前行为
   - 如果未传递，后端需要提供默认值（wallet）以兼容旧版本

2. **是否需要新增错误码 `CodeInvalidPaymentMethodForAdmin`？**
   - 当前可以复用 `CodeInvalidParam`
   - 如果需要区分"参数格式错误"和"支付方式不支持"，可以新增

3. **灰度发布的维度是什么？**
   - 按用户 ID 灰度（推荐）
   - 按流量百分比灰度
   - 需要与运维确认灰度策略

4. **是否需要支持后台创建待支付订单（未来需求）？**
   - 当前不支持（Non-Goal）
   - 如果未来有需求，可以新增 `CreateAdminPendingOrder()` 方法
