充值对账页面+防重复支付+高频补单+轮询器改进
- recharge_orders.php + recharge_orders_api.php: 管理员充值订单对账页面 - admin.php 侧边栏添加充值对账入口 - pay.php: 创建订单时检测已有未支付订单(复用),防重复支付 - player.php: 续支付检测+支付成功后清除本地订单+恢复按钮 - poller_online.php + pay.php: 添加web_transactions唯一索引 - .gitignore: 排除密钥.md 关闭 #1056 #1057 #1055 #1060 部分
This commit is contained in:
+22
-1
@@ -233,6 +233,8 @@ function ensureWebTransactionsTable($db) {
|
||||
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)");
|
||||
}
|
||||
|
||||
// ====================================================================
|
||||
@@ -253,6 +255,24 @@ function createOrder($token) {
|
||||
$db = getDB();
|
||||
ensurePayOrdersTable($db);
|
||||
|
||||
// ★ 防重复支付:检查玩家是否有未支付的已有订单
|
||||
$checkStmt = $db->prepare("SELECT out_trade_no, submit_params, money, bond_amount FROM pay_orders WHERE player_name = :p AND status = 'created' ORDER BY created_at DESC LIMIT 1");
|
||||
$checkStmt->bindValue(':p', $player, SQLITE3_TEXT);
|
||||
$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,
|
||||
], '检测到您有未支付的订单,已为您恢复支付链接');
|
||||
}
|
||||
|
||||
// ===== 0.01 元测试档位(固定映射:0.01 元 → 1 债券) =====
|
||||
$money = PAY_TEST_MONEY;
|
||||
$bonds = PAY_TEST_BONDS;
|
||||
@@ -450,7 +470,8 @@ function handlePayRedirect() {
|
||||
// 辅助:后台异步触发支付轮询器(非阻塞)
|
||||
// ====================================================================
|
||||
function triggerPollerAsync() {
|
||||
$url = 'https://caoyuan.ypshidifu.cn/plugin/api/pay_poller.php';
|
||||
// ★ 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);
|
||||
|
||||
@@ -0,0 +1,267 @@
|
||||
<?php
|
||||
/**
|
||||
* 支付补单器(poller_online.php)— 绕过 Cloudflare WAF 拦截的 HTTP 回调通知
|
||||
*
|
||||
* 工作原理:
|
||||
* 直接读平台 MySQL 中已支付订单(status IN (1,2)) → 与本地 SQLite(pay_orders) 比对 →
|
||||
* 将未处理的订单写入 web_transactions(recharge, pending) → 供 Java 高频定时器拉取 → addBonds 到账
|
||||
*
|
||||
* 触发方式(双保险):
|
||||
* 1. 玩家在 Web 端查询订单时,pay.php 的 query_order 异步调用本脚本(前端触发)
|
||||
* 2. Java 定时拉取 web_transactions 前,先调用本脚本(后台定时触发,最可靠)
|
||||
*
|
||||
* 完全自包含,不依赖 core.php,避免 500 错误。幂等:重复跑不会重复写流水。
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
|
||||
while (ob_get_level() > 0) { ob_end_clean(); }
|
||||
|
||||
// ===== 配置 =====
|
||||
$PLATFORM_DB_HOST = 'localhost';
|
||||
$PLATFORM_DB_NAME = 'caihong';
|
||||
$PLATFORM_DB_USER = 'kH3C3LLinNwYdTF5';
|
||||
$PLATFORM_DB_PASS = 'sRhsdxrpHBhmSsp8';
|
||||
$PLATFORM_DB_PREFIX = 'pay_';
|
||||
|
||||
// SQLite 数据库路径(与 core.php 一致:/caoyuan.ypshidifu.cn/plugin/db/web.db)
|
||||
$SQLITE_DB_PATH = __DIR__ . '/../db/web.db';
|
||||
|
||||
if (!function_exists('debugLog')) {
|
||||
function debugLog($msg, $ctx = []) {
|
||||
$ts = date('Y-m-d H:i:s');
|
||||
$entry = "[$ts] $msg";
|
||||
if ($ctx) $entry .= ' | Context: ' . json_encode($ctx, JSON_UNESCAPED_UNICODE);
|
||||
$entry .= "\n";
|
||||
@file_put_contents($logFile, $entry, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
} // end if (!function_exists('debugLog'))
|
||||
|
||||
function getSQLite() {
|
||||
static $db = null;
|
||||
if ($db === null) {
|
||||
$db = new SQLite3($GLOBALS['SQLITE_DB_PATH']);
|
||||
$db->enableExceptions(true);
|
||||
$db->exec('PRAGMA journal_mode=WAL');
|
||||
$db->exec('PRAGMA busy_timeout=5000');
|
||||
// 确保表存在(幂等兜底)
|
||||
$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
|
||||
)");
|
||||
$db->exec("CREATE TABLE IF NOT EXISTS web_transactions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
player_name TEXT,
|
||||
type TEXT,
|
||||
amount INTEGER,
|
||||
operator TEXT,
|
||||
reason TEXT,
|
||||
detail TEXT,
|
||||
status TEXT DEFAULT 'pending',
|
||||
created_at INTEGER
|
||||
)");
|
||||
// 唯一约束:同 detail+type 不允许重复(防 race condition/重复补单)
|
||||
@$db->exec("CREATE UNIQUE INDEX IF NOT EXISTS idx_web_tx_detail_type ON web_transactions(detail, type)");
|
||||
}
|
||||
return $db;
|
||||
}
|
||||
|
||||
function getPlatformDB() {
|
||||
static $pdo = null;
|
||||
if ($pdo === null) {
|
||||
$dsn = "mysql:host={$GLOBALS['PLATFORM_DB_HOST']};dbname={$GLOBALS['PLATFORM_DB_NAME']};charset=utf8mb4";
|
||||
$pdo = new PDO($dsn, $GLOBALS['PLATFORM_DB_USER'], $GLOBALS['PLATFORM_DB_PASS'], [
|
||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
||||
PDO::ATTR_TIMEOUT => 5,
|
||||
]);
|
||||
}
|
||||
return $pdo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 债券换算(仅当本地 pay_orders 无 bond_amount 时作为兜底估算)
|
||||
* 0.01元 → 1债券;>=1元 → 金额*10债券
|
||||
*/
|
||||
function estimateBonds($money) {
|
||||
$m = (float)$money;
|
||||
if ($m >= 1.0) return (int)($m * 10);
|
||||
return max(1, (int)($m * 100));
|
||||
}
|
||||
|
||||
/**
|
||||
* 进程锁:防止多个触发源(Java 定时、前端 query_order、管理员手动"立即对账")
|
||||
* 并发跑补单造成重复写入 / 资源争用。非阻塞,若已被占用则跳过本次。
|
||||
* 锁文件独立存在,进程退出后内核自动释放。
|
||||
*/
|
||||
function pollerAcquireLock() {
|
||||
static $fp = null;
|
||||
if ($fp === null) {
|
||||
$lockPath = __DIR__ . '/../db/poller.lock';
|
||||
$fp = @fopen($lockPath, 'c');
|
||||
}
|
||||
if ($fp === false) return false;
|
||||
return flock($fp, LOCK_EX | LOCK_NB);
|
||||
}
|
||||
|
||||
function pollPaidOrders() {
|
||||
// 并发保护:已有补单进程在跑则直接跳过,杜绝重复补单
|
||||
if (!pollerAcquireLock()) {
|
||||
echo json_encode(['result' => 'already_running', 'detail' => '另一个补单进程正在运行,本次跳过']);
|
||||
return;
|
||||
}
|
||||
|
||||
$sqlite = getSQLite();
|
||||
|
||||
try {
|
||||
$pdo = getPlatformDB();
|
||||
} catch (\Throwable $e) {
|
||||
debugLog('[poller_online] 平台数据库连接失败: ' . $e->getMessage());
|
||||
echo json_encode(['error' => 'platform_db_connect_failed', 'detail' => $e->getMessage()]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 查所有已支付订单 (status=1 或 status=2)。不过滤 notify:
|
||||
// 平台可能已标记 notify=1 但本地的 DB 未更新(HTTP 回调被 CF WAF 拦截),
|
||||
// 必须靠本地 SQLite 的 status='created' 来判断是否需要补单。
|
||||
$prefix = $GLOBALS['PLATFORM_DB_PREFIX'];
|
||||
$stmt = $pdo->query("SELECT out_trade_no, trade_no, uid, money, status, notify, param, version, addtime FROM {$prefix}order WHERE status IN (1, 2) ORDER BY addtime ASC LIMIT 50");
|
||||
$allOrders = $stmt->fetchAll(PDO::FETCH_ASSOC);
|
||||
|
||||
if (empty($allOrders)) {
|
||||
echo json_encode(['result' => 'no_paid_orders_on_platform']);
|
||||
return;
|
||||
}
|
||||
|
||||
// 过滤掉本地已处理(paid)的
|
||||
$orders = [];
|
||||
foreach ($allOrders as $o) {
|
||||
$check = $sqlite->prepare("SELECT status FROM pay_orders WHERE out_trade_no = :no");
|
||||
$check->bindValue(':no', $o['out_trade_no'], SQLITE3_TEXT);
|
||||
$row = $check->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
if (!$row || $row['status'] !== 'paid') {
|
||||
$orders[] = $o;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($orders)) {
|
||||
echo json_encode(['result' => 'no_pending_orders', 'platform_paid_count' => count($allOrders)]);
|
||||
return;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$skipped = 0;
|
||||
$now = time();
|
||||
|
||||
foreach ($orders as $order) {
|
||||
$outTradeNo = $order['out_trade_no'];
|
||||
$tradeNo = $order['trade_no'];
|
||||
$playerName = $order['param'] ?? 'unknown';
|
||||
$money = (string)$order['money'];
|
||||
|
||||
// 查本地 pay_orders(拿到正确的 bond_amount,创建订单时按档位设置)
|
||||
$check = $sqlite->prepare("SELECT status, bond_amount FROM pay_orders WHERE out_trade_no = :no");
|
||||
$check->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
|
||||
$row = $check->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if ($row && $row['status'] === 'paid') {
|
||||
$skipped++;
|
||||
markNotified($pdo, $tradeNo);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 债券数:优先用本地订单记录的 bond_amount(最准确),否则估算
|
||||
$bonds = 0;
|
||||
if ($row && !empty($row['bond_amount'])) {
|
||||
$bonds = (int)$row['bond_amount'];
|
||||
} else {
|
||||
$bonds = estimateBonds($money);
|
||||
}
|
||||
|
||||
debugLog('[poller_online] 补单处理', [
|
||||
'out_trade_no' => $outTradeNo,
|
||||
'player' => $playerName,
|
||||
'money' => $money,
|
||||
'bonds' => $bonds,
|
||||
'platform_notify'=> $order['notify'],
|
||||
'has_local_order'=> $row ? 1 : 0,
|
||||
]);
|
||||
|
||||
// 写/更新本地 pay_orders
|
||||
if (!$row) {
|
||||
$ins = $sqlite->prepare("INSERT OR IGNORE INTO pay_orders (out_trade_no, trade_no, player_name, money, bond_amount, status, created_at, paid_at) VALUES (:no, :tn, :p, :m, :b, 'paid', :t, :t)");
|
||||
$ins->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
|
||||
$ins->bindValue(':tn', $tradeNo, SQLITE3_TEXT);
|
||||
$ins->bindValue(':p', $playerName, SQLITE3_TEXT);
|
||||
$ins->bindValue(':m', $money, SQLITE3_TEXT);
|
||||
$ins->bindValue(':b', $bonds, SQLITE3_INTEGER);
|
||||
$ins->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$ins->execute();
|
||||
} else {
|
||||
$upd = $sqlite->prepare("UPDATE pay_orders SET status='paid', trade_no=:tn, paid_at=:t, bond_amount=:b WHERE out_trade_no=:no AND status != 'paid'");
|
||||
$upd->bindValue(':tn', $tradeNo, SQLITE3_TEXT);
|
||||
$upd->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$upd->bindValue(':b', $bonds, SQLITE3_INTEGER);
|
||||
$upd->bindValue(':no', $outTradeNo, SQLITE3_TEXT);
|
||||
$upd->execute();
|
||||
}
|
||||
|
||||
// 写 web_transactions(幂等:按 detail=out_trade_no + type=recharge 去重)
|
||||
$txCheck = $sqlite->prepare("SELECT id FROM web_transactions WHERE detail = :d AND type = 'recharge' LIMIT 1");
|
||||
$txCheck->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
|
||||
$txRow = $txCheck->execute()->fetchArray(SQLITE3_ASSOC);
|
||||
|
||||
if (!$txRow) {
|
||||
$txIns = $sqlite->prepare("INSERT INTO web_transactions (player_name, type, amount, operator, reason, detail, status, created_at) VALUES (:p, 'recharge', :a, '支付平台(补单)', '在线充值(支付宝)', :d, 'pending', :t)");
|
||||
$txIns->bindValue(':p', $playerName, SQLITE3_TEXT);
|
||||
$txIns->bindValue(':a', $bonds, SQLITE3_INTEGER);
|
||||
$txIns->bindValue(':d', $outTradeNo, SQLITE3_TEXT);
|
||||
$txIns->bindValue(':t', $now, SQLITE3_INTEGER);
|
||||
$txIns->execute();
|
||||
|
||||
debugLog('[poller_online] 充值交易已写入 web_transactions', [
|
||||
'player' => $playerName, 'bonds' => $bonds,
|
||||
'out_trade_no' => $outTradeNo, 'money' => $money,
|
||||
]);
|
||||
}
|
||||
|
||||
markNotified($pdo, $tradeNo);
|
||||
$processed++;
|
||||
}
|
||||
|
||||
echo json_encode([
|
||||
'result' => 'ok',
|
||||
'processed' => $processed,
|
||||
'skipped' => $skipped,
|
||||
'total' => count($orders),
|
||||
]);
|
||||
}
|
||||
|
||||
function markNotified($pdo, $tradeNo) {
|
||||
try {
|
||||
$prefix = $GLOBALS['PLATFORM_DB_PREFIX'];
|
||||
$stmt = $pdo->prepare("UPDATE {$prefix}order SET notify = 1 WHERE trade_no = ? AND notify = 0");
|
||||
$stmt->execute([$tradeNo]);
|
||||
} catch (\Throwable $e) {
|
||||
debugLog('[poller_online] 标记notify失败: ' . $e->getMessage(), ['trade_no' => $tradeNo]);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 入口 =====
|
||||
// 若被其它脚本 include 并定义了 POLLER_NO_AUTO_RUN,则只暴露函数(getPlatformDB 等),
|
||||
// 不在此处自动执行补单(由调用方自行决定何时触发)。
|
||||
if (!defined('POLLER_NO_AUTO_RUN')) {
|
||||
header('Content-Type: application/json; charset=utf-8');
|
||||
pollPaidOrders();
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
<?php
|
||||
/**
|
||||
* 充值订单API(recharge_orders_api.php)
|
||||
* 提供充值订单查询、统计、筛选功能
|
||||
*/
|
||||
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 0);
|
||||
ob_start();
|
||||
require_once __DIR__ . '/../core.php';
|
||||
ob_end_clean();
|
||||
|
||||
// 检查管理员登录
|
||||
if (!isAdminLoggedIn()) {
|
||||
error('无权限访问', 403);
|
||||
}
|
||||
|
||||
$action = getParam('action', '');
|
||||
|
||||
switch ($action) {
|
||||
case 'list':
|
||||
handleOrderList();
|
||||
break;
|
||||
case 'detail':
|
||||
handleOrderDetail();
|
||||
break;
|
||||
case 'reconcile':
|
||||
handleReconcile();
|
||||
break;
|
||||
default:
|
||||
error('未知操作');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取订单列表
|
||||
*/
|
||||
function handleOrderList() {
|
||||
$db = getDB();
|
||||
ensurePayOrdersTable($db);
|
||||
|
||||
$player = getParam('player', '');
|
||||
$orderNo = getParam('order_no', '');
|
||||
$status = getParam('status', '');
|
||||
$limit = min(200, max(1, (int)getParam('limit', 50)));
|
||||
|
||||
// 构建查询
|
||||
$conditions = [];
|
||||
$params = [];
|
||||
|
||||
if ($player) {
|
||||
$conditions[] = "player_name LIKE :player";
|
||||
$params[':player'] = '%' . $player . '%';
|
||||
}
|
||||
if ($orderNo) {
|
||||
$conditions[] = "out_trade_no LIKE :order_no";
|
||||
$params[':order_no'] = '%' . $orderNo . '%';
|
||||
}
|
||||
if ($status) {
|
||||
$conditions[] = "status = :status";
|
||||
$params[':status'] = $status;
|
||||
}
|
||||
|
||||
$where = '';
|
||||
if ($conditions) {
|
||||
$where = 'WHERE ' . implode(' AND ', $conditions);
|
||||
}
|
||||
|
||||
// 查询订单
|
||||
$sql = "SELECT * FROM pay_orders $where ORDER BY created_at DESC LIMIT :limit";
|
||||
$stmt = $db->prepare($sql);
|
||||
foreach ($params as $k => $v) {
|
||||
$stmt->bindValue($k, $v, SQLITE3_TEXT);
|
||||
}
|
||||
$stmt->bindValue(':limit', $limit, SQLITE3_INTEGER);
|
||||
|
||||
$orders = [];
|
||||
$result = $stmt->execute();
|
||||
while ($row = $result->fetchArray(SQLITE3_ASSOC)) {
|
||||
$orders[] = $row;
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
$statsSql = "SELECT
|
||||
COUNT(*) as total,
|
||||
SUM(CASE WHEN status='paid' THEN 1 ELSE 0 END) as paid,
|
||||
SUM(CASE WHEN status='created' THEN 1 ELSE 0 END) as created,
|
||||
SUM(CASE WHEN status='paid' THEN CAST(money AS REAL) ELSE 0 END) as totalMoney
|
||||
FROM pay_orders";
|
||||
$statsResult = $db->querySingle($statsSql, true);
|
||||
|
||||
$stats = [
|
||||
'total' => $statsResult['total'] ?? 0,
|
||||
'paid' => $statsResult['paid'] ?? 0,
|
||||
'created' => $statsResult['created'] ?? 0,
|
||||
'totalMoney' => number_format($statsResult['totalMoney'] ?? 0, 2),
|
||||
];
|
||||
|
||||
success(['orders' => $orders, 'stats' => $stats]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取单个订单详情
|
||||
*/
|
||||
function handleOrderDetail() {
|
||||
$outTradeNo = getParam('out_trade_no');
|
||||
if (!$outTradeNo) {
|
||||
error('缺少订单号');
|
||||
}
|
||||
|
||||
$db = getDB();
|
||||
ensurePayOrdersTable($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) {
|
||||
error('订单不存在');
|
||||
}
|
||||
|
||||
success(['order' => $row]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动触发对账(调用 poller_online.php)
|
||||
*/
|
||||
function handleReconcile() {
|
||||
// 直接包含 poller_online.php 并调用 pollPaidOrders
|
||||
define('POLLER_NO_AUTO_RUN', true);
|
||||
require_once __DIR__ . '/poller_online.php';
|
||||
|
||||
// 开始对账
|
||||
ob_start();
|
||||
pollPaidOrders();
|
||||
$output = ob_get_clean();
|
||||
|
||||
// 解析结果
|
||||
$result = json_decode($output, true);
|
||||
if (!$result) {
|
||||
error('对账脚本执行异常');
|
||||
}
|
||||
|
||||
success($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保 pay_orders 表存在(从 pay.php 复制)
|
||||
*/
|
||||
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
|
||||
)");
|
||||
|
||||
// 检查并添加缺失的列
|
||||
$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");
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user