使用文档

时光授权系统文档

从快速开始到 API 对接,全面了解授权系统的使用与集成

回调说明

当用户完成支付后,易支付网关会向授权服务器发送异步回调(notify_url)。本章节讲解回调的格式、签名验证、处理逻辑。

实际回调入口位于 /epay_notify.php(不是 /api/ 下),通过 GET 方式接收(兼容彩虹易支付、异次元、YPay 等通用网关)。

回调流程

┌──────────┐    用户支付成功    ┌──────────┐    异步 GET      ┌──────────────┐
│ 用户     │  ──────────────▶ │ 易支付网关 │  ─────────────▶ │ License Server │
│         │                   │          │  notify_url=     │ /epay_notify. │
│         │                   │          │  .../notify      │  php          │
└──────────┘                   └──────────┘                  └──────────────┘
                                                                      │
                                                                      │ 完成订单
                                                                      │ 生成授权码
                                                                      ▼
                                                              ┌──────────────┐
                                                              │ lic_orders    │
                                                              │ lic_licenses  │
                                                              └──────────────┘

回调地址

在创建订单时传给易支付的 notify_url 形如:

https://license.example.com/epay_notify.php

对应的 return_url(同步跳转)为:

https://license.example.com/epay_return.php

回调参数(GET)

参数类型说明
pidint商户 ID(必须与后台 epay_pid 一致)
trade_nostring易支付平台订单号
out_trade_nostring商户订单号(我们的 order_no)
typestring支付方式:alipay / wxpay / qqpay / union
namestring商品名称
moneystring金额(字符串,如 "39.00")
trade_statusstring支付状态:TRADE_SUCCESS
signstring易支付签名
sign_typestring签名类型:MD5 / RSA

签名验证

易支付使用 MD5 或 RSA 签名。验证步骤:

  1. 按 key 排序(排除 sign 和 sign_type)
  2. 拼接成 key=value&key=value... 形式
  3. 末尾追加 &key={商户密钥}
  4. 计算 MD5,与回调里的 sign 比对

实际项目使用 inc/functions.php 中的 epay_verify_sign(),密钥从设置表自动读取:

<?php
function epay_verify_sign($params) {
    // 1. 排除 sign 和 sign_type
    unset($params['sign'], $params['sign_type']);

    // 2. 按 key 排序
    ksort($params);

    // 3. 拼接字符串
    $sign_str = '';
    foreach ($params as $key => $value) {
        $sign_str .= $key . '=' . $value . '&';
    }

    // 4. 优先用 epay_key 验签,失败再尝试 epay2_key(兼容双通道)
    $keys = [(string)get_setting('epay_key', '')];
    if (get_setting('epay2_enabled', '0') === '1') {
        $keys[] = (string)get_setting('epay2_key', '');
    }
    foreach ($keys as $epay_key) {
        if ($epay_key === '') continue;
        $expected = md5($sign_str . 'key=' . $epay_key);
        if (hash_equals($expected, (string)($params['sign'] ?? ''))) {
            return true;
        }
    }
    return false;
}

七层安全校验

epay_notify.php 实现了完整的七层安全校验,任何一层失败都会返回 fail 让易支付重试:

  1. 签名验证:epay_verify_sign() 同时尝试第一/二通道密钥
  2. PID 校验:回调的 pid 必须严格等于后台配置的 epay_pid,防止跨商户攻击
  3. 状态校验:trade_status 必须严格等于 TRADE_SUCCESS
  4. 订单号非空:out_trade_no 和 trade_no 都必须非空
  5. 订单存在:get_order_by_no() 必须能找到
  6. 金额校验:bccomp() 精确到分比对回调金额和订单金额(防 0.01 元买 39 元)
  7. 支付方式校验:回调的 type 必须与订单的 pay_type 一致(防错通道触发)

幂等性

易支付可能多次回调(网络问题),所以必须幂等。实现采用 事务 + rowCount 双重幂等:

  • 订单已是 paid / completed → 直接返回 success(不再重发)
  • 并发场景下用 UPDATE ... WHERE pay_status = 'pending' 锁定,rowCount() 为 0 表示被其他进程抢到,本进程直接返回 success

数据库建议索引:

ALTER TABLE lic_orders ADD UNIQUE KEY uk_trade_no (trade_no);

处理逻辑(精简版)

<?php
// epay_notify.php

require_once __DIR__ . '/inc/functions.php';

$params = $_GET;

// 1-7. 七层安全校验
if (!epay_verify_sign($params)) { echo 'fail'; exit; }
if ($params['pid'] !== get_setting('epay_pid')) { echo 'fail'; exit; }
if ($params['trade_status'] !== 'TRADE_SUCCESS') { echo 'fail'; exit; }

$order = get_order_by_no($params['out_trade_no']);
if (!$order || $order['pay_status'] !== 'pending') { echo 'success'; exit; }
if (bccomp(number_format($order['amount'], 2, '.', ''),
           number_format($params['money'], 2, '.', ''), 2) !== 0) { echo 'fail'; exit; }

// 8. 事务 + 幂等
$db = get_db();
$db->beginTransaction();
try {
    $stmt = $db->prepare("UPDATE " . table('orders') . "
        SET pay_status = 'paid', trade_no = ?, paid_at = NOW()
        WHERE id = ? AND pay_status = 'pending'");
    $stmt->execute([$params['trade_no'], $order['id']]);

    if ($stmt->rowCount() === 0) {
        // 被其他进程抢到了
        $db->commit();
        echo 'success';
        exit;
    }

    // 9. 完成订单(生成授权码 / 续期)
    $order['pay_status'] = 'paid';
    $order['trade_no']   = $params['trade_no'];
    complete_order($order);

    $db->commit();
    echo 'success';
} catch (Throwable $e) {
    $db->rollBack();
    error_log('epay_notify error: ' . $e->getMessage());
    echo 'fail';
}

常见问题

问题原因解决
回调收不到 服务器防火墙拦截 在易支付白名单加上授权服务器 IP
签名验证失败 商户密钥配置错误 核对 epay_key 是否与易支付商户后台一致
金额不匹配(fail) 回调金额与订单金额小数位不一致 已用 bccomp 精确到分比对,确保订单创建时用字符串 "39.00"
支付方式不匹配(fail) 用户实际用支付宝,订单标记为微信 检查用户下单时选择的通道
订单已存在但未生成授权 complete_order() 中途失败 查看 PHP 错误日志,定位 SQL 错误;事务已回滚可手动重试
订单生成授权的完整代码见 设计思路 → 订单生命周期。