Files
Sdf1_login/api/pay.php
T

569 lines
24 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 债券在线充值 · 支付平台联控(彩虹易支付 RSA 版)
*
* 动作:
* - create_order : 校验登录态 → 生成订单 → RSA签名 → 返回跳转支付URL
* - notify : 平台异步回调(GET)→ 平台公钥验签 → 金额校验 → 幂等 → 写 web_transactions(recharge)
* - query_order : 前端补单/轮询订单状态
*
* ★ 安全:本文件不含任何密钥。密钥仅从 pay_secrets.php(git-ignored,仅定义常量)读取;
* 本地 txt 文件不会被代码引用,仅作人工本地备份。
*/
// 防止 PHP 错误/弃用警告以 HTML 形式污染 JSON / notify 纯文本响应
@ini_set('display_errors', '0');
@ini_set('html_errors', '0');
error_reporting(E_ALL);
// 防止任何前置输出污染 JSON / notify 纯文本响应
ob_start();
require_once __DIR__ . '/../core.php';
ob_end_clean();
// ===== 非机密常量(可安全提交) =====
// 彩虹易支付 下单网关
define('PAY_GATEWAY', 'https://zf.ypshidifu.cn/api/pay/submit');
// 异步通知地址(平台服务器回调,必须公网可达)
define('PAY_NOTIFY_URL', 'https://caoyuan.ypshidifu.cn/plugin/api/pay.php?action=notify');
// 同步跳转地址(玩家付款后浏览器跳回)
define('PAY_RETURN_URL', 'https://caoyuan.ypshidifu.cn/plugin/player.php?paid=1');
define('PAY_SIGN_TYPE', 'MD5'); // 提交用 MD5(商户 MD5 密钥 pay_user.key,末尾无点);用户新私钥数据损坏无法自验,暂用 MD5
// ===== 0.01 元测试配置(先跑通全链路,后续替换为 recharge_tiers 档位) =====
define('PAY_TEST_MONEY', '0.01'); // 元(字符串,与回调比对)
define('PAY_TEST_BONDS', 1); // 对应债券数量
define('PAY_TEST_NAME', '债券充值-测试0.01元');
// ====================================================================
// 密钥加载(仅使用 pay_secrets.php,严禁引用本地 txt 记录文件)
// ====================================================================
function loadPayKeys() {
$secretsFile = __DIR__ . '/pay_secrets.php';
if (@is_file($secretsFile) && @is_readable($secretsFile)) {
@require_once $secretsFile;
if (defined('PAY_MCH_PRIVATE_KEY') && defined('PAY_PLATFORM_PUBLIC_KEY')) {
return [
'pid' => defined('PAY_PID') ? PAY_PID : '',
'private_key' => wrapPem(PAY_MCH_PRIVATE_KEY, 'private'),
'public_key' => wrapPem(PAY_PLATFORM_PUBLIC_KEY, 'public'),
'md5_key' => defined('PAY_MD5_KEY') ? PAY_MD5_KEY : '',
];
}
}
return ['pid' => '', 'private_key' => '', 'public_key' => '', 'md5_key' => ''];
}
/** 将裸 base64 包装为 PEM。私钥自动探测 PKCS#1(RSA PRIVATE KEY) / PKCS#8(PRIVATE KEY),公钥用 SubjectPublicKeyInfo(PUBLIC KEY) */
function wrapPem($b64, $type) {
$b64 = preg_replace('/\s+/', '', $b64);
if ($b64 === '') return '';
// 探测 DER:PKCS#8(PRIVATE KEY) 与 PKCS#1(RSA PRIVATE KEY) 都以
// SEQUENCE{INTEGER 0(version)} 开头(即 30 82 .. 02 01 00),无法靠前 7 字节区分。
// 关键在 version 之后紧跟的标签:PKCS#8 是 AlgorithmIdentifier 的 SEQUENCE(0x30),
// PKCS#1 是 modulus 的 INTEGER(0x02)。据此决定用哪个 PEM 头(opensslLoadKey 仍有兜底重试)。
$header = 'PUBLIC KEY';
if ($type === 'private') {
$raw = @base64_decode($b64, true);
$isPkcs1 = ($raw !== false && strlen($raw) > 8
&& $raw[0] === "\x30" && $raw[1] === "\x82"
&& $raw[4] === "\x02" && $raw[5] === "\x01" && $raw[6] === "\x00"
&& $raw[7] === "\x02");
$header = $isPkcs1 ? 'RSA PRIVATE KEY' : 'PRIVATE KEY';
}
$body = trim(chunk_split($b64, 64, "\n"));
return "-----BEGIN $header-----\n$body\n-----END $header-----\n";
}
/**
* 容错加载密钥:首次用给定 PEM 加载失败后,自动在 PKCS#1<->PKCS#8 头之间切换重试,
* 彻底规避「密钥格式与 PEM 头不匹配」导致的签名/验签失败(支付链路关键路径,必须健壮)。
*/
function opensslLoadKey($pem, $isPrivate) {
$fn = $isPrivate ? 'openssl_pkey_get_private' : 'openssl_pkey_get_public';
$key = @$fn($pem);
if ($key !== false) return $key;
if (!$isPrivate) return false;
// 私钥:在 RSA PRIVATE KEY <-> PRIVATE KEY 之间切换重试
$alt = preg_replace('/-----BEGIN RSA PRIVATE KEY-----/', "-----BEGIN PRIVATE KEY-----", $pem);
$alt = preg_replace('/-----END RSA PRIVATE KEY-----/', "-----END PRIVATE KEY-----", $alt);
if ($alt !== $pem && ($key = @$fn($alt)) !== false) return $key;
$alt = preg_replace('/-----BEGIN PRIVATE KEY-----/', "-----BEGIN RSA PRIVATE KEY-----", $pem);
$alt = preg_replace('/-----END PRIVATE KEY-----/', "-----END RSA PRIVATE KEY-----", $alt);
if ($alt !== $pem && ($key = @$fn($alt)) !== false) return $key;
return false;
}
/**
* 请求签名:根据 sign_type 选择算法
* - RSA : 取全部非空参数 → 剔除 sign/sign_type → ASCII 升序 → k=v& 拼接 → 私钥 SHA256WithRSA 签名
* - MD5 : 同上拼接后追加 &key=商户MD5密钥 → md5(彩虹易支付MD5方式,无需密钥对,可绕过RSA密钥不匹配)
*/
function buildSign($params, $keys) {
$filtered = [];
foreach ($params as $k => $v) {
if ($k === 'sign' || $k === 'sign_type') continue;
if ($v === '' || $v === null) continue;
$filtered[$k] = $v;
}
ksort($filtered, SORT_STRING);
$parts = [];
foreach ($filtered as $k => $v) {
$parts[] = $k . '=' . $v;
}
$data = implode('&', $parts);
$type = $params['sign_type'] ?? PAY_SIGN_TYPE;
if ($type === 'RSA') {
$key = opensslLoadKey($keys['private_key'], true);
if ($key === false) {
throw new Exception('商户私钥加载失败: ' . openssl_error_string());
}
openssl_sign($data, $signature, $key, OPENSSL_ALGO_SHA256);
if (PHP_VERSION_ID < 80000) {
@openssl_free_key($key);
}
return base64_encode($signature);
}
// MD5:平台规则为 md5(待签串 + 商户MD5密钥),【无】&key= 后缀(与彩虹易支付源码 Payment::makeSign 一致)
if (empty($keys['md5_key'])) {
throw new Exception('商户MD5密钥未配置');
}
return md5($data . $keys['md5_key']);
}
/**
* 回调验签:根据回调 sign_type 选择算法(兼容平台RSA/MD5两种回调)
*/
function verifyNotifySign($params, $keys) {
if (empty($params['sign'])) {
debugLog('[pay notify] 验签失败: 缺少sign参数', ['params_keys' => array_keys($params)]);
return false;
}
$sign = $params['sign'];
$filtered = [];
foreach ($params as $k => $v) {
// 排除 sign/sign_type(签名字段)和 action(URL路由参数,平台签名不含此项)
if ($k === 'sign' || $k === 'sign_type' || $k === 'action') continue;
if ($v === '' || $v === null) continue;
$filtered[$k] = $v;
}
ksort($filtered, SORT_STRING);
$parts = [];
foreach ($filtered as $k => $v) {
$parts[] = $k . '=' . $v;
}
$data = implode('&', $parts);
$type = $params['sign_type'] ?? PAY_SIGN_TYPE;
if ($type === 'RSA') {
if (empty($keys['public_key'])) {
debugLog('[pay notify] 验签失败: RSA但无公钥', ['type' => $type]);
return false;
}
$key = opensslLoadKey($keys['public_key'], false);
if ($key === false) {
debugLog('[pay notify] 验签失败: RSA公钥加载失败', ['public_key_start' => substr($keys['public_key'], 0, 50)]);
return false;
}
$ok = openssl_verify($data, base64_decode($sign), $key, OPENSSL_ALGO_SHA256);
if (PHP_VERSION_ID < 80000) {
@openssl_free_key($key);
}
debugLog('[pay notify] RSA验签结果', ['data' => $data, 'ok' => $ok]);
return $ok === 1;
}
// MD5:与提交签名规则一致,md5(待签串 + 商户MD5密钥),无 &key= 后缀
if (empty($keys['md5_key'])) {
debugLog('[pay notify] 验签失败: MD5但无密钥', ['type' => $type]);
return false;
}
$expected = strtolower(md5($data . $keys['md5_key']));
$actual = strtolower($sign);
$ok = ($expected === $actual);
debugLog('[pay notify] MD5验签', ['data' => $data, 'md5_key' => $keys['md5_key'], 'expected' => $expected, 'actual' => $actual, 'ok' => $ok]);
return $ok;
}
// ====================================================================
// 数据表
// ====================================================================
function ensurePayOrdersTable($db) {
$db->exec("CREATE TABLE IF NOT EXISTS pay_orders (
id INTEGER PRIMARY KEY AUTOINCREMENT,
out_trade_no TEXT UNIQUE,
trade_no TEXT,
player_name TEXT NOT NULL,
tier_id INTEGER DEFAULT 0,
money TEXT,
bond_amount INTEGER DEFAULT 0,
status TEXT DEFAULT 'created',
name TEXT,
platform_sign TEXT,
submit_params TEXT,
created_at INTEGER,
paid_at INTEGER
)");
// 旧表可能由早期版本创建、不含 submit_params 列;
// SQLite 不支持 ALTER ... ADD COLUMN IF NOT EXISTS,故用 PRAGMA 探测后补齐,
// 否则 CREATE TABLE IF NOT EXISTS 不会加列,导致 UPDATE submit_params 报 “no such column”。
$hasCol = false;
$res = $db->query("PRAGMA table_info(pay_orders)");
while ($col = $res->fetchArray(SQLITE3_ASSOC)) {
if ($col['name'] === 'submit_params') { $hasCol = true; break; }
}
if (!$hasCol) {
$db->exec("ALTER TABLE pay_orders ADD COLUMN submit_params TEXT");
}
}
/**
* 确保充值流水表存在(notify 写入 web_transactions 供 Java 高频定时器拉取)。
* 该表可能由其它模块创建,这里做幂等兜底,避免新部署时表不存在导致 notify 抛异常返回 fail。
*/
function ensureWebTransactionsTable($db) {
$db->exec("CREATE TABLE IF NOT EXISTS web_transactions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
player_name TEXT NOT NULL,
type TEXT,
amount INTEGER DEFAULT 0,
operator TEXT,
reason TEXT,
detail TEXT,
status TEXT DEFAULT 'pending',
created_at INTEGER
)");
// 唯一约束:同 detail+type 不允许重复
@$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_web_tx_detail_type ON web_transactions(detail, type)");
}
// ====================================================================
// 动作:create_order
// ====================================================================
function createOrder($token) {
$keys = loadPayKeys();
if (empty($keys['pid']) || empty($keys['private_key']) || empty($keys['public_key'])) {
error('支付密钥未配置,请联系管理员', 500);
}
$info = validateTokenSilent($token);
if (!$info || empty($info['player'])) {
error('登录状态无效,请重新登录', 401);
}
$player = $info['player'];
$db = getDB();
ensurePayOrdersTable($db);
// ★ 防重复支付:检查玩家是否有未支付的已有订单(仅5分钟内有效)
$staleThreshold = time() - 300; // 5分钟过期
$checkStmt = $db->prepare("SELECT out_trade_no, submit_params, money, bond_amount, created_at FROM pay_orders WHERE player_name = :p AND status = 'created' AND created_at >= :stale ORDER BY created_at DESC LIMIT 1");
$checkStmt->bindValue(':p', $player, SQLITE3_TEXT);
$checkStmt->bindValue(':stale', $staleThreshold, SQLITE3_INTEGER);
$existingRow = $checkStmt->execute()->fetchArray(SQLITE3_ASSOC);
if ($existingRow && !empty($existingRow['submit_params'])) {
// 重用已有订单(返回原有支付链接,不创建新单)
$scriptBase = preg_replace('/\?.*$/', '', PAY_NOTIFY_URL);
$payUrl = $scriptBase . '?action=pay_redirect&out_trade_no=' . urlencode($existingRow['out_trade_no']);
success([
'pay_url' => $payUrl,
'out_trade_no' => $existingRow['out_trade_no'],
'money' => $existingRow['money'],
'bonds' => (int)$existingRow['bond_amount'],
'player' => $player,
'reused' => true,
], '检测到您有未支付的订单,已为您恢复支付链接');
}
// ★ 清理该玩家30分钟前的陈旧created订单(已被实际支付但本地未更新),避免阻塞后续支付
$cleanStmt = $db->prepare("UPDATE pay_orders SET status = 'expired' WHERE player_name = :p AND status = 'created' AND created_at < :stale");
$cleanStmt->bindValue(':p', $player, SQLITE3_TEXT);
$cleanStmt->bindValue(':stale', $staleThreshold, SQLITE3_INTEGER);
$cleanStmt->execute();
// ===== 0.01 元测试档位(固定映射:0.01 元 → 1 债券) =====
$money = PAY_TEST_MONEY;
$bonds = PAY_TEST_BONDS;
$name = PAY_TEST_NAME;
$outTradeNo = 'RE' . date('YmdHis') . sprintf('%04d', mt_rand(0, 9999));
$stmt = $db->prepare("INSERT INTO pay_orders (out_trade_no, player_name, tier_id, money, bond_amount, status, name, created_at) VALUES (:no, :p, 0, :m, :b, 'created', :n, :t)");
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$stmt->bindValue(':p', $player, SQLITE3_TEXT);
$stmt->bindValue(':m', $money, SQLITE3_TEXT);
$stmt->bindValue(':b', $bonds, SQLITE3_INTEGER);
$stmt->bindValue(':n', $name, SQLITE3_TEXT);
$stmt->bindValue(':t', time(), SQLITE3_INTEGER);
$stmt->execute();
// 构造提交参数(原始值,不做 urlencode;sign 在原始值上计算)
$params = [
'pid' => $keys['pid'],
'type' => 'alipay',
'out_trade_no'=> $outTradeNo,
'notify_url' => PAY_NOTIFY_URL,
'return_url' => PAY_RETURN_URL,
'name' => $name,
'money' => $money,
'param' => $player,
'timestamp' => (string)time(), // 10位Unix时间戳,单位秒,符合彩虹易支付接口要求
'sign_type' => PAY_SIGN_TYPE,
];
$params['sign'] = buildSign($params, $keys);
// 保存已签名的提交参数(含 sign),供本地中转页自动 POST 到网关使用
$stmt = $db->prepare("UPDATE pay_orders SET submit_params = :sp WHERE out_trade_no = :no");
$stmt->bindValue(':sp', json_encode($params, JSON_UNESCAPED_SLASHES), SQLITE3_TEXT);
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$stmt->execute();
// 支付网关要求 POST 提交($_POST['pid']),不能 GET 直接跳转。
// 返回本地中转页 URL:浏览器打开后由 JS 自动 POST 表单到网关,规避 GET 传输下 notify_url 内 & 被截断的问题。
$scriptBase = preg_replace('/\?.*$/', '', PAY_NOTIFY_URL);
$payUrl = $scriptBase . '?action=pay_redirect&out_trade_no=' . urlencode($outTradeNo);
success([
'pay_url' => $payUrl,
'out_trade_no' => $outTradeNo,
'money' => $money,
'bonds' => $bonds,
'player' => $player,
], '订单创建成功,正在跳转支付');
}
// ====================================================================
// 动作:notify(平台异步回调,必须输出纯文本 success)
// ====================================================================
function handleNotify() {
// 清空输出缓冲,确保最终只输出 success/fail
while (ob_get_level() > 0) { ob_end_clean(); }
$params = $_REQUEST; // 平台回调以 GET 方式把签名参数 append 到 notify_url 后,统一用 $_REQUEST 兼容 GET/POST
// 记录详细的请求信息用于调试
$requestInfo = [
'method' => $_SERVER['REQUEST_METHOD'] ?? 'unknown',
'remote_addr' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
'user_agent' => $_SERVER['HTTP_USER_AGENT'] ?? 'unknown',
'query_string' => $_SERVER['QUERY_STRING'] ?? '',
'params_count' => count($params),
'params_keys' => array_keys($params),
];
debugLog('[pay notify] 收到异步回调', array_merge($requestInfo, $params));
$keys = loadPayKeys();
// 1) 验签(兼容平台RSA/MD5回调)
if (!verifyNotifySign($params, $keys)) {
debugLog('[pay notify] 验签失败,疑似伪造', ['out_trade_no' => $params['out_trade_no'] ?? '']);
echo 'fail';
exit;
}
// 2) 交易状态必须是成功
if (($params['trade_status'] ?? '') !== 'TRADE_SUCCESS') {
debugLog('[pay notify] 交易状态非成功: ' . ($params['trade_status'] ?? ''), ['out_trade_no' => $params['out_trade_no'] ?? '']);
// 非成功状态(如 WAIT_BUYER_PAY)返回 fail,让平台继续重试直到成功
echo 'fail';
exit;
}
$outTradeNo = $params['out_trade_no'] ?? '';
if ($outTradeNo === '') {
echo 'fail';
exit;
}
$db = getDB();
ensurePayOrdersTable($db);
ensureWebTransactionsTable($db);
$stmt = $db->prepare("SELECT * FROM pay_orders WHERE out_trade_no = :no");
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) {
debugLog('[pay notify] 订单不存在: ' . $outTradeNo);
// 订单未知也返回 success,避免平台死循环重试无效通知
echo 'success';
exit;
}
// 3) 金额校验:回调 money 必须与订单完全一致(防篡改少付多充)
if (function_exists('bccomp')) {
$moneyOk = bccomp((string)$row['money'], (string)($params['money'] ?? ''), 2) === 0;
} else {
$moneyOk = ((string)$row['money'] === (string)($params['money'] ?? ''));
}
if (!$moneyOk) {
debugLog('[pay notify] 金额不匹配(疑似篡改)', ['order_money' => $row['money'], 'notify_money' => $params['money'] ?? '']);
echo 'fail';
exit;
}
// 4) 幂等:已支付则直接返回 success,绝不重复写 web_transactions
if ($row['status'] === 'paid') {
echo 'success';
exit;
}
// 5) 标记订单已支付
$stmt = $db->prepare("UPDATE pay_orders SET status='paid', trade_no=:tn, paid_at=:t WHERE out_trade_no=:no");
$stmt->bindValue(':tn', $params['trade_no'] ?? '', SQLITE3_TEXT);
$stmt->bindValue(':t', time(), SQLITE3_INTEGER);
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$stmt->execute();
// 6) 写 web_transactions(recharge) 供 Java 高频定时器拉取 → addBonds 到账
$stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:p, 'recharge', :a, '支付平台', '在线充值(支付宝)', :d, 'pending', :t)");
$stmt->bindValue(':p', $row['player_name'], SQLITE3_TEXT);
$stmt->bindValue(':a', (int)$row['bond_amount'], SQLITE3_INTEGER);
$stmt->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
$stmt->bindValue(':t', time(), SQLITE3_INTEGER);
$stmt->execute();
debugLog('[pay notify] 充值交易已写入', ['player' => $row['player_name'], 'bonds' => $row['bond_amount'], 'out_trade_no' => $outTradeNo]);
echo 'success';
exit;
}
// ====================================================================
// 动作:pay_redirect(本地中转页,浏览器打开后自动 POST 到支付网关)
// ====================================================================
function handlePayRedirect() {
while (ob_get_level() > 0) { ob_end_clean(); }
$outTradeNo = getParam('out_trade_no');
if (!$outTradeNo) {
header('Content-Type: text/html; charset=utf-8');
echo '缺少订单号';
exit;
}
$db = getDB();
ensurePayOrdersTable($db);
$stmt = $db->prepare("SELECT submit_params, status FROM pay_orders WHERE out_trade_no = :no");
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row || empty($row['submit_params'])) {
header('Content-Type: text/html; charset=utf-8');
echo '订单不存在或尚未生成支付参数';
exit;
}
$params = json_decode($row['submit_params'], true);
if (!is_array($params)) {
header('Content-Type: text/html; charset=utf-8');
echo '支付参数损坏';
exit;
}
// 已支付则直接跳回结果页,不再重复提交
if ($row['status'] === 'paid') {
header('Location: ' . PAY_RETURN_URL, true, 302);
exit;
}
$fields = '';
foreach ($params as $k => $v) {
$fields .= '<input type="hidden" name="' . htmlspecialchars($k, ENT_QUOTES) . '" value="' . htmlspecialchars($v, ENT_QUOTES) . '">' . "\n";
}
header('Content-Type: text/html; charset=utf-8');
echo '<!DOCTYPE html><html><head><meta charset="utf-8"><title>正在跳转支付...</title></head><body>' .
'<form id="payform" action="' . htmlspecialchars(PAY_GATEWAY, ENT_QUOTES) . '" method="post">' .
$fields .
'</form>' .
'<script>document.getElementById("payform").submit();</script>' .
'</body></html>';
exit;
}
// ====================================================================
// 辅助:后台异步触发支付轮询器(非阻塞)
// ====================================================================
function triggerPollerAsync() {
// ★ 2026-07-15 改用 poller_online.php 补单(绕过 HTTP 回调,CF WAF 拦截 notify)
$url = 'https://caoyuan.ypshidifu.cn/plugin/api/poller_online.php';
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
curl_setopt($ch, CURLOPT_NOSIGNAL, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
// 不等待结果,后台执行
curl_exec($ch);
curl_close($ch);
}
// ====================================================================
// 动作:query_order(前端补单 / 轮询)
// ====================================================================
function queryOrder($token) {
// 后台触发轮询器(非阻塞),处理 Cloudflare WAF 拦截导致的未通知订单
@triggerPollerAsync();
$info = validateTokenSilent($token);
if (!$info || empty($info['player'])) {
error('登录状态无效', 401);
}
$outTradeNo = getParam('out_trade_no');
if (!$outTradeNo) {
error('缺少订单号');
}
$db = getDB();
ensurePayOrdersTable($db);
$stmt = $db->prepare("SELECT * FROM pay_orders WHERE out_trade_no=:no AND player_name=:p");
$stmt->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
$stmt->bindValue(':p', $info['player'], SQLITE3_TEXT);
$row = $stmt->execute()->fetchArray(SQLITE3_ASSOC);
if (!$row) {
error('订单不存在');
}
$status = $row['status'];
$message = '订单创建中';
if ($status === 'paid') {
$message = '已支付,游戏内债券发放中(若未到账请稍候或联系管理员)';
}
success([
'out_trade_no' => $outTradeNo,
'status' => $status,
'money' => $row['money'],
'bonds' => (int)$row['bond_amount'],
'paid_at' => $row['paid_at'],
'created_at' => $row['created_at'],
], $message);
}
// ====================================================================
// 入口分发
// ====================================================================
$action = getParam('action', '');
try {
switch ($action) {
case 'create_order':
createOrder(getParam('token'));
break;
case 'notify':
handleNotify(); // 内部已 exit
break;
case 'pay_redirect':
handlePayRedirect(); // 内部已 exit
break;
case 'sync_poller':
syncPoller(getParam('token'));
break;
case 'query_order':
queryOrder(getParam('token'));
break;
default:
error('未知操作: ' . $action);
}
// ★ WAL checkpoint:释放WAL锁,减少 database is locked 概率
try { walCheckpoint(); } catch (\Throwable $ignored) {}
} catch (\Throwable $e) {
if ($action === 'notify') {
// notify 异常时返回 fail,让平台重试
while (ob_get_level() > 0) { ob_end_clean(); }
echo 'fail';
exit;
}
http_response_code(500);
header('Content-Type: application/json; charset=utf-8');
echo json_encode(['error' => ['code' => 500, 'message' => 'Internal error: ' . $e->getMessage()]], JSON_UNESCAPED_UNICODE);
exit;
}