## 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 验签
