From dd425f4946958cf676e290b2172ec0f76cbcce31 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Mon, 6 Jul 2026 17:33:15 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=95=86=E5=9F=8Edatabase=20?= =?UTF-8?q?is=20locked=EF=BC=9B=E8=B4=AD=E7=89=A9=E8=BD=A6=E7=BB=93?= =?UTF-8?q?=E7=AE=97=E5=A2=9E=E5=BC=BA=EF=BC=9A=E2=89=A51000=E5=85=83?= =?UTF-8?q?=E5=BC=BA=E5=88=B6=E5=AF=86=E7=A0=81=E6=A0=A1=E9=AA=8C=E3=80=81?= =?UTF-8?q?=E6=BD=9C=E5=BD=B1=E7=9B=92=E9=A2=9C=E8=89=B2=E5=8F=AF=E9=80=89?= =?UTF-8?q?=E6=94=B6=E8=B4=B9=EF=BC=88=E9=BB=98=E8=AE=A4=E7=B4=AB=E8=89=B2?= =?UTF-8?q?=E5=85=8D=E8=B4=B9=EF=BC=89=EF=BC=9B=E7=8E=A9=E5=AE=B6=E7=AB=AF?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E8=B4=AD=E7=89=A9=E8=BD=A6=E6=A8=A1=E5=BC=8F?= =?UTF-8?q?=E4=B8=8E=E9=A2=9C=E8=89=B2=E9=80=89=E6=8B=A9=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/shop.php | 42 ++++++++++++++++++++++++++++++----- core.php | 5 +++++ player.php | 62 +++++++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 101 insertions(+), 8 deletions(-) diff --git a/api/shop.php b/api/shop.php index b6a4e3f..f0acf8d 100644 --- a/api/shop.php +++ b/api/shop.php @@ -39,6 +39,8 @@ switch ($action) { try { walCheckpoint(); } catch (\Throwable $ignored) {} } catch (\Throwable $e) { + // ★ 强制回滚可能残留的事务(防止database is locked连锁故障) + try { $db = getDB(); $db->exec('ROLLBACK'); } catch (\Throwable $_) {} while (ob_get_level() > 0) ob_end_clean(); error('服务器内部错误: ' . $e->getMessage(), 500); } @@ -274,6 +276,7 @@ function cartConfig() { function shopBuyCart($token) { $rawItems = getParam('items'); $settlement = getParam('settlement', 'backpack'); // backpack=塞背包, shulker=潜影盒打包 + $shulkerColor = getParam('shulker_color', 'purple'); // 潜影盒颜色(purple免费,其它+2元) $player = getParam('player'); $password = getParam('password'); $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; @@ -326,10 +329,17 @@ function shopBuyCart($token) { if ($settlement === 'shulker') { $rate = (float)getShopConfig('cart_shulker_rate', '1.00'); $modeName = '潜影盒打包'; + // 潜影盒颜色额外收费(紫色免费,其它+2元) + $colorFee = ($shulkerColor !== 'purple') ? 2 : 0; + // 颜色名称映射 + $colorNames = ['purple'=>'紫色','white'=>'白色','black'=>'黑色','red'=>'红色','blue'=>'蓝色','green'=>'绿色','yellow'=>'黄色','orange'=>'橙色']; + $colorName = $colorNames[$shulkerColor] ?? $shulkerColor; } else { $settlement = 'backpack'; $rate = (float)getShopConfig('cart_backpack_rate', '0.98'); $modeName = '塞背包'; + $colorFee = 0; + $colorName = ''; } $db = getDB(); @@ -356,8 +366,24 @@ function shopBuyCart($token) { } unset($p); - $totalPrice = (int)round($subtotal * $rate); - $saved = $subtotal - $totalPrice; // 折扣省下的(潜影盒加价时为负) + $totalPrice = (int)round($subtotal * $rate) + $colorFee; + $saved = $subtotal - $totalPrice; // 折扣省下的(潜影盒加价+颜色费时为负) + + // ★ 安全检查:服务端计算的总额≥1000时,必须已通过密码验证(防止前端绕过) + // 管理员token(admin/all)在上方已跳过密码验证,此处补检 + if ($totalPrice >= 1000 && !$isPreview) { + $tokenInfo = validateToken($token); + $isAdminToken = $tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all'); + if ($isAdminToken && (!$password || trim($password) === '')) { + // 管理员代购大额也需密码确认 + jsonResponse(['success' => false, 'need_password' => true, + 'player' => $player, 'message' => '金额≥1000债券需确认密码'], 401); + } + if (!$isAdminToken && !isset($accessResult['ok'])) { + // 非管理员且未通过密码验证 → 拒绝 + error('大额交易需验证游戏登录密码', 401); + } + } // 检查余额 if (!$isPreview && $totalPrice > 0) { @@ -410,14 +436,20 @@ function shopBuyCart($token) { $stmt = $db->prepare("INSERT INTO web_transactions (player_name, type, amount, reason, detail, status, created_at) VALUES (:player, 'shop_cart', :amount, :reason, :detail, 'pending', :time)"); $stmt->bindValue(':player', $player, SQLITE3_TEXT); $stmt->bindValue(':amount', $totalPrice, SQLITE3_INTEGER); - $stmt->bindValue(':reason', "购物车结算({$modeName}): " . implode('、', $reasonItems), SQLITE3_TEXT); - $stmt->bindValue(':detail', json_encode([ + $stmt->bindValue(':reason', "购物车结算({$modeName}" . ($colorName ? "-{$colorName}潜影盒" : '') . "): " . implode('、', $reasonItems), SQLITE3_TEXT); + $detailData = [ 'settlement' => $settlement, 'rate' => $rate, 'subtotal' => $subtotal, 'total_price' => $totalPrice, 'items' => $lines - ], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT); + ]; + if ($settlement === 'shulker') { + $detailData['shulker_color'] = $shulkerColor; + $detailData['color_name'] = $colorName; + $detailData['color_fee'] = $colorFee; + } + $stmt->bindValue(':detail', json_encode($detailData, JSON_UNESCAPED_UNICODE), SQLITE3_TEXT); $stmt->bindValue(':time', time(), SQLITE3_INTEGER); $stmt->execute(); diff --git a/core.php b/core.php index 73d7772..395eed1 100644 --- a/core.php +++ b/core.php @@ -77,6 +77,11 @@ function getDB() { debugLog("getDB: migrateDatabase失败: " . $e->getMessage()); } } + + // ★ 安全网:每次获取连接时强制清理上一次请求可能残留的未提交事务 + // (PHP-FPM/opcache复用进程时,前一个请求异常退出可能遗留BEGIN但无COMMIT/ROLLBACK) + try { $db->exec('ROLLBACK'); } catch (\Throwable $_) {} + return $db; } diff --git a/player.php b/player.php index f814a3b..b021635 100644 --- a/player.php +++ b/player.php @@ -1306,6 +1306,17 @@ if ($currentVersion !== $BUILD_VERSION) { let cartCfg = {backpack_rate: 0.98, shulker_rate: 1.0}; let cartSettlement = 'backpack'; let cartSubtotalVal = 0; + let cartShulkerColor = 'purple'; // 潜影盒颜色(purple免费,其它+2元) + const SHULKER_COLORS = [ + {id:'purple', name:'§5紫色', price:0, css:'#AA00FF'}, + {id:'white', name:'§f白色', price:2, css:'#FFFFFF'}, + {id:'black', name:'§8黑色', price:2, css:'#1A1A1A'}, + {id:'red', name:'§c红色', price:2, css:'#FF3333'}, + {id:'blue', name:'§9蓝色', price:2, css:'#3366FF'}, + {id:'green', name:'§a绿色', price:2, css:'#33FF66'}, + {id:'yellow', name:'§e黄色', price:2, css:'#FFFF33'}, + {id:'orange', name:'§6橙色', price:2, css:'#FF9933'} + ]; function loadCart() { try { cart = JSON.parse(localStorage.getItem('sdf1_cart') || '[]'); } catch (e) { cart = []; } @@ -1447,6 +1458,16 @@ if ($currentVersion !== $BUILD_VERSION) { html += settleModeHtml('backpack', '🎒 塞背包', cartCfg.backpack_rate, bpTotal, subtotal, '直接发放到背包,享 ' + (cartCfg.backpack_rate * 10).toFixed(1) + ' 折'); html += settleModeHtml('shulker', '📦 潜影盒打包', cartCfg.shulker_rate, shTotal, subtotal, shulkerDesc(cartCfg.shulker_rate, subtotal, shTotal)); html += ''; + // 潜影盒颜色选择(仅shulker模式显示) + html += ''; html += '
'; html += '
'; html += ''; @@ -1468,7 +1489,8 @@ if ($currentVersion !== $BUILD_VERSION) { if (saved > 0) badge = '省 ' + saved + ' 债券'; else if (saved < 0) badge = '加价 ' + (-saved) + ' 债券'; else badge = '原价'; - return '
' + title + '
' + total + ' 债券
' + badge + '
' + desc + '
'; + const priceId = mode === 'shulker' ? 'id="shulkerPriceDisplay"' : ''; + return '
' + title + '
' + total + ' 债券
' + badge + '
' + desc + '
'; } function selectSettleMode(mode) { @@ -1478,9 +1500,36 @@ if ($currentVersion !== $BUILD_VERSION) { c.style.borderColor = active ? 'var(--accent)' : ''; c.style.background = active ? 'color-mix(in srgb, var(--accent) 10%, transparent)' : ''; }); + // 显示/隐藏潜影盒颜色选择器 + const colorWrap = document.getElementById('shulkerColorWrap'); + if (colorWrap) colorWrap.style.display = mode === 'shulker' ? '' : 'none'; updateSettlePwd(); } + function selectShulkerColor(colorId, price) { + cartShulkerColor = colorId; + document.querySelectorAll('.shulker-color-opt').forEach(c => { + const sel = c.dataset.color === colorId; + c.style.borderColor = sel ? 'var(--accent)' : 'var(--border)'; + c.style.background = sel ? 'color-mix(in srgb, var(--accent) 8%, transparent)' : ''; + const radio = c.querySelector('input[type="radio"]'); + if (radio) radio.checked = sel; + }); + const hint = document.getElementById('colorFeeHint'); + if (hint) hint.textContent = price > 0 ? '(额外收费 +' + price + ' 债券)' : '(免费)'; + // 更新价格显示 + updateSettleTotal(); + } + + function updateSettleTotal() { + const priceEl = document.getElementById('shulkerPriceDisplay'); + if (priceEl && cartSettlement === 'shulker') { + const base = cartSubtotalVal * cartCfg.shulker_rate; + const colorExtra = SHULKER_COLORS.find(c => c.id === cartShulkerColor)?.price || 0; + priceEl.textContent = Math.round(base + colorExtra) + ' 债券'; + } + } + function updateSettlePwd() { const wrap = document.getElementById('settlePwdWrap'); if (!wrap) return; @@ -1494,16 +1543,23 @@ if ($currentVersion !== $BUILD_VERSION) { async function doSettle() { const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0); - const total = Math.round(subtotal * (cartSettlement === 'shulker' ? cartCfg.shulker_rate : cartCfg.backpack_rate)); + let total = Math.round(subtotal * (cartSettlement === 'shulker' ? cartCfg.shulker_rate : cartCfg.backpack_rate)); + // 潜影盒颜色额外收费 + if (cartSettlement === 'shulker') { + const colorExtra = SHULKER_COLORS.find(c => c.id === cartShulkerColor)?.price || 0; + total += colorExtra; + } const password = document.getElementById('settlePassword')?.value || ''; if (total > 1000 && !password) { toast('请输入密码确认', 'error'); return; } const itemsPayload = cart.map(i => ({item_id: i.item_id, amount: i.amount})); const btn = document.getElementById('settleConfirmBtn'); if (btn) { btn.disabled = true; btn.textContent = '结算中...'; } try { + const bodyData = {token: TOKEN, items: JSON.stringify(itemsPayload), settlement: cartSettlement, player: currentPlayer, password: password || undefined}; + if (cartSettlement === 'shulker') bodyData.shulker_color = cartShulkerColor; const res = await fetch(API + 'shop.php?action=buy_cart', { method: 'POST', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({token: TOKEN, items: JSON.stringify(itemsPayload), settlement: cartSettlement, player: currentPlayer, password: password || undefined}) + body: JSON.stringify(bodyData) }); const data = await res.json(); if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; }