# 四方支付平台 · API 对接文档（v1）

> 适用对象：直接对接本平台 JSON 网关的商户系统。
> 更新日期：2026-08-30
> 易支付 Pro 用户：可使用第 9 节的官方插件，也可直接使用本平台提供的易支付兼容端点（`/submit.php`、`/mapi.php`、`/api.php`）。

---

## 1. 接入前准备

1. 登录商户后台（`/merchant/login`），在「接口信息」获取：
   - **AppKey**：请求头 `X-App-Key` 使用
   - **AppSecret**：签名密钥（切勿泄露）
2. 协议基础地址：`https://平台域名/`（下文 `<BASE>` 代指）。
3. 所有接口：HTTP POST，请求体 JSON（UTF-8），两个请求头：
   - `Content-Type: application/json`
   - `X-App-Key: {AppKey}`
4. 统一应答格式：

```json
{"code": 0, "message": "ok", "data": {}}
```

- `code=0` 成功；非 0 失败（`message` 为中文原因）。
- 金额单位一律为**分**（整数）；时间字段为 RFC3339（UTC，带 Z 后缀）。

---

## 2. 签名算法（MD5，大写）

1. 取除 `sign` 外所有**值非空**的参数；
2. 按参数名 ASCII 升序排序，拼接为 `k1=v1&k2=v2...`；
3. 末尾拼接 `&key={AppSecret}`；
4. 计算 MD5，取 **32 位大写**十六进制即为 `sign`。

```
raw  = "amount=1000&merchantOrderNo=NO123&subject=测试" + "&key=" + secret
sign = MD5(raw).大写
```

> 注意：本网关签名固定以 `&key=` 结尾且结果大写；易支付协议（submit.php）为密钥直接拼接且小写，两者请勿混用。

---

## 3. 下单 `POST /api/gateway/orders`

### 请求参数

| 参数 | 必填 | 说明 |
|---|---|---|
| merchantOrderNo | 是 | 商户系统内唯一订单号 |
| amount | 是 | 支付金额，单位分（10.00 元传 `1000`） |
| subject | 是 | 商品标题 |
| notifyUrl | 是 | 异步通知地址，必须为 https 公网地址 |
| payMethod | 否 | `alipay` / `wxpay` / `qqpay` 等；留空由聚合收银台让用户选择 |
| channelCode | 否 | 指定支付通道编码（通常留空由平台路由） |
| returnUrl | 否 | 支付完成回跳地址 |
| expireMinutes | 否 | 订单有效期（分钟），默认 5 |
| extra | 否 | 附加参数 JSON 字符串；可传 `openid` / `buyerId` / `payer`（支付账号，用于风控黑名单匹配） |
| sign | 是 | 按第 2 节计算 |

### 应答 data

```json
{
  "orderNo": "Pxxxxxxxx",         // 平台订单号
  "merchantOrderNo": "NO123",
  "payUrl": "https://...",        // 直接支付地址（部分通道返回）
  "payType": "page",              // page | qrcode | h5
  "cashierUrl": "https://平台域名/pay/Pxxxxxxxx"   // 聚合收银台，推荐跳转
}
```

- 幂等：同一 `merchantOrderNo` 原单仍为待支付时返回原单；已终态则报「merchantOrderNo 已存在」。

---

## 4. 查询订单 `POST /api/gateway/orders/query`

请求：`orderNo` 与 `merchantOrderNo` **二选一**。

应答 data：

```json
{
  "orderNo": "Pxxxxxxxx", "merchantOrderNo": "NO123",
  "amount": 1000,          // 下单金额（分）
  "fee": 0,                // 手续费（分）
  "actualAmount": 1000,    // 实收金额（分）
  "refundedAmount": 0,     // 已退款金额（分）
  "status": "SUCCESS",
  "channelCode": "...", "payMethod": "alipay",
  "createdAt": "2026-08-30T02:00:00Z", "paidAt": "2026-08-30T02:01:00Z",
  "payUrl": "...", "payType": "page", "extra": "..."
}
```

订单状态枚举：

| 状态 | 含义 |
|---|---|
| PENDING | 待支付 |
| SUCCESS | 已支付 |
| FAILED | 支付失败 |
| CLOSED | 已关闭 |
| REFUNDING | 退款中 |
| REFUNDED | 已全额退款 |
| PARTIAL_REFUND | 部分退款 |

> 查询待支付订单时会同步向上游查证一次，若上游已实际支付将自动补单并返回最新状态。

---

## 5. 退款 `POST /api/gateway/refunds`

| 参数 | 必填 | 说明 |
|---|---|---|
| orderNo / merchantOrderNo | 二选一 | 目标订单 |
| refundAmount | 是 | 退款金额（分）；支持部分退款，累计不可超原单金额 |
| reason | 否 | 退款原因 |

应答 data：`refundNo` 平台退款单号（R 开头）。仅 `SUCCESS` / `PARTIAL_REFUND` 状态可退。

---

## 6. 异步通知（notifyUrl）

支付完成后平台向 `notifyUrl` 发起 **POST**（Content-Type: application/json）：

```json
{
  "orderNo": "Pxxxxxxxx",
  "merchantOrderNo": "NO123",
  "amount": "1000",          // 订单金额（分，字符串），与下单金额一致；手续费不改变通知金额
  "status": "SUCCESS",
  "channelCode": "...",
  "payTime": "2026-08-30T02:01:00Z",   // 可能为空字符串
  "sign": "..."
}
```

**验签**：剔除 `sign` 后，其余字段按第 2 节规则（空值不参与、ASCII 升序、`&key=` 结尾、MD5 大写）重算并与 `sign` 比对；一致且 `status=SUCCESS` 后再发货。

