# Admin Order Creation

## Purpose

后台订单创建流程，为代理和平台账号提供订单创建功能。与 H5 端订单创建的核心区别：后台仅支持 wallet/offline 支付方式，且 wallet 支付立即完成扣款和套餐激活（一步到位），不创建待支付订单。

This capability supports:
- 参数验证和支付方式限制
- 钱包余额检查和一步扣款
- 权限校验（代理、平台、超管）
- 错误处理和防御性编程

## ADDED Requirements

### Requirement: 后台订单创建 API 参数验证

系统 SHALL 在后台订单创建 API 中强制验证请求参数，拒绝非法的支付方式。

后台订单创建使用独立的 DTO（`CreateAdminOrderRequest`），仅允许 `wallet` 和 `offline` 两种支付方式。Handler 层 MUST 调用 `middleware.ValidateStruct(&req)` 验证参数，确保 DTO 的 `validate:"oneof=wallet offline"` 规则生效。

#### Scenario: DTO 验证拒绝非法支付方式

- **WHEN** 后台创建订单请求中 `payment_method` 为 `wechat` 或 `alipay`
- **THEN** 系统在 Handler 层验证失败，返回错误"请求参数解析失败"（`CodeInvalidParam`），订单创建失败

#### Scenario: DTO 验证拒绝空支付方式

- **WHEN** 后台创建订单请求中缺少 `payment_method` 字段或值为空字符串
- **THEN** 系统在 Handler 层验证失败，返回错误"请求参数解析失败"（`CodeInvalidParam`），订单创建失败

#### Scenario: DTO 验证允许 wallet 支付

- **WHEN** 后台创建订单请求中 `payment_method` 为 `wallet`
- **THEN** 系统通过 DTO 验证，继续后续业务逻辑

#### Scenario: DTO 验证允许 offline 支付

- **WHEN** 后台创建订单请求中 `payment_method` 为 `offline`
- **THEN** 系统通过 DTO 验证，继续后续业务逻辑

---

### Requirement: 后台订单创建权限检查

系统 SHALL 在后台订单创建时完整检查支付方式权限，所有支付方式（包括非法的）都必须经过权限校验。

权限规则：
- `offline` 支付：仅超管和平台账号可用
- `wallet` 支付：代理、平台、超管均可用
- 其他支付方式：一律拒绝（兜底检查）

#### Scenario: 超管可以使用 offline 支付

- **WHEN** 超管账号创建订单，支付方式为 `offline`
- **THEN** 系统通过权限检查，继续创建订单

#### Scenario: 平台账号可以使用 offline 支付

- **WHEN** 平台账号创建订单，支付方式为 `offline`
- **THEN** 系统通过权限检查，继续创建订单

#### Scenario: 代理账号不能使用 offline 支付

- **WHEN** 代理账号创建订单，支付方式为 `offline`
- **THEN** 系统返回错误"只有平台可以使用线下支付"（`CodeForbidden`），订单创建失败

#### Scenario: 代理账号可以使用 wallet 支付

- **WHEN** 代理账号创建订单，支付方式为 `wallet`，钱包余额充足
- **THEN** 系统通过权限检查，继续创建订单

#### Scenario: 兜底检查拒绝其他支付方式

- **WHEN** 后台创建订单请求中 `payment_method` 为 `wechat`（虽然 DTO 验证应该已拒绝，但作为防御性编程）
- **THEN** 系统在 Handler 层返回错误"后台仅支持钱包支付或线下支付"（`CodeInvalidParam`）

---

### Requirement: 后台 wallet 订单一步到位

系统 SHALL 在后台创建 wallet 订单时立即完成余额扣款和套餐激活，不创建待支付订单。订单创建成功后 `payment_status` MUST 为 2（已支付）。

与 H5 端的核心区别：
- **后台**：检查余额 → 扣款 → 创建已支付订单 → 激活套餐（一步完成）
- **H5 端**：冻结余额 → 创建待支付订单 → 用户调用支付接口 → 扣款 + 激活（两步流程）

#### Scenario: 后台 wallet 订单立即扣款

