diff --git a/api/pay.php b/api/pay.php index fb2b5bb..0fd3d8c 100644 --- a/api/pay.php +++ b/api/pay.php @@ -466,6 +466,14 @@ function handleNotify() { $txIns->execute(); debugLog('[pay notify] callback同步成功', ['out_trade_no' => $outTradeNo, 'player' => $platformPlayer]); + + // 发送付款凭据邮件(异步静默) + try { + sendPaymentReceiptEmail($platformPlayer, $outTradeNo, $platformResult['money'] ?? '0.01', $bonds, '在线充值', $now); + } catch (\Throwable $e) { + debugLog('[pay notify] callback同步邮件发送失败', ['player' => $platformPlayer, 'error' => $e->getMessage()]); + } + echo 'success'; exit; } @@ -508,6 +516,14 @@ function handleNotify() { $stmt->execute(); debugLog('[pay notify] 充值交易已写入', ['player' => $row['player_name'], 'bonds' => $row['bond_amount'], 'out_trade_no' => $outTradeNo]); + + // 7) 发送付款凭据邮件(异步静默,不影响主流程) + try { + sendPaymentReceiptEmail($row['player_name'], $outTradeNo, $row['money'], (int)$row['bond_amount'], $row['name'] ?? '在线充值', time()); + } catch (\Throwable $e) { + debugLog('[pay notify] 邮件发送失败(不影响主流程)', ['player' => $row['player_name'], 'error' => $e->getMessage()]); + } + echo 'success'; exit; } @@ -663,6 +679,14 @@ function queryOrder($token) { } debugLog('[queryOrder] 平台同步成功', ['out_trade_no' => $outTradeNo, 'player' => $platformPlayer]); + + // 发送付款凭据邮件(异步静默) + try { + sendPaymentReceiptEmail($platformPlayer, $outTradeNo, $money, $bonds, '在线充值', $now); + } catch (\Throwable $e) { + debugLog('[queryOrder] 平台同步邮件发送失败', ['player' => $platformPlayer, 'error' => $e->getMessage()]); + } + success([ 'out_trade_no' => $outTradeNo, 'status' => 'paid', @@ -720,6 +744,13 @@ function queryOrder($token) { $status = 'paid'; debugLog('[queryOrder] 直查平台补单成功', ['out_trade_no' => $outTradeNo, 'player' => $info['player']]); + + // 发送付款凭据邮件(异步静默) + try { + sendPaymentReceiptEmail($info['player'], $outTradeNo, $row['money'] ?? '0.01', (int)$row['bond_amount'], $row['name'] ?? '在线充值', $now); + } catch (\Throwable $e) { + debugLog('[queryOrder] 邮件发送失败(不影响主流程)', ['player' => $info['player'], 'error' => $e->getMessage()]); + } } else { // 检查是否已过期(5分钟) $nowSec = time(); @@ -946,6 +977,88 @@ function parseExpireDate($expireStr) { return ['valid' => false, 'timestamp' => 0, 'display' => '日期格式错误: ' . $expireStr]; } +// ==================================================================== +// 邮件:支付成功后发送付款凭据邮件 +// ==================================================================== + +/** + * 发送付款凭据邮件给玩家(仅在玩家绑定了邮箱时发送) + * @param string $playerName 玩家名 + * @param string $outTradeNo 订单号 + * @param string $money 付款金额 + * @param int $bondAmount 到账债券 + * @param string $productName 商品名称 + * @param int $paidAt 支付时间(Unix时间戳) + */ +function sendPaymentReceiptEmail($playerName, $outTradeNo, $money, $bondAmount, $productName, $paidAt) { + // 1) 查询玩家邮箱 + $db = getDB(); + try { + $stmt = $db->prepare("SELECT email FROM users WHERE player_name = :player"); + $stmt->bindValue(':player', $playerName, SQLITE3_TEXT); + $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); + } catch (\Throwable $e) { + debugLog('[sendPaymentReceiptEmail] 查询邮箱失败', ['player' => $playerName, 'error' => $e->getMessage()]); + return; + } + + if (!$row || empty($row['email'])) { + debugLog('[sendPaymentReceiptEmail] 玩家未绑定邮箱,跳过', ['player' => $playerName]); + return; + } + + $email = trim($row['email']); + // 简单邮箱格式校验 + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + debugLog('[sendPaymentReceiptEmail] 邮箱格式无效', ['player' => $playerName, 'email' => $email]); + return; + } + + // 2) 构建邮件内容 + $paidTimeStr = date('Y-m-d H:i:s', $paidAt); + $baseUrl = getBaseUrl(); + $orderLink = $baseUrl . '/player.php?page=orders'; + + $subject = '[Sdf1] 付款凭据 - 订单 ' . $outTradeNo; + + $html = '' + . '' + . '付款凭据' + . '
' + . '
' + // 头部 + . '
' + . '

✅ 付款成功

' + . '

Sdf1 Minecraft 充值中心

' + // 正文 + . '
' + . '

亲爱的 ' . htmlspecialchars($playerName) . ':

' + . '

您的充值订单已支付成功,以下是您的付款凭据:

' + // 订单信息卡片 + . '
' + . '' + . '' + . '' + . '' + . '' + . '' + . '
订单号' . htmlspecialchars($outTradeNo) . '
商品' . htmlspecialchars($productName) . '
付款金额¥' . htmlspecialchars($money) . '
到账债券' . intval($bondAmount) . ' 债券
支付时间' . $paidTimeStr . '
' + // 提示 + . '

债券将在游戏内自动发放,如未到账请稍候或联系管理员。

' + . '
查看我的订单
' + . '

本邮件由系统自动发送,请勿直接回复。如有疑问请联系管理员。

' + . '
'; + + // 3) 发送邮件 + $headers = "From: " . SMTP_SENDER_NAME . " <" . SMTP_USER . ">\r\n" + . "Reply-To: " . SMTP_USER . "\r\n" + . "MIME-Version: 1.0\r\n" + . "Content-Type: text/html; charset=UTF-8\r\n"; + + smtpSendEmail(SMTP_HOST, SMTP_PORT, SMTP_USER, SMTP_PASS, $email, $subject, $html, $headers, SMTP_USE_SSL); + debugLog('[sendPaymentReceiptEmail] 邮件发送成功', ['player' => $playerName, 'email' => $email, 'out_trade_no' => $outTradeNo]); +} + function getShopProducts() { $db = getOrdersDB(); @@ -1019,6 +1132,42 @@ function getShopProducts() { success(['products' => $products]); } +// ==================================================================== +// 玩家订单查询 +// ==================================================================== + +function getPlayerOrders($token) { + $info = validateTokenSilent($token); + if (!$info || empty($info['player'])) { + error('登录状态无效', 401); + } + $player = $info['player']; + + $db = getOrdersDB(); + ensurePayOrdersTable($db); + + // 查询该玩家的所有订单(最近50条) + $stmt = $db->prepare("SELECT out_trade_no, trade_no, player_name, money, bond_amount, status, name, created_at, paid_at FROM pay_orders WHERE player_name = :p ORDER BY created_at DESC LIMIT 50"); + $stmt->bindValue(':p', $player, SQLITE3_TEXT); + $result = $stmt->execute(); + + $orders = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + $orders[] = [ + 'out_trade_no' => $row['out_trade_no'], + 'trade_no' => $row['trade_no'] ?? '', + 'money' => $row['money'], + 'bond_amount' => (int)$row['bond_amount'], + 'status' => $row['status'], + 'name' => $row['name'] ?? '在线充值', + 'created_at' => (int)$row['created_at'], + 'paid_at' => $row['paid_at'] ? (int)$row['paid_at'] : null, + ]; + } + + success(['orders' => $orders, 'player' => $player]); +} + // ==================================================================== // 入口分发 // ==================================================================== @@ -1046,6 +1195,9 @@ try { case 'get_shop_products': getShopProducts(); break; + case 'my_orders': + getPlayerOrders(getParam('token')); + break; default: error('未知操作: ' . $action); } diff --git a/player.php b/player.php index f915963..d4adb1c 100644 --- a/player.php +++ b/player.php @@ -277,6 +277,7 @@ if ($currentVersion !== $BUILD_VERSION) { + @@ -1052,6 +1053,7 @@ if ($currentVersion !== $BUILD_VERSION) { else if (page === 'account') renderAccount(c); else if (page === 'ticket') renderTicket(c); else if (page === 'recharge') renderRecharge(c); + else if (page === 'orders') renderMyOrders(c); } // ==================== 债券在线充值(动态加载商品) ==================== @@ -1472,6 +1474,93 @@ if ($currentVersion !== $BUILD_VERSION) { } } + // ==================== 我的订单 ==================== + async function renderMyOrders(el) { + el.innerHTML = '
加载订单中...
'; + + try { + const res = await api('pay.php', {action: 'my_orders'}); + if (!res.success || !res.data || !res.data.orders) { + el.innerHTML = '
加载失败: ' + (res.message || '未知错误') + '
'; + return; + } + + const orders = res.data.orders; + if (orders.length === 0) { + el.innerHTML = ` +
+

📋 我的订单

+

查看您的在线充值订单记录。

+
+
📭
+
暂无订单记录
+
前往「在线充值」发起第一笔订单
+
+
+ `; + return; + } + + // 状态标签样式 + const statusMap = { + 'paid': { text: '✅ 已支付', color: '#1a7f37', bg: '#dafbe1' }, + 'created': { text: '⏳ 待支付', color: '#9a6700', bg: '#fff8c5' }, + 'expired': { text: '❌ 已过期', color: '#656d76', bg: '#f6f8fa' }, + 'failed': { text: '⚠️ 失败', color: '#cf222e', bg: '#ffebe9' }, + }; + + let html = ` +
+
+
+

📋 我的订单

+

共 ${orders.length} 条记录

+
+ +
+ `; + + orders.forEach(order => { + const st = statusMap[order.status] || { text: order.status, color: '#656d76', bg: '#f6f8fa' }; + const createdTime = order.created_at ? new Date(order.created_at * 1000).toLocaleString('zh-CN', {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}) : '-'; + const paidTime = order.paid_at ? new Date(order.paid_at * 1000).toLocaleString('zh-CN', {month:'2-digit',day:'2-digit',hour:'2-digit',minute:'2-digit'}) : '-'; + + html += ` +
+
+
+
${escHtml(order.name)}
+
${escHtml(order.out_trade_no)}
+
+ ${st.text} +
+
+
+ 创建: ${createdTime} + ${order.paid_at ? '支付: ' + paidTime + '' : ''} +
+
+
¥${escHtml(order.money)}
+
→ ${order.bond_amount} 债券
+
+
+
+ `; + }); + + html += '
'; + el.innerHTML = html; + + } catch (e) { + el.innerHTML = '
加载失败: ' + e.message + '
'; + } + } + + function escHtml(s) { + if (!s) return ''; + return String(s).replace(/&/g,'&').replace(//g,'>').replace(/"/g,'"'); + } + // ===== 商城(分类标签页) ===== let shopItems = []; let shopCategories = [];