186 lines
5.5 KiB
TypeScript
186 lines
5.5 KiB
TypeScript
import sqlite3 from 'sqlite3';
|
||
import path from 'path';
|
||
import fs from 'fs';
|
||
|
||
const DEFAULT_DB_DIR = path.join(process.cwd(), 'data');
|
||
const DEFAULT_DB_PATH = path.join(DEFAULT_DB_DIR, 'survey.db');
|
||
|
||
const DB_PATH = process.env.DB_PATH || DEFAULT_DB_PATH;
|
||
const DB_DIR = path.dirname(DB_PATH);
|
||
|
||
// 确保数据目录存在
|
||
if (!fs.existsSync(DB_DIR)) {
|
||
fs.mkdirSync(DB_DIR, { recursive: true });
|
||
}
|
||
|
||
// 创建数据库连接
|
||
export const db = new sqlite3.Database(DB_PATH, (err) => {
|
||
if (err) {
|
||
console.error('数据库连接失败:', err);
|
||
} else {
|
||
console.log('数据库连接成功');
|
||
}
|
||
});
|
||
|
||
// 启用外键约束
|
||
db.run('PRAGMA foreign_keys = ON');
|
||
|
||
const exec = (sql: string): Promise<void> => {
|
||
return new Promise((resolve, reject) => {
|
||
db.exec(sql, (err) => {
|
||
if (err) reject(err);
|
||
else resolve();
|
||
});
|
||
});
|
||
};
|
||
|
||
const tableExists = async (tableName: string): Promise<boolean> => {
|
||
const row = await get(
|
||
"SELECT name FROM sqlite_master WHERE type='table' AND name=?",
|
||
[tableName]
|
||
);
|
||
return Boolean(row);
|
||
};
|
||
|
||
const columnExists = async (tableName: string, columnName: string): Promise<boolean> => {
|
||
const columns = await query(`PRAGMA table_info(${tableName})`);
|
||
return columns.some((col: any) => col.name === columnName);
|
||
};
|
||
|
||
const ensureColumn = async (tableName: string, columnDefSql: string, columnName: string) => {
|
||
if (!(await columnExists(tableName, columnName))) {
|
||
await exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnDefSql}`);
|
||
}
|
||
};
|
||
|
||
const ensureTable = async (createTableSql: string) => {
|
||
await exec(createTableSql);
|
||
};
|
||
|
||
const ensureIndex = async (createIndexSql: string) => {
|
||
await exec(createIndexSql);
|
||
};
|
||
|
||
const migrateDatabase = async () => {
|
||
await ensureColumn('users', `password TEXT NOT NULL DEFAULT ''`, 'password');
|
||
|
||
await ensureColumn('questions', `category TEXT NOT NULL DEFAULT '通用'`, 'category');
|
||
await run(`UPDATE questions SET category = '通用' WHERE category IS NULL OR category = ''`);
|
||
|
||
await ensureTable(`
|
||
CREATE TABLE IF NOT EXISTS question_categories (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT UNIQUE NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
await run(
|
||
`INSERT OR IGNORE INTO question_categories (id, name) VALUES ('default', '通用')`
|
||
);
|
||
|
||
await ensureTable(`
|
||
CREATE TABLE IF NOT EXISTS exam_subjects (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT UNIQUE NOT NULL,
|
||
type_ratios TEXT NOT NULL,
|
||
category_ratios TEXT NOT NULL,
|
||
total_score INTEGER NOT NULL,
|
||
duration_minutes INTEGER NOT NULL DEFAULT 60,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||
);
|
||
`);
|
||
|
||
await ensureTable(`
|
||
CREATE TABLE IF NOT EXISTS exam_tasks (
|
||
id TEXT PRIMARY KEY,
|
||
name TEXT NOT NULL,
|
||
subject_id TEXT NOT NULL,
|
||
start_at DATETIME NOT NULL,
|
||
end_at DATETIME NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
FOREIGN KEY (subject_id) REFERENCES exam_subjects(id)
|
||
);
|
||
`);
|
||
|
||
await ensureTable(`
|
||
CREATE TABLE IF NOT EXISTS exam_task_users (
|
||
id TEXT PRIMARY KEY,
|
||
task_id TEXT NOT NULL,
|
||
user_id TEXT NOT NULL,
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
UNIQUE(task_id, user_id),
|
||
FOREIGN KEY (task_id) REFERENCES exam_tasks(id) ON DELETE CASCADE,
|
||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||
);
|
||
`);
|
||
|
||
if (await tableExists('quiz_records')) {
|
||
await ensureColumn('quiz_records', `subject_id TEXT`, 'subject_id');
|
||
await ensureColumn('quiz_records', `task_id TEXT`, 'task_id');
|
||
}
|
||
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_questions_category ON questions(category);`);
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_exam_tasks_subject_id ON exam_tasks(subject_id);`);
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_exam_task_users_task_id ON exam_task_users(task_id);`);
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_exam_task_users_user_id ON exam_task_users(user_id);`);
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_quiz_records_subject_id ON quiz_records(subject_id);`);
|
||
await ensureIndex(`CREATE INDEX IF NOT EXISTS idx_quiz_records_task_id ON quiz_records(task_id);`);
|
||
};
|
||
|
||
// 数据库初始化函数
|
||
export const initDatabase = async () => {
|
||
const initSQL = fs.readFileSync(path.join(process.cwd(), 'api', 'database', 'init.sql'), 'utf8');
|
||
|
||
const hasUsersTable = await tableExists('users');
|
||
if (!hasUsersTable) {
|
||
await exec(initSQL);
|
||
console.log('数据库初始化成功');
|
||
} else {
|
||
console.log('数据库已初始化,准备执行迁移检查');
|
||
}
|
||
|
||
await migrateDatabase();
|
||
};
|
||
|
||
// 数据库查询工具函数
|
||
export const query = (sql: string, params: any[] = []): Promise<any[]> => {
|
||
return new Promise((resolve, reject) => {
|
||
db.all(sql, params, (err, rows) => {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
resolve(rows);
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
// all函数是query函数的别名,用于向后兼容
|
||
export const all = query;
|
||
|
||
export const run = (sql: string, params: any[] = []): Promise<{ id: string }> => {
|
||
return new Promise((resolve, reject) => {
|
||
db.run(sql, params, function(err) {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
resolve({ id: this.lastID.toString() });
|
||
}
|
||
});
|
||
});
|
||
};
|
||
|
||
export const get = (sql: string, params: any[] = []): Promise<any> => {
|
||
return new Promise((resolve, reject) => {
|
||
db.get(sql, params, (err, row) => {
|
||
if (err) {
|
||
reject(err);
|
||
} else {
|
||
resolve(row);
|
||
}
|
||
});
|
||
});
|
||
};
|