Files
MC_Report/backend/db.js
canglan 8c5ce78fd0 chore: add AGPLv3 copyright header to all source files
- 52 JS files (backend + public/js): header with
  Copyright (C) 2026 Sea Network Technology Studio
  Author: CangLan <admin@sea-studio.top>
  + AGPLv3 notice
- idempotent (skips if header present), all syntax-checked
2026-08-19 20:27:05 +08:00

482 lines
24 KiB
JavaScript
Raw Permalink 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.
/*
* MC Report System
* Copyright (C) 2026 Sea Network Technology Studio
* Author: CangLan <admin@sea-studio.top>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
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;
let _configCache = null;
let _configMtime = 0;
function getConfig() {
try {
const stat = fs.statSync(CONFIG_PATH);
if (_configCache && stat.mtimeMs === _configMtime) return _configCache;
_configCache = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8'));
_configMtime = stat.mtimeMs;
return _configCache;
} catch { 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',
});
await migrateAdditions(pool);
}
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 DEFAULT '',
role ENUM('owner','admin','player') NOT NULL DEFAULT 'player',
active TINYINT(1) NOT NULL DEFAULT 0,
email_verified TINYINT(1) NOT NULL DEFAULT 0,
source VARCHAR(20) NOT NULL DEFAULT '',
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('user_identities', `CREATE TABLE user_identities (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
source VARCHAR(20) NOT NULL,
game_name VARCHAR(50) NOT NULL,
game_uid VARCHAR(50) NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_source (user_id, source),
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('sources', `CREATE TABLE sources (
id INT AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(20) NOT NULL UNIQUE,
label VARCHAR(50) NOT NULL,
enabled TINYINT(1) NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('api_sessions', `CREATE TABLE api_sessions (
id INT AUTO_INCREMENT PRIMARY KEY,
client_id VARCHAR(64) NOT NULL,
session_token VARCHAR(128) NOT NULL UNIQUE,
expires_at DATETIME NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_client (client_id),
INDEX idx_expires (expires_at)
) 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','result_appeal') NOT NULL,
server_name VARCHAR(100) NOT NULL DEFAULT '',
title VARCHAR(100) NOT NULL,
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 DEFAULT '',
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,
is_admin_complaint TINYINT(1) NOT NULL DEFAULT 0,
parent_ticket_id INT,
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('polls', `CREATE TABLE 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`);
await createIfNotExists('poll_votes', `CREATE TABLE 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`);
await createIfNotExists('feature_items', `CREATE TABLE feature_items (
id INT AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(200) NOT NULL,
description TEXT,
group_name VARCHAR(100) NOT NULL DEFAULT '',
server_name VARCHAR(100) NOT NULL DEFAULT '',
status ENUM('pending','planned','done') NOT NULL DEFAULT 'pending',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('feature_votes', `CREATE TABLE feature_votes (
id INT AUTO_INCREMENT PRIMARY KEY,
item_id INT NOT NULL,
user_id INT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_item_user (item_id, user_id),
INDEX idx_item (item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('feature_comments', `CREATE TABLE feature_comments (
id INT AUTO_INCREMENT PRIMARY KEY,
item_id INT NOT NULL,
user_id INT NOT NULL,
content TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_item (item_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('server_groups', `CREATE TABLE server_groups (
id INT AUTO_INCREMENT PRIMARY KEY,
group_name VARCHAR(100) NOT NULL,
server_name VARCHAR(100) NOT NULL,
alias VARCHAR(100) NOT NULL DEFAULT '',
UNIQUE KEY uk_group_server (group_name, server_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('api_clients', `CREATE TABLE api_clients (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(100) NOT NULL,
client_id VARCHAR(64) NOT NULL UNIQUE,
secret_hash VARCHAR(255) NOT NULL,
active TINYINT(1) NOT NULL DEFAULT 1,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_active (active)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('user_servers', `CREATE TABLE 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`);
await createIfNotExists('bans', `CREATE TABLE bans (
id INT AUTO_INCREMENT PRIMARY KEY,
player_name VARCHAR(50) NOT NULL,
player_uid VARCHAR(50),
server_name VARCHAR(100) NOT NULL DEFAULT '',
source VARCHAR(10) DEFAULT '',
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,
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 createIfNotExists('email_logs', `CREATE TABLE email_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
to_email VARCHAR(100) NOT NULL,
template_code VARCHAR(50) NOT NULL DEFAULT '',
subject VARCHAR(255) NOT NULL DEFAULT '',
status ENUM('sent','failed') NOT NULL,
error TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (to_email),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await createIfNotExists('system_logs', `CREATE TABLE system_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
level ENUM('info','warn','error') NOT NULL DEFAULT 'info',
source VARCHAR(50) NOT NULL DEFAULT '',
message TEXT NOT NULL,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_level (level),
INDEX idx_source (source),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await seedTemplates(db);
await migrateAdditions(db);
}
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 {}
try { await db.execute("ALTER TABLE users ADD COLUMN source VARCHAR(20) NOT NULL DEFAULT ''"); } 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), source VARCHAR(10) DEFAULT '', 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("ALTER TABLE bans ADD COLUMN source VARCHAR(10) DEFAULT ''"); } catch {}
// 多来源身份: 建表 + 从 users 主身份回填(幂等)
try {
await db.execute(`CREATE TABLE IF NOT EXISTS user_identities (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
source ENUM('netease','skin') NOT NULL,
game_name VARCHAR(50) NOT NULL,
game_uid VARCHAR(50) NOT NULL DEFAULT '',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_user_source (user_id, source),
INDEX idx_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await db.execute(`INSERT IGNORE INTO user_identities (user_id, source, game_name, game_uid)
SELECT id, source, game_name, game_uid FROM users WHERE game_name != ''`);
} catch {}
// 日志表(邮件日志 + 系统日志)
try {
await db.execute(`CREATE TABLE IF NOT EXISTS email_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
to_email VARCHAR(100) NOT NULL,
template_code VARCHAR(50) NOT NULL DEFAULT '',
subject VARCHAR(255) NOT NULL DEFAULT '',
status ENUM('sent','failed') NOT NULL,
error TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_email (to_email),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`);
await db.execute(`CREATE TABLE IF NOT EXISTS system_logs (
id INT AUTO_INCREMENT PRIMARY KEY,
level ENUM('info','warn','error') NOT NULL DEFAULT 'info',
source VARCHAR(50) NOT NULL DEFAULT '',
message TEXT NOT NULL,
details TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_level (level),
INDEX idx_source (source),
INDEX idx_created (created_at)
) 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 {}
try { await db.execute(`CREATE TABLE IF NOT EXISTS feature_items (id INT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, description TEXT, group_name VARCHAR(100) NOT NULL DEFAULT '', server_name VARCHAR(100) NOT NULL DEFAULT '', status ENUM('pending','planned','done') NOT NULL DEFAULT 'pending', created_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute(`CREATE TABLE IF NOT EXISTS feature_votes (id INT AUTO_INCREMENT PRIMARY KEY, item_id INT NOT NULL, user_id INT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_item_user (item_id, user_id), INDEX idx_item (item_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute(`CREATE TABLE IF NOT EXISTS feature_comments (id INT AUTO_INCREMENT PRIMARY KEY, item_id INT NOT NULL, user_id INT NOT NULL, content TEXT NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_item (item_id)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute(`CREATE TABLE IF NOT EXISTS server_groups (id INT AUTO_INCREMENT PRIMARY KEY, group_name VARCHAR(100) NOT NULL, server_name VARCHAR(100) NOT NULL, UNIQUE KEY uk_group_server (group_name, server_name)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute("ALTER TABLE server_groups ADD COLUMN alias VARCHAR(100) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute(`CREATE TABLE IF NOT EXISTS api_clients (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(100) NOT NULL, client_id VARCHAR(64) NOT NULL UNIQUE, secret_hash VARCHAR(255) NOT NULL, active TINYINT(1) NOT NULL DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_active (active)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try { await db.execute("ALTER TABLE tickets ADD COLUMN server_name VARCHAR(100) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE bans ADD COLUMN server_name VARCHAR(100) NOT NULL DEFAULT ''"); } catch {}
// ---- 动态来源: sources 表 + users/user_identities.source 改 VARCHAR + game_uid 可空 ----
try { await db.execute(`CREATE TABLE IF NOT EXISTS sources (id INT AUTO_INCREMENT PRIMARY KEY, code VARCHAR(20) NOT NULL UNIQUE, label VARCHAR(50) NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, sort_order INT NOT NULL DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
try {
// 预置来源(幂等), 老数据来源保持兼容
await db.execute("INSERT IGNORE INTO sources(code, label, sort_order) VALUES ('netease','网易端',1), ('skin','皮肤站',2)");
} catch {}
try { await db.execute("ALTER TABLE users MODIFY source VARCHAR(20) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE users MODIFY game_uid VARCHAR(50) NOT NULL DEFAULT ''"); } catch {}
try { await db.execute("ALTER TABLE user_identities MODIFY source VARCHAR(20) NOT NULL"); } catch {}
try { await db.execute("ALTER TABLE tickets MODIFY reporter_game_uid VARCHAR(50) NOT NULL DEFAULT ''"); } catch {}
// ---- API SESSION 表 ----
try { await db.execute(`CREATE TABLE IF NOT EXISTS api_sessions (id INT AUTO_INCREMENT PRIMARY KEY, client_id VARCHAR(64) NOT NULL, session_token VARCHAR(128) NOT NULL UNIQUE, expires_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_client (client_id), INDEX idx_expires (expires_at)) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4`); } catch {}
}
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}}'],
['email_code', '邮箱验证码', '【{{site_name}}】您的邮箱验证码',
'<p>您好,</p><p>您的邮箱验证码为:</p><p style="font-size:24px;font-weight:bold;letter-spacing:4px">{{code}}</p><p>验证码10分钟内有效。如非本人操作请忽略此邮件。</p><p>— {{site_name}}</p>',
'变量: {{site_name}}, {{code}}'],
['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}}'],
['reset_password', '密码重置', '【{{site_name}}】密码重置请求',
'<p>您好 <strong>{{username}}</strong></p><p>我们收到了您的密码重置请求。请点击以下链接设置新密码:</p><p><a href="{{reset_link}}">{{reset_link}}</a></p><p>链接1小时内有效。如非本人操作请忽略此邮件。</p><p>— {{site_name}}</p>',
'变量: {{site_name}}, {{username}}, {{reset_link}}'],
];
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 };