Files
huang 817d0d6e04
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 46s
更新openspec
2026-03-17 14:22:01 +08:00

566 lines
22 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## ADDED Requirements
### Requirement: 线下支付方式
系统 SHALL 支持线下支付方式offline仅用于代购订单。线下支付的订单创建后直接标记为已支付跳过支付流程。
#### Scenario: 创建线下支付订单
- **WHEN** 平台账号创建订单时选择支付方式为 offline
- **THEN** 系统创建订单payment_status 直接设为 2已支付payment_method = "offline"
#### Scenario: 线下支付权限限制
- **WHEN** 非平台账号(代理/个人客户)尝试使用线下支付
- **THEN** 系统返回错误 "只有平台账号可以使用线下支付"
#### Scenario: 线下支付订单自动激活套餐
- **WHEN** 创建线下支付订单成功
- **THEN** 系统自动激活套餐,创建 PackageUsage 记录
#### Scenario: 线下支付不扣钱包
- **WHEN** 订单使用线下支付
- **THEN** 系统不扣减任何钱包余额
---
### Requirement: 钱包支付
系统 SHALL 支持使用钱包余额支付订单。支付成功后 MUST 扣减钱包余额并激活套餐。
#### Scenario: 钱包余额充足
- **WHEN** 买家使用钱包支付,余额充足
- **THEN** 系统扣减钱包余额,更新订单状态为已支付,创建套餐使用记录
#### Scenario: 钱包余额不足
- **WHEN** 买家使用钱包支付,余额不足
- **THEN** 系统返回错误 "钱包余额不足"
#### Scenario: 订单已支付
- **WHEN** 买家尝试支付已支付的订单
- **THEN** 系统返回错误 "订单已支付"
#### Scenario: 订单已取消
- **WHEN** 买家尝试支付已取消的订单
- **THEN** 系统返回错误 "订单已取消"
---
### Requirement: 第三方支付回调
系统 SHALL 处理微信支付和支付宝的支付回调,支持订单支付和钱包充值两种场景。回调处理 MUST 幂等。
#### Scenario: 微信支付成功回调(订单)
- **WHEN** 收到微信支付成功回调,订单号格式为 ORD 开头
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
#### Scenario: 微信支付成功回调(充值)
- **WHEN** 收到微信支付成功回调,订单号格式为 RCH 开头
- **THEN** 系统验证签名,更新充值订单状态,增加钱包余额,更新累计充值,触发佣金判断,返回成功响应
#### Scenario: 支付宝成功回调(订单)
- **WHEN** 收到支付宝支付成功回调,订单号格式为 ORD 开头
- **THEN** 系统验证签名,更新订单状态,激活套餐,返回成功响应
#### Scenario: 支付宝成功回调(充值)
- **WHEN** 收到支付宝支付成功回调,订单号格式为 RCH 开头
- **THEN** 系统验证签名,更新充值订单状态,增加钱包余额,更新累计充值,触发佣金判断,返回成功响应
#### Scenario: 重复回调
- **WHEN** 收到已处理订单的重复回调
- **THEN** 系统返回成功响应,不重复处理
#### Scenario: 签名验证失败
- **WHEN** 回调签名验证失败
- **THEN** 系统拒绝处理,返回失败响应
---
### Requirement: 套餐激活
支付成功后系统 MUST 激活套餐,创建 PackageUsage 记录。代购订单也需激活套餐,但不更新累计充值。
#### Scenario: 单卡套餐激活
- **WHEN** 单卡订单支付成功
- **THEN** 系统创建 PackageUsageusage_type 为 single_card关联 iot_card_id
#### Scenario: 设备套餐激活
- **WHEN** 设备订单支付成功
- **THEN** 系统创建 PackageUsageusage_type 为 device关联 device_id
#### Scenario: 套餐有效期计算
- **WHEN** 套餐激活
- **THEN** 有效期 = 激活时间 + 套餐时长(月)
#### Scenario: 代购订单激活套餐
- **WHEN** 代购订单is_purchase_on_behalf = true创建成功
- **THEN** 系统激活套餐,但不更新卡/设备的 accumulated_recharge
---
### Requirement: 支付事务保证
钱包支付 MUST 在事务中完成:余额扣减、订单状态更新、套餐激活。任一步骤失败则全部回滚。
#### Scenario: 事务成功
- **WHEN** 所有步骤成功
- **THEN** 事务提交,支付完成
#### Scenario: 余额扣减后套餐激活失败
- **WHEN** 余额扣减成功但套餐激活失败
- **THEN** 事务回滚,余额恢复,订单状态不变
---
### Requirement: 后台钱包一步支付
系统 SHALL 支持后台订单创建时使用钱包支付立即完成订单,无需后续调用支付接口。后台订单创建使用独立的 Service 方法(`CreateAdminOrder()`),与 H5 端的 `CreateH5Order()` 方法隔离,避免逻辑混淆。
**后台钱包支付流程**(一步到位):
1. 检查钱包余额是否充足(事务外快速失败)
2. 在事务中:扣减钱包余额 → 创建已支付订单(`payment_status` = 2→ 激活套餐
3. 返回已支付的订单信息
**与 H5 端的区别**
- 后台:立即扣款,订单创建后即为已支付状态(`payment_status` = 2
- H5 端:冻结余额,创建待支付订单(`payment_status` = 1需用户调用支付接口
#### Scenario: 后台订单创建时钱包支付
- **WHEN** 代理在后台创建订单,支付方式为 wallet钱包余额充足
- **THEN** 系统调用 `CreateAdminOrder()` 方法,创建订单,立即扣减钱包余额,订单状态为已支付(`payment_status` = 2激活套餐
#### Scenario: 后台钱包支付余额不足
- **WHEN** 代理在后台创建订单,支付方式为 wallet钱包余额不足
- **THEN** 系统调用 `CreateAdminOrder()` 方法,在事务外检查余额,返回错误"余额不足",订单创建失败
#### Scenario: 后台钱包支付订单响应
- **WHEN** 后台钱包支付订单创建成功
- **THEN** API 响应包含已支付的订单信息,`payment_status` = 2`payment_method` = "wallet"`paid_at` 为当前时间
#### Scenario: 后台钱包支付不创建待支付订单
- **WHEN** 代理在后台创建 wallet 订单
- **THEN** 系统不创建待支付订单(`payment_status` != 1直接完成支付和套餐激活
#### Scenario: 后台钱包支付使用独立方法
- **WHEN** 代理在后台创建 wallet 订单
- **THEN** Handler 层调用 `OrderService.CreateAdminOrder()` 方法,不调用通用的 `Create()``CreateH5Order()` 方法
---
### Requirement: H5 钱包两步支付保持不变
系统 SHALL 保持 H5 端钱包支付的两步流程(创建待支付订单 → 调用支付接口。H5 端订单创建使用独立的 Service 方法(`CreateH5Order()`),与后台的 `CreateAdminOrder()` 方法隔离。
**H5 钱包支付流程**(两步流程):
1. 创建订单:冻结钱包余额 → 创建待支付订单(`payment_status` = 1
2. 用户调用支付接口:扣减钱包余额 → 更新订单状态为已支付 → 激活套餐
**与后台的区别**
- H5 端:创建待支付订单,用户需调用支付接口完成支付
- 后台:立即扣款,订单创建后即为已支付状态
#### Scenario: H5 创建待支付订单
- **WHEN** 个人客户在 H5 端创建订单,支付方式为 wallet
- **THEN** 系统调用 `CreateH5Order()` 方法,创建订单,`payment_status` = 1待支付冻结钱包余额不立即扣款
#### Scenario: H5 调用 WalletPay 接口支付
- **WHEN** 个人客户调用 WalletPay 接口支付待支付订单
- **THEN** 系统扣减钱包余额,更新订单状态为已支付,激活套餐
#### Scenario: H5 和后台钱包支付流程独立
- **WHEN** H5 端创建 wallet 订单
- **THEN** 系统调用 `CreateH5Order()` 方法,不影响后台 wallet 订单的一步支付逻辑
#### Scenario: H5 钱包支付使用独立方法
- **WHEN** 个人客户在 H5 端创建 wallet 订单
- **THEN** Handler 层调用 `OrderService.CreateH5Order()` 方法,不调用 `CreateAdminOrder()` 方法
---
### Requirement: 钱包流水记录扩展
系统 SHALL 在钱包流水中记录交易子类型和关联店铺,支持按场景筛选。
#### Scenario: 自购钱包流水
- **WHEN** 代理为自己的资源购买套餐,使用 wallet
- **THEN** 钱包流水的 `transaction_subtype` = "self_purchase"`related_shop_id` 为 NULL`remark` = "购买套餐"
#### Scenario: 代购钱包流水
- **WHEN** 代理为下级代理购买套餐,使用 wallet
- **THEN** 钱包流水的 `transaction_subtype` = "purchase_for_subordinate"`related_shop_id` = 下级代理店铺 ID`remark` = "为下级代理【XX】购买套餐"
#### Scenario: 钱包流水查询店铺名称
- **WHEN** 创建代购钱包流水
- **THEN** 系统查询下级店铺名称,填充到 `remark` 字段
#### Scenario: 钱包流水筛选
- **WHEN** 代理查询钱包流水,筛选 `transaction_subtype` = "purchase_for_subordinate"
- **THEN** 系统返回所有为下级代理购买的流水记录
---
### Requirement: 钱包支付乐观锁
系统 SHALL 使用乐观锁防止钱包并发扣款导致余额不一致。
#### Scenario: 钱包扣款使用 version 字段
- **WHEN** 扣减钱包余额
- **THEN** SQL 语句包含 `WHERE balance >= ? AND version = ?`,更新时 `version + 1`
#### Scenario: 钱包并发扣款失败
- **WHEN** 两个请求同时扣减同一钱包
- **THEN** 只有一个请求成功,另一个返回"余额不足或并发冲突"
#### Scenario: 乐观锁重试逻辑
- **WHEN** 钱包扣款因 version 冲突失败
- **THEN** 系统不自动重试,返回错误(由客户端决定是否重试)
---
### Requirement: 钱包支付幂等性
系统 SHALL 防止同一订单重复创建和重复扣款。
#### Scenario: 订单创建幂等性检查
- **WHEN** 同一买家对同一载体的同一套餐组合在短时间内重复创建订单
- **THEN** 系统返回已创建的订单,不重复扣款
#### Scenario: 幂等性使用 Redis 业务键
- **WHEN** 检查订单幂等性
- **THEN** 系统使用 Redis key `order:idempotency:{buyer_type}:{buyer_id}:{order_type}:{carrier_type}:{carrier_id}:{sorted_package_ids}`
#### Scenario: 幂等性 TTL
- **WHEN** 订单创建成功后标记幂等性
- **THEN** Redis key 的 TTL 为 3 分钟
#### Scenario: 分布式锁防止并发
- **WHEN** 订单创建前检查幂等性
- **THEN** 系统使用分布式锁 `order:create:lock:{carrier_type}:{carrier_id}`TTL 10 秒
---
### Requirement: 后台订单 API 响应扩展
系统 SHALL 在后台订单创建和查询 API 响应中包含钱包支付相关字段。
#### Scenario: 订单响应包含实际支付金额
- **WHEN** 查询钱包支付的订单
- **THEN** 响应包含 `actual_paid_amount` 字段
#### Scenario: 订单响应包含操作者信息
- **WHEN** 查询代购订单
- **THEN** 响应包含 `operator_id``operator_type``operator_name` 字段
#### Scenario: 订单响应包含购买备注
- **WHEN** 查询上级代理购买的订单
- **THEN** 响应包含 `purchase_remark` 字段,如"由上级代理【XX】购买"
---
### Requirement: 钱包支付错误处理
系统 SHALL 在钱包支付失败时返回明确的错误信息。
#### Scenario: 钱包不存在
- **WHEN** 钱包支付时钱包不存在
- **THEN** 系统返回错误"钱包不存在"`CodeWalletNotFound`
#### Scenario: 余额不足
- **WHEN** 钱包支付时余额不足
- **THEN** 系统返回错误"余额不足"`CodeInsufficientBalance`
#### Scenario: 并发冲突
- **WHEN** 钱包扣款因 version 冲突失败
- **THEN** 系统返回错误"余额不足或并发冲突"`CodeInsufficientBalance`
#### Scenario: 套餐激活失败
- **WHEN** 钱包扣款成功但套餐激活失败
- **THEN** 事务回滚,钱包余额恢复,返回激活失败错误
---
### Requirement: 钱包支付与第三方支付的区别
系统 SHALL 区分后台钱包支付和第三方支付的业务逻辑。后台订单创建 MUST 在 Handler 层强制验证支付方式,拒绝 `wechat``alipay` 支付方式。
**后台支付方式限制**
- 允许:`wallet``offline`
- 拒绝:`wechat``alipay`、其他任何值
**实现层级**
1. **DTO 验证**(第一道防线):`CreateAdminOrderRequest``payment_method` 字段使用 `validate:"oneof=wallet offline"` 规则
2. **Handler 验证**(第二道防线):调用 `middleware.ValidateStruct(&req)` 验证 DTO
3. **Handler 兜底检查**(第三道防线):对所有支付方式进行权限检查,包括非法值
#### Scenario: 后台参数验证拒绝第三方支付
- **WHEN** 代理在后台创建订单时 `payment_method` 为 wechat 或 alipay
- **THEN** 系统在 Handler 层的 DTO 验证阶段拒绝请求,返回错误"请求参数解析失败"`CodeInvalidParam`),订单创建失败
#### Scenario: 后台兜底检查拒绝其他支付方式
- **WHEN** 代理在后台创建订单时 `payment_method` 为未知值(防御性编程)
- **THEN** 系统在 Handler 层的兜底检查阶段拒绝请求,返回错误"后台仅支持钱包支付或线下支付"`CodeInvalidParam`
#### Scenario: H5 支持第三方支付
- **WHEN** 个人客户在 H5 端创建订单时选择 wechat 或 alipay
- **THEN** 系统调用 `CreateH5Order()` 方法创建待支付订单返回支付参数prepay_id 或 h5_url
#### Scenario: 钱包支付不需要支付参数
- **WHEN** 后台钱包支付订单创建成功
- **THEN** 响应不包含 prepay_id、h5_url 等第三方支付参数
#### Scenario: 后台使用独立的 DTO
- **WHEN** 后台创建订单
- **THEN** Handler 层使用 `CreateAdminOrderRequest` DTO仅允许 wallet/offlineH5 端使用 `CreateOrderRequest` DTO允许 wallet/wechat/alipay
---
### Requirement: 订单取消与钱包余额解冻
系统 SHALL 根据支付方式正确处理订单支付,包括钱包扣款、在线支付、混合支付等。**新增订单取消(手动或自动)时的钱包余额解冻逻辑。**
**钱包支付流程**
1. 检查钱包可用余额是否充足
2. 冻结钱包余额(`frozen_balance` 增加)
3. 创建订单,状态为"待支付"
4. 订单完成后,扣减钱包余额(`balance` 减少,`frozen_balance` 减少),创建钱包明细记录
5. 订单取消时(手动或自动),解冻钱包余额(`frozen_balance` 减少)
**在线支付流程**
1. 创建订单,状态为"待支付"
2. 调用第三方支付接口
3. 用户完成支付后,订单状态变更为"已支付"
4. 订单完成后,订单状态变更为"已完成"
**混合支付流程**
1. 检查钱包可用余额是否充足(钱包支付部分)
2. 冻结钱包余额
3. 创建订单,状态为"待支付"
4. 调用第三方支付接口(在线支付部分)
5. 用户完成在线支付后,扣减钱包余额,订单状态变更为"已支付"
6. 订单完成后,订单状态变更为"已完成"
7. 订单取消时(手动或自动),解冻钱包余额
#### Scenario: 订单手动取消,解冻钱包余额
- **WHEN** 用户使用钱包支付创建订单,订单金额为 3000 分,然后手动取消订单
- **THEN** 系统解冻钱包余额 3000 分(`frozen_balance` 减少 3000订单状态变更为"已取消"
#### Scenario: 订单超时自动取消,解冻钱包余额
- **WHEN** 用户使用混合支付创建订单,钱包预扣 2000 分30 分钟后订单超时
- **THEN** 系统自动取消订单,解冻钱包余额 2000 分(`frozen_balance` 减少 2000订单状态变更为"已取消"
#### Scenario: 订单取消(纯在线支付),无需解冻
- **WHEN** 用户使用纯在线支付创建订单30 分钟后订单超时
- **THEN** 系统自动取消订单,不执行钱包解冻操作(因为没有钱包预扣)
#### Scenario: 钱包解冻事务保证
- **WHEN** 订单取消涉及钱包解冻
- **THEN** 订单状态更新(`payment_status = 3``expires_at = NULL`)和钱包余额解冻在同一事务中完成,任一失败则全部回滚
#### Scenario: 钱包解冻失败回滚
- **WHEN** 订单取消时,钱包解冻失败(如钱包不存在、冻结余额不足)
- **THEN** 事务回滚,订单状态不变,返回错误信息"订单取消失败"
---
## MODIFIED Requirements (from: add-payment-config-management)
### Requirement: 订单关联支付配置
系统 SHALL 在创建订单时记录当前生效的支付配置 ID用于回调处理时加载正确的配置验签。
#### Scenario: 创建订单时记录支付配置 ID
- **WHEN** 用户创建订单H5 或后台)
- **THEN** 系统查询当前生效的微信参数配置(`is_active=true`
- **THEN** 将 `payment_config_id` 写入订单记录
**订单模型变更**
`tb_order` 新增字段:
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `payment_config_id` | bigint | ❌ | 下单时使用的微信参数配置 ID钱包/线下支付时为 NULL |
**OrderResponse 新增返回字段**
```json
{
"code": 0,
"data": {
"id": 1,
"order_no": "ORD20260316100000123456",
"payment_config_id": 1,
"...": "(现有字段不变)"
},
"msg": "success",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 钱包/线下支付不记录配置 ID
- **WHEN** 用户创建订单,支付方式为 `wallet``offline`
- **THEN** 订单的 `payment_config_id` 为 NULL
#### Scenario: 无生效配置时拒绝第三方支付
- **WHEN** 用户创建订单时选择第三方支付wechat/fuiou但当前无生效的微信参数配置
- **THEN** 系统返回错误
```json
{
"code": 1175,
"data": null,
"msg": "暂无可用的第三方支付渠道,请使用钱包支付",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 无生效配置时允许钱包支付
- **WHEN** 当前无生效支付配置,用户选择钱包支付
- **THEN** 系统正常创建订单,`payment_config_id` 为 NULL
---
### Requirement: 第三方支付回调
系统 SHALL 处理微信支付和富友支付的支付回调。回调验签 MUST 使用订单关联的 `payment_config_id` 加载对应配置,而非当前生效配置。系统新增富友支付回调端点和代理充值回调分发。
#### Scenario: 微信支付成功回调(订单)
```
POST /api/callback/wechat-pay
Content-Type: 由微信服务器决定
无需认证
```
- **WHEN** 收到微信支付成功回调,订单号格式为 `ORD` 开头
- **THEN** 系统查询订单,通过 `order.payment_config_id` 加载对应支付配置
- **THEN** 系统使用该配置的凭证验证签名,更新订单状态,激活套餐
**成功响应**
```json
{
"code": 0,
"data": {
"return_code": "SUCCESS"
},
"msg": "success",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 微信支付成功回调(资产充值)
- **WHEN** 收到微信支付成功回调,订单号格式为 `CRCH` 开头(修复:当前代码误用废弃的 `RCH` 前缀)
- **THEN** 系统查询 `tb_asset_recharge_record`,通过 `payment_config_id` 加载配置验签
- **THEN** 系统更新充值订单状态,增加钱包余额,触发佣金判断
#### Scenario: 微信支付成功回调(代理充值)
- **WHEN** 收到微信支付成功回调,订单号格式为 `ARCH` 开头(全新支持)
- **THEN** 系统查询 `tb_agent_recharge_record`,通过 `payment_config_id` 加载配置验签
- **THEN** 系统更新充值订单状态,增加代理余额钱包余额
#### Scenario: 富友支付成功回调
```
POST /api/callback/fuiou-pay
Content-Type: application/x-www-form-urlencoded
无需认证
```
- **WHEN** 收到富友支付回调,`result_code=000000`
- **THEN** 系统解析 XMLGBK → UTF-8通过 `mchnt_order_no` 判断订单类型ORD/CRCH/ARCH
- **THEN** 查询对应表,通过 `payment_config_id` 加载富友配置,使用富友公钥验签
- **THEN** 验证金额匹配后,调用对应 Service 的 HandlePaymentCallback
**成功响应XMLGBK 编码)**
```xml
<?xml version="1.0" encoding="GBK"?>
<xml>
<result_code>000000</result_code>
<result_msg>success</result_msg>
</xml>
```
#### Scenario: 重复回调
- **WHEN** 收到已处理订单/充值的重复回调(微信或富友)
- **THEN** 系统返回成功响应,不重复处理
#### Scenario: 签名验证失败
- **WHEN** 回调签名验证失败(微信或富友)
- **THEN** 系统拒绝处理,记录 ERROR 日志,返回失败响应
#### Scenario: 订单号不存在
- **WHEN** 回调中的订单号在系统中不存在
- **THEN** 系统记录 ERROR 日志,返回失败响应
---
### Requirement: 钱包支付与第三方支付的区别
系统 SHALL 区分后台钱包支付和第三方支付的业务逻辑。第三方支付方式对前端统一显示为"微信支付",后端根据生效配置自动路由。
**后台支付方式限制**`CreateAdminOrderRequest`
- 允许:`wallet``offline`
- 拒绝:`wechat``alipay``fuiou`、其他任何值
**H5/小程序支付方式**(两步走):
- 步骤 1 创建订单:`payment_method``wallet`
- 步骤 2 发起第三方支付:通过独立端点 `/orders/:id/wechat-pay/jsapi`
#### Scenario: 后台参数验证拒绝第三方支付
- **WHEN** 代理在后台创建订单时 `payment_method` 为 wechat 或 fuiou
- **THEN** DTO 验证阶段拒绝请求
```json
{
"code": 1001,
"data": null,
"msg": "请求参数解析失败",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: H5 两步走支付
- **WHEN** 个人客户在 H5 创建订单(步骤 1
- **THEN** 订单创建为待支付状态,记录 `payment_config_id`
- **WHEN** 客户调用 `POST /orders/:id/wechat-pay/jsapi`(步骤 2
- **THEN** 系统按 `payment_config_id` 加载配置,根据 `provider_type` 发起对应渠道支付(本次留桩)
---
### Requirement: 配置切换不取消在途订单
- **WHEN** 管理员激活新配置时,系统中存在使用旧配置创建的待支付订单
- **THEN** 系统不取消这些订单
- **THEN** 旧订单若支付成功,回调按 `payment_config_id` 加载旧配置验签
- **THEN** 旧订单若未支付,由 30 分钟超时机制自动取消