317 lines
12 KiB
PHP
317 lines
12 KiB
PHP
<?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));
|
||
}
|