This commit is contained in:
181
openspec/specs/fuiou-payment/spec.md
Normal file
181
openspec/specs/fuiou-payment/spec.md
Normal file
@@ -0,0 +1,181 @@
|
||||
## 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 验签
|
||||
Reference in New Issue
Block a user