- **WHEN** 代理在后台创建订单，支付方式为 `wallet`，钱包余额 5000 分，订单金额 3000 分
- **THEN** 系统立即扣减钱包余额 3000 分，余额变为 2000 分，创建订单时 `payment_status` = 2，`paid_at` 为当前时间

#### Scenario: 后台 wallet 订单立即激活套餐

- **WHEN** 代理在后台创建 wallet 订单成功
- **THEN** 系统在同一事务中创建 `PackageUsage` 记录，套餐状态为已激活

#### Scenario: 后台 wallet 订单不创建待支付状态

- **WHEN** 代理在后台创建 wallet 订单
- **THEN** 系统不创建 `payment_status` = 1（待支付）的订单，订单创建后立即为已支付状态

#### Scenario: 后台 wallet 订单余额不足直接拒绝

- **WHEN** 代理在后台创建 wallet 订单，钱包余额 1000 分，订单金额 3000 分
- **THEN** 系统在事务外快速检查余额，返回错误"余额不足"（`CodeInsufficientBalance`），订单创建失败

#### Scenario: 后台 wallet 订单事务保证

- **WHEN** 代理在后台创建 wallet 订单
- **THEN** 订单创建、余额扣减、套餐激活在同一事务中完成，任一步骤失败则全部回滚

---

### Requirement: 后台 offline 订单立即激活

系统 SHALL 在后台创建 offline 订单时立即激活套餐，不扣减钱包余额。订单创建成功后 `payment_status` MUST 为 2（已支付）。

#### Scenario: 平台创建 offline 订单立即激活

- **WHEN** 平台账号创建订单，支付方式为 `offline`
- **THEN** 系统创建订单时 `payment_status` = 2，`paid_at` 为当前时间，立即激活套餐

#### Scenario: offline 订单不扣钱包

- **WHEN** 平台账号创建 offline 订单
- **THEN** 系统不扣减任何钱包余额（因为是线下支付）

#### Scenario: offline 订单不检查余额

- **WHEN** 平台账号创建 offline 订单，钱包余额为 0
- **THEN** 系统仍然创建订单成功（因为线下支付不依赖钱包）

---

### Requirement: 后台订单创建错误处理

系统 SHALL 在后台订单创建失败时返回明确的错误信息，不泄露底层细节。

错误码使用规范：
- 参数验证失败：`CodeInvalidParam`（不泄露具体校验错误）
- 权限不足：`CodeForbidden`
- 余额不足：`CodeInsufficientBalance`
- 钱包不存在：`CodeWalletNotFound`
- 其他错误：`CodeInternalError`

#### Scenario: 参数验证失败不泄露细节

- **WHEN** 后台创建订单请求参数验证失败（如支付方式非法）
- **THEN** 系统返回 `CodeInvalidParam` 错误码，错误消息为通用的"请求参数解析失败"，不包含具体的 validator 错误信息

#### Scenario: 钱包余额不足返回明确错误

- **WHEN** 代理创建 wallet 订单，余额不足
- **THEN** 系统返回 `CodeInsufficientBalance` 错误码，错误消息为"余额不足"

#### Scenario: 钱包不存在返回明确错误

- **WHEN** 代理创建 wallet 订单，钱包不存在
- **THEN** 系统返回 `CodeWalletNotFound` 错误码，错误消息为"钱包不存在"

#### Scenario: 套餐激活失败回滚并返回错误

- **WHEN** 后台创建订单时余额扣减成功但套餐激活失败
- **THEN** 事务回滚，钱包余额恢复，返回套餐激活失败错误（`CodeInternalError`）

---

### Requirement: 后台订单创建防重复

系统 SHALL 使用幂等性检查防止同一订单重复创建和重复扣款。

幂等性策略：
- 使用 Redis 业务键：`order:idempotency:{buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}`
- TTL：3 分钟
- 分布式锁：`order:create:lock:{carrier_type}:{carrier_id}`，TTL 10 秒

#### Scenario: 重复创建订单返回已创建结果

