diff --git a/.gitignore b/.gitignore index c148942..eade95a 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,6 @@ pnpm-debug.log* *.sw? #Electron-builder output -/dist_electron \ No newline at end of file +/dist_electron + +/high_version \ No newline at end of file diff --git a/data/.gitignore b/data/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/data/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/package.json b/package.json index e795f95..1133d88 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "start": "vue-cli-service electron:serve", "serve": "vue-cli-service serve", "build": "vue-cli-service build", "electron:build": "vue-cli-service electron:build", @@ -12,7 +13,7 @@ "rebuild-deps": "electron-builder install-app-deps", "rebuild-sqlite3": "npm_config_runtime=electron npm_config_target=11.5.0 npm_config_disturl=https://electronjs.org/headers npm_config_build_from_source=true node-pre-gyp rebuild --target=11.5.0 --runtime=electron --fallback-to-build --directory=./node_modules/sqlite3/ --dist-url=https://electronjs.org/headers" }, - "main": "background.js", + "main": "background/main.js", "dependencies": { "@fortawesome/fontawesome-svg-core": "1.2.36", "@fortawesome/free-brands-svg-icons": "5.15.4", diff --git a/src/background/db/index.js b/src/background/db/index.js new file mode 100644 index 0000000..6770839 --- /dev/null +++ b/src/background/db/index.js @@ -0,0 +1,423 @@ +const { getSystemDbPath, getUserDbPath } = require('./path.js'); +const { systemSchema, userSchema, defaultData } = require('./schema.js'); +const { openDatabase, batchInsert } = require('./utils.js'); +const bcrypt = require('bcryptjs'); + +// 数据库连接池 +const dbConnections = new Map(); + +// 获取数据库连接 +exports.getDbConnection = async function getDbConnection(dbPath) { + if (dbConnections.has(dbPath)) { + console.log(`使用现有数据库连接: ${dbPath}`); + return dbConnections.get(dbPath); + } + + const db = await openDatabase(dbPath); + dbConnections.set(dbPath, db); + return db; +}; + +// 关闭所有数据库连接 +exports.closeAllConnections = function closeAllConnections() { + dbConnections.forEach((db, path) => { + try { + db.close(); + console.log(`关闭数据库连接: ${path}`); + } catch (error) { + console.error(`关闭数据库连接失败: ${path}`, error); + } + }); + dbConnections.clear(); +}; + +// 检查数据库是否已初始化 +exports.checkDatabaseInitialized = async function checkDatabaseInitialized() { + try { + console.log('开始检查数据库初始化状态...'); + const systemDb = await exports.getDbConnection(getSystemDbPath()); + console.log('成功打开系统数据库'); + + const result = await systemDb.getAsync('SELECT value FROM config WHERE key = ?', ['initialized']); + console.log('查询初始化状态结果:', result); + + const isInitialized = result && result.value === 'true'; + console.log('数据库初始化状态:', isInitialized ? '已初始化' : '未初始化'); + return isInitialized; + } catch (error) { + console.error('检查数据库初始化状态失败:', error); + return false; + } +}; + +// 初始化系统数据库 +async function initializeSystemDatabase() { + console.log('开始初始化系统数据库...'); + const systemDbPath = getSystemDbPath(); + const systemDb = await exports.getDbConnection(systemDbPath); + + // 记录成功和失败的操作 + const results = { success: [], failed: [] }; + + // 创建表结构 + console.log('开始创建系统数据库表结构...'); + + // 创建config表 + try { + await systemDb.execAsync(systemSchema.config.trim()); + console.log('创建 config 表成功'); + results.success.push('创建 config 表'); + } catch (error) { + console.error('创建 config 表失败:', error); + results.failed.push({ operation: '创建 config 表', error: error.message }); + } + + // 创建dict_types表 + try { + await systemDb.execAsync(systemSchema.dictTypes.trim()); + console.log('创建 dict_types 表成功'); + results.success.push('创建 dict_types 表'); + } catch (error) { + console.error('创建 dict_types 表失败:', error); + results.failed.push({ operation: '创建 dict_types 表', error: error.message }); + } + + // 创建dict_items表 + try { + await systemDb.execAsync(systemSchema.dictItems.trim()); + console.log('创建 dict_items 表成功'); + results.success.push('创建 dict_items 表'); + } catch (error) { + console.error('创建 dict_items 表失败:', error); + results.failed.push({ operation: '创建 dict_items 表', error: error.message }); + } + + // 创建questions表 + try { + await systemDb.execAsync(systemSchema.questions.trim()); + console.log('创建 questions 表成功'); + results.success.push('创建 questions 表'); + } catch (error) { + console.error('创建 questions 表失败:', error); + results.failed.push({ operation: '创建 questions 表', error: error.message }); + } + + // 创建question_datasets表 + try { + await systemDb.execAsync(systemSchema.questionDatasets.trim()); + console.log('创建 question_datasets 表成功'); + results.success.push('创建 question_datasets 表'); + } catch (error) { + console.error('创建 question_datasets 表失败:', error); + results.failed.push({ operation: '创建 question_datasets 表', error: error.message }); + } + + // 创建question_images表 + try { + await systemDb.execAsync(systemSchema.questionImages.trim()); + console.log('创建 question_images 表成功'); + results.success.push('创建 question_images 表'); + } catch (error) { + console.error('创建 question_images 表失败:', error); + results.failed.push({ operation: '创建 question_images 表', error: error.message }); + } + + // 创建question_fill_table表 + try { + await systemDb.execAsync(systemSchema.questionFillTable.trim()); + console.log('创建 question_fill_table 表成功'); + results.success.push('创建 question_fill_table 表'); + } catch (error) { + console.error('创建 question_fill_table 表失败:', error); + results.failed.push({ operation: '创建 question_fill_table 表', error: error.message }); + } + + // 创建question_fill_table_blanks表 + try { + await systemDb.execAsync(systemSchema.questionFillTableBlanks.trim()); + console.log('创建 question_fill_table_blanks 表成功'); + results.success.push('创建 question_fill_table_blanks 表'); + } catch (error) { + console.error('创建 question_fill_table_blanks 表失败:', error); + results.failed.push({ operation: '创建 question_fill_table_blanks 表', error: error.message }); + } + + // 创建question_choices表 + try { + await systemDb.execAsync(systemSchema.questionChoices.trim()); + console.log('创建 question_choices 表成功'); + results.success.push('创建 question_choices 表'); + } catch (error) { + console.error('创建 question_choices 表失败:', error); + results.failed.push({ operation: '创建 question_choices 表', error: error.message }); + } + + // 创建question_fill_blanks表 + try { + await systemDb.execAsync(systemSchema.questionFillBlanks.trim()); + console.log('创建 question_fill_blanks 表成功'); + results.success.push('创建 question_fill_blanks 表'); + } catch (error) { + console.error('创建 question_fill_blanks 表失败:', error); + results.failed.push({ operation: '创建 question_fill_blanks 表', error: error.message }); + } + + // 创建question_judge表 + try { + await systemDb.execAsync(systemSchema.questionJudge.trim()); + console.log('创建 question_judge 表成功'); + results.success.push('创建 question_judge 表'); + } catch (error) { + console.error('创建 question_judge 表失败:', error); + results.failed.push({ operation: '创建 question_judge 表', error: error.message }); + } + + // 创建question_short表 + try { + await systemDb.execAsync(systemSchema.questionShort.trim()); + console.log('创建 question_short 表成功'); + results.success.push('创建 question_short 表'); + } catch (error) { + console.error('创建 question_short 表失败:', error); + results.failed.push({ operation: '创建 question_short 表', error: error.message }); + } + + // 创建exam表 + try { + await systemDb.execAsync(systemSchema.exam.trim()); + console.log('创建 exam 表成功'); + results.success.push('创建 exam 表'); + } catch (error) { + console.error('创建 exam 表失败:', error); + results.failed.push({ operation: '创建 exam 表', error: error.message }); + } + + // 创建examinee表 + try { + await systemDb.execAsync(systemSchema.examinee.trim()); + console.log('创建 examinee 表成功'); + results.success.push('创建 examinee 表'); + } catch (error) { + console.error('创建 examinee 表失败:', error); + results.failed.push({ operation: '创建 examinee 表', error: error.message }); + } + + // 插入默认数据 + console.log('开始插入默认数据...'); + + // 处理密码哈希 + try { + const plainPassword = defaultData.config.find(item => item.key === 'admin_password').value; + const hashedPassword = await bcrypt.hash(plainPassword, 10); + + // 更新密码为哈希值 + const configData = defaultData.config.map(item => { + if (item.key === 'admin_password') { + return { ...item, value: hashedPassword }; + } + return item; + }); + + // 插入config表数据 + try { + await batchInsert(systemDb, 'config', configData); + console.log('插入 config 表数据成功'); + results.success.push('插入 config 表数据'); + } catch (error) { + console.error('插入 config 表数据失败:', error); + results.failed.push({ operation: '插入 config 表数据', error: error.message }); + } + } catch (error) { + console.error('处理密码哈希失败:', error); + results.failed.push({ operation: '处理密码哈希', error: error.message }); + } + + // 插入dict_types表数据 + try { + await batchInsert(systemDb, 'dict_types', defaultData.dictTypes); + console.log('插入 dict_types 表数据成功'); + results.success.push('插入 dict_types 表数据'); + } catch (error) { + console.error('插入 dict_types 表数据失败:', error); + results.failed.push({ operation: '插入 dict_types 表数据', error: error.message }); + } + + // 插入dict_items表数据 + try { + await batchInsert(systemDb, 'dict_items', defaultData.dictItems); + console.log('插入 dict_items 表数据成功'); + results.success.push('插入 dict_items 表数据'); + } catch (error) { + console.error('插入 dict_items 表数据失败:', error); + results.failed.push({ operation: '插入 dict_items 表数据', error: error.message }); + } + + console.log('系统数据库初始化结果:'); + console.log('成功操作:', results.success); + console.log('失败操作:', results.failed); + + // 如果有失败操作,抛出错误 + if (results.failed.length > 0) { + console.log(`系统数据库初始化有 ${results.failed.length} 个操作失败,请查看日志`); + // 输出详细的失败信息 + results.failed.forEach((item, index) => { + console.log(`${index + 1}. ${item.operation} 失败: ${item.error}`); + }); + } + + return true; +} + +// 初始化用户数据库 +async function initializeUserDatabase() { + console.log('开始初始化用户数据库...'); + const userDbPath = getUserDbPath(); + const userDb = await exports.getDbConnection(userDbPath); + + // 记录成功和失败的操作 + const results = { success: [], failed: [] }; + + // 创建表结构 + console.log('开始创建用户数据库表结构...'); + + // 创建examinee表 + try { + await userDb.execAsync(userSchema.examinee.trim()); + console.log('创建 examinee 表成功'); + results.success.push('创建 examinee 表'); + } catch (error) { + console.error('创建 examinee 表失败:', error); + results.failed.push({ operation: '创建 examinee 表', error: error.message }); + } + + // 创建examinee_papers表 + try { + await userDb.execAsync(userSchema.examinee_papers.trim()); + console.log('创建 examinee_papers 表成功'); + results.success.push('创建 examinee_papers 表'); + } catch (error) { + console.error('创建 examinee_papers 表失败:', error); + results.failed.push({ operation: '创建 examinee_papers 表', error: error.message }); + } + + // 创建paper_questions表 + try { + await userDb.execAsync(userSchema.paper_questions.trim()); + console.log('创建 paper_questions 表成功'); + results.success.push('创建 paper_questions 表'); + } catch (error) { + console.error('创建 paper_questions 表失败:', error); + results.failed.push({ operation: '创建 paper_questions 表', error: error.message }); + } + + // 创建question_datasets表 + try { + await userDb.execAsync(userSchema.question_datasets.trim()); + console.log('创建 question_datasets 表成功'); + results.success.push('创建 question_datasets 表'); + } catch (error) { + console.error('创建 question_datasets 表失败:', error); + results.failed.push({ operation: '创建 question_datasets 表', error: error.message }); + } + + // 创建question_images表 + try { + await userDb.execAsync(userSchema.question_images.trim()); + console.log('创建 question_images 表成功'); + results.success.push('创建 question_images 表'); + } catch (error) { + console.error('创建 question_images 表失败:', error); + results.failed.push({ operation: '创建 question_images 表', error: error.message }); + } + + // 创建question_choices表 + try { + await userDb.execAsync(userSchema.question_choices.trim()); + console.log('创建 question_choices 表成功'); + results.success.push('创建 question_choices 表'); + } catch (error) { + console.error('创建 question_choices 表失败:', error); + results.failed.push({ operation: '创建 question_choices 表', error: error.message }); + } + + // 创建question_fill_blanks表 + try { + await userDb.execAsync(userSchema.question_fill_blanks.trim()); + console.log('创建 question_fill_blanks 表成功'); + results.success.push('创建 question_fill_blanks 表'); + } catch (error) { + console.error('创建 question_fill_blanks 表失败:', error); + results.failed.push({ operation: '创建 question_fill_blanks 表', error: error.message }); + } + + console.log('用户数据库初始化结果:'); + console.log('成功操作:', results.success); + console.log('失败操作:', results.failed); + + // 如果有失败操作,仅打印错误信息,不抛出异常 + if (results.failed.length > 0) { + console.error(`用户数据库初始化有 ${results.failed.length} 个操作失败,请查看日志`); + // 输出详细的失败信息 + results.failed.forEach((item, index) => { + console.error(`${index + 1}. ${item.operation} 失败: ${item.error}`); + }); + } + + return true; +} + +// 初始化数据库 +exports.initializeDatabase = async function initializeDatabase() { + try { + console.log('开始初始化数据库...'); + + // 确保只有一个初始化请求在执行 + if (global.isInitializing) { + console.log('数据库初始化已在进行中,等待完成...'); + while (global.isInitializing) { + await new Promise(resolve => setTimeout(resolve, 100)); + } + return global.initResult; + } + + global.isInitializing = true; + global.initResult = false; + + // 先初始化系统数据库 + console.log('开始初始化系统数据库...'); + const systemResult = await initializeSystemDatabase(); + console.log('系统数据库初始化结果:', systemResult ? '成功' : '失败'); + + if (!systemResult) { + throw new Error('系统数据库初始化失败'); + } + + // 再初始化用户数据库 + console.log('开始初始化用户数据库...'); + const userResult = await initializeUserDatabase(); + console.log('用户数据库初始化结果:', userResult ? '成功' : '失败'); + + if (!userResult) { + throw new Error('用户数据库初始化失败'); + } + + // 更新初始化状态 + console.log('更新数据库初始化状态...'); + const systemDb = await exports.getDbConnection(getSystemDbPath()); + await systemDb.runAsync('UPDATE config SET value = ? WHERE key = ?', ['true', 'initialized']); + console.log('数据库初始化状态更新成功'); + + console.log('数据库整体初始化成功'); + global.initResult = true; + return { success: true }; + } catch (error) { + console.error('数据库初始化失败:', error); + global.initResult = false; + return { success: false, error: error.message }; + } finally { + global.isInitializing = false; + } +}; + +// 应用退出时关闭所有连接 +process.on('exit', exports.closeAllConnections); \ No newline at end of file diff --git a/src/background/db/path.js b/src/background/db/path.js new file mode 100644 index 0000000..845f42f --- /dev/null +++ b/src/background/db/path.js @@ -0,0 +1,53 @@ +const path = require('path'); +const fs = require('fs'); +const { app } = require('electron'); + +// 判断是否为开发环境 +const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged; + +// 检测是否为便携模式 +let isPortable = false; +const exePath = app.getPath('exe'); +const appDir = path.dirname(exePath); +const portableFlagPath = path.join(appDir, 'portable.txt'); + +// 如果应用目录中存在portable.txt文件,则启用便携模式 +if (!isDev && fs.existsSync(portableFlagPath)) { + isPortable = true; + console.log('启用便携模式'); +} + +// 根据模式选择数据目录 +let dataDir; +if (isDev) { + // 开发环境:数据存储在工程的data目录 + dataDir = path.join(process.cwd(), 'data'); +} else if (isPortable) { + // 便携模式:数据存储在应用目录下的data文件夹 + dataDir = path.join(appDir, 'data'); +} else { + // 非便携模式:数据存储在应用同级的data目录 + dataDir = path.join(appDir, 'data'); +} + +// 确保数据目录存在 +if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir, { recursive: true }); + console.log(`创建数据目录: ${dataDir}`); +} + +// 系统数据库路径 +function getSystemDbPath() { + return path.join(dataDir, 'system.db'); +} + +// 用户数据库路径 +function getUserDbPath() { + return path.join(dataDir, 'user.db'); +} + +// 导出函数供其他模块使用 +module.exports = { + getSystemDbPath, + getUserDbPath +}; \ No newline at end of file diff --git a/src/background/db/schema.js b/src/background/db/schema.js new file mode 100644 index 0000000..2f08a6d --- /dev/null +++ b/src/background/db/schema.js @@ -0,0 +1,411 @@ +// 执行SQL语句的工具函数 +const runAsync = (db, sql, params = []) => { + return new Promise((resolve, reject) => { + db.run(sql, params, function (err) { + if (err) reject(err); + else resolve(this); + }); + }); +}; + +// 系统数据库表结构 +systemSchema = { + config: ` + CREATE TABLE IF NOT EXISTS config ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + key TEXT NOT NULL UNIQUE, + value TEXT NOT NULL, + protected INTEGER DEFAULT 0 + ); + `, + dictTypes: ` + CREATE TABLE IF NOT EXISTS dict_types ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_code TEXT NOT NULL UNIQUE, + type_name TEXT NOT NULL, + description TEXT, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + dictItems: ` + CREATE TABLE IF NOT EXISTS dict_items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + type_code TEXT NOT NULL, + item_code TEXT NOT NULL, + item_name TEXT NOT NULL, + item_description TEXT DEFAULT '', + parent_code TEXT, + is_active BOOLEAN DEFAULT 1, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + UNIQUE(type_code, item_code), + FOREIGN KEY (type_code) REFERENCES dict_types(type_code) + ); + `, + questions: ` + CREATE TABLE IF NOT EXISTS questions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_type TEXT NOT NULL, + question_name TEXT NOT NULL DEFAULT '', + question_description TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_type) REFERENCES dict_types(type_code) + ); + `, + questionDatasets: ` + CREATE TABLE IF NOT EXISTS question_datasets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + dataset_name TEXT NOT NULL DEFAULT '', + dataset_data TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionImages: ` + CREATE TABLE IF NOT EXISTS question_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + image_name TEXT NOT NULL DEFAULT '', + image_base64 TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionFillTable: ` + CREATE TABLE IF NOT EXISTS question_fill_table ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + table_name TEXT NOT NULL DEFAULT '', + table_data TEXT NOT NULL, + table_description TEXT NOT NULL DEFAULT '', + score REAL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionFillTableBlanks: ` + CREATE TABLE IF NOT EXISTS question_fill_table_blanks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + table_id INTEGER NOT NULL, + cell_position TEXT NOT NULL, + cell_type TEXT NOT NULL DEFAULT 'number', + correct_answer TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id), + FOREIGN KEY (table_id) REFERENCES question_fill_table(id) + ); + `, + questionChoices: ` + CREATE TABLE IF NOT EXISTS question_choices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + choice_description TEXT NOT NULL DEFAULT '', + choice_type TEXT NOT NULL DEFAULT 'single', + choice_options TEXT NOT NULL, + correct_answers TEXT NOT NULL, + score REAL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionFillBlanks: ` + CREATE TABLE IF NOT EXISTS question_fill_blanks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + blank_description TEXT NOT NULL DEFAULT '', + blank_count INTEGER NOT NULL DEFAULT 0, + correct_answers TEXT NOT NULL DEFAULT '', + score REAL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionJudge: ` + CREATE TABLE IF NOT EXISTS question_judge ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + judge_ask TEXT NOT NULL DEFAULT '', + judge_answer INTEGER NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + questionShort: ` + CREATE TABLE IF NOT EXISTS question_short ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + short_ask TEXT NOT NULL DEFAULT '', + short_answer_ref TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (question_id) REFERENCES questions(id) + ); + `, + exam: ` + CREATE TABLE IF NOT EXISTS exam ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + exam_name TEXT NOT NULL DEFAULT '', + exam_description TEXT NOT NULL DEFAULT '', + exam_examinee_type TEXT NOT NULL DEFAULT '', + exam_notice TEXT NOT NULL DEFAULT '', + exam_minutes INTEGER NOT NULL DEFAULT 0, + exam_minutes_min INTEGER NOT NULL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + examinee: ` + CREATE TABLE IF NOT EXISTS examinee ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + examinee_name TEXT NOT NULL DEFAULT '', + examinee_gender TEXT NOT NULL DEFAULT '', + examinee_unit TEXT NOT NULL DEFAULT '', + written_exam_room TEXT NOT NULL DEFAULT '', + written_exam_seat TEXT NOT NULL DEFAULT '', + computer_exam_room TEXT NOT NULL DEFAULT '', + computer_exam_seat TEXT NOT NULL DEFAULT '', + examinee_id_card TEXT NOT NULL DEFAULT '', + examinee_admission_ticket TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + ` +}; + +// 用户数据库表结构 +userSchema = { + examinee: ` + CREATE TABLE IF NOT EXISTS examinee ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + examinee_name TEXT NOT NULL DEFAULT '', + examinee_gender TEXT, + examinee_unit TEXT, + written_exam_room TEXT, + written_exam_seat TEXT, + computer_exam_room TEXT, + computer_exam_seat TEXT, + examinee_id_card TEXT NOT NULL DEFAULT '', + examinee_admission_ticket TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + examinee_papers: ` + CREATE TABLE IF NOT EXISTS examinee_papers ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + examinee_id INTEGER NOT NULL, + paper_minutes INTEGER NOT NULL DEFAULT 0, + paper_minutes_min INTEGER NOT NULL DEFAULT 0, + paper_start_time TEXT, + paper_last_time TEXT, + paper_submit_time TEXT, + paper_end_time TEXT, + paper_status INTEGER NOT NULL DEFAULT 0, -- 试卷状态:0未开始,1进行中,2已交卷 + paper_score_real REAL DEFAULT 0, + paper_score REAL DEFAULT 0, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + paper_questions: ` + CREATE TABLE IF NOT EXISTS paper_questions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + examinee_id INTEGER NOT NULL, + paper_id INTEGER NOT NULL, + question_type TEXT NOT NULL, + question_name TEXT NOT NULL DEFAULT '', + question_description TEXT NOT NULL DEFAULT '', + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + question_datasets: ` + CREATE TABLE IF NOT EXISTS question_datasets ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + dataset_name TEXT NOT NULL DEFAULT '', + dataset_data TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + question_images: ` + CREATE TABLE IF NOT EXISTS question_images ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + image_name TEXT NOT NULL DEFAULT '', + image_base64 TEXT NOT NULL, + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + question_choices: ` + CREATE TABLE IF NOT EXISTS question_choices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + choice_description TEXT NOT NULL DEFAULT '', + choice_type TEXT NOT NULL DEFAULT 'single', + choice_options TEXT NOT NULL, + correct_answers TEXT NOT NULL, + examinee_answers TEXT NOT NULL DEFAULT '', + score REAL, + score_real REAL DEFAULT 0, -- 本题得分 + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + `, + question_fill_blanks: ` + CREATE TABLE IF NOT EXISTS question_fill_blanks ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + question_id INTEGER NOT NULL, + blank_description TEXT NOT NULL DEFAULT '', + blank_count INTEGER NOT NULL DEFAULT 0, + correct_answers TEXT NOT NULL DEFAULT '', + examinee_answers TEXT NOT NULL DEFAULT '', + score REAL, + score_real REAL DEFAULT 0, -- 本题得分 + created_at TEXT DEFAULT CURRENT_TIMESTAMP, + updated_at TEXT DEFAULT CURRENT_TIMESTAMP + ); + ` +}; + +// 初始化默认数据 +// 系统配置默认数据 +const plainPassword = "t2t6a9"; // 明文密码变量定义 + +// 注意:在实际初始化数据库时,需要使用bcryptjs对plainPassword进行哈希 +// 这里只定义默认数据结构,哈希操作应在index.js中的初始化函数中完成 +defaultData = { + config: [ + { key: "admin_password", value: plainPassword, protected: 1 }, + { key: "question_bank_version", value: "1", protected: 1 }, + { key: "exam_version", value: "1", protected: 1 }, + { key: "initialized", value: "1", protected: 1 }, + ], + // 字典类型默认数据 + dictTypes: [ + { + type_code: "question_category", + type_name: "题型分类", + description: "用于区分客观题和主观题", + }, + { + type_code: "question_type", + type_name: "题型", + description: "存储所有题型(选择题、填空题等)", + }, + { + type_code: "user_role", + type_name: "用户角色", + description: "区分不同用户类型", + }, + ], + // 字典项默认数据 + dictItems: [ + // 题型分类 + { + type_code: "question_category", + item_code: "objective", + item_name: "客观题", + item_description: "有固定答案,机器可自动评分", + is_active: 1, + parent_code: null, + }, + { + type_code: "question_category", + item_code: "subjective", + item_name: "主观题", + item_description: "需人工评分,答案不唯一", + is_active: 1, + parent_code: null, + }, + // 题型 + { + type_code: "question_type", + item_code: "choice", + item_name: "选择题", + item_description: "包含单选和多选", + is_active: 1, + parent_code: "objective", + }, + { + type_code: "question_type", + item_code: "fill_blank", + item_name: "填空题", + item_description: "填写空白处的答案", + is_active: 1, + parent_code: "objective", + }, + { + type_code: "question_type", + item_code: "fill_table", + item_name: "填表题", + item_description: "填写表格内容", + is_active: 0, + parent_code: "objective", + }, + { + type_code: "question_type", + item_code: "true_false", + item_name: "判断题", + item_description: "判断对错", + is_active: 0, + parent_code: "objective", + }, + { + type_code: "question_type", + item_code: "short_answer", + item_name: "问答题", + item_description: "简短回答问题", + is_active: 0, + parent_code: "subjective", + }, + { + type_code: "question_type", + item_code: "analysis", + item_name: "分析题", + item_description: "需要分析问题", + is_active: 0, + parent_code: "subjective", + }, + { + type_code: "question_type", + item_code: "essay", + item_name: "论述题", + item_description: "详细论述", + is_active: 0, + parent_code: "subjective", + }, + // 用户角色 + { + type_code: "user_role", + item_code: "admin", + item_name: "管理员", + item_description: "系统管理员", + is_active: 1, + parent_code: null, + }, + { + type_code: "user_role", + item_code: "student", + item_name: "考生", + item_description: "参加考试的用户", + is_active: 1, + parent_code: null, + }, + ], +}; + +// 导出函数和常量供其他模块使用 +module.exports = { + runAsync, + systemSchema, + userSchema, + defaultData +}; \ No newline at end of file diff --git a/src/background/db/utils.js b/src/background/db/utils.js new file mode 100644 index 0000000..aa0f73a --- /dev/null +++ b/src/background/db/utils.js @@ -0,0 +1,108 @@ +const sqlite3 = require('sqlite3'); +const { promisify } = require('util'); + +sqlite3.verbose(); + +// 数据库连接池简单实现 +const dbConnections = new Map(); + +// 打开数据库并添加到连接池 +async function openDatabase(dbPath) { + console.log(`打开数据库连接: ${dbPath}`); + + // 检查是否已有连接 + if (dbConnections.has(dbPath)) { + console.log(`使用现有数据库连接: ${dbPath}`); + return dbConnections.get(dbPath); + } + + return new Promise((resolve, reject) => { + const db = new sqlite3.Database(dbPath, (err) => { + if (err) { + console.error(`打开数据库失败: ${err.message}`); + reject(err); + return; + } + + console.log(`成功打开数据库: ${dbPath}`); + + // promisify数据库方法 + db.getAsync = promisify(db.get).bind(db); + db.allAsync = promisify(db.all).bind(db); + + // 自定义实现runAsync以获取lastID + db.runAsync = function(sql, params) { + return new Promise((resolve, reject) => { + this.run(sql, params, function(err) { + if (err) { + reject(err); + } else { + resolve({ + lastID: this.lastID, + changes: this.changes + }); + } + }); + }); + }; + + db.execAsync = promisify(db.exec).bind(db); + + // 添加到连接池 + dbConnections.set(dbPath, db); + + resolve(db); + }); + }); +} + +// 添加重试机制的执行函数 +async function executeWithRetry(db, operation, maxRetries = 3) { + let retries = 0; + while (retries < maxRetries) { + try { + return await operation(); + } catch (error) { + if (error.code === 'SQLITE_BUSY' && retries < maxRetries - 1) { + retries++; + console.log(`数据库忙,正在重试 (${retries}/${maxRetries})...`); + await new Promise(resolve => setTimeout(resolve, 500 * retries)); // 指数退避 + } else { + throw error; + } + } + } +} + +// 修改batchInsert函数使用重试机制 +async function batchInsert(db, table, data) { + if (!data || data.length === 0) { + return; + } + + const columns = Object.keys(data[0]).join(', '); + const placeholders = data.map(() => { + return '(' + Object.keys(data[0]).map(() => '?').join(', ') + ')'; + }).join(', '); + + const values = []; + data.forEach(item => { + Object.values(item).forEach(value => { + values.push(value); + }); + }); + + const sql = `INSERT INTO ${table} (${columns}) VALUES ${placeholders}`; + + return executeWithRetry(db, async () => { + await db.runAsync(sql, values); + console.log(`成功插入 ${data.length} 条记录到 ${table} 表`); + }); +} + +// 导出函数供其他模块使用 +module.exports = { + openDatabase, + batchInsert, + executeWithRetry +}; \ No newline at end of file diff --git a/src/background/font/SourceHanSansSC-Bold.otf b/src/background/font/SourceHanSansSC-Bold.otf new file mode 100644 index 0000000..3b33e77 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Bold.otf differ diff --git a/src/background/font/SourceHanSansSC-ExtraLight.otf b/src/background/font/SourceHanSansSC-ExtraLight.otf new file mode 100644 index 0000000..9e51269 Binary files /dev/null and b/src/background/font/SourceHanSansSC-ExtraLight.otf differ diff --git a/src/background/font/SourceHanSansSC-Heavy.otf b/src/background/font/SourceHanSansSC-Heavy.otf new file mode 100644 index 0000000..149f6b8 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Heavy.otf differ diff --git a/src/background/font/SourceHanSansSC-Light.otf b/src/background/font/SourceHanSansSC-Light.otf new file mode 100644 index 0000000..ebb1728 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Light.otf differ diff --git a/src/background/font/SourceHanSansSC-Medium.otf b/src/background/font/SourceHanSansSC-Medium.otf new file mode 100644 index 0000000..304b3e6 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Medium.otf differ diff --git a/src/background/font/SourceHanSansSC-Normal.otf b/src/background/font/SourceHanSansSC-Normal.otf new file mode 100644 index 0000000..5e805a0 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Normal.otf differ diff --git a/src/background/font/SourceHanSansSC-Regular.otf b/src/background/font/SourceHanSansSC-Regular.otf new file mode 100644 index 0000000..8113ad5 Binary files /dev/null and b/src/background/font/SourceHanSansSC-Regular.otf differ diff --git a/src/background/font/simsun.ttc b/src/background/font/simsun.ttc new file mode 100644 index 0000000..40e9693 Binary files /dev/null and b/src/background/font/simsun.ttc differ diff --git a/src/background/font/simsun.ttf b/src/background/font/simsun.ttf new file mode 100644 index 0000000..e0115ab Binary files /dev/null and b/src/background/font/simsun.ttf differ diff --git a/src/background.js b/src/background/main.js similarity index 86% rename from src/background.js rename to src/background/main.js index 37c584c..203c717 100644 --- a/src/background.js +++ b/src/background/main.js @@ -7,6 +7,9 @@ import installExtension, { VUEJS_DEVTOOLS } from 'electron-devtools-installer' const bcrypt = require('bcryptjs') const isDevelopment = process.env.NODE_ENV !== 'production' +// 导入数据库相关函数 +const { checkDatabaseInitialized, initializeDatabase } = require('./db/index.js'); + // Scheme must be registered before the app is ready protocol.registerSchemesAsPrivileged([ { scheme: 'app', privileges: { secure: true, standard: true } } @@ -18,7 +21,11 @@ async function createWindow() { width: 800, height: 600, webPreferences: { + // 修改这一行 preload: require('path').join(__dirname, 'preload.js'), + + // 改为使用绝对路径解析 + preload: require('path').join(process.cwd(), 'src/preload.js'), nodeIntegration: false, contextIsolation: true } @@ -142,3 +149,22 @@ ipcMain.handle('hashTest', async (event, inputString) => { throw error } }) + +// 数据库相关IPC接口 +ipcMain.handle('check-database-initialized', async () => { + try { + return await checkDatabaseInitialized() + } catch (error) { + console.error('Failed to check database initialization:', error) + return false + } +}) + +ipcMain.handle('initialize-database', async () => { + try { + return await initializeDatabase() + } catch (error) { + console.error('Failed to initialize database:', error) + return false + } +}) diff --git a/src/preload.js b/src/preload.js index 153f444..53d03d9 100644 --- a/src/preload.js +++ b/src/preload.js @@ -1,7 +1,19 @@ // Preload script runs in a context that has access to both Node.js and browser APIs const { contextBridge, ipcRenderer } = require('electron') -// Expose ipcRenderer to the renderer process through contextBridge +// 暴露API给渲染进程 +contextBridge.exposeInMainWorld('electronAPI', { + // 数据库相关 + checkDatabaseInitialized: () => ipcRenderer.invoke('check-database-initialized'), + initializeDatabase: () => ipcRenderer.invoke('initialize-database'), + + // 保留原有的ipcRenderer接口,确保兼容性 + ipcRenderer: { + invoke: (channel, data) => ipcRenderer.invoke(channel, data) + } +}) + +// 也保留原来的electron对象,确保现有功能正常 contextBridge.exposeInMainWorld('electron', { ipcRenderer: { invoke: (channel, data) => ipcRenderer.invoke(channel, data) diff --git a/src/views/Home.vue b/src/views/Home.vue index 850c060..e6f3a80 100644 --- a/src/views/Home.vue +++ b/src/views/Home.vue @@ -75,6 +75,61 @@ + +