true, 'msg' => $msg, 'data' => $data], JSON_UNESCAPED_UNICODE); exit; } function json_err($code = 400, $msg = '操作失败') { http_response_code($code); echo json_encode(['ok' => false, 'msg' => $msg], JSON_UNESCAPED_UNICODE); exit; } // ─── CSRF 校验(写操作必须带 token)───────────────────────────── function requireCsrf() { $token = trim($_SERVER['HTTP_X_CSRF_TOKEN'] ?? $_POST['csrf_token'] ?? ''); if (!$token || !verifyCsrfToken($token)) { json_err(403, 'CSRF 验证失败,请刷新页面重试'); } } $action = trim($_GET['action'] ?? $_POST['action'] ?? ''); // ─── 路由表 ─── $ROUTES = [ // 购物车(需登录) 'cart_get' => true, // GET - 获取购物车 'cart_add' => true, // POST - 加入购物车 'cart_update' => true, // POST - 修改数量 'cart_remove' => true, // POST - 删除一项 'cart_clear' => true, // POST - 清空购物车 // 用户操作(需登录) 'sign_in' => true, // POST - 每日签到 'ticket_submit'=> true, // POST - 提交工单 // 公开读取 'search' => false, // GET - 搜索商品 'products' => false, // GET - 分类筛选商品 'product_info' => false, // GET - 商品详情 'announcements'=> false, // GET - 公告列表 ]; if (!isset($ROUTES[$action])) { json_err(404, '未知操作: ' . h($action)); } $needAuth = $ROUTES[$action]; if ($needAuth && empty($_SESSION['user_id'])) { json_err(401, '请先登录'); } $user = null; if ($needAuth) { $user = currentUser(); if (!$user) { json_err(401, '登录已过期'); } } // ─── 写操作统一 CSRF ─── $WRITE_ACTIONS = ['cart_add', 'cart_update', 'cart_remove', 'cart_clear', 'sign_in', 'ticket_submit']; if (in_array($action, $WRITE_ACTIONS)) { requireCsrf(); } // ─── 分发处理 ─── switch ($action) { /* ================================================================ * 购物车 * ================================================================ */ case 'cart_get': $items = []; $total = 0; if (!empty($_SESSION['cart']) && is_array($_SESSION['cart'])) { $ids = array_keys($_SESSION['cart']); if (!empty($ids)) { $inPlaceholders = str_repeat('?', count($ids)); $stmt = db()->prepare( 'SELECT id,name,image,icon,category,points_price,stock,status FROM ' . tn('products') . ' WHERE id IN (' . $inPlaceholders . ') AND status=1' ); $stmt->execute($ids); while ($p = $stmt->fetch()) { $qty = (int)($_SESSION['cart'][$p['id']] ?? 0); if ($qty <= 0) continue; $price = !empty($p['points_price']) ? (int)$p['points_price'] : 0; $items[] = [ 'id' => (int)$p['id'], 'name' => $p['name'], 'image' => $p['image'] ?: '', 'icon' => $p['icon'] ?: '', 'category' => $p['category'], 'qty' => $qty, 'points_price' => $price, 'stock' => (int)$p['stock'], 'subtotal' => $price * $qty, ]; $total += $price * $qty; } } } json_ok(['items' => $items, 'count' => count($items), 'total_points' => $total]); case 'cart_add': $pid = (int)($_POST['product_id'] ?? 0); $qty = max(1, min(99, (int)($_POST['qty'] ?? 1))); if ($pid <= 0) { json_err(400, '商品 ID 无效'); } $stmt = db()->prepare('SELECT id,status,stock FROM ' . tn('products') . ' WHERE id=?'); $stmt->execute([$pid]); $p = $stmt->fetch(); if (!$p || (int)$p['status'] !== 1) { json_err(400, '商品不存在或已下架'); } if (!isset($_SESSION['cart'])) { $_SESSION['cart'] = []; } $current = (int)($_SESSION['cart'][$pid] ?? 0); $newQty = $current + $qty; if ((int)$p['stock'] >= 0 && $newQty > (int)$p['stock']) { json_err(400, '库存不足(剩余 ' . (int)$p['stock'] . ' 件)'); } $_SESSION['cart'][$pid] = $newQty; $count = array_sum(array_map('intval', $_SESSION['cart'])); json_ok(['cart_count' => $count, 'item_qty' => $newQty], '已加入购物车'); case 'cart_update': $pid = (int)($_POST['product_id'] ?? 0); $qty = max(1, min(99, (int)($_POST['qty'] ?? 1))); if ($pid <= 0) { json_err(400, '商品 ID 无效'); } if (!isset($_SESSION['cart'][$pid])) { json_err(400, '购物车中没有该商品'); } $stmt = db()->prepare('SELECT stock FROM ' . tn('products') . ' WHERE id=? AND status=1'); $stmt->execute([$pid]); $p = $stmt->fetch(); if (!$p) { json_err(400, '商品不存在或已下架'); } if ((int)$p['stock'] >= 0 && $qty > (int)$p['stock']) { json_err(400, '库存不足'); } $_SESSION['cart'][$pid] = $qty; $count = array_sum(array_map('intval', $_SESSION['cart'])); json_ok(['cart_count' => $count], '数量已更新'); case 'cart_remove': $pid = (int)($_POST['product_id'] ?? 0); if ($pid <= 0) { json_err(400, '商品 ID 无效'); } unset($_SESSION['cart'][$pid]); $count = isset($_SESSION['cart']) ? array_sum(array_map('intval', $_SESSION['cart'])) : 0; json_ok(['cart_count' => $count], '已移除'); case 'cart_clear': $_SESSION['cart'] = []; json_ok([], '购物车已清空'); /* ================================================================ * 每日签到 * ================================================================ */ case 'sign_in': $enabled = getSetting('points_enabled', '1') === '1'; if (!$enabled) { json_err(400, '积分系统未启用'); } $today = date('Y-m-d'); $stmt = db()->prepare('SELECT id FROM ' . tn('sign_logs') . ' WHERE user_id=? AND sign_date=?'); $stmt->execute([$user['id'], $today]); if ($stmt->fetch()) { json_err(400, '今天已经签到了'); } $points = max(0, (int)getSetting('points_sign', 5)); db()->beginTransaction(); try { db()->prepare('INSERT INTO ' . tn('sign_logs') . ' (user_id,sign_date,points) VALUES (?,?,?)') ->execute([$user['id'], $today, $points]); db()->prepare('UPDATE ' . tn('users') . ' SET points=points+? WHERE id=?') ->execute([$points, $user['id']]); db()->commit(); } catch (Exception $e) { db()->rollBack(); json_err(500, '签到失败: 数据库错误'); } $newTotal = getUserPoints($user['id']); json_ok(['points_gained' => $points, 'points_total' => $newTotal], "签到成功!+{$points} 积分"); /* ================================================================ * 提交工单 * ================================================================ */ case 'ticket_submit': $deptId = (int)($_POST['dept_id'] ?? 0); $title = trim($_POST['title'] ?? ''); $body = trim($_POST['body'] ?? ''); $priority = in_array($_POST['priority'] ?? '', ['low','normal','high']) ? $_POST['priority'] : 'normal'; if ($title === '' || strlen($title) < 3) { json_err(400, '标题至少 3 个字'); } if ($body === '' || strlen($body) < 5) { json_err(400, '描述至少 5 个字'); } // 验证部门是否存在 if ($deptId > 0) { $ds = db()->prepare('SELECT id FROM ' . tn('departments') . ' WHERE id=?'); $ds->execute([$deptId]); if (!$ds->fetch()) { $deptId = 0; } } $orderId = !empty($_POST['order_id']) ? (int)$_POST['order_id'] : null; db()->prepare( 'INSERT INTO ' . tn('tickets') . ' (user_id,dept_id,title,body,priority,order_id,status,created_at) VALUES (?,?,?,?,?,?,\'open\',NOW())' )->execute([$user['id'], $deptId, $title, $body, $priority, $orderId]); $ticketId = (int)db()->lastInsertId(); // 通知处理人员 notifyAdmins('新工单 #' . $ticketId . ': ' . $title, "用户 {$user['username']} 提交了新工单,请及时处理。"); json_ok(['ticket_id' => $ticketId], '工单提交成功!我们会尽快处理。'); /* ================================================================ * 商品搜索 / 分类筛选(公开) * ================================================================ */ case 'search': $q = trim($_GET['q'] ?? ''); if ($q === '') { json_err(400, '请输入搜索关键词'); } $where = ['status = 1', 'name LIKE ?']; $params = ['%' . $q . '%']; $sql = 'SELECT id,name,category,image,icon,points_price,stock,description FROM ' . tn('products') . ' WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC LIMIT 50'; $stmt = db()->prepare($sql); $stmt->execute($params); $products = []; while ($r = $stmt->fetch()) { $products[] = [ 'id' => (int)$r['id'], 'name' => $r['name'], 'category' => $r['category'], 'image' => $r['image'] ?: '', 'icon' => $r['icon'] ?: '', 'points_price' => !empty($r['points_price']) ? (int)$r['points_price'] : null, 'stock' => (int)$r['stock'], 'description' => mb_substr($r['description'] ?? '', 0, 100), ]; } json_ok(['products' => $products, 'query' => $q, 'count' => count($products)]); case 'products': $cat = trim($_GET['cat'] ?? ''); $where = ['status = 1']; $params = []; if ($cat !== '') { $where[] = 'category = ?'; $params[] = $cat; } $sql = 'SELECT id,name,category,image,icon,points_price,stock FROM ' . tn('products') . ' WHERE ' . implode(' AND ', $where) . ' ORDER BY created_at DESC LIMIT 100'; $stmt = db()->prepare($sql); $stmt->execute($params); $products = []; while ($r = $stmt->fetch()) { $products[] = [ 'id' => (int)$r['id'], 'name' => $r['name'], 'category' => $r['category'], 'image' => $r['image'] ?: '', 'icon' => $r['icon'] ?: '', 'points_price' => !empty($r['points_price']) ? (int)$r['points_price'] : null, 'stock' => (int)$r['stock'], ]; } json_ok(['products' => $products, 'category' => $cat, 'count' => count($products)]); case 'product_info': $pid = (int)($_GET['id'] ?? 0); if ($pid <= 0) { json_err(400, '商品 ID 无效'); } $stmt = db()->prepare( 'SELECT id,name,category,image,icon,points_price,cash_price,stock,period,description,created_at FROM ' . tn('products') . ' WHERE id=? AND status=1' ); $stmt->execute([$pid]); $p = $stmt->fetch(); if (!$p) { json_err(404, '商品不存在'); } json_ok([ 'id' => (int)$p['id'], 'name' => $p['name'], 'category' => $p['category'], 'image' => $p['image'] ?: '', 'icon' => $p['icon'] ?: '', 'points_price' => !empty($p['points_price']) ? (int)$p['points_price'] : null, 'cash_price' => !empty($p['cash_price']) ? floatval($p['cash_price']) : null, 'stock' => (int)$p['stock'], 'period' => $p['period'] ?: '', 'description' => $p['description'] ?: '', ]); case 'announcements': $limit = min(20, max(1, (int)($_GET['limit'] ?? 10))); $stmt = db()->query( 'SELECT id,title,pinned,content,created_at FROM ' . tn('announcements') . ' WHERE status=1 ORDER BY pinned DESC, created_at DESC LIMIT ' . $limit ); $list = []; while ($a = $stmt->fetch()) { $list[] = [ 'id' => (int)$a['id'], 'title' => $a['title'], 'pinned' => (int)$a['pinned'], 'excerpt' => mb_substr(strip_tags($a['content'] ?? ''), 0, 120), 'created_at' => $a['created_at'], ]; } json_ok(['announcements' => $list]); default: json_err(404, '未知操作: ' . h($action)); }