概述

PatMall支付系统为商户提供统一的支付接口,支持微信支付、支付宝等多种支付方式。 本文档将帮助您快速集成PatMall支付服务。

统一接口

一套API支持所有支付方式

安全可靠

MD5签名机制保障安全

快速接入

5分钟完成集成

快速开始

1

获取商户凭证

登录商户后台,在"API配置"中获取以下信息:

商户号(merchantNo): MCH17625364836767533(字符串,创建订单时使用)
API密钥(apiSecret): 9557b487d93c9281a754039852907c33(32位MD5值,用于签名)
通道编码(channelCode): wechat_native(必填,如wechat_native、alipay_h5等)
接口地址(apiUrl): https://api.yourdomain.com(系统自动生成,不要以/结尾)

说明:
merchantNo:商户号(字符串,如:MCH17625364836767533,创建订单时使用)
apiSecret:API密钥(32位MD5值,用于API请求和回调通知的MD5签名)
channelCode:通道编码(必填,如wechat_native、alipay_h5等,不传则无法创建订单)
apiUrl:接口地址(系统自动生成,从商户后台API配置中获取真实的后端地址,不要以/结尾)
• 回调通知使用apiSecret进行MD5签名(与API请求签名方式完全相同)

2

理解签名机制

所有API请求都需要MD5签名验证,使用apiSecret进行签名

• 排除sign字段和空值

• 签名参数按ASCII码升序排列

• 拼接成 key=value&key=value 格式

• 追加 &key=apiSecret

• MD5加密后转大写

3

调用API创建订单

发送POST请求到创建订单接口

POST http://127.0.0.1:8001/open/pay/create
Content-Type: application/json

{
  "merchantNo": "MCH17625364836767533",
  "outTradeNo": "MCH20231201001",
  "amount": 1.00,
  "subject": "测试商品",
  "channelCode": "wechat_native",
  "timestamp": 1234567890,
  "sign": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}
4

引导用户支付

使用返回的paymentUrl引导用户跳转到支付页面

5

接收支付通知

配置回调地址,接收异步支付通知

签名机制

签名生成步骤

步骤1: 准备参数

merchantNo: MCH17625364836767533
amount: 1.00
subject: 测试商品
timestamp: 1234567890

步骤2: 过滤并排序

排除sign字段,按ASCII码升序排列

amount, merchantNo, subject, timestamp

步骤3: 拼接字符串

amount=1.00&merchantNo=MCH17625364836767533&subject=测试商品×tamp=1234567890

步骤4: 追加密钥

amount=1.00&merchantNo=MCH17625364836767533&subject=%E6%B5%8B%E8%AF%95%E5%95%86%E5%93%81×tamp=1234567890&key=YOUR_API_SECRET

步骤5: MD5加密并转大写

sign = MD5(stringSignTemp).toUpperCase()

在线签名计算器

必填,如:wechat_native、alipay_h5等

用于API请求和回调通知签名,在商户后台"API配置"中获取(32位MD5值)

API文档

基础信息

接口地址:
http://127.0.0.1:8001(请联系客服获取真实接口地址)
请求方式:POST (JSON)
字符编码:UTF-8
签名算法:MD5
金额单位:元 (人民币, 最多2位小数)

创建支付订单

创建支付订单并获取支付链接

POST
http://127.0.0.1:8001/open/pay/create

请求参数

参数名类型必填说明
merchantNoString商户号(字符串,如:MCH17625364836767533,在商户后台API配置中获取)
outTradeNoString商户订单号(必填,用于异步通知时识别订单)
amountNumber订单金额(元,最多2位小数)
subjectString订单标题
channelCodeString通道编码(必填,如wechat_native、alipay_h5等,不传则无法创建订单)
timestampNumber时间戳(秒)
signStringMD5签名
bodyString商品描述
notifyUrlString异步通知地址(用于接收支付结果通知,支持占位符。如果不传,则使用商户后台配置的默认notifyUrl)
returnUrlString同步跳转地址(如果不传,则使用商户后台配置的默认returnUrl)
requestIpString发送订单IP(可以是商户服务器IP或用户真实IP,由商户决定传什么)
attachString附加数据(原样返回,用于商户自定义参数)

返回参数