- **WHEN** 代理在后台对同一张卡的同一套餐组合在 3 分钟内重复创建订单
- **THEN** 系统返回第一次创建的订单信息，不重复扣款

#### Scenario: 并发创建订单使用分布式锁

- **WHEN** 两个请求同时为同一张卡创建订单
- **THEN** 只有一个请求获取到分布式锁并创建订单，另一个请求返回"操作进行中，请勿重复提交"（`CodeTooManyRequests`）

#### Scenario: 幂等性 key 超时后可重新创建

- **WHEN** 订单创建成功 3 分钟后，代理再次创建相同订单
- **THEN** 系统创建新订单（因为幂等性 key 已过期）

---

### Requirement: 后台订单 API 响应格式

系统 SHALL 在后台订单创建成功后返回完整的订单信息，包含支付状态、实际支付金额、操作者信息等。

响应字段（`OrderResponse`）：
- `id`：订单 ID
- `order_no`：订单号
- `payment_status`：支付状态（后台订单必为 2-已支付）
- `payment_method`：支付方式（wallet 或 offline）
- `paid_at`：支付时间（不为 NULL）
- `total_amount`：订单总金额
- `actual_paid_amount`：实际支付金额（仅 wallet 有值）
- `operator_id`：操作者 ID
- `operator_type`：操作者类型（agent/platform）
- `purchase_role`：购买角色（self_purchase/purchase_for_subordinate/purchased_by_platform）

#### Scenario: wallet 订单响应包含实际支付金额

- **WHEN** 代理在后台创建 wallet 订单成功
- **THEN** 响应包含 `actual_paid_amount` 字段，值为实际扣减的钱包金额

#### Scenario: offline 订单响应不包含实际支付金额

- **WHEN** 平台创建 offline 订单成功
- **THEN** 响应的 `actual_paid_amount` 字段为 NULL（因为线下支付不扣钱包）

#### Scenario: 代购订单响应包含操作者信息

- **WHEN** 上级代理为下级代理购买套餐
- **THEN** 响应包含 `operator_id`（上级店铺 ID）、`operator_type` = "agent"、`purchase_role` = "purchase_for_subordinate"

---

### Requirement: 后台订单创建与 H5 端隔离

系统 SHALL 使用独立的 Service 方法处理后台订单创建，避免与 H5 端订单创建逻辑混淆。

架构设计：
- 后台：`OrderHandler.Create()` → `OrderService.CreateAdminOrder()`
- H5 端：`OrderHandler.Create()` → `OrderService.CreateH5Order()`

#### Scenario: 后台调用独立的 Service 方法

- **WHEN** 后台创建订单
- **THEN** Handler 层调用 `OrderService.CreateAdminOrder()` 方法，不调用通用的 `Create()` 方法

#### Scenario: H5 端调用独立的 Service 方法

- **WHEN** H5 端创建订单
- **THEN** Handler 层调用 `OrderService.CreateH5Order()` 方法，不影响后台订单创建逻辑

#### Scenario: Service 方法命名明确职责

- **WHEN** 开发人员查看代码
- **THEN** 方法命名（`CreateAdminOrder` vs `CreateH5Order`）清楚表明了后台和 H5 端的差异，防止误用

---

## MODIFIED Requirements

### Requirement: 后台订单钱包支付权限校验
后台创建订单时，若支付方式为钱包支付，Handler 层 SHALL 校验当前操作人为代理账号（`user_type = agent`）。非代理账号使用钱包支付 SHALL 在 Handler 层即被拦截，返回参数错误。

#### Scenario: 非代理账号使用钱包支付被拦截
- **WHEN** 平台用户或企业用户提交订单且 `payment_method = wallet`
- **THEN** Handler SHALL 返回 `CodeInvalidParam` 错误，提示"仅代理账号可使用钱包支付"，请求不会到达 Service 层

#### Scenario: 代理账号使用钱包支付正常通过
- **WHEN** 代理账号提交订单且 `payment_method = wallet`
- **THEN** Handler SHALL 放行，由 Service 层继续处理钱包扣款逻辑
