Files
MC_Report/backend/db.js

218 lines
8.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 UNIQUE,
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 };