2.1 版本文件

This commit is contained in:
2026-08-14 10:55:06 +08:00
commit a3434ec620
67 changed files with 12559 additions and 0 deletions
+316
View File
@@ -0,0 +1,316 @@
<?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));
}