订单状态通知
订单开始审核,不合格驳回,订单完成,会以POST(application/x-www-form-urlencoded)方式向商家“回调地址(notice_url)”发送通知。
通知字段:
| 名称 | 固定字段 | 类型 | 说明 |
|---|---|---|---|
| status | 是 | int | 订单状态:2=审核中,3=办理完成,4=打印发货,-1=审核失败,-2=取消完成。 |
| order_no | 是 | string | 订单号 |
| sign | 是 | string | 当前应用的“apikey”md5后的结果,用此字段校验数据安全 |
| image_receipt | 否 | string | 回执照地址,订单完成(status=3)返回 |
| image | 否 | string | 证件照地址,订单完成(status=3)返回,仅在证件照有调整时返回 |
| image_print | 否 | string | 排版照地址,订单完成(status=3)返回,支持规格:广东驾驶证,港澳通行证 |
| reject_msg | 否 | string | 审核驳回理由,审核失败(status=-1)返回 |
| logistics_info | 否 | array | 物流信息,打印发货(status=4)返回
|
商家响应返回示例(JSON):
商家接收并处理通知数据后;需要返回一条json告诉平台当前订单状态同步完成。
{
"data": {},
"code": 0,
"msg": "同步完成"
}
回调处理示例代码:
cURL 用于向本地回调地址发送模拟通知,其余语言为服务端处理函数,并非完整 Web 服务。由框架解析 application/x-www-form-urlencoded 表单,再传入应用 key 和业务处理函数;业务处理必须校验记录归属、幂等落库,失败时不得确认同步成功。 sign 按本文既有 MD5 规则校验,不替代记录归属检查;请勿记录或公开 sign 和应用 key。
# 仅用于本地联调:向你自己的 notice_url 发送模拟通知,不是调用平台 API。
# 将 sign 替换为测试应用 key 的 MD5 小写十六进制结果。
# Bash / zsh,cURL 7.76+。
curl --fail-with-body --max-time 30 --request POST 'http://127.0.0.1:8080/receipt/notice' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'status=3' \
--data-urlencode 'order_no=YOUR_ORDER_NO' \
--data-urlencode 'sign=YOUR_APP_KEY_MD5' \
--data-urlencode 'image_receipt=https://example.com/receipt.jpg'
// Java 11+,服务端处理函数。由 Web 框架解析表单后调用,不是发起 API 请求。
// saveNotification 必须验证记录归属并幂等落库;失败抛异常,不能提前确认同步成功。
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Set;
import java.util.function.Consumer;
public class NoticeHandler {
public static String handleNotification(
Map<String, String> form, String appKey,
Consumer<Map<String, String>> saveNotification) throws Exception {
if (appKey == null || appKey.isEmpty()) {
throw new IllegalArgumentException("未配置应用 key");
}
byte[] digest = MessageDigest.getInstance("MD5")
.digest(appKey.getBytes(StandardCharsets.UTF_8));
StringBuilder expected = new StringBuilder();
for (byte value : digest) {
expected.append(String.format("%02x", value & 0xff));
}
String sign = form.getOrDefault("sign", "");
if (sign == null || !sign.matches("[0-9a-f]{32}") ||
!MessageDigest.isEqual(expected.toString().getBytes(StandardCharsets.UTF_8),
sign.getBytes(StandardCharsets.UTF_8))) {
throw new SecurityException("通知校验失败");
}
String recordId = form.get("order_no");
String status = form.get("status");
if (recordId == null || recordId.isEmpty() || status == null ||
!Set.of("2", "3", "4", "-1", "-2").contains(status)) {
throw new IllegalArgumentException("通知字段无效");
}
Map<String, String> payload = new LinkedHashMap<>(form);
payload.remove("sign");
saveNotification.accept(payload);
// 由 Web 框架以 application/json 输出该字符串。
return "{\"data\":{},\"code\":0,\"msg\":\"同步完成\"}";
}
}
# Python 3.8+,仅使用标准库;由服务端框架解析表单后调用。
# save_notification 必须验证记录归属并幂等落库;失败抛异常。
import hashlib
import hmac
import re
def handle_notification(form, app_key, save_notification):
if not app_key:
raise ValueError("未配置应用 key")
sign = form.get("sign", "")
expected = hashlib.md5(app_key.encode("utf-8")).hexdigest()
if not isinstance(sign, str) or not re.fullmatch(r"[0-9a-f]{32}", sign):
raise ValueError("通知校验失败")
if not hmac.compare_digest(expected, sign):
raise ValueError("通知校验失败")
record_id = form.get("order_no")
status = form.get("status")
if not record_id or status not in ["2","3","4","-1","-2"]:
raise ValueError("通知字段无效")
payload = dict(form)
payload.pop("sign", None)
save_notification(payload)
# 由框架序列化为 JSON;业务处理失败时不要返回 code=0。
return {"data": {}, "code": 0, "msg": "同步完成"}
<?php
// PHP 7.2+,由服务端路由将 $_POST、应用 key 和业务处理函数传入。
// $save_notification 必须验证记录归属并幂等落库;失败抛异常。
function handle_notification(array $form, string $app_key, callable $save_notification): array
{
if ($app_key === '') {
throw new InvalidArgumentException('未配置应用 key');
}
$sign = $form['sign'] ?? '';
if (!is_string($sign) || !preg_match('/^[0-9a-f]{32}$/D', $sign)
|| !hash_equals(md5($app_key), $sign)) {
throw new RuntimeException('通知校验失败');
}
$record_id = $form['order_no'] ?? '';
$status = $form['status'] ?? '';
if (!is_string($record_id) || $record_id === ''
|| !in_array($status, ['2', '3', '4', '-1', '-2'], true)) {
throw new InvalidArgumentException('通知字段无效');
}
unset($form['sign']);
$save_notification($form);
// 调用方使用 json_encode 输出,Content-Type 设置为 application/json。
return ['data' => (object) [], 'code' => 0, 'msg' => '同步完成'];
}
// Go 1.16+,标准库处理函数;由路由调用 request.ParseForm() 后传入 request.PostForm。
// saveNotification 必须验证记录归属并幂等落库;失败返回 error。
package callback
import (
"crypto/md5"
"crypto/subtle"
"fmt"
"net/url"
)
func HandleNotification(form url.Values, appKey string, saveNotification func(url.Values) error) (string, error) {
if appKey == "" {
return "", fmt.Errorf("未配置应用 key")
}
expected := fmt.Sprintf("%x", md5.Sum([]byte(appKey)))
if subtle.ConstantTimeCompare([]byte(expected), []byte(form.Get("sign"))) != 1 {
return "", fmt.Errorf("通知校验失败")
}
allowedStatus := map[string]bool{"2": true, "3": true, "4": true, "-1": true, "-2": true}
if form.Get("order_no") == "" || !allowedStatus[form.Get("status")] {
return "", fmt.Errorf("通知字段无效")
}
payload := url.Values{}
for key, values := range form {
if key != "sign" {
payload[key] = append([]string(nil), values...)
}
}
if err := saveNotification(payload); err != nil {
return "", err
}
// 路由在无错误时以 application/json 输出响应。
return `{"data":{},"code":0,"msg":"同步完成"}`, nil
}
// Node.js 18+,服务端 ES Module;由框架解析 urlencoded 表单后调用。
// saveNotification 必须验证记录归属并幂等落库;失败抛异常。
import { createHash, timingSafeEqual } from 'node:crypto';
export async function handleNotification(form, appKey, saveNotification) {
if (!appKey) throw new Error('未配置应用 key');
const sign = form.sign;
if (typeof sign !== 'string' || !/^[0-9a-f]{32}$/.test(sign)) {
throw new Error('通知校验失败');
}
const expected = createHash('md5').update(appKey, 'utf8').digest('hex');
if (!timingSafeEqual(Buffer.from(expected), Buffer.from(sign))) {
throw new Error('通知校验失败');
}
if (typeof form.order_no !== 'string' || !form.order_no
|| !["2","3","4","-1","-2"].includes(form.status)) {
throw new Error('通知字段无效');
}
const { sign: ignoredSign, ...payload } = form;
await saveNotification(payload);
// 由框架以 JSON 返回;不要在异步落库完成前确认同步成功。
return { data: {}, code: 0, msg: '同步完成' };
}
// C# 10 / .NET 6+,服务端处理类;将框架解析后的表单转成字符串字典传入。
// saveNotification 必须验证记录归属并幂等落库;失败抛异常。
using System;
using System.Collections.Generic;
using System.Security.Cryptography;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
public static class NoticeHandler
{
public static async Task<string> HandleNotification(
Dictionary<string, string> form,
string appKey,
Func<Dictionary<string, string>, Task> saveNotification)
{
if (string.IsNullOrEmpty(appKey))
throw new ArgumentException("未配置应用 key");
form.TryGetValue("sign", out var sign);
using var md5 = MD5.Create();
var expected = Convert.ToHexString(md5.ComputeHash(Encoding.UTF8.GetBytes(appKey))).ToLowerInvariant();
if (sign == null || !Regex.IsMatch(sign, @"\A[0-9a-f]{32}\z") ||
!CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected), Encoding.UTF8.GetBytes(sign)))
throw new InvalidOperationException("通知校验失败");
form.TryGetValue("order_no", out var recordId);
form.TryGetValue("status", out var status);
var allowedStatus = new HashSet<string> { "2", "3", "4", "-1", "-2" };
if (string.IsNullOrEmpty(recordId) || status == null || !allowedStatus.Contains(status))
throw new ArgumentException("通知字段无效");
var payload = new Dictionary<string, string>(form);
payload.Remove("sign");
await saveNotification(payload);
// 由 Web 框架以 application/json 输出该字符串。
return "{\"data\":{},\"code\":0,\"msg\":\"同步完成\"}";
}
}