feat: MC Report System - MySQL + Express + Vanilla JS SPA
This commit is contained in:
217
backend/db.js
Normal file
217
backend/db.js
Normal file
@@ -0,0 +1,217 @@
|
||||
const mysql = require('mysql2/promise');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const crypto = require('crypto');
|
||||
|
||||
const CONFIG_PATH = path.join(__dirname, '..', 'data', 'config.json');
|
||||
|
||||
let pool = null;
|
||||
|
||||
function getConfig() {
|
||||
if (fs.existsSync(CONFIG_PATH)) {
|
||||
return JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function saveConfig(cfg) {
|
||||
const dir = path.dirname(CONFIG_PATH);
|
||||
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(cfg, null, 2), 'utf-8');
|
||||
}
|
||||
|
||||
function isInstalled() {
|
||||
return fs.existsSync(CONFIG_PATH);
|
||||
}
|
||||
|
||||
async function getPool() {
|
||||
if (!pool) {
|
||||
const cfg = getConfig();
|
||||
if (!cfg) throw new Error('SYSTEM_NOT_INSTALLED');
|
||||
pool = mysql.createPool({
|
||||
host: cfg.db_host || 'localhost',
|
||||
port: cfg.db_port || 3306,
|
||||
user: cfg.db_user,
|
||||
password: cfg.db_pass,
|
||||
database: cfg.db_name,
|
||||
waitForConnections: true,
|
||||
connectionLimit: 10,
|
||||
charset: 'utf8mb4',
|
||||
timezone: '+08:00',
|
||||
});
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
|
||||
async function query(sql, params = []) {
|
||||
const p = await getPool();
|
||||
const [rows, fields] = await p.execute(sql, params);
|
||||
return rows;
|
||||
}
|
||||
|
||||
async function getRow(sql, params = []) {
|
||||
const rows = await query(sql, params);
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
async function initSchema(connection) {
|
||||
const db = connection || await getPool();
|
||||
|
||||
const createIfNotExists = async (tableName, sql) => {
|
||||
const [rows] = await db.execute(`SELECT COUNT(*) as c FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = ?`, [tableName]);
|
||||
if (rows[0].c > 0) return; // Table exists
|
||||
await db.execute(sql);
|
||||
};
|
||||
|
||||
await createIfNotExists('users', `CREATE TABLE users (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(50) NOT NULL UNIQUE,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
game_name VARCHAR(50) NOT NULL,
|
||||
game_uid VARCHAR(50) NOT NULL,
|
||||
role ENUM('owner','admin','player') NOT NULL DEFAULT 'player',
|
||||
active TINYINT(1) NOT NULL DEFAULT 0,
|
||||
email_verified TINYINT(1) NOT NULL DEFAULT 0,
|
||||
verify_token VARCHAR(255),
|
||||
verify_expires DATETIME,
|
||||
reset_token VARCHAR(255),
|
||||
reset_expires DATETIME,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('tickets', `CREATE TABLE tickets (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
type ENUM('report','suggestion','appeal') NOT NULL,
|
||||
title VARCHAR(100) NOT NULL,
|
||||
status ENUM('pending','processing','awaiting_info','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,
|
||||
target_game_name VARCHAR(50),
|
||||
target_game_uid VARCHAR(50),
|
||||
reason VARCHAR(500),
|
||||
description TEXT,
|
||||
assigned_to INT,
|
||||
claim_note TEXT,
|
||||
claimed_at DATETIME,
|
||||
tracking_token VARCHAR(255) UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_type (type),
|
||||
INDEX idx_status (status),
|
||||
INDEX idx_user (user_id),
|
||||
INDEX idx_tracking (tracking_token),
|
||||
INDEX idx_assigned (assigned_to)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('ticket_transfers', `CREATE TABLE ticket_transfers (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ticket_id INT NOT NULL,
|
||||
from_user_id INT NOT NULL,
|
||||
to_user_id INT NOT NULL,
|
||||
reason TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ticket (ticket_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('attachments', `CREATE TABLE attachments (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ticket_id INT NOT NULL,
|
||||
original_name VARCHAR(255) NOT NULL,
|
||||
stored_name VARCHAR(255) NOT NULL,
|
||||
mime_type VARCHAR(100) NOT NULL,
|
||||
size INT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ticket (ticket_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('responses', `CREATE TABLE responses (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
ticket_id INT NOT NULL,
|
||||
user_id INT,
|
||||
content TEXT NOT NULL,
|
||||
is_staff TINYINT(1) NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_ticket (ticket_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('email_templates', `CREATE TABLE email_templates (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
code VARCHAR(50) NOT NULL UNIQUE,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
subject VARCHAR(255) NOT NULL,
|
||||
body TEXT NOT NULL,
|
||||
description TEXT,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('notification_configs', `CREATE TABLE notification_configs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
type ENUM('webhook','email') NOT NULL DEFAULT 'webhook',
|
||||
webhook_url TEXT,
|
||||
events VARCHAR(500) NOT NULL DEFAULT 'all',
|
||||
active TINYINT(1) NOT NULL DEFAULT 1,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('settings', `CREATE TABLE settings (
|
||||
k VARCHAR(100) PRIMARY KEY,
|
||||
v TEXT NOT NULL,
|
||||
label VARCHAR(255),
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('captchas', `CREATE TABLE captchas (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
question VARCHAR(100) NOT NULL,
|
||||
answer VARCHAR(20) NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await createIfNotExists('audit_logs', `CREATE TABLE audit_logs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
user_id INT,
|
||||
username VARCHAR(50),
|
||||
action VARCHAR(100) NOT NULL,
|
||||
entity_type VARCHAR(50),
|
||||
entity_id INT,
|
||||
details TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
INDEX idx_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
|
||||
|
||||
await seedTemplates(db);
|
||||
}
|
||||
|
||||
async function seedTemplates(db) {
|
||||
const [rows] = await db.execute('SELECT COUNT(*) as c FROM email_templates');
|
||||
if (rows[0].c > 0) return;
|
||||
|
||||
const temps = [
|
||||
['verify_email', '邮箱验证', '【{{site_name}}】请验证您的邮箱',
|
||||
'<p>您好 <strong>{{username}}</strong>(游戏名: {{game_name}},UID: {{game_uid}}),</p><p>感谢注册!请点击以下链接验证邮箱:</p><p><a href="{{verify_link}}">{{verify_link}}</a></p><p>链接有效期24小时。</p><p>— {{site_name}}</p>',
|
||||
'变量: {{site_name}}, {{username}}, {{game_name}}, {{game_uid}}, {{verify_link}}'],
|
||||
['ticket_created', '工单创建通知', '【{{site_name}}】{{ticket_type}}已提交 #{{ticket_id}}',
|
||||
'<p><strong>{{reporter_game_name}}</strong>(UID: {{reporter_game_uid}}),您的{{ticket_type}}已提交。</p><p>工单编号: <strong>#{{ticket_id}}</strong></p><p>标题: {{ticket_title}}</p><p><a href="{{tracking_link}}">查看工单</a></p><p>— {{site_name}}</p>',
|
||||
'变量: {{site_name}}, {{reporter_game_name}}, {{ticket_type}}, {{ticket_id}}, {{ticket_title}}, {{tracking_link}}'],
|
||||
['ticket_updated', '工单状态更新', '【{{site_name}}】工单 #{{ticket_id}} 状态更新',
|
||||
'<p>工单 <strong>#{{ticket_id}}</strong> 状态已更新为 <strong>{{ticket_status}}</strong>。</p><p>标题: {{ticket_title}}</p><p><a href="{{tracking_link}}">查看详情</a></p><p>— {{site_name}}</p>',
|
||||
'变量: {{site_name}}, {{ticket_id}}, {{ticket_title}}, {{ticket_status}}, {{tracking_link}}'],
|
||||
['ticket_claimed', '工单被认领', '【{{site_name}}】工单 #{{ticket_id}} 已被认领',
|
||||
'<p>工单 <strong>#{{ticket_id}}</strong>({{ticket_title}})已被 <strong>{{assigned_to}}</strong> 认领。</p><p>— {{site_name}}</p>',
|
||||
'变量: {{site_name}}, {{ticket_id}}, {{ticket_title}}, {{assigned_to}}'],
|
||||
['ticket_transferred', '工单已转交', '【{{site_name}}】工单 #{{ticket_id}} 已转交',
|
||||
'<p>工单 <strong>#{{ticket_id}}</strong>({{ticket_title}})已由 {{from_user}} 转交给 {{to_user}}。</p><p>— {{site_name}}</p>',
|
||||
'变量: {{site_name}}, {{ticket_id}}, {{ticket_title}}, {{from_user}}, {{to_user}}'],
|
||||
];
|
||||
|
||||
for (const t of temps) {
|
||||
await db.execute('INSERT INTO email_templates(code, name, subject, body, description) VALUES (?,?,?,?,?)', t);
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getPool, query, getRow, initSchema, isInstalled, getConfig, saveConfig, CONFIG_PATH };
|
||||
Reference in New Issue
Block a user