From 5ed109ec0ec04bd2fdb9c7de11362a709fb9577b Mon Sep 17 00:00:00 2001 From: canglan Date: Sat, 18 Jul 2026 00:52:33 +0800 Subject: [PATCH] feat: ban list with dashboard widget, owner add/edit bans --- backend/db.js | 15 ++++++++++++ backend/routes/bans.js | 44 ++++++++++++++++++++++++++++++++++++ backend/server.js | 1 + public/js/pages/dashboard.js | 27 +++++++++++++++++++--- 4 files changed, 84 insertions(+), 3 deletions(-) create mode 100644 backend/routes/bans.js diff --git a/backend/db.js b/backend/db.js index e4464ac..ed5bdc7 100644 --- a/backend/db.js +++ b/backend/db.js @@ -250,6 +250,20 @@ async function initSchema(connection) { INDEX idx_user (user_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await createIfNotExists('bans', `CREATE TABLE bans ( + id INT AUTO_INCREMENT PRIMARY KEY, + player_name VARCHAR(50) NOT NULL, + player_uid VARCHAR(50), + reason TEXT, + type ENUM('ban','mute','warn','other') NOT NULL DEFAULT 'ban', + duration VARCHAR(20) DEFAULT '', + ticket_id INT, + created_by INT, + status ENUM('active','expired','appealed','lifted') NOT NULL DEFAULT 'active', + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + expires_at DATETIME + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); + await createIfNotExists('audit_logs', `CREATE TABLE audit_logs ( id INT AUTO_INCREMENT PRIMARY KEY, user_id INT, @@ -273,6 +287,7 @@ async function migrateAdditions(db) { try { await db.execute("ALTER TABLE tickets MODIFY status ENUM('pending','processing','awaiting_info','appealing','resolved','rejected','closed') NOT NULL"); } catch {} try { await db.execute("ALTER TABLE users ADD COLUMN source ENUM('netease','skin') NOT NULL DEFAULT 'netease'"); } catch {} try { await db.execute(`CREATE TABLE IF NOT EXISTS user_servers (user_id INT NOT NULL, group_name VARCHAR(100) NOT NULL, server_name VARCHAR(100) NOT NULL, UNIQUE KEY uk_user_server (user_id, group_name, server_name), INDEX idx_user (user_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {} + try { await db.execute(`CREATE TABLE IF NOT EXISTS bans (id INT AUTO_INCREMENT PRIMARY KEY, player_name VARCHAR(50) NOT NULL, player_uid VARCHAR(50), reason TEXT, type ENUM('ban','mute','warn','other') NOT NULL DEFAULT 'ban', duration VARCHAR(20) DEFAULT '', ticket_id INT, created_by INT, status ENUM('active','expired','appealed','lifted') NOT NULL DEFAULT 'active', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, expires_at DATETIME) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {} try { await db.execute(`CREATE TABLE IF NOT EXISTS polls (id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT, options JSON NOT NULL, group_name VARCHAR(100) NOT NULL DEFAULT '', server_name VARCHAR(100) NOT NULL DEFAULT '', start_time DATETIME, end_time DATETIME, active TINYINT(1) NOT NULL DEFAULT 1, created_by INT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_active (active), INDEX idx_group_server (group_name, server_name)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {} try { await db.execute(`CREATE TABLE IF NOT EXISTS poll_votes (id INT AUTO_INCREMENT PRIMARY KEY, poll_id INT NOT NULL, user_id INT NOT NULL, option_index INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_poll_user (poll_id, user_id), INDEX idx_poll (poll_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {} diff --git a/backend/routes/bans.js b/backend/routes/bans.js new file mode 100644 index 0000000..7f3875f --- /dev/null +++ b/backend/routes/bans.js @@ -0,0 +1,44 @@ +const express = require('express'); +const { query, getRow } = require('../db'); +const { authenticate, requireRole } = require('../middleware/auth'); + +const router = express.Router(); + +router.get('/', authenticate, async (req, res) => { + const rows = await query('SELECT * FROM bans ORDER BY created_at DESC LIMIT 200'); + res.json(rows); +}); + +router.get('/active', authenticate, async (req, res) => { + res.json(await query("SELECT * FROM bans WHERE status = 'active' ORDER BY created_at DESC")); +}); + +router.post('/', authenticate, requireRole('owner'), async (req, res) => { + const { player_name, player_uid, reason, type, duration, ticket_id, expires_at } = req.body; + if (!player_name) return res.status(400).json({ error: '玩家名为必填' }); + const r = await query('INSERT INTO bans(player_name, player_uid, reason, type, duration, ticket_id, created_by, expires_at) VALUES (?,?,?,?,?,?,?,?)', + [player_name, player_uid||'', reason||'', type||'ban', duration||'', ticket_id||null, req.user.id, expires_at||null]); + if (ticket_id) { + await query("INSERT INTO responses(ticket_id, user_id, content, is_staff) VALUES (?,?,?,1)", + [ticket_id, req.user.id, `【处罚记录】${type==='ban'?'封禁':type==='mute'?'禁言':type==='warn'?'警告':'其他'}: ${player_name}${duration?' 时长:'+duration:''}${reason?' 原因:'+reason:''}`]); + } + res.status(201).json({ id: r.insertId }); +}); + +router.put('/:id', authenticate, requireRole('owner'), async (req, res) => { + const fields = {}; + if (req.body.status) fields.status = req.body.status; + if (req.body.reason) fields.reason = req.body.reason; + if (req.body.duration) fields.duration = req.body.duration; + if (!Object.keys(fields).length) return res.status(400).json({ error: '无更新内容' }); + const sets = Object.keys(fields).map(k => `${k} = ?`).join(', '); + await query(`UPDATE bans SET ${sets} WHERE id = ?`, [...Object.values(fields), req.params.id]); + res.json({ message: '更新成功' }); +}); + +router.delete('/:id', authenticate, requireRole('owner'), async (req, res) => { + await query('DELETE FROM bans WHERE id = ?', [req.params.id]); + res.json({ message: '已删除' }); +}); + +module.exports = router; diff --git a/backend/server.js b/backend/server.js index 947a0bd..0fdc7ff 100644 --- a/backend/server.js +++ b/backend/server.js @@ -118,6 +118,7 @@ if (isInstalled()) { app.use('/api/notifications', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/notifications')); app.use('/api/polls', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/polls')); app.use('/api/features', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/features')); + app.use('/api/bans', methodGuard(['GET','POST','PUT','DELETE']), generalLimiter, require('./routes/bans')); app.use('/api/external', require('./routes/external')); app.get('/api/verify', (req, res) => res.redirect(`/#/verify?token=${req.query.token}`)); diff --git a/public/js/pages/dashboard.js b/public/js/pages/dashboard.js index cd8b27c..d9545df 100644 --- a/public/js/pages/dashboard.js +++ b/public/js/pages/dashboard.js @@ -15,7 +15,8 @@ const Dashboard = { API.get('/tickets/stats'), Auth.isOwner() ? API.get('/export/dashboard') : null, ]); - this.renderContent(ct, stats, exports); + const bans = Auth.isAdmin() ? await API.get('/bans/active').catch(()=>[]) : []; + this.renderContent(ct, stats, exports, bans); } catch (e) { ct.innerHTML = `
${e.message}
`; } }, @@ -30,8 +31,10 @@ const Dashboard = {
${byStatus.resolved || 0}
已解决
- ${Auth.isAdmin() && exports ? ` -
+ ${Auth.isAdmin() ? ` +
封禁列表 ${Auth.isOwner() ? ``:''}
+
${bans.length === 0 ? '暂无封禁记录' : `${bans.map(b=>``).join('')}
玩家类型原因时长时间
${U.esc(b.player_name)}${b.type==='ban'?'封禁':b.type==='mute'?'禁言':'其他'}${U.esc(b.reason||'')}${b.duration||'永久'}${U.date(b.created_at)}
`}
+
` : ''}
类型分布
${(exports.byType||[]).map(r => `
${U.TYPE[r.type]||r.type}${r.c} 条
`).join('')}
@@ -44,5 +47,23 @@ const Dashboard = { ` : ''}

提示: 可在"工单列表"中查看和管理所有工单

`; + }, + + showAddBan() { + U.modal('添加封禁', ` +
+
+
+
+ +
+ `); + document.getElementById('ban-form').onsubmit = async e => { + e.preventDefault(); + try { + await API.post('/bans', { player_name: document.getElementById('ban-name').value, player_uid: document.getElementById('ban-uid').value, type: document.getElementById('ban-type').value, duration: document.getElementById('ban-dur').value, reason: document.getElementById('ban-reason').value }); + App.closeModal(); this.mount(); + } catch(ex) { alert(ex.message); } + }; } };