321 lines
13 KiB
PHP
321 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* 收银台 API(独立页面后端)
|
|
* 支持:收银员登录/登出/会话、玩家查询、订单列表、收银员管理(仅管理员)
|
|
*/
|
|
|
|
// ★ 清除所有缓冲 + 发送 JSON Content-Type
|
|
while (ob_get_level() > 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));
|
|
}
|