{
  "code": 1000,
  "message": "success",
  "data": {
    "orderNo": "P20231201120000001",
    "mchOrderNo": "MCH20231201001",
    "amount": 1.00,
    "status": 1,
    "paymentUrl": "https://pay.yourdomain.com/payment/wechat?orderNo=P20231201120000001",
    "expireTime": "2023-12-01 12:30:00"
  }
}

请求示例

const crypto = require('crypto');

// 1. 准备参数
const params = {
  merchantNo: 'MCH17625364836767533',
  outTradeNo: 'MCH20231201001', // 必填,商户订单号
  amount: 1.00,
  subject: '测试商品',
  channelCode: 'wechat_native', // 必填,通道编码
  timestamp: Math.floor(Date.now() / 1000)
};

// 2. 生成签名(使用apiSecret,32位MD5值)
const apiSecret = 'YOUR_API_SECRET'; // 商户后台API配置中的apiSecret(32位MD5值)
// 过滤空值和sign字段
const filteredParams = {};
Object.keys(params).forEach(key => {
  if (key !== 'sign' && params[key] !== undefined && params[key] !== null && params[key] !== '') {
    filteredParams[key] = params[key];
  }
});
// 按ASCII码升序排序
const sortedKeys = Object.keys(filteredParams).sort();
const stringA = sortedKeys.map(k => `${k}=${filteredParams[k]}`).join('&');
const stringSignTemp = `${stringA}&key=${apiSecret}`;
const sign = crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();

// 3. 发送请求
const requestData = { ...params, sign };
const response = await fetch('http://127.0.0.1:8001/open/pay/create', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify(requestData)
});

const result = await response.json();
console.log(result);

// 4. 处理返回结果
if (result.code === 1000) {
  // 创建成功,引导用户跳转到支付页面
  window.location.href = result.data.paymentUrl;
} else {
  // 创建失败
  console.error('创建订单失败:', result.message);
}

查询订单

查询订单支付状态和详情

POST
http://127.0.0.1:8001/open/pay/query

请求参数

参数名类型必填说明
orderNoString二选一系统订单号
outTradeNoString二选一商户订单号

返回参数

{
  "code": 1000,
  "message": "success",
  "data": {
    "orderNo": "P20231201120000001",
    "mchOrderNo": "MCH20231201001",
    "amount": 1.00,
    "status": 2,
    "payTime": "2023-12-01 12:05:30",
    "channelType": "wxpay",
    "payMethod": "native",
    "subject": "测试商品"
  }
}

关闭订单

关闭未支付的订单

POST
http://127.0.0.1:8001/open/pay/close

请求参数

与查询订单接口相同,提供orderNo或outTradeNo即可

返回参数

{
  "code": 1000,
  "message": "订单关闭成功",
  "data": {
    "orderNo": "P20231201120000001",
    "status": 3
  }
}

申请退款

对已支付订单申请退款

POST
http://127.0.0.1:8001/open/pay/refund

请求参数

参数名类型必填说明
orderNoString系统订单号
refundAmountNumber退款金额(元,最多2位小数)
refundReasonString退款原因

返回参数

{
  "code": 1000,
  "message": "退款申请成功",
  "data": {
    "refundNo": "R20231201120000001",
    "orderNo": "P20231201120000001",
    "refundAmount": 1.00,
    "status": 1
  }
}

回调通知

支付成功后,系统会向商户配置的notifyUrl发送异步通知。

通知参数

参数名类型说明
orderNoString系统订单号(用于商户查询订单状态)
mchOrderNoString商户订单号 ✅ 核心字段
amountString订单金额(元,字符串格式,保留2位小数)
statusNumber订单状态(1=待支付 2=已支付 3=已关闭 4=已退款)
payTimeNumber支付时间(Unix时间戳,秒)
timestampNumber请求时间戳(Unix时间戳,秒,用于防重放攻击)
nonce_strString随机字符串(32位,用于防重放攻击)
channelTypeString通道类型(wxpay/alipay等)
timestampNumber通知时间戳(秒)
nonce_strString随机字符串
isTestNumber是否测试回调(0=否, 1=是)
signStringMD5签名(使用apiSecret签名,与API请求签名方式完全相同)

⚠️ 重要说明

  • • 回调通知使用 apiSecret 进行MD5签名(与API请求签名方式完全相同)
  • • 请务必验证签名以确保通知来自平台
  • • 收到通知后,应返回纯文本 SUCCESS 或JSON格式 {"code": 1000}{"code": "SUCCESS"}
  • • 未返回SUCCESS,系统将重试通知(最多3次)
  • • 回调通知为POST请求,Content-Type为application/json

