diff --git a/backend/db.js b/backend/db.js index c148123..2f97464 100644 --- a/backend/db.js +++ b/backend/db.js @@ -86,7 +86,7 @@ async function initSchema(connection) { user_id INT, type ENUM('report','suggestion','appeal','result_appeal') NOT NULL, title VARCHAR(100) NOT NULL, - status ENUM('pending','processing','awaiting_info','resolved','rejected','closed') NOT NULL DEFAULT 'pending', + status ENUM('pending','processing','awaiting_info','appealing','resolved','rejected','closed') NOT NULL DEFAULT 'pending', priority ENUM('low','medium','high','urgent') NOT NULL DEFAULT 'medium', reporter_game_name VARCHAR(50) NOT NULL, reporter_game_uid VARCHAR(50) NOT NULL, @@ -194,6 +194,7 @@ async function migrateAdditions(db) { try { await db.execute("ALTER TABLE tickets ADD COLUMN is_admin_complaint TINYINT(1) NOT NULL DEFAULT 0"); } catch {} try { await db.execute("ALTER TABLE tickets ADD COLUMN parent_ticket_id INT"); } catch {} try { await db.execute("ALTER TABLE tickets MODIFY type ENUM('report','suggestion','appeal','result_appeal') NOT NULL"); } catch {} + try { await db.execute("ALTER TABLE tickets MODIFY status ENUM('pending','processing','awaiting_info','appealing','resolved','rejected','closed') NOT NULL"); } catch {} } async function seedTemplates(db) { diff --git a/backend/routes/export.js b/backend/routes/export.js index 544bc3b..bc6d771 100644 --- a/backend/routes/export.js +++ b/backend/routes/export.js @@ -3,7 +3,7 @@ const { query } = require('../db'); const { authenticate, requireRole } = require('../middleware/auth'); const router = express.Router(); -const SL = { pending:'待处理', processing:'处理中', awaiting_info:'待补充', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }; +const SL = { pending:'待处理', processing:'处理中', awaiting_info:'待补充', appealing:'申诉中', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }; const TL = { report:'举报', suggestion:'建议', appeal:'申诉', result_appeal:'结果申诉' }; function toCSV(headers, rows, filename) { diff --git a/backend/routes/tickets.js b/backend/routes/tickets.js index cb41acc..0885194 100644 --- a/backend/routes/tickets.js +++ b/backend/routes/tickets.js @@ -9,10 +9,10 @@ const { validateLengths } = require('../middleware/security'); const router = express.Router(); -const SL = { pending:'待处理', processing:'处理中', awaiting_info:'待补充', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }; +const SL = { pending:'待处理', processing:'处理中', awaiting_info:'待补充', appealing:'申诉中', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }; const TL = { report:'举报', suggestion:'建议', appeal:'申诉', result_appeal:'结果申诉' }; -const SC = { pending:'#f59e0b', processing:'#7c3aed', awaiting_info:'#d97706', resolved:'#059669', rejected:'#dc2626', closed:'#6b7280' }; -const SO = { processing:1, awaiting_info:2, pending:3, resolved:4, rejected:5, closed:6 }; +const SC = { pending:'#f59e0b', processing:'#7c3aed', awaiting_info:'#d97706', appealing:'#f97316', resolved:'#059669', rejected:'#dc2626', closed:'#6b7280' }; +const SO = { processing:1, awaiting_info:2, appealing:3, pending:4, resolved:5, rejected:6, closed:7 }; async function siteUrl() { const r = await getRow("SELECT v FROM settings WHERE k='site_url'"); return r?.v || 'http://localhost:3100'; } @@ -108,7 +108,8 @@ router.post('/', optionalAuth, upload.array('files', 5), finalizeUpload, validat if (!parent_ticket_id) return res.status(400).json({ error: '缺少原工单信息' }); const orig = await getRow("SELECT * FROM tickets WHERE id = ? AND user_id = ? AND status IN ('resolved','rejected','closed')", [parent_ticket_id, req.user?.id||0]); if (!orig) return res.status(400).json({ error: '原工单不存在或状态不允许申诉' }); - const existing = await getRow("SELECT id FROM tickets WHERE type = 'result_appeal' AND parent_ticket_id = ?", [parent_ticket_id]); + if (orig.status === 'closed') return res.status(400).json({ error: '已关闭的工单无法申诉' }); + const existing = await getRow("SELECT id FROM tickets WHERE type = 'result_appeal' AND parent_ticket_id = ? AND status != 'closed'", [parent_ticket_id]); if (existing) return res.status(400).json({ error: '该工单已有结果申诉在处理中' }); if (!reason) return res.status(400).json({ error: '请填写申诉理由' }); if (!description) return res.status(400).json({ error: '请填写详细申诉内容' }); @@ -140,6 +141,11 @@ router.post('/', optionalAuth, upload.array('files', 5), finalizeUpload, validat } } + if (type === 'result_appeal' && parent_ticket_id) { + await conn.execute("UPDATE tickets SET status = 'appealing' WHERE id = ?", [parent_ticket_id]); + await conn.execute("INSERT INTO responses(ticket_id, user_id, content, is_staff) VALUES (?,?,?,0)", [parent_ticket_id, req.user?.id||null, '【系统】玩家已对处理结果提出申诉,工单状态变更为申诉中']); + } + const respContent = type === 'report' ? `举报已提交\n举报人: ${rgn} (UID: ${rgu})\n被举报人: ${target_game_name||'无'} (UID: ${target_game_uid||'无'})\n原因: ${reason}${description?'\n详情: '+description:''}` : type === 'appeal' @@ -166,6 +172,7 @@ router.post('/', optionalAuth, upload.array('files', 5), finalizeUpload, validat router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res) => { const t = await getRow('SELECT * FROM tickets WHERE id = ?', [req.params.id]); if (!t) return res.status(404).json({ error: '工单不存在' }); + if (t.status === 'closed') return res.status(400).json({ error: '已关闭的工单不可再修改' }); const fields = {}; if (req.body.status) fields.status = req.body.status; if (req.body.priority) fields.priority = req.body.priority; @@ -176,6 +183,12 @@ router.put('/:id', authenticate, requireRole('owner','admin'), async (req, res) if (req.body.status && req.body.status !== t.status) { await query("INSERT INTO responses(ticket_id, user_id, content, is_staff) VALUES (?,?,?,1)", [req.params.id, req.user.id, `状态变更为: ${SL[req.body.status]||req.body.status}`]); + + if (t.type === 'result_appeal' && (req.body.status === 'resolved' || req.body.status === 'rejected')) { + await query(`UPDATE tickets SET status = 'closed' WHERE id = ?`, [t.parent_ticket_id]); + await query("INSERT INTO responses(ticket_id, user_id, content, is_staff) VALUES (?,?,?,1)", [t.parent_ticket_id, req.user.id, `【系统】申诉已处理,原工单已关闭`]); + } + await notifyStatusChange(t, req.body.status); } await query("INSERT INTO audit_logs(user_id, username, action, entity_type, entity_id, details) VALUES (?,?,?,?,?,?)", [req.user.id, req.user.username, 'update_ticket', 'ticket', req.params.id, '更新工单']); @@ -186,6 +199,7 @@ router.post('/:id/claim', authenticate, requireRole('owner','admin'), async (req const t = await getRow('SELECT * FROM tickets WHERE id = ?', [req.params.id]); if (!t) return res.status(404).json({ error: '工单不存在' }); if (t.assigned_to && t.assigned_to !== req.user.id && t.status !== 'pending') return res.status(400).json({ error: '该工单已被他人认领' }); + if (t.status === 'closed') return res.status(400).json({ error: '已关闭的工单不可认领' }); if (t.type === 'suggestion' && req.user.role !== 'owner') return res.status(403).json({ error: '建议工单仅服主可处理' }); if ((t.type === 'result_appeal' || t.is_admin_complaint) && req.user.role !== 'owner') return res.status(403).json({ error: '该工单仅服主可处理' }); @@ -206,6 +220,7 @@ router.post('/:id/claim', authenticate, requireRole('owner','admin'), async (req router.post('/:id/transfer', authenticate, requireRole('owner','admin'), async (req, res) => { const t = await getRow('SELECT * FROM tickets WHERE id = ?', [req.params.id]); if (!t) return res.status(404).json({ error: '工单不存在' }); + if (t.status === 'closed') return res.status(400).json({ error: '已关闭的工单不可转交' }); if (t.assigned_to !== req.user.id && req.user.role !== 'owner') return res.status(403).json({ error: '只能转交自己负责的工单' }); const { to_user_id, reason } = req.body; if (!to_user_id) return res.status(400).json({ error: '请选择转交目标' }); diff --git a/public/css/style.css b/public/css/style.css index d4d7449..8b2c7af 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -184,6 +184,7 @@ tbody tr:last-child td{border-bottom:none} .bg-pending{background:#fef9c3;color:#854d0e} .bg-processing{background:#ede9fe;color:#6d28d9} .bg-awaiting_info{background:#fed7aa;color:#9a3412} +.bg-appealing{background:#ffedd5;color:#c2410c;border:1px solid #fdba74} .bg-resolved{background:#d1fae5;color:#065f46} .bg-rejected{background:#fee2e2;color:#991b1b} .bg-closed{background:var(--g2);color:var(--g7)} diff --git a/public/js/pages/ticket-detail.js b/public/js/pages/ticket-detail.js index 0ba78d7..5e4ff91 100644 --- a/public/js/pages/ticket-detail.js +++ b/public/js/pages/ticket-detail.js @@ -67,7 +67,7 @@ const TicketDetail = { `:''} - ${!isStaff && t.user_id === Auth.user().id && ['resolved','rejected','closed'].includes(t.status) && t.type !== 'result_appeal' ? ` + ${!isStaff && t.user_id === Auth.user().id && ['resolved','rejected'].includes(t.status) && t.type !== 'result_appeal' ? `
结果申诉

对处理结果有异议?可以提交一次申诉,由服主复核。

diff --git a/public/js/utils.js b/public/js/utils.js index 25181c3..7f80b3f 100644 --- a/public/js/utils.js +++ b/public/js/utils.js @@ -1,5 +1,5 @@ const U = { - STATUS: { pending:'待处理', processing:'处理中', awaiting_info:'待补充', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }, + STATUS: { pending:'待处理', processing:'处理中', awaiting_info:'待补充', appealing:'申诉中', resolved:'已解决', rejected:'已驳回', closed:'已关闭' }, TYPE: { report:'举报', suggestion:'建议', appeal:'申诉', result_appeal:'结果申诉' }, ROLE: { owner:'服主', admin:'管理员', player:'玩家' }, PRIORITY: { low:'低', medium:'中', high:'高', urgent:'紧急' },