# 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 端的差异,防止误用