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

412 lines
15 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: 系统必须支持 JSAPI 支付
系统 MUST 支持在微信内网页发起 JSAPI 支付,用户在微信客户端内完成支付。
#### Scenario: 用户在微信内成功发起支付
- **WHEN** 用户在微信内选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/jsapi` 端点,传入用户 OpenID
- **THEN** 系统验证订单状态为 `pending`(待支付)
- **THEN** 系统调用 PowerWeChat SDK 的 `Order.JSAPITransaction()` 创建支付订单
- **THEN** 系统生成 JSSDK 支付配置(包含 prepay_id、timestamp、nonceStr、paySign
- **THEN** 系统返回支付配置给前端
- **THEN** 前端调用 `wx.requestPayment()` 唤起微信支付
#### Scenario: 订单不存在或状态不正确
- **WHEN** 用户提交的订单 ID 不存在,或订单状态不是 `pending`
- **THEN** 系统返回错误码 1000参数错误和中文错误消息"订单不存在或不可支付"
#### Scenario: 订单金额为 0
- **WHEN** 订单金额为 0 元
- **THEN** 系统跳过微信支付,直接更新订单状态为 `paid`
- **THEN** 系统触发套餐激活和分佣计算
#### Scenario: 微信支付 API 调用失败
- **WHEN** 调用 PowerWeChat SDK 创建支付订单时失败(网络超时、参数错误等)
- **THEN** 系统记录详细的错误日志Request ID、错误码、错误消息
- **THEN** 系统返回错误码 1042微信支付发起失败和中文错误消息"支付发起失败,请重试"
### Requirement: 系统必须支持 H5 支付
系统 MUST 支持在移动端浏览器外发起 H5 支付,用户可唤起微信 APP 完成支付。
#### Scenario: 用户在浏览器中成功发起 H5 支付
- **WHEN** 用户在移动端浏览器选择订单并点击"微信支付",前端调用 `/api/h5/orders/:id/wechat-pay/h5` 端点,传入用户终端 IP 和场景信息
- **THEN** 系统验证订单状态为 `pending`
- **THEN** 系统调用 PowerWeChat SDK 的 `Order.TransactionH5()` 创建 H5 支付订单
- **THEN** 系统返回微信支付跳转 URLh5_url
- **THEN** 前端跳转到该 URL用户在微信 H5 页面完成支付
#### Scenario: 缺少必填参数
- **WHEN** 请求缺少 `payer_client_ip``scene_info` 参数
- **THEN** 系统返回错误码 1000参数错误和中文错误消息"缺少必填参数"
#### Scenario: 订单已支付
- **WHEN** 用户提交的订单状态已是 `paid`
- **THEN** 系统返回错误码 1000参数错误和中文错误消息"订单已支付"
### Requirement: 系统必须支持微信支付回调
系统 SHALL 接收并处理微信支付成功通知,更新订单状态并触发后续业务逻辑。
#### Scenario: 接收到合法的支付成功通知
- **WHEN** 微信回调 `/api/callback/wechat-pay` 端点,传入支付成功通知
- **THEN** PowerWeChat SDK 自动验证回调签名
- **THEN** 系统解析通知内容提取商户订单号out_trade_no
- **THEN** 系统调用 `orderService.HandlePaymentCallback()` 更新订单状态为 `paid`(幂等处理)
- **THEN** 系统触发套餐激活和分佣计算
- **THEN** 系统返回 HTTP 200 和 `{"return_code": "SUCCESS"}` 给微信
#### Scenario: 接收到重复的支付通知
- **WHEN** 微信多次发送同一订单的支付成功通知
- **THEN** 系统通过幂等检查识别订单已支付
- **THEN** 系统直接返回成功响应,不重复处理业务逻辑
#### Scenario: 回调签名验证失败
- **WHEN** 微信回调的签名无效或被篡改
- **THEN** PowerWeChat SDK 自动拒绝该请求
- **THEN** 系统记录 ERROR 级别日志Request ID、签名验证失败详情
- **THEN** 系统返回 HTTP 400 错误
#### Scenario: 订单号不存在
- **WHEN** 微信回调中的商户订单号在系统中不存在
- **THEN** 系统记录 ERROR 级别日志
- **THEN** 系统返回失败响应给微信(让微信稍后重试)
#### Scenario: 支付回调处理失败
- **WHEN** 系统在处理支付回调时发生数据库错误或其他异常
- **THEN** 系统记录 ERROR 级别日志Request ID、错误详情
- **THEN** 系统返回失败响应给微信(让微信稍后重试)
### Requirement: 支付回调处理必须幂等
系统 MUST 确保多次接收到同一支付通知时,业务逻辑只执行一次。
#### Scenario: 订单状态条件更新
- **WHEN** 系统更新订单状态为 `paid`
- **THEN** 系统使用条件更新:`UPDATE ... WHERE id = ? AND payment_status = ?`(只更新状态为 pending 的订单)
- **THEN** 如果更新影响行数为 0系统检查当前订单状态
- 如果已支付,返回成功(幂等)
- 如果已取消/已退款,返回错误
#### Scenario: 套餐激活幂等性
- **WHEN** 订单支付成功后触发套餐激活
- **THEN** 系统检查 `tb_package_usage` 表是否已存在该订单的激活记录
- **THEN** 如果已存在,跳过激活逻辑(幂等)
### Requirement: 系统必须支持查询微信支付订单
系统 SHALL 支持根据商户订单号查询微信支付订单状态。
#### Scenario: 查询到支付成功的订单
- **WHEN** 调用 `PaymentService.Order.QueryByOutTradeNumber()` 查询订单
- **THEN** 系统返回订单详情,包含:
- 订单号out_trade_no
- 微信支付单号transaction_id
- 支付状态trade_state: SUCCESS
- 支付时间success_time
- 支付金额total
#### Scenario: 查询到待支付的订单
- **WHEN** 查询的订单尚未支付
- **THEN** 系统返回订单详情,支付状态为 `NOTPAY`
#### Scenario: 查询不存在的订单
- **WHEN** 查询的商户订单号在微信侧不存在
- **THEN** PowerWeChat SDK 返回错误
- **THEN** 系统记录日志并返回错误码 1042
### Requirement: 系统必须支持关闭未支付订单
系统 SHALL 支持关闭超时未支付的微信订单。
#### Scenario: 成功关闭未支付订单
- **WHEN** 调用 `PaymentService.Order.Close()` 关闭订单,传入商户订单号
- **THEN** 系统调用微信 API 关闭订单
- **THEN** 系统返回成功响应
#### Scenario: 尝试关闭已支付订单
- **WHEN** 调用关闭接口,但订单已支付
- **THEN** 微信 API 返回错误(订单已支付,无法关闭)
- **THEN** 系统记录日志并返回错误
#### Scenario: 订单创建后 5 分钟内关闭
- **WHEN** 订单创建后不足 5 分钟就调用关闭接口
- **THEN** 系统可能因订单状态同步不及时而关闭失败
- **THEN** 系统建议在创建 5 分钟后再关闭
### Requirement: 系统必须支持配置管理
微信支付相关配置 MUST 通过 Viper + 环境变量管理。
#### Scenario: 从环境变量读取配置
- **WHEN** 系统启动时
- **THEN** 系统从环境变量读取以下配置:
- `JUNHONG_WECHAT_PAYMENT_APP_ID`(支付 AppID
- `JUNHONG_WECHAT_PAYMENT_MCH_ID`(商户号)
- `JUNHONG_WECHAT_PAYMENT_API_V3_KEY`API V3 密钥)
- `JUNHONG_WECHAT_PAYMENT_API_V2_KEY`API V2 密钥)
- `JUNHONG_WECHAT_PAYMENT_CERT_PATH`(商户证书路径)
- `JUNHONG_WECHAT_PAYMENT_KEY_PATH`(商户私钥路径)
- `JUNHONG_WECHAT_PAYMENT_SERIAL_NO`(证书序列号)
- `JUNHONG_WECHAT_PAYMENT_NOTIFY_URL`(支付回调地址)
#### Scenario: 证书文件不存在时启动失败
- **WHEN** 配置的证书路径指向的文件不存在或无读取权限
- **THEN** 系统记录 FATAL 级别日志
- **THEN** 系统启动失败并退出
#### Scenario: 必填配置缺失时启动失败
- **WHEN** 必填配置项AppID、商户号、API 密钥)缺失
- **THEN** 系统记录 FATAL 级别日志
- **THEN** 系统启动失败并退出
### Requirement: API 必须遵循统一响应格式
所有微信支付相关 API MUST 返回统一的 JSON 响应格式(同微信公众号规范)。
#### Scenario: 支付发起成功响应
- **WHEN** JSAPI 支付发起成功
- **THEN** 系统返回 HTTP 200 和以下格式:
```json
{
"code": 0,
"message": "success",
"data": {
"prepay_id": "wx...",
"pay_config": {
"appId": "...",
"timeStamp": "...",
"nonceStr": "...",
"package": "prepay_id=...",
"signType": "RSA",
"paySign": "..."
}
},
"timestamp": 1706789012345
}
```
#### Scenario: H5 支付发起成功响应
- **WHEN** H5 支付发起成功
- **THEN** 系统返回 HTTP 200 和以下格式:
```json
{
"code": 0,
"message": "success",
"data": {
"h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?..."
},
"timestamp": 1706789012345
}
```
### Requirement: 系统必须记录完整的日志
所有微信支付 API 调用 MUST 记录完整的日志。
#### Scenario: 记录支付发起日志
- **WHEN** 系统调用微信支付 API 创建订单
- **THEN** 系统记录 INFO 级别日志包含Request ID、订单号、支付类型JSAPI/H5、订单金额
#### Scenario: 记录支付回调日志
- **WHEN** 系统收到微信支付回调
- **THEN** 系统记录 INFO 级别日志包含Request ID、订单号、微信支付单号、支付时间
#### Scenario: 记录支付错误日志
- **WHEN** 微信支付 API 调用失败
- **THEN** 系统记录 ERROR 级别日志包含Request ID、订单号、错误码、错误消息、完整的错误详情
### Requirement: 系统必须支持 Redis 缓存
微信支付的 Access Token MUST 使用 Redis 缓存(与微信公众号共享同一缓存机制)。
#### Scenario: Token 缓存与公众号共享
- **WHEN** 微信支付和公众号使用相同的 AppID
- **THEN** 系统复用同一个 Redis Cache 实例
- **THEN** Token 缓存 Key 相同,避免重复获取
---
## MODIFIED Requirements (from: add-payment-config-management)
### Requirement: 微信支付配置动态加载
微信支付配置 MUST 从数据库动态加载(通过 `tb_wechat_config` 表替代原有的环境变量静态配置。Payment 实例按需创建,支持请求级 AppID 覆盖(区分公众号和小程序)。
#### Scenario: 从数据库加载配置创建 Payment 实例
- **WHEN** 支付流程需要使用微信支付
- **THEN** 系统从 Redis 缓存或数据库加载当前生效的微信参数配置(`is_active=true` 且 `provider_type=wechat`
- **THEN** 系统使用配置中的 `wx_mch_id`、`wx_api_v3_key`、`wx_cert_content`、`wx_key_content`、`wx_serial_no` 创建 `payment.Payment` 实例
- **THEN** 证书内容从 Base64 解码后写入临时文件供 PowerWeChat SDK 使用
> **本次留桩**WechatPayJSAPI 和 WechatPayH5 方法保留现有 wechatPayment 单例调用,添加 TODO 注释标记后续替换点。
#### Scenario: 无生效微信支付配置时拒绝支付
- **WHEN** 系统查询不到 `is_active=true` 的微信参数配置,或生效配置的 `provider_type` 非 `wechat`
- **THEN** 微信支付相关接口返回错误
```json
{
"code": 1175,
"data": null,
"msg": "当前无可用的支付渠道",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 公众号 JSAPI 支付使用公众号 AppID
```
POST /api/h5/orders/:id/wechat-pay/jsapi
Authorization: Bearer {token}
Content-Type: application/json
```
**请求体**
```json
{
"openid": "oUpF8uMuAJO_M2pxb1Q9zNjWeS6o"
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `openid` | string | ✅ | 用户在公众号下的 OpenID |
- **THEN** 系统使用配置中的 `oa_app_id`(公众号 AppID创建支付订单
- **THEN** Payer OpenID 为用户在该公众号下的 OpenID
**成功响应 `200 OK`**(本次留桩,返回结构不变)
```json
{
"code": 0,
"data": {
"prepay_id": "wx26112221580621e9b071c00d9e093b0000",
"pay_config": {
"appId": "wx1234567890abcdef",
"timeStamp": "1711411341",
"nonceStr": "abc123",
"package": "prepay_id=wx26112221580621e9b071c00d9e093b0000",
"signType": "RSA",
"paySign": "..."
}
},
"msg": "success",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 小程序支付使用小程序 AppID
- **WHEN** 用户在小程序中发起支付
- **THEN** 系统在调用 `JSAPITransaction` 时将 AppID 覆盖为配置中的 `miniapp_app_id`
- **THEN** Payer OpenID 为用户在该小程序下的 OpenID
#### Scenario: 微信 H5 支付
```
POST /api/h5/orders/:id/wechat-pay/h5
Authorization: Bearer {token}
Content-Type: application/json
```
**请求体**
```json
{
"scene_info": {
"payer_client_ip": "14.23.150.211",
"h5_info": {
"type": "Wap"
}
}
}
```
| 字段 | 类型 | 必填 | 说明 |
|------|------|------|------|
| `scene_info.payer_client_ip` | string | ✅ | 用户终端 IP |
| `scene_info.h5_info.type` | string | ❌ | 场景类型:`iOS` / `Android` / `Wap` |
**成功响应 `200 OK`**
```json
{
"code": 0,
"data": {
"h5_url": "https://wx.tenpay.com/cgi-bin/mmpayweb-bin/checkmweb?prepay_id=wx..."
},
"msg": "success",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 配置缺失时系统正常启动
- **WHEN** 系统启动时数据库中无微信参数配置或配置不完整
- **THEN** 系统正常启动,支付功能降级为仅支持钱包/线下
- **THEN** 系统记录 WARN 日志"无可用微信参数配置,第三方支付功能不可用"
---
### Requirement: 微信支付回调按配置验签
系统 SHALL 接收并处理微信支付成功通知。回调验签 MUST 使用订单关联的支付配置(而非当前生效配置)。
#### Scenario: 接收到合法的支付成功通知
```
POST /api/callback/wechat-pay
Content-Type: 由微信服务器决定
无需认证
```
- **WHEN** 微信回调端点收到支付成功通知
- **THEN** 系统解析通知中的商户订单号(`out_trade_no`
- **THEN** 按订单号前缀分发(`ORD` → 套餐订单,`CRCH` → 资产充值,`ARCH` → 代理充值)
- **THEN** 查询对应表记录,通过 `payment_config_id` 加载对应的微信参数配置
- **THEN** 使用该配置的凭证通过 PowerWeChat SDK 验证回调签名
- **THEN** 调用对应 Service 的 HandlePaymentCallback
- **THEN** 返回成功响应
**成功响应**
```json
{
"code": 0,
"data": {
"return_code": "SUCCESS"
},
"msg": "success",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
#### Scenario: 订单关联的配置已被软删除
- **WHEN** 回调到达,但 `payment_config_id` 对应的配置已被软删除
- **THEN** 系统使用 `GetByIDUnscoped` 加载该配置(软删除不影响回调处理)
- **THEN** 正常完成验签和订单处理
#### Scenario: 重复回调幂等处理
- **WHEN** 微信多次发送同一订单的支付成功通知
- **THEN** 系统通过幂等检查识别已支付,直接返回成功响应
#### Scenario: 回调签名验证失败
- **WHEN** 签名无效或被篡改
- **THEN** PowerWeChat SDK 自动拒绝,系统记录 ERROR 日志,返回 HTTP 400
#### Scenario: 订单号不存在
- **WHEN** 回调中的商户订单号在系统中不存在
- **THEN** 系统记录 ERROR 日志,返回失败响应