# 易支付对接指南

服务根地址：`https://pay.strcpy.cn`。当前只支持支付宝（`alipay`）。

本页与 [AI 对接 Skill](https://pay.strcpy.cn/skills.md) 均无需登录。商户 PID、商户 KEY 从管理员提供的网站接入信息中获取，每个网站使用独立凭据。商户 KEY 用于易支付签名，与支付宝应用私钥不同。

## 1. 配置接入网站

管理员在「接入网站」创建网站，选择收款应用，登记通知与返回地址。接入方填写：

| 配置 | 填写内容 |
| --- | --- |
| 支付地址 / API 地址 | `https://pay.strcpy.cn`，填写根地址，不追加 `submit.php` |
| 商户 PID | 该网站的 PID |
| 商户 KEY / PKey | 该网站的 KEY，保存在接入网站服务端 |
| 支付方式 | `alipay` |
| 通知地址 | 接入网站接收付款结果的完整 HTTP / HTTPS 地址 |
| 返回地址 | 用户付款后返回接入网站的完整 HTTP / HTTPS 地址 |

通知与返回地址登记时不带查询参数。实际请求可带查询参数，但协议、域名、端口、路径必须与登记值一致；路径末尾斜杠也要一致。通知必须公网可达，网关不跟随通知重定向，不向本机、内网及保留地址发送通知。

### NewAPI

在 NewAPI 的易支付设置中填写以上根地址、PID、KEY，并启用支付宝。后台网站类型选择 NewAPI。

常用通知路径：`/api/user/epay/notify`、`/api/subscription/epay/notify`；订阅返回路径：`/api/subscription/epay/return`。充值返回路径因版本而异，例如 `/console/log` 或 `/usage-logs`。**以实际版本下单请求里的完整地址为准**，在支付后台登记；不要照搬其他站点的域名。

### Sub2API

支付服务商选择 EasyPay，API 地址填写根地址，PID 和 PKey 填写网站凭据，支付方式选择 `alipay`，通道 ID 留空。本后台网站类型选择「通用易支付」。常用通知路径为 `/api/v1/payment/webhook/easypay`，返回路径为 `/payment/result`。

Sub2API 返回地址中的 `order_id`、`out_trade_no`、`resume_token`、`status` 查询参数在签名时原样保留。返回链接带有 `status=success` 不代表网关已确认付款，接入方仍需验签并查单或处理异步通知。

## 2. 接口与请求格式

| 接口 | 方法 | 行为 |
| --- | --- | --- |
| `/submit.php` | GET / POST | 成功后以 HTTP 303 跳转至收银台 |
| `/mapi.php` | GET / POST | 成功返回 JSON，包含收银台链接 |
| `/api.php` | GET / POST | 使用 PID / KEY 查询订单 |

三个接口同时支持 `/xpay/epay/` 前缀，例如 `/xpay/epay/submit.php`。POST 下单使用 `Content-Type: application/x-www-form-urlencoded`；不接受 JSON 下单。POST 参数放在请求体中，不混入 URL 查询参数。相同一级参数不得重复。

### 下单参数

| 字段 | 必填 | 含义与限制 |
| --- | --- | --- |
| `pid` | 是 | 网站商户 PID |
| `type` | 是 | 固定为 `alipay` |
| `out_trade_no` | 是 | 网站订单号，1–64 个英文字母、数字、下划线或短横线；同一网站唯一 |
| `name` | 是 | 商品名称，UTF-8，最长 127 字节；原样传给支付宝 |
| `money` | 是 | 元，十进制字符串，例如 `10.06`；最低 `0.01`，最多两位小数，且不超过网站单笔限额 |
| `notify_url` | 是 | 已登记的异步通知地址 |
| `return_url` | 是 | 已登记的付款返回地址 |
| `sign` | 是 | 小写 MD5 签名，32 位十六进制 |
| `sign_type` | 建议 | `MD5`；省略时也按 MD5 校验 |
| `device` | 否 | `pc` 或 `mobile`，选择支付页面；也会参考浏览器 User-Agent 与应用可用产品 |
| `param` | 否 | 透传字符串，最长 1024 字节；通知中原样返回 |
| `expiry_minutes` | 否 | 本次订单有效期，1–21600 分钟的整数；不传时使用网站配置，默认 30 分钟 |

金额不要使用浮点数计算后再直接拼接。有效期截止时间按分钟向上取整，可能额外增加不足 60 秒。同一网站订单号重复提交返回 HTTP 409；发生超时先查原订单，不要直接生成另一笔订单。

## 3. MD5 签名与字段扩展

1. 取表单解码一次后的所有一级字段，排除一级 `sign`、`sign_type` 及值为空字符串的字段。
2. 按字段名升序排列，拼接为 `key=value&key=value`，字段名大小写保留。
3. 在拼接字符串末尾直接追加商户 KEY，不加 `&key=`。
4. 对 UTF-8 字节计算 MD5，输出小写十六进制。
5. 将签名加入参数后，再用标准表单编码生成请求体或查询字符串。

一级新增字段自动参与签名。URL、JSON 或 N 层嵌套数据作为一个完整字符串参与签名，不递归展开，不重新排序，也不再次解码。不要只签固定的几个字段；验签也必须包含收到的所有非空一级字段。

V1 对原始值使用 `&` 拼接，存在边界歧义。网关会拒绝值中 `&字段名=` 与同名保留字段取值冲突的请求。保留字段为 `pid`、`type`、`money`、`out_trade_no`、`trade_no`、`trade_status`、`name`、`param`、`notify_url`、`return_url`、`expiry_minutes`、`sign`、`sign_type`（边界检查不区分大小写）。普通 `R&D`、`uid=5&plan=pro`、非保留字段或重复值相同的合法参数可以使用。Sub2API 返回地址重复相同的 `out_trade_no` 可以通过。

回跳参数也不能与网关支付字段冲突。对于有歧义的业务数据，优先改用结构化 JSON 放入 `param`，或调整字段名；若必须对内层值编码，接入方需约定同一编码方式，不能对整个请求反复编码。

### Python 下单示例

以下示例只生成请求体，不创建订单。将环境变量设为该网站实际配置后，将输出通过服务端 POST 发给 `/mapi.php`。`PAY_RETURN_URL` 可带接入网站需要的查询参数。

```python
import hashlib
import os
from urllib.parse import urlencode

def epay_sign(fields, key):
    pairs = [f"{k}={fields[k]}" for k in sorted(fields)
             if k not in ("sign", "sign_type") and fields[k] != ""]
    return hashlib.md5(("&".join(pairs) + key).encode("utf-8")).hexdigest()

fields = {
    "pid": os.environ["PAY_PID"],
    "type": "alipay",
    "out_trade_no": os.environ["PAY_ORDER_NO"],
    "name": "API 额度充值",
    "money": "10.06",
    "notify_url": os.environ["PAY_NOTIFY_URL"],
    "return_url": os.environ["PAY_RETURN_URL"],
    "param": '{"user_id":"42","plan":"pro"}',
    "sign_type": "MD5",
}
fields["sign"] = epay_sign(fields, os.environ["PAY_KEY"])
print(urlencode(fields))
```

### 下单响应

`/mapi.php` 成功：

```json
{"code":1,"msg":"success","trade_no":"CPexample","payurl":"https://pay.strcpy.cn/checkout/CPexample","money":"10.06"}
```

跳转用户到 `payurl` 完成付款。`trade_no` 是网关支付订单号，并非支付宝交易号。获得链接仅代表网关创建了待支付订单。

下单失败返回非 2xx HTTP 状态，响应体为文本。常见情况：400 参数、金额或回调地址错误；401 PID 或签名无效；409 订单重复或收款应用不可用；415 下单 POST 使用了错误 Content-Type。接入方先判断 HTTP 状态，再解析成功 JSON。

## 4. 异步通知与付款后返回

网关确认付款后，POST 表单到订单原始 `notify_url`，包含 `pid`、`trade_no`、`out_trade_no`、`type`、`name`、`money`、`trade_status=TRADE_SUCCESS`、`sign_type=MD5`、`sign`；非空 `param` 也包含在内。

接入网站按以下顺序处理：

1. 拒绝重复一级参数，按本站 KEY 验证全部一级字段签名。
2. 验证 PID、网站订单号、金额、支付方式和 `TRADE_SUCCESS`；金额按整数分比较。
3. 在数据库事务内将订单标记为已支付并完成余额或订阅更新。以订单号做唯一约束，重复通知不重复入账。
4. 事务成功提交后返回 **HTTP 200**，响应体为纯文本 **`success`**。已处理的合法重复通知也返回 `success`。

通知失败会自动重试，间隔逐步增加；管理员也可手动重试。不要在写入失败时返回 `success`。

用户回跳为 GET，网关会将付款参数与原 `return_url` 查询参数合并并重新签名。验签必须包含所有合并后的非空一级字段。浏览器跳转本身不能证明付款，也不能仅靠 `status=success`、前端状态或收银台链接给用户入账。

**付款状态与通知状态独立**：已付款但通知失败仍是已付款。异步通知只发送付款结果，不提供退款扣回事件；退款需人工核对，业务系统自行处理退款后的余额和权益。

## 5. 订单查询

服务端 POST 到 `https://pay.strcpy.cn/api.php`，使用表单字段：

| 字段 | 含义 |
| --- | --- |
| `act` | 固定 `order` |
| `pid` | 网站 PID |
| `key` | 网站 KEY；该接口直接校验凭据，不使用下单 MD5 签名 |
| `out_trade_no` | 网站订单号；与 `trade_no` 至少传一个 |
| `trade_no` | 网关支付订单号，通常以 `CP` 开头；两者都传时必须对应同一订单 |

成功返回 `code=1`、`msg`、`pid`、`trade_no`、`out_trade_no`、`name`、`money`、`type`、`status`、`trade_status`。只查询当前 PID 的订单。此接口返回网关已记录的付款状态，不实时调用支付宝。

| `status` | `trade_status` | 含义 |
| --- | --- | --- |
| `0` | `WAIT_BUYER_PAY` | 尚未确认付款 |
| `1` | `TRADE_SUCCESS` | 已确认付款，与网站通知是否成功无关 |
| `0` | `TRADE_CLOSED` | 已关闭或已过期 |

查单业务错误使用 HTTP 200、`code=-1`、`msg`。因此 HTTP 200 不等于业务成功。退款待核对订单也返回 `code=-1`（订单存在退款，请人工核对），不能当作可再次发货的已支付订单。没有退款、提现或修改订单接口。

## 6. 联调与常见问题

- 在管理员 Playground 模拟下单，核对请求参数、签名、响应、收银台和通知结果。首次真实付款需接入方按其测试安排完成。
- “易支付签名无效”：检查 PID / KEY 是否属于同一网站，是否签了全部非空一级字段，是否在签名后才进行表单编码。
- “通知或返回地址未在网站配置中登记”：对比实际协议、域名、端口、路径，去掉查询参数后与后台登记值逐项核对。
- 已付款但网站未入账：查看独立的通知状态，检查接入网站验签、事务及纯文本 `success` 响应，再重试通知。
- 请求超时或订单重复：用原 `out_trade_no` 查单，不重新发起另一笔充值。
- “参数边界”错误：检查字段值中的 `&保留字段=`，不要更改已经签名的值，参照第 3 节调整内层数据。

本页描述当前服务实现。接入资料中的网站凭据、账号权限及支付宝应用资料仍由管理员管理，公开文档不包含它们。
