60 lines
5.6 KiB
TypeScript
60 lines
5.6 KiB
TypeScript
import { getDb } from './db'
|
||
import bcrypt from 'bcryptjs'
|
||
|
||
export function initDatabase(): void {
|
||
const db = getDb()
|
||
const schema = [
|
||
"CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, password_hash TEXT NOT NULL, display_name TEXT NOT NULL, email TEXT, role TEXT NOT NULL DEFAULT 'viewer', is_active INTEGER NOT NULL DEFAULT 1, created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS roles (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, display_name TEXT NOT NULL, permissions TEXT NOT NULL DEFAULT '[]', created_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS sessions (id TEXT PRIMARY KEY, user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE, expires_at TEXT NOT NULL, created_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS tickets (id INTEGER PRIMARY KEY, device_ip TEXT, device_sn TEXT, device_name TEXT, content TEXT, assign_time TEXT, close_time TEXT, duration_minutes INTEGER, availability REAL, process_summary TEXT, conclusion TEXT, fault_category TEXT, fault_subcategory TEXT, parts_replaced TEXT, parts_name TEXT, current_status TEXT NOT NULL DEFAULT 'open', counted_in_sla INTEGER NOT NULL DEFAULT 1, responsibility TEXT, created_by INTEGER REFERENCES users(id), updated_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now')), updated_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS ticket_steps (id INTEGER PRIMARY KEY AUTOINCREMENT, ticket_id INTEGER NOT NULL REFERENCES tickets(id) ON DELETE CASCADE, step_order INTEGER NOT NULL, time_node TEXT, handler TEXT, description TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS reports (id INTEGER PRIMARY KEY AUTOINCREMENT, report_type TEXT NOT NULL, period_start TEXT, period_end TEXT, format TEXT NOT NULL DEFAULT 'pdf', file_path TEXT, file_name TEXT, status TEXT NOT NULL DEFAULT 'pending', error_message TEXT, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS audit_logs (id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER REFERENCES users(id), action TEXT NOT NULL, entity_type TEXT NOT NULL, entity_id INTEGER, details TEXT, ip_address TEXT, created_at TEXT NOT NULL DEFAULT (datetime('now')));",
|
||
"CREATE TABLE IF NOT EXISTS api_keys (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, key_hash TEXT NOT NULL, permissions TEXT NOT NULL DEFAULT '[\"tickets:read\"]', last_used_at TEXT, expires_at TEXT, is_active INTEGER NOT NULL DEFAULT 1, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now')));"
|
||
]
|
||
for (const sql of schema) db.exec(sql)
|
||
|
||
// 迁移:添加 parts_name 列
|
||
try { db.exec('ALTER TABLE tickets ADD COLUMN parts_name TEXT') } catch { /* 列已存在 */ }
|
||
|
||
// 迁移:添加 ticket_type 列
|
||
try { db.exec('ALTER TABLE tickets ADD COLUMN ticket_type TEXT') } catch { /* 列已存在 */ }
|
||
|
||
// 迁移:将已有 OEM 分类迁移到 ticket_type
|
||
try {
|
||
db.prepare("UPDATE tickets SET ticket_type = 'OEM诊断' WHERE fault_category = 'OEM诊断' AND ticket_type IS NULL").run()
|
||
db.prepare("UPDATE tickets SET ticket_type = 'OEM维修' WHERE fault_category = 'OEM维修' AND ticket_type IS NULL").run()
|
||
db.prepare("UPDATE tickets SET ticket_type = 'OEM诊断' WHERE ticket_type IS NULL AND fault_category = '无故障'").run()
|
||
db.prepare("UPDATE tickets SET ticket_type = 'OEM维修' WHERE ticket_type IS NULL AND fault_category IN ('硬件故障', '网络故障', '存储故障', '电源故障')").run()
|
||
} catch { /* 迁移失败则保持原样 */ }
|
||
|
||
// 迁移:metadata 列(报告元数据 JSON)
|
||
try { db.exec('ALTER TABLE reports ADD COLUMN metadata TEXT') } catch { /* 已存在 */ }
|
||
|
||
// 迁移:修正 reports 表 created_at 为北京时间(UTC+8),并修正 format 默认值
|
||
try {
|
||
const info = db.prepare('SELECT sql FROM sqlite_master WHERE type=? AND name=?').get('table', 'reports') as any
|
||
if (info && info.sql && !info.sql.includes("+8 hours")) {
|
||
db.exec('DROP TABLE IF EXISTS reports')
|
||
db.exec("CREATE TABLE reports (id INTEGER PRIMARY KEY AUTOINCREMENT, report_type TEXT NOT NULL, period_start TEXT, period_end TEXT, format TEXT NOT NULL DEFAULT 'docx', file_path TEXT, file_name TEXT, status TEXT NOT NULL DEFAULT 'pending', error_message TEXT, created_by INTEGER REFERENCES users(id), created_at TEXT NOT NULL DEFAULT (datetime('now', '+8 hours')), metadata TEXT)")
|
||
}
|
||
} catch { /* 迁移失败则保持原样 */ }
|
||
|
||
const existing = db.prepare('SELECT id FROM users WHERE username = ?').get('admin')
|
||
if (!existing) {
|
||
const defaultPassword = process.env.ADMIN_PASSWORD || 'admin123'
|
||
const hash = bcrypt.hashSync(defaultPassword, 10)
|
||
db.prepare('INSERT INTO users (username, password_hash, display_name, role) VALUES (?, ?, ?, ?)').run('admin', hash, '系统管理员', 'admin')
|
||
}
|
||
const roles = [
|
||
{ name: 'admin', display_name: '管理员', permissions: '["*"]' },
|
||
{ name: 'operator', display_name: '运维人员', permissions: '["tickets:read","tickets:write","reports:read"]' },
|
||
{ name: 'viewer', display_name: '查看者', permissions: '["tickets:read","reports:read"]' },
|
||
]
|
||
for (const r of roles) {
|
||
const ex = db.prepare('SELECT id FROM roles WHERE name = ?').get(r.name)
|
||
if (!ex) db.prepare('INSERT INTO roles (name, display_name, permissions) VALUES (?, ?, ?)').run(r.name, r.display_name, r.permissions)
|
||
}
|
||
}
|