Files
2026-08-14 10:55:06 +08:00

317 lines
12 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
/**
* 商城统一 API 端点(AJAX/XHR
*
* 所有请求通过 action 参数路由,返回 JSON。
* 需登录的操作会校验 session。
* 用法: api.php?action=xxx [&参数]
*/
require __DIR__ . '/includes/init.php';
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');
// ─── 通用响应函数 ───
function json_ok($data = null, $msg = '') {
echo json_encode(['ok' => 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));
}