From 7a283ae4e0bd013d395f823fe496347f1ad66054 Mon Sep 17 00:00:00 2001 From: ypsdf1 Date: Mon, 6 Jul 2026 16:39:47 +0800 Subject: [PATCH] =?UTF-8?q?=E5=95=86=E5=9F=8E=E8=B4=AD=E7=89=A9=E8=BD=A6?= =?UTF-8?q?=E6=A8=A1=E5=BC=8F+=E6=94=B6=E9=93=B6=E5=8F=B0=E7=BB=93?= =?UTF-8?q?=E7=AE=97+=E5=90=8E=E5=8F=B0=E6=8A=98=E6=89=A3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=EF=BC=9A=E5=89=8D=E7=AB=AF=E8=B4=AD=E7=89=A9=E8=BD=A6?= =?UTF-8?q?FAB/=E6=8A=BD=E5=B1=89/=E7=BB=93=E7=AE=97=E5=BC=B9=E7=AA=97?= =?UTF-8?q?=EF=BC=8C=E5=90=8E=E7=AB=AFbuy=5Fcart=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E4=BA=8B=E5=8A=A1=E6=89=B9=E9=87=8F=E8=B4=AD=E4=B9=B0=EF=BC=8C?= =?UTF-8?q?admin=E8=B4=AD=E7=89=A9=E8=BD=A6=E9=85=8D=E7=BD=AE=E5=8D=A1?= =?UTF-8?q?=E7=89=87=EF=BC=8Ccart=5Fconfig/save=5Fshop=5Fconfig=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- admin.php | 53 +++++++++++ api/admin.php | 76 ++++++++++++++-- api/shop.php | 205 +++++++++++++++++++++++++++++++++++++++++++ api/sync.php | 70 +++++++++++++++ core.php | 11 ++- player.php | 237 +++++++++++++++++++++++++++++++++++++++++++++++++- 6 files changed, 643 insertions(+), 9 deletions(-) diff --git a/admin.php b/admin.php index 07fb700..341b185 100644 --- a/admin.php +++ b/admin.php @@ -397,8 +397,61 @@ async function loadShop(el) {
+ +
+

🛒 购物车结算配置

+

配置购物车结算时「塞背包」折扣与「潜影盒打包」加价系数,系数作用于商品原价合计。

+
+
+
+ + +
`; renderShopAdminContent(); + loadCartConfig(); +} + +// 加载购物车结算配置(读公开 cart_config 接口) +async function loadCartConfig() { + try { + const r = await jsonApi('shop.php?action=cart_config'); + if (r && r.success && r.data) { + const bp = document.getElementById('cfgBackpack'); + const sh = document.getElementById('cfgShulker'); + if (bp && r.data.backpack_rate != null) bp.value = r.data.backpack_rate; + if (sh && r.data.shulker_rate != null) sh.value = r.data.shulker_rate; + updateCartCfgHint(); + } + } catch (e) {} +} + +// 实时计算并显示折扣/加价说明 +function updateCartCfgHint() { + const bpEl = document.getElementById('cfgBackpack'); + const shEl = document.getElementById('cfgShulker'); + const hint = document.getElementById('cartCfgHint'); + if (!bpEl || !shEl || !hint) return; + const bp = parseFloat(bpEl.value); + const sh = parseFloat(shEl.value); + if (isNaN(bp) || isNaN(sh)) { hint.textContent = ''; return; } + const bpDisc = (100 - bp * 100).toFixed(0); + const shAdd = ((sh - 1) * 100).toFixed(0); + hint.textContent = `塞背包:打 ${(bp * 10).toFixed(1)} 折(-${bpDisc}%) | 潜影盒:加价 ${shAdd}%`; +} + +// 保存购物车结算配置 +async function saveCartConfig() { + const bpEl = document.getElementById('cfgBackpack'); + const shEl = document.getElementById('cfgShulker'); + const bp = parseFloat(bpEl.value); + const sh = parseFloat(shEl.value); + if (isNaN(bp) || isNaN(sh)) { toast('请输入有效数字', 'err'); return; } + if (bp <= 0 || bp > 1) { toast('塞背包折扣系数应在 0.01 ~ 1.00 之间', 'err'); return; } + if (sh < 1 || sh > 3) { toast('潜影盒加价系数应在 1.00 ~ 3.00 之间', 'err'); return; } + const r = await postApi('save_shop_config', { cart_backpack_rate: bp, cart_shulker_rate: sh }); + toast(r.message || (r.success ? '已保存' : '保存失败'), r.success ? 'ok' : 'err'); + if (r.success) updateCartCfgHint(); } function switchShopCat(cat) { diff --git a/api/admin.php b/api/admin.php index 631773a..bbb402d 100644 --- a/api/admin.php +++ b/api/admin.php @@ -53,6 +53,7 @@ switch ($action) { case 'shop_update': adminShopUpdate(); break; case 'shop_add': adminShopAdd(); break; case 'shop_remove': adminShopRemove(); break; + case 'save_shop_config': adminSaveShopConfig(); break; case 'stats': adminStats(); break; case 'all_tx': adminAllTx(); break; case 'player_tx': adminPlayerTx(); break; @@ -201,14 +202,40 @@ function adminShopUpdate() { $itemId = getParam('id'); if (!$itemId) error('缺少商品ID'); $stock = getParam('stock'); + $buyPrice = getParam('buy_price'); + $sellPrice = getParam('sell_price'); // ★ 库存改动写入admin_stock(防止Java全量同步覆盖) // admin_stock非NULL时pullShopStock优先返回此值给Java - // 前端只发送id和stock,所以只更新stock和admin_stock - $stmt = $db->prepare("UPDATE shop_items SET stock=:st, admin_stock=:ast, last_modified=:lm WHERE id=:id"); - $stmt->bindValue(':st', getParam('stock')); - $stmt->bindValue(':ast', $stock, $stock !== null ? SQLITE3_INTEGER : SQLITE3_NULL); - $stmt->bindValue(':lm', time(), SQLITE3_INTEGER); - $stmt->bindValue(':id', $itemId, SQLITE3_TEXT); + // ★ 价格改动写入admin_buy_price/admin_sell_price(同理) + $sql = "UPDATE shop_items SET last_modified=:lm"; + $params = [':lm' => time(), ':id' => $itemId]; + $types = [':lm' => SQLITE3_INTEGER, ':id' => SQLITE3_TEXT]; + if ($stock !== null) { + $sql .= ", stock=:st, admin_stock=:ast"; + $params[':st'] = $stock; + $params[':ast'] = $stock; + $types[':st'] = SQLITE3_INTEGER; + $types[':ast'] = SQLITE3_INTEGER; + } + if ($buyPrice !== null) { + $sql .= ", buy_price=:bp, admin_buy_price=:abp"; + $params[':bp'] = $buyPrice; + $params[':abp'] = $buyPrice; + $types[':bp'] = SQLITE3_INTEGER; + $types[':abp'] = SQLITE3_INTEGER; + } + if ($sellPrice !== null) { + $sql .= ", sell_price=:sp, admin_sell_price=:asp"; + $params[':sp'] = $sellPrice; + $params[':asp'] = $sellPrice; + $types[':sp'] = SQLITE3_INTEGER; + $types[':asp'] = SQLITE3_INTEGER; + } + $sql .= " WHERE id=:id"; + $stmt = $db->prepare($sql); + foreach ($params as $key => $value) { + $stmt->bindValue($key, $value, $types[$key] ?? SQLITE3_TEXT); + } $stmt->execute(); $rowsChanged = $db->changes(); // ★ 同步通知Java插件立即拉取库存改动 @@ -262,6 +289,43 @@ function adminShopRemove() { } } +// ===== 购物车结算折扣/加价配置 ===== +function adminSaveShopConfig() { + requireAdminSession(); + $db = getDB(); + try { + $bp = getParam('cart_backpack_rate'); + $sh = getParam('cart_shulker_rate'); + if ($bp === null || $sh === null) error('缺少配置参数'); + $bp = (float)$bp; + $sh = (float)$sh; + // 校验范围:塞背包折扣系数 0.01~1.00;潜影盒加价系数 1.00~3.00 + if ($bp <= 0 || $bp > 1.0) error('塞背包折扣系数必须介于 0.01 ~ 1.00 之间'); + if ($sh < 1.0 || $sh > 3.0) error('潜影盒加价系数必须介于 1.00 ~ 3.00 之间'); + + $db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)"); + $upsert = $db->prepare("INSERT OR REPLACE INTO shop_config (cfg_key, cfg_value) VALUES (:k, :v)"); + $upsert->bindValue(':k', 'cart_backpack_rate', SQLITE3_TEXT); + $upsert->bindValue(':v', number_format($bp, 2, '.', ''), SQLITE3_TEXT); + $upsert->execute(); + $upsert->bindValue(':k', 'cart_shulker_rate', SQLITE3_TEXT); + $upsert->bindValue(':v', number_format($sh, 2, '.', ''), SQLITE3_TEXT); + $upsert->execute(); + + exit(json_encode([ + 'success' => true, + 'message' => '购物车结算配置已保存', + 'data' => [ + 'backpack_rate' => $bp, + 'shulker_rate' => $sh + ] + ], JSON_UNESCAPED_UNICODE)); + } catch (\Throwable $e) { + @error_log('[save_shop_config] Error: ' . $e->getMessage()); + exit(json_encode(['success' => false, 'message' => '保存失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE)); + } +} + function adminStats() { requireAdminSession(); $db = getDB(); diff --git a/api/shop.php b/api/shop.php index ddb8e24..b6a4e3f 100644 --- a/api/shop.php +++ b/api/shop.php @@ -22,6 +22,12 @@ switch ($action) { case 'buy': shopBuy($token); break; + case 'buy_cart': + shopBuyCart($token); + break; + case 'cart_config': + cartConfig(); + break; case 'categories': shopCategories($token); break; @@ -241,6 +247,205 @@ function shopBuy($token) { ], '购买成功'); } +// ===== 购物车配置读取 ===== +function getShopConfig($key, $default = null) { + try { + $db = getDB(); + $db->exec("CREATE TABLE IF NOT EXISTS shop_config (cfg_key TEXT PRIMARY KEY, cfg_value TEXT NOT NULL)"); + $stmt = $db->prepare("SELECT cfg_value FROM shop_config WHERE cfg_key = :k"); + $stmt->bindValue(':k', $key, SQLITE3_TEXT); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + return $row ? $row['cfg_value'] : $default; + } catch (Exception $e) { + return $default; + } +} + +// 返回购物车折扣/加价配置(前端展示用) +function cartConfig() { + success([ + 'backpack_rate' => (float)getShopConfig('cart_backpack_rate', '0.98'), + 'shulker_rate' => (float)getShopConfig('cart_shulker_rate', '1.00') + ]); +} + +// ===== 购物车结算(多商品一次性购买) ===== +function shopBuyCart($token) { + $rawItems = getParam('items'); + $settlement = getParam('settlement', 'backpack'); // backpack=塞背包, shulker=潜影盒打包 + $player = getParam('player'); + $password = getParam('password'); + $ipAddress = $_SERVER['REMOTE_ADDR'] ?? '0.0.0.0'; + + if (!$rawItems) error('购物车为空'); + $items = json_decode($rawItems, true); + if (!is_array($items) || count($items) === 0) error('购物车格式错误'); + + // 校验每一项 + $parsed = []; + foreach ($items as $it) { + if (!isset($it['item_id']) || !isset($it['amount'])) error('商品项缺少字段'); + $pid = (string)$it['item_id']; + $amt = (int)$it['amount']; + if ($amt < 1) error('数量无效: ' . $pid); + $parsed[] = ['item_id' => $pid, 'amount' => $amt]; + } + if (!$player) error('缺少player'); + + // ★ 双保险验证(与单件购买一致) + $isPreview = true; + $isOnline = false; + $isRegistered = false; + + if ($token) { + $tokenInfo = validateToken($token); + if ($tokenInfo && ($tokenInfo['purpose'] === 'admin' || $tokenInfo['purpose'] === 'all')) { + $isPreview = false; + if (validateToken($token)) validateAndUseToken($token); + } else { + $accessResult = validateWebAccess($token, 'buy', $password, $ipAddress); + if ($accessResult['ok']) { + $isPreview = false; + $player = $accessResult['player']; + $isOnline = $accessResult['online'] ?? false; + $isRegistered = $accessResult['registered'] ?? false; + } elseif (($accessResult['mode'] ?? '') === 'need_password') { + jsonResponse(['success' => false, 'need_password' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401); + } elseif (($accessResult['mode'] ?? '') === 'need_game_login') { + jsonResponse(['success' => false, 'need_game_login' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401); + } elseif (($accessResult['mode'] ?? '') === 'need_register') { + jsonResponse(['success' => false, 'need_register' => true, 'player' => $accessResult['player'], 'message' => $accessResult['message']], 401); + } else { + error($accessResult['message'], 401); + } + } + } + + // 结算模式对应的折扣/加价系数 + if ($settlement === 'shulker') { + $rate = (float)getShopConfig('cart_shulker_rate', '1.00'); + $modeName = '潜影盒打包'; + } else { + $settlement = 'backpack'; + $rate = (float)getShopConfig('cart_backpack_rate', '0.98'); + $modeName = '塞背包'; + } + + $db = getDB(); + $lines = []; + $subtotal = 0; + foreach ($parsed as &$p) { + $stmt = $db->prepare("SELECT * FROM shop_items WHERE id = :id"); + $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT); + $result = $stmt->execute(); + $item = $result->fetchArray(SQLITE3_ASSOC); + if (!$item) error('商品不存在: ' . $p['item_id']); + if ($item['stock'] == 0) error('商品已售罄: ' . $item['display_name']); + if ($item['stock'] > 0 && $item['stock'] < $p['amount']) error('库存不足: ' . $item['display_name'] . '(剩余' . $item['stock'] . ')'); + $lineTotal = (int)$item['buy_price'] * $p['amount']; + $subtotal += $lineTotal; + $p['item'] = $item; + $lines[] = [ + 'item_id' => $p['item_id'], + 'name' => $item['display_name'], + 'unit_price' => (int)$item['buy_price'], + 'amount' => $p['amount'], + 'line_total' => $lineTotal + ]; + } + unset($p); + + $totalPrice = (int)round($subtotal * $rate); + $saved = $subtotal - $totalPrice; // 折扣省下的(潜影盒加价时为负) + + // 检查余额 + if (!$isPreview && $totalPrice > 0) { + $balanceStmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :player"); + $balanceStmt->bindValue(':player', $player, SQLITE3_TEXT); + $balanceResult = $balanceStmt->execute(); + $balanceRow = $balanceResult->fetchArray(SQLITE3_ASSOC); + if (!$balanceRow) error('玩家余额查询失败'); + $balance = (int)$balanceRow['amount']; + if ($balance < $totalPrice) { + error('余额不足,当前余额: ' . $balance . ' 债券,需要: ' . $totalPrice . ' 债券'); + } + } + + if ($isPreview) { + preview([ + 'settlement' => $settlement, + 'mode_name' => $modeName, + 'rate' => $rate, + 'items' => $lines, + 'subtotal' => $subtotal, + 'total_price' => $totalPrice, + 'saved' => $saved, + 'player' => $player + ], '预览模式 - 结算不会生效'); + } + + // ★ 实际购买 — 包装在事务中 + $db->exec('BEGIN IMMEDIATE'); + try { + foreach ($parsed as $p) { + $item = $p['item']; + if ($item['stock'] > 0) { + $newStock = $item['stock'] - $p['amount']; + $stmt = $db->prepare("UPDATE shop_items SET stock = :ns, admin_stock = :ns, hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id"); + $stmt->bindValue(':ns', $newStock, SQLITE3_INTEGER); + $stmt->bindValue(':amount', $p['amount'], SQLITE3_INTEGER); + $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT); + $stmt->execute(); + } else { + $stmt = $db->prepare("UPDATE shop_items SET hourly_sales = hourly_sales + :amount, total_sales = total_sales + :amount WHERE id = :id"); + $stmt->bindValue(':amount', $p['amount'], SQLITE3_INTEGER); + $stmt->bindValue(':id', $p['item_id'], SQLITE3_TEXT); + $stmt->execute(); + } + } + + // 记录一条合并交易 + $reasonItems = array_map(function ($l) { return $l['name'] . ' x' . $l['amount']; }, $lines); + $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([ + 'settlement' => $settlement, + 'rate' => $rate, + 'subtotal' => $subtotal, + 'total_price' => $totalPrice, + 'items' => $lines + ], JSON_UNESCAPED_UNICODE), SQLITE3_TEXT); + $stmt->bindValue(':time', time(), SQLITE3_INTEGER); + $stmt->execute(); + + // 写入sync_requests + $db->exec("CREATE TABLE IF NOT EXISTS sync_requests (player_name TEXT PRIMARY KEY, created_at INTEGER NOT NULL)"); + $stmt2 = $db->prepare("INSERT OR REPLACE INTO sync_requests (player_name, created_at) VALUES (:player, :time)"); + $stmt2->bindValue(':player', $player, SQLITE3_TEXT); + $stmt2->bindValue(':time', time(), SQLITE3_INTEGER); + $stmt2->execute(); + + $db->exec('COMMIT'); + } catch (Exception $e) { + try { $db->exec('ROLLBACK'); } catch (Exception $e2) {} + error('购物车结算失败: ' . $e->getMessage(), 500); + } + + success([ + 'settlement' => $settlement, + 'mode_name' => $modeName, + 'rate' => $rate, + 'items' => $lines, + 'subtotal' => $subtotal, + 'total_price' => $totalPrice, + 'saved' => $saved, + 'player' => $player + ], '购物车结算成功'); +} + // ===== Weblogin Token验证(用于商城购买)===== function validateWebloginTokenForShop($webToken) { $db = getDB(); diff --git a/api/sync.php b/api/sync.php index 84e3a6f..ddb4505 100644 --- a/api/sync.php +++ b/api/sync.php @@ -269,6 +269,15 @@ switch ($action) { case 'check_shop_stock_changed': checkShopStockChanged(); break; + case 'pull_shop_prices': + pullShopPrices(); + break; + case 'clear_admin_prices': + clearAdminPrices(); + break; + case 'check_shop_prices_changed': + checkShopPricesChanged(); + break; case 'notify_sync': notifySync(); break; @@ -2750,6 +2759,67 @@ function clearAdminStock() { success([], '已清除管理员库存标记'); } +// ===== 拉取价格改动:admin_buy_price/admin_sell_price非NULL时返回给Java ===== +function pullShopPrices() { + $secret = getParam('secret'); + if (!$secret || $secret !== SECRET_KEY) error('认证失败'); + + $db = getDB(); + // ★ 只返回有价格改动的商品(admin_buy_price或admin_sell_price非NULL) + $stmt = $db->prepare("SELECT id, buy_price, sell_price, admin_buy_price, admin_sell_price FROM shop_items WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL"); + $result = $stmt->execute(); + $items = []; + while ($row = $result->fetchArray(SQLITE3_ASSOC)) { + // ★ 如果admin字段有值,用admin值替代原始价格返回给Java + if ($row['admin_buy_price'] !== null && $row['admin_buy_price'] !== '') { + $row['buy_price'] = (int)$row['admin_buy_price']; + $row['admin_buy_price_override'] = true; + } + if ($row['admin_sell_price'] !== null && $row['admin_sell_price'] !== '') { + $row['sell_price'] = (int)$row['admin_sell_price']; + $row['admin_sell_price_override'] = true; + } + unset($row['admin_buy_price'], $row['admin_sell_price']); // 不暴露内部字段 + $items[] = $row; + } + + // ★ 成功获取数据后立即清除admin价格标记(同一次请求完成,消除竞态) + $db->exec("UPDATE shop_items SET admin_buy_price = NULL, admin_sell_price = NULL WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL"); + + success(['items' => $items, 'count' => count($items)]); +} + +// ===== 插件确认已应用管理员价格改动 ===== +function clearAdminPrices() { + $secret = getParam('secret'); + if (!$secret || $secret !== SECRET_KEY) error('认证失败'); + + $db = getDB(); + $db->exec("UPDATE shop_items SET admin_buy_price = NULL, admin_sell_price = NULL WHERE admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL"); + success([], '已清除管理员价格标记'); +} + +// ===== 高频轮询:检测价格是否有改动(轻量级接口) ===== +function checkShopPricesChanged() { + $secret = getParam('secret'); + if (!$secret || $secret !== SECRET_KEY) error('认证失败'); + + $db = getDB(); + try { + $stmt = $db->prepare("SELECT SUM(CASE WHEN admin_buy_price IS NOT NULL OR admin_sell_price IS NOT NULL THEN 1 ELSE 0 END) as admin_price_count FROM shop_items"); + $result = $stmt->execute(); + $row = $result->fetchArray(SQLITE3_ASSOC); + $adminPriceCount = $row['admin_price_count'] ? (int)$row['admin_price_count'] : 0; + } catch (\Throwable $e) { + @error_log("[checkShopPricesChanged] error: " . $e->getMessage()); + $adminPriceCount = 0; + } + + // ★ 如果admin价格非NULL(管理员改了价格但Java还没拉取),返回changed=true + $changed = ($adminPriceCount > 0); + success(['changed' => $changed, 'admin_price_count' => $adminPriceCount]); +} + // ===== 高频轮询:检测库存是否有改动(轻量级接口) ===== function checkShopStockChanged() { $secret = getParam('secret'); diff --git a/core.php b/core.php index 3b5c34f..73d7772 100644 --- a/core.php +++ b/core.php @@ -222,7 +222,9 @@ function initTables(SQLite3 $db) { hourly_sales INTEGER DEFAULT 0, total_sales INTEGER DEFAULT 0, last_sync INTEGER DEFAULT 0, - admin_stock INTEGER DEFAULT NULL + admin_stock INTEGER DEFAULT NULL, + admin_buy_price INTEGER DEFAULT NULL, + admin_sell_price INTEGER DEFAULT NULL )"); // ★ 兼容升级:给shop_items加admin_stock字段(已存在则跳过) @@ -233,6 +235,13 @@ function initTables(SQLite3 $db) { try { $db->exec("ALTER TABLE shop_items ADD COLUMN last_modified INTEGER DEFAULT 0"); } catch (\Throwable $e) { /* 字段已存在,忽略 */ } + // ★ 兼容升级:给shop_items加admin价格覆盖字段(PHP后台修改价格时写入,Java拉取后清除) + try { + $db->exec("ALTER TABLE shop_items ADD COLUMN admin_buy_price INTEGER DEFAULT NULL"); + } catch (\Throwable $e) { /* 字段已存在,忽略 */ } + try { + $db->exec("ALTER TABLE shop_items ADD COLUMN admin_sell_price INTEGER DEFAULT NULL"); + } catch (\Throwable $e) { /* 字段已存在,忽略 */ } // CDK表 $db->exec("CREATE TABLE IF NOT EXISTS cdk ( diff --git a/player.php b/player.php index 299efe3..f814a3b 100644 --- a/player.php +++ b/player.php @@ -1154,9 +1154,11 @@ if ($currentVersion !== $BUILD_VERSION) { let shopItems = []; let shopCategories = []; let currentShopCat = ''; + let currentBuyItem = null; // 当前购买弹窗对应的商品,供"加入购物车"使用 async function renderShop(el) { el.innerHTML = '
加载中...
'; + loadCart(); try { const res = await api('shop.php', {action: 'list'}); if (!res.success) { el.innerHTML = '
' + (res.message || '加载失败') + '
'; return; } @@ -1189,6 +1191,7 @@ if ($currentVersion !== $BUILD_VERSION) { // 渲染当前分类的商品 renderShopContent(); + ensureCartButton(); } catch (e) { el.innerHTML = '
商城加载失败: ' + e.message + '
'; } @@ -1221,7 +1224,7 @@ if ($currentVersion !== $BUILD_VERSION) { const matIcon = getMaterialIcon(item.material); const stockText = item.stock == -1 ? '∞ 无限' : (item.stock == 0 ? '售罄' : item.stock + ' 个'); const stockClass = item.stock == 0 ? 'out' : ''; - html += `
${matIcon}
${item.display_name}
${item.buy_price} 债券
库存: ${stockText}
`; + html += `
${matIcon}
${item.display_name}
${item.buy_price} 债券
库存: ${stockText}
`; }); html += ''; contentEl.innerHTML = html; @@ -1232,9 +1235,10 @@ if ($currentVersion !== $BUILD_VERSION) { return icons[mat] || '🧱'; } - function showBuyModal(id, name, price, stock) { + function showBuyModal(id, name, price, stock, material) { if (IS_PREVIEW) { toast('预览模式下无法购买', 'error'); return; } if (stock == 0) { toast('商品已售罄', 'error'); return; } + currentBuyItem = {id, name, price, stock, material: material || ''}; document.getElementById('modalTitle').textContent = '购买: ' + name; const needPwd = price > 1000; const maxQty = stock > 0 ? stock : 999; @@ -1243,6 +1247,11 @@ if ($currentVersion !== $BUILD_VERSION) {
${needPwd ? '
' : '
小额交易(≤1000债券),免密码确认
'}`; + // 加入购物车按钮 + const cartBtn = document.createElement('div'); + cartBtn.style.cssText = 'margin-top:12px'; + cartBtn.innerHTML = ''; + document.getElementById('modalBody').appendChild(cartBtn); document.getElementById('modalConfirm').onclick = () => doBuy(id); document.getElementById('buyAmount').oninput = function() { const total = this.value * price; @@ -1292,6 +1301,230 @@ if ($currentVersion !== $BUILD_VERSION) { } } + // ===== 购物车系统 ===== + let cart = []; + let cartCfg = {backpack_rate: 0.98, shulker_rate: 1.0}; + let cartSettlement = 'backpack'; + let cartSubtotalVal = 0; + + function loadCart() { + try { cart = JSON.parse(localStorage.getItem('sdf1_cart') || '[]'); } catch (e) { cart = []; } + if (!Array.isArray(cart)) cart = []; + updateCartButton(); + } + function saveCart() { try { localStorage.setItem('sdf1_cart', JSON.stringify(cart)); } catch (e) {} updateCartButton(); } + + function updateCartButton() { + const btn = document.getElementById('cartFab'); + if (!btn) return; + const count = cart.reduce((s, i) => s + (i.amount || 1), 0); + const total = cart.reduce((s, i) => s + i.price * i.amount, 0); + btn.querySelector('.cart-fab-count').textContent = count; + btn.querySelector('.cart-fab-total').textContent = total; + btn.style.display = count > 0 ? 'flex' : 'none'; + } + + function ensureCartButton() { + if (document.getElementById('cartFab')) { updateCartButton(); return; } + const btn = document.createElement('div'); + btn.id = 'cartFab'; + btn.style.cssText = 'position:fixed;right:18px;bottom:18px;z-index:9000;display:none;align-items:center;gap:8px;padding:12px 18px;background:var(--accent);color:#fff;border-radius:30px;cursor:pointer;box-shadow:0 8px 24px rgba(0,0,0,0.35);font-size:14px;transition:transform .2s cubic-bezier(0.16,1,0.3,1)'; + btn.innerHTML = '🛒0件0债券'; + btn.onclick = openCartDrawer; + btn.onmouseenter = () => btn.style.transform = 'scale(1.06)'; + btn.onmouseleave = () => btn.style.transform = 'scale(1)'; + document.body.appendChild(btn); + updateCartButton(); + } + + function addToCartFromModal() { + if (!currentBuyItem) return; + const amount = parseInt(document.getElementById('buyAmount')?.value || '1') || 1; + addToCart(currentBuyItem.id, currentBuyItem.name, currentBuyItem.price, currentBuyItem.material || '', currentBuyItem.stock, amount); + closeModal(); + } + + function addToCart(id, name, price, material, stock, amount) { + if (IS_PREVIEW) { toast('预览模式下无法加入购物车', 'error'); return; } + amount = Math.max(1, parseInt(amount) || 1); + if (stock > 0 && amount > stock) amount = stock; + const exist = cart.find(i => i.item_id == id); + if (exist) { + const max = stock > 0 ? stock : 9999; + exist.amount = Math.min((exist.amount || 1) + amount, max); + } else { + cart.push({item_id: id, name: name, price: parseInt(price), material: material, stock: parseInt(stock), amount: amount}); + } + saveCart(); + toast('已加入购物车', 'success'); + } + + function changeCartQty(id, delta) { + const i = cart.find(x => x.item_id == id); + if (!i) return; + const max = i.stock > 0 ? i.stock : 9999; + i.amount = Math.min(Math.max((i.amount || 1) + delta, 1), max); + saveCart(); + renderCartList(); + } + function removeFromCart(id) { + cart = cart.filter(x => x.item_id != id); + saveCart(); + renderCartList(); + } + function clearCart() { + cart = []; + saveCart(); + renderCartList(); + } + + function openCartDrawer() { + if (cart.length === 0) { toast('购物车是空的', 'info'); return; } + document.getElementById('cartDrawer')?.remove(); + const overlay = document.createElement('div'); + overlay.id = 'cartDrawer'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.55);backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);z-index:9500;display:flex;justify-content:flex-end;animation:glassFadeIn .2s ease'; + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + let html = '
'; + html += '

🛒 购物车

'; + html += '
'; + html += '
'; + html += '
商品原价合计0 债券
'; + html += '
预估总价0 债券
'; + html += '
'; + html += '
'; + overlay.innerHTML = html; + document.body.appendChild(overlay); + renderCartList(); + } + + function renderCartList() { + const list = document.getElementById('cartList'); + if (!list) return; + if (cart.length === 0) { + list.innerHTML = '
购物车空空如也
'; + } else { + let h = ''; + cart.forEach(i => { + const matIcon = getMaterialIcon(i.material); + h += '
'; + h += '
' + matIcon + '
'; + h += '
' + escapeHtml(i.name) + '
'; + h += '
' + i.price + ' 债券/个
'; + h += '
'; + h += '' + i.amount + ''; + h += '
'; + h += '
' + (i.price * i.amount) + '
'; + h += '
'; + }); + list.innerHTML = h; + } + const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0); + cartSubtotalVal = subtotal; + const grand = document.getElementById('cartGrand'); + if (grand) grand.textContent = subtotal + ' 债券'; + const sub = document.getElementById('cartSubtotal'); + if (sub) sub.textContent = subtotal + ' 债券'; + updateCartButton(); + } + + async function openSettlement() { + if (cart.length === 0) { toast('购物车是空的', 'info'); return; } + if (IS_PREVIEW) { toast('预览模式无法结算', 'error'); return; } + try { const r = await api('shop.php', {action: 'cart_config'}); if (r.success && r.data) cartCfg = r.data; } catch (e) {} + const subtotal = cart.reduce((s, i) => s + i.price * i.amount, 0); + cartSubtotalVal = subtotal; + const bpTotal = Math.round(subtotal * cartCfg.backpack_rate); + const shTotal = Math.round(subtotal * cartCfg.shulker_rate); + const overlay = document.createElement('div'); + overlay.id = 'cartSettle'; + overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);z-index:9800;display:flex;justify-content:center;align-items:center;animation:glassFadeIn .2s ease'; + overlay.onclick = (e) => { if (e.target === overlay) overlay.remove(); }; + let html = '
'; + html += '

🧾 选择结算方式

'; + html += '

共 ' + cart.reduce((s, i) => s + i.amount, 0) + ' 件 · 原价 ' + subtotal + ' 债券

'; + html += '
'; + 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 += '
'; + html += '
'; + html += '
'; + html += '
'; + overlay.innerHTML = html; + document.body.appendChild(overlay); + selectSettleMode('backpack'); + } + + function shulkerDesc(rate, subtotal, total) { + const diff = total - subtotal; + if (diff > 0) return '打包进潜影盒,加价 ' + diff + ' 债券'; + if (diff < 0) return '打包进潜影盒,优惠 ' + (-diff) + ' 债券'; + return '打包进潜影盒,原价结算'; + } + + function settleModeHtml(mode, title, rate, total, subtotal, desc) { + const saved = subtotal - total; + let badge; + if (saved > 0) badge = '省 ' + saved + ' 债券'; + else if (saved < 0) badge = '加价 ' + (-saved) + ' 债券'; + else badge = '原价'; + return '
' + title + '
' + total + ' 债券
' + badge + '
' + desc + '
'; + } + + function selectSettleMode(mode) { + cartSettlement = mode; + document.querySelectorAll('.settle-mode').forEach(c => { + const active = c.dataset.mode === mode; + c.style.borderColor = active ? 'var(--accent)' : ''; + c.style.background = active ? 'color-mix(in srgb, var(--accent) 10%, transparent)' : ''; + }); + updateSettlePwd(); + } + + function updateSettlePwd() { + const wrap = document.getElementById('settlePwdWrap'); + if (!wrap) return; + const total = Math.round(cartSubtotalVal * (cartSettlement === 'shulker' ? cartCfg.shulker_rate : cartCfg.backpack_rate)); + if (total > 1000) { + wrap.innerHTML = '
'; + } else { + wrap.innerHTML = '
小额结算(≤1000债券),免密码确认
'; + } + } + + 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)); + 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 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}) + }); + const data = await res.json(); + if (data.need_password) { showPasswordModal(data.message || '请输入游戏登录密码'); return; } + document.getElementById('cartSettle')?.remove(); + if (data.success) { + const paid = data.data.total_price; + const modeName = data.data.mode_name; + cart = []; + saveCart(); + toast('结算成功(' + modeName + '):' + paid + ' 债券', 'success'); + setTimeout(() => { notifyJavaSync(); switchPage('shop'); }, 500); + } else { + toast(data.message || '结算失败', 'error'); + } + } catch (e) { + toast('结算请求失败: ' + e.message, 'error'); + } finally { + if (btn) { btn.disabled = false; btn.textContent = '确认结算'; } + } + } + // ===== 个人中心仪表盘 ===== async function renderDashboard(el) { if (IS_PREVIEW || !currentPlayer || (!AUTHENTICATED && NEED_PASSWORD)) {