0) { @ob_end_clean(); } ob_start(); header('Content-Type: application/json; charset=utf-8'); header('X-Content-Type-Options: nosniff'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Pragma: no-cache'); error_reporting(E_ALL); ini_set('display_errors', '0'); ini_set('log_errors', '1'); ini_set('memory_limit', '256M'); if (function_exists('opcache_invalidate')) { @opcache_invalidate(__FILE__); } register_shutdown_function(function() { while (ob_get_level() > 0) { @ob_end_flush(); } }); if (!extension_loaded('sqlite3')) { exit(json_encode(['success' => false, 'message' => 'Missing SQLite3'], JSON_UNESCAPED_UNICODE)); } require_once __DIR__ . '/../core.php'; if (!function_exists('error') || !function_exists('success') || !function_exists('getParam')) { exit(json_encode(['success' => false, 'message' => 'core.php load failed'], JSON_UNESCAPED_UNICODE)); } if (session_status() === PHP_SESSION_NONE) { session_start(); } $action = getParam('action', 'status'); try { switch ($action) { case 'login': cashierDoLogin(); break; case 'logout': cashierDoLogout(); break; case 'session': cashierSession(); break; case 'player_check': cashierPlayerCheck(); break; case 'order_list': cashierOrderList(); break; case 'cashier_list': cashierList(); break; case 'add': cashierAdd(); break; case 'edit': cashierEdit(); break; case 'delete': cashierDelete(); break; default: exit(json_encode(['success' => false, 'message' => '未知操作: ' . $action], JSON_UNESCAPED_UNICODE)); } } catch (\Throwable $e) { // ★ 2026-07-07 补强:顶层统一捕获 Throwable(含 Error),避免致命错误逃逸导致前端收到 // 非 JSON 响应(表现为“加载失败”却无具体原因)。同时落盘 debug.log 便于事后定位。 debugLog('[cashier.php] 顶层未捕获异常: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); exit(json_encode(['success' => false, 'message' => '服务器错误: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE)); } // ===== 收银员登录 ===== function cashierDoLogin() { $username = trim(getParam('username', '')); $password = getParam('password', ''); if ($username === '' || $password === '') { exit(json_encode(['success' => false, 'message' => '请输入账号和密码'], JSON_UNESCAPED_UNICODE)); } if (cashierLogin($username, $password)) { $c = getCurrentCashier(); exit(json_encode([ 'success' => true, 'data' => [ 'username' => $c['username'], 'discount_limit_percent' => $c['discount_limit_percent'], 'can_cash' => (int)($c['can_cash'] ?? 0) ] ], JSON_UNESCAPED_UNICODE)); } exit(json_encode(['success' => false, 'message' => '账号或密码错误'], JSON_UNESCAPED_UNICODE)); } // ===== 收银员登出 ===== function cashierDoLogout() { cashierLogout(); exit(json_encode(['success' => true, 'message' => '已退出登录'], JSON_UNESCAPED_UNICODE)); } // ===== 当前会话信息 ===== function cashierSession() { if (isAdminLoggedIn()) { exit(json_encode([ 'success' => true, 'data' => [ 'role' => 'admin', 'username' => 'admin', 'discount_limit_percent' => 100 ] ], JSON_UNESCAPED_UNICODE)); } if (isCashierLoggedIn()) { $c = getCurrentCashier(); exit(json_encode([ 'success' => true, 'data' => [ 'role' => 'cashier', 'username' => $c['username'], 'discount_limit_percent' => $c['discount_limit_percent'], 'can_cash' => (int)($c['can_cash'] ?? 0) ] ], JSON_UNESCAPED_UNICODE)); } exit(json_encode(['success' => false, 'message' => '未登录'], JSON_UNESCAPED_UNICODE)); } // ===== 查询目标玩家余额/在线状态(收银员或管理员均可)===== function cashierPlayerCheck() { requireCashierOrAdminSession(); $player = trim(getParam('player')); if (!$player) exit(json_encode(['success' => false, 'message' => '请输入玩家名'], JSON_UNESCAPED_UNICODE)); if (!preg_match('/^[a-zA-Z0-9_]{3,16}$/', $player)) { exit(json_encode(['success' => false, 'message' => '玩家名格式不正确(3-16位英文/数字/下划线)'], JSON_UNESCAPED_UNICODE)); } $db = getDB(); try { $db->exec("INSERT OR IGNORE INTO bond_cache (player_name, amount, updated_at) VALUES ('" . str_replace("'", "''", $player) . "', 0, " . time() . ")"); $stmt = $db->prepare("SELECT amount FROM bond_cache WHERE player_name = :name"); $stmt->bindValue(':name', $player, SQLITE3_TEXT); $row = $stmt->execute()->fetchArray(SQLITE3_ASSOC); $balance = $row ? (int)$row['amount'] : 0; $online = false; try { $ostmt = $db->prepare("SELECT player_name FROM online_players WHERE player_name = :name"); $ostmt->bindValue(':name', $player, SQLITE3_TEXT); $orow = $ostmt->execute()->fetchArray(SQLITE3_ASSOC); $online = $orow ? true : false; } catch (\Throwable $e) {} exit(json_encode([ 'success' => true, 'data' => ['player' => $player, 'exists' => true, 'balance' => $balance, 'online' => $online] ], JSON_UNESCAPED_UNICODE)); } catch (Exception $e) { @error_log('[cashier.php] Player check error: ' . $e->getMessage()); exit(json_encode(['success' => false, 'message' => '查询失败: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE)); } } // ===== 订单列表 ===== function cashierOrderList() { $role = requireCashierOrAdminSession(); // 收银员只能看自己的订单 $c = null; if ($role === 'cashier') { $c = getCurrentCashier(); } // ★ 尽早释放会话锁:查询期间不再占用 session 文件,避免阻塞同会话并发请求 if (session_status() === PHP_SESSION_ACTIVE) session_write_close(); // ★ 直接从独立 orders.db 读取:该库 Java 永不读写,彻底脱离 web.db 文件锁竞争, // 因此不再需要 busy_timeout=15000 与 5 次重试兜底 hack —— 干净稳定的单次查询。 try { $db = getOrdersDB(); } catch (\Throwable $e) { exit(json_encode(['success' => false, 'message' => '订单库不可用: ' . $e->getMessage()], JSON_UNESCAPED_UNICODE)); } $limit = (int)getParam('limit', 100); if ($limit <= 0 || $limit > 500) $limit = 100; $sql = "SELECT * FROM cashier_orders"; $params = []; if ($role === 'cashier' && $c) { $sql .= " WHERE operator_name = :op"; $params[':op'] = $c['username']; } $sql .= " ORDER BY created_at DESC LIMIT " . (int)$limit; $rows = []; try { $stmt = $db->prepare($sql); if (!$stmt) { exit(json_encode(['success' => false, 'message' => '查询失败: ' . $db->lastErrorMsg()], JSON_UNESCAPED_UNICODE)); } foreach ($params as $k => $v) { $stmt->bindValue($k, $v, SQLITE3_TEXT); } $result = $stmt->execute(); while ($r = $result->fetchArray(SQLITE3_ASSOC)) { if (!empty($r['items_detail'])) { $r['items_detail'] = json_decode($r['items_detail'], true) ?: []; } $rows[] = $r; } } catch (\Throwable $e) { // ★ 2026-07-07 加固:任何查询异常都返回合法 JSON,绝不把 PHP 报错文本注入响应 // (否则前端 r.json() 失败,订单页表现为“加载失败/报错”)。 debugLog('[cashier.php] order_list 查询异常: ' . $e->getMessage() . ' @ ' . $e->getFile() . ':' . $e->getLine()); debugLog('[cashier.php] order_list 异常堆栈: ' . $e->getTraceAsString()); exit(json_encode([ 'success' => false, 'message' => '订单查询异常: ' . $e->getMessage() ], JSON_UNESCAPED_UNICODE)); } exit(json_encode(['success' => true, 'data' => $rows], JSON_UNESCAPED_UNICODE)); } // ===== 收银员列表(仅管理员)===== function cashierList() { requireAdminSession(); $db = getDB(); $result = $db->prepare("SELECT id, username, discount_limit_percent, can_cash, created_at, created_by FROM cashiers ORDER BY id ASC")->execute(); $rows = []; while ($r = $result->fetchArray(SQLITE3_ASSOC)) { $rows[] = $r; } exit(json_encode(['success' => true, 'data' => $rows], JSON_UNESCAPED_UNICODE)); } // ===== 新增收银员(仅管理员)===== function cashierAdd() { requireAdminSession(); $username = trim(getParam('username', '')); $password = getParam('password', ''); $discountLimit = (int)getParam('discount_limit_percent', 0); $canCash = (int)getParam('can_cash', 0); if ($username === '' || $password === '') { exit(json_encode(['success' => false, 'message' => '账号和密码不能为空'], JSON_UNESCAPED_UNICODE)); } if (!preg_match('/^[a-zA-Z0-9_]{3,20}$/', $username)) { exit(json_encode(['success' => false, 'message' => '账号格式不正确(3-20位英文/数字/下划线)'], JSON_UNESCAPED_UNICODE)); } if ($discountLimit < 0 || $discountLimit > 100) { exit(json_encode(['success' => false, 'message' => '折扣上限需在0-100之间'], JSON_UNESCAPED_UNICODE)); } if ($canCash < 0 || $canCash > 1) { exit(json_encode(['success' => false, 'message' => '现金收款权限需为0或1'], JSON_UNESCAPED_UNICODE)); } $db = getDB(); // 查重 $chk = $db->prepare("SELECT id FROM cashiers WHERE username = :u"); $chk->bindValue(':u', $username, SQLITE3_TEXT); if ($chk->execute()->fetchArray(SQLITE3_ASSOC)) { exit(json_encode(['success' => false, 'message' => '账号已存在'], JSON_UNESCAPED_UNICODE)); } $hash = password_hash($password, PASSWORD_DEFAULT); $salt = bin2hex(random_bytes(8)); $stmt = $db->prepare("INSERT INTO cashiers (username, password_hash, salt, discount_limit_percent, can_cash, created_at, created_by) VALUES (:u,:h,:s,:d,:c,:t,'admin')"); $stmt->bindValue(':u', $username, SQLITE3_TEXT); $stmt->bindValue(':h', $hash, SQLITE3_TEXT); $stmt->bindValue(':s', $salt, SQLITE3_TEXT); $stmt->bindValue(':d', $discountLimit, SQLITE3_INTEGER); $stmt->bindValue(':c', $canCash, SQLITE3_INTEGER); $stmt->bindValue(':t', time(), SQLITE3_INTEGER); $stmt->execute(); exit(json_encode(['success' => true, 'message' => '收银员创建成功'], JSON_UNESCAPED_UNICODE)); } // ===== 编辑收银员(仅管理员):可改密码 / 折扣上限 ===== function cashierEdit() { requireAdminSession(); $id = (int)getParam('id', 0); if ($id <= 0) exit(json_encode(['success' => false, 'message' => '缺少id'], JSON_UNESCAPED_UNICODE)); $db = getDB(); $chk = $db->prepare("SELECT id FROM cashiers WHERE id = :id"); $chk->bindValue(':id', $id, SQLITE3_INTEGER); if (!$chk->execute()->fetchArray(SQLITE3_ASSOC)) { exit(json_encode(['success' => false, 'message' => '收银员不存在'], JSON_UNESCAPED_UNICODE)); } $sets = []; $params = [':id' => $id]; $newPassword = getParam('password', ''); if ($newPassword !== '') { $sets[] = "password_hash = :h"; $params[':h'] = password_hash($newPassword, PASSWORD_DEFAULT); $sets[] = "salt = :s"; $params[':s'] = bin2hex(random_bytes(8)); } if (getParam('discount_limit_percent') !== null && getParam('discount_limit_percent') !== '') { $dl = (int)getParam('discount_limit_percent', 0); if ($dl < 0 || $dl > 100) { exit(json_encode(['success' => false, 'message' => '折扣上限需在0-100之间'], JSON_UNESCAPED_UNICODE)); } $sets[] = "discount_limit_percent = :d"; $params[':d'] = $dl; } if (getParam('can_cash') !== null && getParam('can_cash') !== '') { $cc = (int)getParam('can_cash', 0); if ($cc < 0 || $cc > 1) { exit(json_encode(['success' => false, 'message' => '现金收款权限需为0或1'], JSON_UNESCAPED_UNICODE)); } $sets[] = "can_cash = :c"; $params[':c'] = $cc; } if (empty($sets)) { exit(json_encode(['success' => false, 'message' => '没有可更新的字段'], JSON_UNESCAPED_UNICODE)); } $sql = "UPDATE cashiers SET " . implode(', ', $sets) . " WHERE id = :id"; $stmt = $db->prepare($sql); foreach ($params as $k => $v) { $stmt->bindValue($k, $v, is_int($v) ? SQLITE3_INTEGER : SQLITE3_TEXT); } $stmt->execute(); exit(json_encode(['success' => true, 'message' => '收银员信息已更新'], JSON_UNESCAPED_UNICODE)); } // ===== 删除收银员(仅管理员)===== function cashierDelete() { requireAdminSession(); $id = (int)getParam('id', 0); if ($id <= 0) exit(json_encode(['success' => false, 'message' => '缺少id'], JSON_UNESCAPED_UNICODE)); $db = getDB(); $stmt = $db->prepare("DELETE FROM cashiers WHERE id = :id"); $stmt->bindValue(':id', $id, SQLITE3_INTEGER); $stmt->execute(); exit(json_encode(['success' => true, 'message' => '收银员已删除'], JSON_UNESCAPED_UNICODE)); }