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

182 lines
5.4 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 下单
系统 SHALL 支持通过富友支付 `wxPreCreate` 接口发起微信公众号 JSAPI 支付。
> **本次留桩**`FuiouPayJSAPI` 方法在 Service 层定义,但实际调用第三方获取支付参数的逻辑暂不实现,返回"富友支付发起暂未实现"错误。`pkg/fuiou/` SDK 包完整实现。
#### Scenario: 公众号 JSAPI 下单成功
- **WHEN** 系统调用富友 `wxPreCreate` 接口
- `trade_type=JSAPI`
- `sub_appid=公众号AppID`(从 `tb_wechat_config.oa_app_id` 读取)
- `sub_openid=用户公众号OpenID`
- 传入订单号、金额(分)、商品描述、终端 IP、回调地址
- **THEN** 富友返回 `result_code=000000`,包含支付参数
**富友返回支付参数结构**
```json
{
"sdk_appid": "wx1234567890abcdef",
"sdk_timestamp": "1711411341",
"sdk_noncestr": "abc123def456",
"sdk_prepayid": "wx26112221580621e9b071c00d9e093b0000",
"sdk_package": "Sign=WXPay",
"sdk_signtype": "RSA",
"sdk_paysign": "..."
}
```
- **THEN** 系统将支付参数返回给前端,前端调用 `WeixinJSBridge.invoke('getBrandWCPayRequest', ...)` 拉起支付
#### Scenario: 公众号 JSAPI 下单失败
- **WHEN** 富友返回 `result_code``000000`
- **THEN** 系统记录 ERROR 日志(订单号、错误码、错误消息)
- **THEN** 系统返回错误
```json
{
"code": 1173,
"data": null,
"msg": "支付发起失败,请重试",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
---
### Requirement: 富友支付小程序下单
系统 SHALL 支持通过富友支付 `wxPreCreate` 接口发起微信小程序支付。
#### Scenario: 小程序下单成功
- **WHEN** 系统调用富友 `wxPreCreate` 接口
- `trade_type=LETPAY`
- `sub_appid=小程序AppID`(从 `tb_wechat_config.miniapp_app_id` 读取)
- `sub_openid=用户小程序OpenID`
- **THEN** 富友返回 `result_code=000000`,包含支付参数
- **THEN** 系统将支付参数返回给前端,前端调用 `wx.requestPayment(...)` 拉起支付
#### Scenario: 小程序下单缺少 OpenID
- **WHEN** 系统发起小程序支付但未传入 `sub_openid`
- **THEN** 系统返回错误
```json
{
"code": 1001,
"data": null,
"msg": "小程序支付必须提供用户 OpenID",
"timestamp": "2026-03-16T10:00:00+08:00"
}
```
---
### Requirement: 富友支付回调处理
系统 SHALL 接收并处理富友支付成功回调通知,验证签名后更新订单/充值状态。
#### Scenario: 接收到合法的支付成功回调
```
POST /api/callback/fuiou-pay
Content-Type: application/x-www-form-urlencoded
无需认证
```
**请求体格式**`req=<双重URL编码的GBK XML>`
- **THEN** 系统将请求体从 GBK 转换为 UTF-8
- **THEN** 系统解析 XML 格式的回调数据
- **THEN** 系统根据 `mchnt_order_no` 判断订单类型:
- `ORD` 开头 → 套餐订单 → 查询 `tb_order`
- `CRCH` 开头 → 资产充值 → 查询 `tb_asset_recharge_record`
- `ARCH` 开头 → 代理充值 → 查询 `tb_agent_recharge_record`
- **THEN** 通过记录的 `payment_config_id` 加载对应的富友配置
- **THEN** 使用该配置的富友公钥验证 RSA 签名
- **THEN** 验证 `result_code=000000` 且金额匹配
- **THEN** 调用对应 Service 的 HandlePaymentCallback
- **THEN** 返回成功 XML 响应GBK 编码)
**成功响应**
```xml
<?xml version="1.0" encoding="GBK"?>
<xml>
<result_code>000000</result_code>
<result_msg>success</result_msg>
</xml>
```
#### Scenario: 回调签名验证失败
- **WHEN** 富友回调的 RSA 签名与本地计算不匹配
- **THEN** 系统记录 ERROR 日志
- **THEN** 返回失败 XML 响应
```xml
<?xml version="1.0" encoding="GBK"?>
<xml>
<result_code>999999</result_code>
<result_msg>signature verification failed</result_msg>
</xml>
```
#### Scenario: 回调订单号不存在
- **WHEN** `mchnt_order_no` 在系统中不存在
- **THEN** 系统记录 ERROR 日志,返回失败 XML 响应
#### Scenario: 重复回调幂等处理
- **WHEN** 富友对同一订单多次发送支付成功回调
- **THEN** 系统识别已支付,直接返回成功 XML 响应
---
### Requirement: 富友 XML 通信协议
系统 SHALL 正确处理富友支付的 XML + GBK 编码通信协议。
#### Scenario: 请求编码
- **WHEN** 系统向富友发送请求
- **THEN** 请求体为 XML 格式GBK 编码声明
- **THEN** XML 内容经 GBK 编码后进行两次 URL 编码
- **THEN** 以 `req=<encoded_xml>` 的 form 格式发送
#### Scenario: 响应解码
- **WHEN** 系统接收富友响应
- **THEN** 先进行 URL 解码
- **THEN** 将 GBK 内容转换为 UTF-8
- **THEN** 替换 XML 声明中的 `encoding="GBK"``encoding="UTF-8"`
- **THEN** 解析 XML 到结构体
---
### Requirement: 富友 RSA 签名算法
系统 SHALL 实现富友支付的 RSA + MD5 签名验签算法。
#### Scenario: 生成请求签名
- **WHEN** 系统需要对富友请求签名
- **THEN** 提取所有非空字段(排除 `sign``reserved_` 开头字段)
- **THEN** 按字典序排列为 `key=value&key=value` 格式
- **THEN** 将签名原文转换为 GBK 编码
- **THEN** 计算 MD5 哈希
- **THEN** 使用商户私钥对 MD5 哈希进行 RSA PKCS1v15 签名
- **THEN** 对签名结果进行 Base64 编码
#### Scenario: 验证回调签名
- **WHEN** 系统需要验证富友回调签名
- **THEN** 使用相同算法计算签名原文的 MD5 哈希
- **THEN** 使用富友公钥对回调中的 `sign` 字段进行 RSA PKCS1v15 验签