主题
服务端代码示例
示例位于 web/openapi/examples/。默认使用虚构应用与密钥,仅生成请求,不向网关发起交易。不能将示例密钥用于真实环境。
PHP:签名与生成请求
需要 PHP CLI。以下命令在 web/openapi 目录运行:
sh
php examples/sign.php cashier
php examples/sign.php wechat
php examples/sign.php alipay分别生成 QR_CASHIER、WX_NATIVE、ALI_QR 请求。输出包含真实可计算的示例签名,商户号及应用编号均为虚构。
签名函数:
php
<?php
declare(strict_types=1);
/**
* Mpay 商户服务端签名示例。
*
* 只生成虚构应用的已签名请求,不发送支付请求;真实密钥必须保存在服务端。
*/
function mpaySign(array $params, string $appSecret): string
{
unset($params['sign']);
$params = array_filter($params, static fn ($value) => $value !== null && $value !== '');
uksort($params, 'strcasecmp');
$plain = '';
foreach ($params as $name => $value) {
if (is_array($value) || is_object($value)) {
$value = json_encode($value, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
} elseif (is_bool($value)) {
$value = $value ? 'true' : 'false';
}
$plain .= $name . '=' . $value . '&';
}
return strtoupper(md5($plain . 'key=' . $appSecret));
}
if (realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) {
$ways = ['cashier' => 'QR_CASHIER', 'wechat' => 'WX_NATIVE', 'alipay' => 'ALI_QR'];
$mode = $argv[1] ?? 'cashier';
if (!isset($ways[$mode])) {
fwrite(STDERR, "使用:php examples/sign.php [cashier|wechat|alipay]\n");
exit(1);
}
$params = [
'mchNo' => 'M_DEMO', 'appId' => 'APP_DEMO', 'mchOrderNo' => 'ORDER_DEMO_001',
'wayCode' => $ways[$mode], 'amount' => 100, 'currency' => 'cny',
'subject' => '测试商品', 'body' => '一笔测试订单',
'notifyUrl' => 'https://merchant.example.com/payment/notify',
'reqTime' => '1792108800000', 'signType' => 'MD5',
];
$params['sign'] = mpaySign($params, 'DEMO_SECRET');
echo json_encode($params, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR), "\n";
}接入时通过服务端配置或环境变量取得真实应用资料,替换参数后重新签名。不要编辑本项目公开文档写入生产密钥。
PHP:发送请求与验签
下面代码放在商户服务端,与签名文件配合。$gateway 为实际网关;$params 必须使用真实应用参数重新构造;$secret 来自服务端安全配置。
php
require 'examples/sign.php';
$params['sign'] = mpaySign($params, $secret);
$ch = curl_init(rtrim($gateway, '/') . '/api/pay/unifiedOrder');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode($params, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 30,
]);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($raw === false || $status < 200 || $status >= 300) {
// 传输失败仍可能已创建订单;按原商户订单号查询。
throw new RuntimeException('请求未取得可确认结果:' . $error);
}
$result = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
if (($result['code'] ?? null) !== 0) {
throw new RuntimeException($result['msg'] ?? '接口调用失败');
}
$data = $result['data'] ?? null;
if (!is_array($data) || $data === [] || !isset($result['sign'])
|| !hash_equals(mpaySign($data, $secret), strtoupper(trim($result['sign'])))) {
throw new RuntimeException('响应验签失败');
}
// 按 payDataType 展示支付;不能因 code=0 就发货。
echo json_encode($data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);TLS 校验使用 cURL 默认安全配置;正式调用不关闭证书校验。不要在客户端直接持有密钥调用开放 API。
Node.js:签名
需要 Node.js 20 或更高版本:
sh
npm run check:examplesjs
import { createHash } from 'node:crypto'
import assert from 'node:assert/strict'
import { pathToFileURL } from 'node:url'
// 仅供 Node.js 服务端使用;参数对象应与最终发送内容完全一致。
export function sign(params, appSecret) {
const values = Object.entries(params)
.filter(([key, value]) => key !== 'sign' && value !== null && value !== undefined && value !== '')
.sort(([a], [b]) => {
const x = a.toLowerCase(), y = b.toLowerCase()
return x < y ? -1 : x > y ? 1 : 0
})
.map(([key, value]) => {
const text = typeof value === 'object' ? JSON.stringify(value) : String(value)
return `${key}=${text}&`
}).join('')
return createHash('md5').update(values + 'key=' + appSecret, 'utf8').digest('hex').toUpperCase()
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
// 固定向量覆盖空值过滤、0、布尔值、大小写排序及中文 JSON。
const vector = { Z: 0, a: '中文', empty: '', nullable: null, enabled: false, extra: { url: 'https://example.com/路径' }, sign: 'ignored' }
const plain = 'a=中文&enabled=false&extra={"url":"https://example.com/路径"}&Z=0&key=DEMO_SECRET'
assert.equal(sign(vector, 'DEMO_SECRET'), createHash('md5').update(plain).digest('hex').toUpperCase())
const params = {
mchNo: 'M_DEMO', appId: 'APP_DEMO', mchOrderNo: 'ORDER_20261016_001',
wayCode: 'ALI_QR', amount: 100, currency: 'cny', subject: '测试商品', body: '一笔测试订单',
notifyUrl: 'https://merchant.example.com/payment/notify', reqTime: '1792108800000', signType: 'MD5'
}
console.log(JSON.stringify({ ...params, sign: sign(params, 'DEMO_SECRET') }, null, 2))
}创建签名时过滤 null 与空串,保留 0 / false。数组、对象必须与最终发送内容保持同一 JSON 序列化顺序。
curl:已签名请求
将真实参数在服务端签名后保存为 request.json,再执行:
sh
curl --connect-timeout 5 --max-time 30 \
-H 'Content-Type: application/json' \
--data-binary @request.json \
https://pay.example.com/api/pay/unifiedOrder不要直接发送示例占位签名。超时先 查原单,响应成功先 验签 data。
通知处理伪代码
text
解析 POST 表单 → 用本地订单定位应用密钥
→ 对全部非空通知字段验签(排除 sign)
→ 核对商户号、应用、订单号、金额、币种、状态
→ 在商户数据库事务内幂等入账 / 发货
→ 提交成功后返回 HTTP 2xx + 纯文本 SUCCESS只收到通知但业务事务失败时,不返回 SUCCESS。重复通知读取业务幂等结果后可直接确认。完整协议见 商户异步通知。