**应答**：HTTP 200 且响应体包含 `SUCCESS`（纯文本即可）。
**重试**：商户未正确应答时，平台最多重试 3 次。

**同步回跳（returnUrl）**：支付完成后浏览器跳转，仅作展示用途，**不可作为发货依据**。

---

## 7. 对接演示代码

### PHP

```php
<?php
$base = 'https://平台域名'; $appKey = '商户AppKey'; $secret = '商户AppSecret';
function sign_json(array $p, string $secret): string {
  unset($p['sign']); ksort($p); $pairs = [];
  foreach ($p as $k => $v) { if ($v !== '' && $v !== null) $pairs[] = "$k=$v"; }
  return strtoupper(md5(implode('&', $pairs) . '&key=' . $secret));
}
$order = ['merchantOrderNo' => 'NO'.time(), 'amount' => 1000, 'subject' => '测试商品',
          'notifyUrl' => 'https://商户域名/notify', 'payMethod' => 'alipay'];
$order['sign'] = sign_json($order, $secret);
$ch = curl_init($base.'/api/gateway/orders');
curl_setopt_array($ch, [CURLOPT_POST => true,
  CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'X-App-Key: '.$appKey],
  CURLOPT_POSTFIELDS => json_encode($order), CURLOPT_RETURNTRANSFER => true]);
$resp = json_decode(curl_exec($ch), true);
if ($resp['code'] === 0) header('Location: '.$resp['data']['cashierUrl']);
// 回调验签（notify.php）：
// $input = json_decode(file_get_contents('php://input'), true);
// $sign = $input['sign']; unset($input['sign']);
// if (sign_json($input, $secret) === $sign && $input['status'] === 'SUCCESS') { /* 发货 */ echo 'SUCCESS'; }
```

### Python

```python
import hashlib, json, time, urllib.request
BASE, APPKEY, SECRET = 'https://平台域名', '商户AppKey', '商户AppSecret'
def sign(p):
    items = sorted((k, str(v)) for k, v in p.items() if k != 'sign' and str(v) != '')
    return hashlib.md5(('&'.join(f'{k}={v}' for k, v in items) + '&key=' + SECRET).encode()).hexdigest().upper()
p = {'merchantOrderNo': 'NO%d' % time.time(), 'amount': 1000, 'subject': '测试商品',
     'notifyUrl': 'https://商户域名/notify', 'payMethod': 'alipay'}
p['sign'] = sign(p)
req = urllib.request.Request(BASE + '/api/gateway/orders', data=json.dumps(p).encode(),
    headers={'Content-Type': 'application/json', 'X-App-Key': APPKEY})
print(json.load(urllib.request.urlopen(req))['data']['cashierUrl'])
# 回调验签：同一 sign() 重算比对，status==SUCCESS 后发货并响应 SUCCESS
```

### Go

```go
func sign(p map[string]string, secret string) string {
    keys := []string{}
    for k, v := range p { if v != "" && k != "sign" { keys = append(keys, k) } }
    sort.Strings(keys)
    parts := []string{}
    for _, k := range keys { parts = append(parts, k+"="+p[k]) }
    sum := md5.Sum([]byte(strings.Join(parts, "&") + "&key=" + secret))
    return strings.ToUpper(hex.EncodeToString(sum[:]))
}
// 下单：POST JSON 到 /api/gateway/orders，头带 X-App-Key；回调验签用同一 sign()
// 完整可运行示例见接入文档页「Go 对接示例 SDK」
```

> 更多语言（Node.js / Java / C#/.NET / Ruby）完整示例见平台接入文档页 `/docs`。

---

## 8. 常见问题

| 现象 | 原因与处理 |
|---|---|
| 签名错误 | 检查空值是否剔除、是否按 ASCII 升序、是否以 `&key=` 结尾、结果是否大写 |
| notifyUrl 无效 | 必须为 https 公网地址（http 会被强制升级，301 会导致回调失败） |
| 回调收不到，商户日志显示 301 | 回调地址为 http 且站点强制 https，请直接配置最终 https 地址 |
| 应答 200 但平台仍显示回调失败 | 应答体必须包含 `SUCCESS` |
| merchantOrderNo 已存在 | 更换商户订单号，或改用查询接口 |
| 无可用支付通道 | 检查通道状态为启用、插件配置完整 |
| 风控拦截 | 检查单笔/单日限额、IP 黑名单与下单频率（系统设置 → 风控管理） |

---

## 9. 易支付 Pro 插件（xypay）

正在使用彩虹易支付 Pro 的站点，可下载官方支付插件（xypay），把本平台添加为上游支付通道：

- 插件下载：`/static/fourpay-epay-plugin.zip`
- 安装：解压得到 `xypay` 文件夹 → 上传到易支付网站 `plugins/` 目录 → 易支付后台「支付接口 → 支付插件」刷新列表并启用「HUAYIPAY」
- 通道配置：
  - 网关地址：`https://pay.mum3a.com`（文档地址 = 网关地址 + `/docs`）
  - AppKey / AppSecret：商户对接密钥（本平台商户详情页获取）
  - 通道编码（选填）：`alipay` / `wxpay` / `qqpay`，留空按拉起端自动判断
- 支持支付方式：alipay / wxpay / qqpay；支持易支付后台发起退款与订单查询
- 回调/回跳：平台异步通知 POST 到易支付标准通知地址（MD5 验签，应答 SUCCESS），回跳携带签名参数
- 要求：易支付站点需 HTTPS（平台异步通知要求 https 地址）