通知示例

{
  "orderNo": "P20231201120000001",
  "mchOrderNo": "MCH20231201001",
  "amount": "1.00",
  "status": 2,
  "payTime": 1701406530,
  "timestamp": 1701406531,
  "nonce_str": "abc123xyz",
  "sign": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
}

验证签名示例

const crypto = require('crypto');

// 1. 接收通知数据(POST请求,Content-Type: application/json)
const notifyData = req.body;

// 2. 提取签名
const receivedSign = notifyData.sign;

// 3. 生成签名(使用apiSecret,32位MD5值,与API请求签名方式完全相同)
const apiSecret = 'YOUR_API_SECRET'; // 商户后台API配置中的apiSecret(32位MD5值)
const params = { ...notifyData };
delete params.sign; // 排除sign字段

// 过滤空值
const filteredParams = {};
Object.keys(params).forEach(key => {
  if (params[key] !== undefined && params[key] !== null && params[key] !== '') {
    filteredParams[key] = params[key];
  }
});

// 按ASCII码升序排序
const sortedKeys = Object.keys(filteredParams).sort();
const stringA = sortedKeys.map(k => `${k}=${filteredParams[k]}`).join('&');
const stringSignTemp = `${stringA}&key=${apiSecret}`;
const calculatedSign = crypto.createHash('md5')
  .update(stringSignTemp)
  .digest('hex')
  .toUpperCase();

// 4. 验证签名
if (receivedSign === calculatedSign) {
  // 签名验证通过,处理业务逻辑
  console.log('支付成功:', notifyData.orderNo);
  
  // 5. 返回SUCCESS(支持多种格式)
  res.send('SUCCESS');
  // 或返回JSON格式
  // res.json({ code: 1000 });
} else {
  // 签名验证失败
  console.error('签名验证失败');
  res.status(400).send('FAIL');
}

状态码说明

响应状态码

状态码说明
1000成功
1001参数错误
1002签名验证失败
1003商户不存在或已禁用
1004订单不存在
1005支付通道不可用

订单状态

状态值说明
1待支付
2已支付
3已关闭
4已退款

代码示例

Node.js

const crypto = require('crypto');
const axios = require('axios');

class PatMallPayClient {
  constructor(merchantNo, apiSecret) {
    this.merchantNo = merchantNo; // 商户号(字符串,如:MCH17625364836767533)
    this.apiSecret = apiSecret; // API密钥Secret(32位MD5值,用于API请求和回调通知签名)
    this.baseUrl = 'http://127.0.0.1:8001';
  }

  // 生成签名
  generateSign(params) {
    // 过滤空值和sign字段
    const filteredParams = {};
    Object.keys(params).forEach(key => {
      if (key !== 'sign' && params[key] !== undefined && params[key] !== null && params[key] !== '') {
        filteredParams[key] = params[key];
      }
    });
    // 按ASCII码升序排序
    const sortedKeys = Object.keys(filteredParams).sort();
    const stringA = sortedKeys.map(k => `${k}=${filteredParams[k]}`).join('&');
    const stringSignTemp = `${stringA}&key=${this.apiSecret}`;
    return crypto.createHash('md5').update(stringSignTemp).digest('hex').toUpperCase();
  }

  // 创建订单
  async createOrder(orderData) {
    const params = {
      merchantNo: this.merchantNo,
      outTradeNo: orderData.outTradeNo, // 必填,商户订单号
      amount: orderData.amount,
      subject: orderData.subject,
      channelCode: orderData.channelCode, // 必填,通道编码
      timestamp: Math.floor(Date.now() / 1000)
    };
    
    // 可选参数(不为空时才添加)
    if (orderData.body) params.body = orderData.body;
    if (orderData.notifyUrl) params.notifyUrl = orderData.notifyUrl;
    if (orderData.returnUrl) params.returnUrl = orderData.returnUrl;

    params.sign = this.generateSign(params);

    const response = await axios.post(`${this.baseUrl}/open/pay/create`, params);
    return response.data;
  }

  // 查询订单
  async queryOrder(orderNo) {
    const response = await axios.post(`${this.baseUrl}/open/pay/query`, { orderNo });
    return response.data;
  }
}

