主题
回调通知机制
当订单状态变更为 confirmed 或 completed 时,系统会异步发送回调通知到商户配置的回调 URL。回调通知采用队列机制,确保可靠投递。
回调 URL 配置
回调 URL 从商户表 sys_huanyu_merchant 的 callback_url 字段读取。商户需要在后台正确配置回调地址以接收支付结果通知。
回调触发时机
- 买单: 支付渠道确认收款后(状态:
pending→confirmed) - 卖单: 支付渠道确认付款后(状态:
pending→confirmed)
回调数据格式
系统向商户回调 URL 发送 POST 请求(application/x-www-form-urlencoded 格式),包含以下参数:
| 参数名 | 类型 | 说明 |
|---|---|---|
merchant_id | int | 商户 ID |
order_id | int | 订单 ID |
order_no | string | 订单编号 |
order_type | int | 订单类型(1=买入,2=卖出) |
status | string | 订单状态(confirmed=已确认) |
cny_amount | string | 人民币金额 |
merchant_amount | string | 商户人民币数量 |
merchant_actual_amount | int | 商户实际到账人民币数量 |
service_amount | int | 服务费金额 |
createtime | int | 订单创建时间戳 |
updatetime | int | 订单更新时间戳 |
timestamp | int | 回调时间戳 |
nonce | string | 随机字符串(16 位) |
signature | string | 回调签名,用于验证回调真实性 |
回调数据示例
merchant_id=42&order_id=123&order_no=ORD202601202128386362&order_type=1&status=confirmed&cny_amount=73000&merchant_amount=73000&merchant_actual_amount=70808&service_amount=2192&createtime=1768915718&updatetime=1768915900×tamp=1768915900&nonce=abc123def456&signature=GENERATED_SIGNATURE_HERE回调签名验证
商户端需要验证回调签名的真实性,验证步骤如下:
- 移除回调数据中的
signature参数 - 按照参数名字典序排序所有参数
- 拼接参数为
key=value&格式 - 在末尾追加
&api_secret=你的商户密钥 - 对完整字符串进行 MD5 加密并转为大写
- 与回调中的
signature参数对比
WARNING
收到回调后必须进行签名验证,防止伪造通知。验证通过后,建议通过 查询订单详情 二次确认订单状态,避免被篡改的回调误导。
回调响应要求
商户的回调接口需要返回特定的响应内容:
- HTTP 状态码:
200 - 响应内容: 包含
success或SUCCESS字符串
text
success或者
json
{"status": "success"}回调重试机制
当回调失败时(非 200、响应不含 success、或请求超时),系统按以下间隔重试:
| 重试次数 | 间隔 |
|---|---|
| 第 1 次 | 60 秒后 |
| 第 2 次 | 120 秒后 |
| 第 3 次 | 180 秒后 |
| ... | 递增 |
- 最多重试: 默认 5 次(可配置)
- 最终失败: 记录日志,不再重试
回调处理示例
php
<?php
// 回调处理示例
class CallbackHandler {
private $apiSecret = 'your_api_secret';
public function handleCallback() {
// 获取 POST 数据
$callbackData = $_POST;
// 验证签名
if (!$this->verifySignature($callbackData)) {
http_response_code(400);
echo 'error: Invalid signature';
return;
}
// 处理业务逻辑
$orderNo = $callbackData['order_no'];
$status = $callbackData['status'];
// 更新本地订单状态
$this->updateOrderStatus($orderNo, $status);
// 返回成功响应
echo 'success';
}
private function verifySignature($data) {
$signature = $data['signature'] ?? '';
unset($data['signature']);
ksort($data);
$signString = '';
foreach ($data as $key => $value) {
if ($value !== '' && $value !== null) {
$signString .= $key . '=' . $value . '&';
}
}
$signString .= 'api_secret=' . $this->apiSecret;
$expectedSignature = strtoupper(md5($signString));
return hash_equals($expectedSignature, $signature);
}
private function updateOrderStatus($orderNo, $status) {
// 更新数据库中的订单状态
// TODO: 实现具体的业务逻辑
}
}
// 处理回调
$handler = new CallbackHandler();
$handler->handleCallback();javascript
const express = require('express');
const crypto = require('crypto');
const app = express();
app.use(express.urlencoded({ extended: true }));
class CallbackHandler {
constructor() {
this.apiSecret = 'your_api_secret';
}
handleCallback(req, res) {
const callbackData = req.body;
// 验证签名
if (!this.verifySignature(callbackData)) {
return res.status(400).send('error: Invalid signature');
}
// 处理业务逻辑
const { order_no, status } = callbackData;
// 更新本地订单状态
this.updateOrderStatus(order_no, status);
// 返回成功响应
res.send('success');
}
verifySignature(data) {
const signature = data.signature || '';
delete data.signature;
const sortedKeys = Object.keys(data).sort();
let signString = '';
sortedKeys.forEach(key => {
if (data[key] !== '' && data[key] !== null) {
signString += `${key}=${data[key]}&`;
}
});
signString += `api_secret=${this.apiSecret}`;
const expectedSignature = crypto.createHash('md5')
.update(signString)
.digest('hex')
.toUpperCase();
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(signature)
);
}
updateOrderStatus(orderNo, status) {
// 更新数据库中的订单状态
// TODO: 实现具体的业务逻辑
console.log(`更新订单 ${orderNo} 状态为 ${status}`);
}
}
const handler = new CallbackHandler();
// 回调接口
app.post('/callback', (req, res) => {
handler.handleCallback(req, res);
});
app.listen(3000, () => {
console.log('回调服务器运行在端口 3000');
});