fix: 修复代理钱包订单创建逻辑,拆分后台/H5端下单方法并归档变更
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 6m54s
- 拆分订单创建为 CreateAdminOrder(后台一步支付)和 CreateH5Order(H5 两步支付) - 新增 CreateAdminOrderRequest DTO,后台仅允许 wallet/offline 支付方式 - 同步 delta specs 到主规格(order-payment 更新 + admin-order-creation 新增) - 归档 fix-agent-wallet-order-creation 变更 - 新增 implement-order-expiration 变更提案
This commit is contained in:
248
openspec/specs/admin-order-creation/spec.md
Normal file
248
openspec/specs/admin-order-creation/spec.md
Normal file
@@ -0,0 +1,248 @@
|
||||
# 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 端的差异,防止误用
|
||||
Reference in New Issue
Block a user