// 使用示例
const client = new PatMallPayClient('MCH17625364836767533', 'YOUR_API_SECRET');

// 创建订单
const result = await client.createOrder({
  outTradeNo: 'MCH20231201001', // 必填,商户订单号
  amount: 1.00,
  subject: '测试商品',
  channelCode: 'wechat_native', // 必填,通道编码
  body: '商品详情',
  notifyUrl: 'https://your-domain.com/notify',
  returnUrl: 'https://your-domain.com/return'
});

console.log('支付链接:', result.data.paymentUrl);

PHP

<?php
class PatMallPayClient {
    private $merchantNo;
    private $apiSecret;
    private $baseUrl;

    public function __construct($merchantNo, $apiSecret) {
        $this->merchantNo = $merchantNo; // 商户号(字符串,如:MCH17625364836767533)
        $this->apiSecret = $apiSecret; // API密钥Secret(32位MD5值)
        $this->baseUrl = 'http://127.0.0.1:8001';
    }

    // 生成签名
    private function generateSign($params) {
        unset($params['sign']);
        // 过滤空值
        $params = array_filter($params, function($value) {
            return $value !== null && $value !== '';
        });
        ksort($params);
        $stringA = http_build_query($params);
        $stringSignTemp = $stringA . '&key=' . $this->apiSecret;
        return strtoupper(md5($stringSignTemp));
    }

    // 创建订单
    public function createOrder($orderData) {
        $params = [
            'merchantNo' => $this->merchantNo,
            'outTradeNo' => $orderData['outTradeNo'], // 必填,商户订单号
            'amount' => $orderData['amount'],
            'subject' => $orderData['subject'],
            'channelCode' => $orderData['channelCode'], // 必填,通道编码
            'timestamp' => time()
        ];
        
        // 可选参数(不为空时才添加)
        if (!empty($orderData['body'])) $params['body'] = $orderData['body'];
        if (!empty($orderData['notifyUrl'])) $params['notifyUrl'] = $orderData['notifyUrl'];
        if (!empty($orderData['returnUrl'])) $params['returnUrl'] = $orderData['returnUrl'];

        $params['sign'] = $this->generateSign($params);

        $ch = curl_init($this->baseUrl . '/open/pay/create');
        curl_setopt($ch, CURLOPT_POST, 1);
        curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($params));
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
        curl_setopt($ch, CURLOPT_HTTPHEADER, [
            'Content-Type: application/json'
        ]);

        $response = curl_exec($ch);
        curl_close($ch);

        return json_decode($response, true);
    }

    // 验证回调签名(使用apiSecret,与API请求签名方式相同)
    public function verifyNotify($notifyData, $apiSecret) {
        $sign = $notifyData['sign'];
        unset($notifyData['sign']);
        
        // 过滤空值
        $notifyData = array_filter($notifyData, function($value) {
            return $value !== null && $value !== '';
        });
        
        ksort($notifyData);
        $stringA = http_build_query($notifyData);
        $stringSignTemp = $stringA . '&key=' . $apiSecret;
        $calculatedSign = strtoupper(md5($stringSignTemp));

        return $sign === $calculatedSign;
    }
}

// 使用示例
$client = new PatMallPayClient('MCH17625364836767533', 'YOUR_API_SECRET');

$result = $client->createOrder([
    'outTradeNo' => 'MCH20231201001', // 必填,商户订单号
    'amount' => 1.00,
    'subject' => '测试商品',
    'channelCode' => 'wechat_native', // 必填,通道编码
    'body' => '商品详情',
    'notifyUrl' => 'https://your-domain.com/notify.php',
    'returnUrl' => 'https://your-domain.com/return.php'
]);

echo '支付链接: ' . $result['data']['paymentUrl'];
?>

Python

import hashlib
import requests
import time

