Skip to content

服务端代码示例 ​

示例位于 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:examples
js
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。重复通知读取业务幂等结果后可直接确认。完整协议见 商户异步通知。

接口行为以当前 payment 实现为准。示例商户号与密钥均为虚构。