const express = require('express'); const { query, getRow } = require('../db'); const { authenticate } = require('../middleware/auth'); const router = express.Router(); const { getConfig } = require('../db'); function getExternalKey() { try { return getConfig()?.external_api_key || process.env.EXTERNAL_API_KEY || ''; } catch { return process.env.EXTERNAL_API_KEY || ''; } } function externalAuth(req, res, next) { const required = getExternalKey(); if (!required) return next(); if (req.headers['x-external-key'] === required) return next(); return res.status(401).json({ error: '未授权' }); } router.use(externalAuth); router.post('/tickets', async (req, res) => { const { type, title, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description } = req.body; if (!type || !['report','suggestion','appeal'].includes(type)) return res.status(400).json({ error: '类型不正确' }); if (!title) return res.status(400).json({ error: '标题不能为空' }); if (!reporter_game_name || !reporter_game_uid) return res.status(400).json({ error: '请填写游戏名称和UID' }); if (type === 'report') { if (!reason) return res.status(400).json({ error: '请填写举报原因' }); } if (type === 'suggestion' && !description) return res.status(400).json({ error: '建议内容不能为空' }); if (type === 'appeal') { if (!reason) return res.status(400).json({ error: '请填写申诉理由' }); if (!description) return res.status(400).json({ error: '请填写详细申诉内容' }); } const countRow = await getRow("SELECT COUNT(*) as c FROM tickets WHERE (reporter_game_name = ? OR reporter_game_uid = ?) AND status IN ('pending','processing','awaiting_info','appealing')", [reporter_game_name, reporter_game_uid]); if (countRow.c >= 5) return res.status(400).json({ error: '待处理工单已达上限' }); const trackingToken = require('uuid').v4(); const [r] = await query(`INSERT INTO tickets(type,title,reporter_game_name,reporter_game_uid, target_game_name,target_game_uid,reason,description,tracking_token) VALUES (?,?,?,?,?,?,?,?,?)`, [type, title, reporter_game_name, reporter_game_uid, (type==='report')?(target_game_name||''):null, (type==='report')?(target_game_uid||''):null, (type==='report'||type==='appeal')?reason:null, (type==='suggestion'||type==='appeal')?description:'', trackingToken]); await query("INSERT INTO responses(ticket_id, content, is_staff) VALUES (?,?,0)", [r.insertId, `游戏内${type==='report'?'举报':type==='appeal'?'申诉':'建议'}\n提交人: ${reporter_game_name} (UID: ${reporter_game_uid})\n${reason||description||''}`]); res.status(201).json({ id: r.insertId, tracking_token: trackingToken }); }); router.get('/tickets', async (req, res) => { const { type, status, page, limit } = req.query; let q = `SELECT t.id, t.type, t.title, t.status, t.priority, t.reporter_game_name, t.reporter_game_uid, t.target_game_name, t.target_game_uid, t.reason, t.description, t.assigned_to, t.claim_note, t.created_at, t.updated_at FROM tickets t WHERE 1=1`; const p = []; if (type) { q += ' AND t.type = ?'; p.push(type); } if (status) { q += ' AND t.status = ?'; p.push(status); } const pg = parseInt(page) || 1; const lm = Math.min(parseInt(limit) || 50, 200); q += ` ORDER BY t.updated_at DESC LIMIT ${(pg-1)*lm}, ${lm}`; res.json(await query(q, p)); }); router.get('/tickets/:id', async (req, res) => { const t = await getRow(`SELECT id, type, title, status, priority, reporter_game_name, reporter_game_uid, target_game_name, target_game_uid, reason, description, assigned_to, claim_note, created_at, updated_at FROM tickets WHERE id = ?`, [req.params.id]); if (!t) return res.status(404).json({ error: '工单不存在' }); t.responses = await query('SELECT content, is_staff, created_at FROM responses WHERE ticket_id = ? ORDER BY created_at ASC', [req.params.id]); t.attachments = await query('SELECT id, original_name, mime_type, size FROM attachments WHERE ticket_id = ?', [req.params.id]); res.json(t); }); router.get('/reported-players', async (req, res) => { const rows = await query(`SELECT target_game_name, target_game_uid, COUNT(*) as total, SUM(CASE WHEN status IN ('pending','processing','awaiting_info','appealing') THEN 1 ELSE 0 END) as active, SUM(CASE WHEN status='resolved' THEN 1 ELSE 0 END) as resolved, MAX(created_at) as last_report FROM tickets WHERE type='report' AND target_game_name IS NOT NULL AND target_game_name != '' GROUP BY target_game_name, target_game_uid ORDER BY total DESC LIMIT 500`); res.json(rows); }); router.get('/stats', async (req, res) => { const [[{c:total}], byType, byStatus] = await Promise.all([ query('SELECT COUNT(*) as c FROM tickets'), query('SELECT type, COUNT(*) as c FROM tickets GROUP BY type'), query('SELECT status, COUNT(*) as c FROM tickets GROUP BY status'), ]); res.json({ total, byType, byStatus }); }); router.get('/users', async (req, res) => { res.json(await query('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users ORDER BY id')); }); router.get('/user/:id', async (req, res) => { const u = await getRow('SELECT id, username, email, game_name, game_uid, role, active, created_at FROM users WHERE id = ?', [req.params.id]); if (!u) return res.status(404).json({ error: '用户不存在' }); res.json(u); }); module.exports = router;