class PatMallPayClient:
    def __init__(self, merchant_no, api_secret):
        self.merchant_no = merchant_no  # 商户号(字符串,如:MCH17625364836767533)
        self.api_secret = api_secret  # API密钥Secret(32位MD5值)
        self.base_url = 'http://127.0.0.1:8001'

    def generate_sign(self, params):
        """生成签名"""
        # 过滤sign字段和空值
        filtered = {k: v for k, v in params.items() if k != 'sign' and v is not None and v != ''}
        
        # 按key排序
        sorted_keys = sorted(filtered.keys())
        
        # 拼接字符串
        string_a = '&'.join([f'{k}={filtered[k]}' for k in sorted_keys])
        string_sign_temp = f'{string_a}&key={self.api_secret}'
        
        # MD5加密并转大写
        return hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()

    def create_order(self, order_data):
        """创建订单"""
        params = {
            'merchantNo': self.merchant_no,
            'outTradeNo': order_data['outTradeNo'],  # 必填,商户订单号
            'amount': order_data['amount'],
            'subject': order_data['subject'],
            'channelCode': order_data['channelCode'],  # 必填,通道编码
            'timestamp': int(time.time())
        }
        
        # 可选参数(不为空时才添加)
        if order_data.get('body'):
            params['body'] = order_data['body']
        if order_data.get('notifyUrl'):
            params['notifyUrl'] = order_data['notifyUrl']
        if order_data.get('returnUrl'):
            params['returnUrl'] = order_data['returnUrl']

        params['sign'] = self.generate_sign(params)

        response = requests.post(f'{self.base_url}/open/pay/create', json=params)
        return response.json()

    def query_order(self, order_no):
        """查询订单"""
        response = requests.post(f'{self.base_url}/open/pay/query', json={'orderNo': order_no})
        return response.json()

    def verify_notify(self, notify_data, api_secret):
        """验证回调签名(使用apiSecret,与API请求签名方式相同)"""
        sign = notify_data.get('sign')
        if not sign:
            return False
        
        # 生成签名(使用apiSecret)
        params = {k: v for k, v in notify_data.items() if k != 'sign'}
        calculated_sign = self.generate_sign_with_secret(params, api_secret)
        
        return sign == calculated_sign

    def generate_sign_with_secret(self, params, secret):
        """使用指定密钥生成签名"""
        # 过滤空值
        filtered = {k: v for k, v in params.items() if v is not None and v != ''}
        sorted_keys = sorted(filtered.keys())
        # 拼接字符串
        string_a = '&'.join([f'{k}={filtered[k]}' for k in sorted_keys])
        string_sign_temp = f'{string_a}&key={secret}'
        return hashlib.md5(string_sign_temp.encode('utf-8')).hexdigest().upper()

# 使用示例
client = PatMallPayClient('MCH17625364836767533', 'YOUR_API_SECRET')

result = client.create_order({
    'outTradeNo': 'MCH20231201001',  # 必填,商户订单号
    'amount': 1.00,
    'subject': '测试商品',
    'channelCode': 'wechat_native',  # 必填,通道编码
    'body': '商品详情',
    'notifyUrl': 'https://your-domain.com/notify',
    'returnUrl': 'https://your-domain.com/return'
})

print('支付链接:', result['data']['paymentUrl'])

常见问题

Q: 签名验证失败怎么办?

A: 请检查以下几点:

  • 确认使用的是正确的API密钥(apiSecret)
  • 确认参数按ASCII码升序排列
  • 确认MD5结果已转为大写
  • 确认没有将sign字段参与签名计算

Q: 回调通知如何验签?

A: 回调通知使用apiSecret进行MD5签名,与API请求签名方式完全相同。验签方法与请求签名相同,使用相同的apiSecret(32位MD5值)。

Q: 对接需要填写哪些信息?

A: 在商户后台"API配置"中可以获取以下信息:

  • merchantNo: 商户号(字符串,如:MCH17625364836767533,创建订单时使用)
  • apiSecret: API密钥(32位MD5值,用于API请求和回调通知的MD5签名)
  • apiUrl: 接口地址(系统自动生成,从商户后台API配置中获取真实的后端地址)
  • channelCode: 通道编码(必填,如wechat_native、alipay_h5等,不传则无法创建订单)
  • notifyUrl: 默认异步通知地址(可选,也可在创建订单时传入)
  • returnUrl: 默认同步跳转地址(可选,也可在创建订单时传入)

Q: 订单金额单位是什么?

A: 所有金额单位为,最多支持2位小数。例如:1.00、99.99、0.01。

Q: 如何选择支付方式?

A: 创建订单时可以指定channelCode(如wechat_native、alipay_h5等),如不指定则由系统根据用户环境自动选择最佳支付方式。

Q: 回调通知会重试吗?

A: 如果您的服务器未返回"SUCCESS",系统会进行重试,最多重试3次。