feat: 更新构建流程,添加 API 构建脚本和 SQL 文件复制脚本
- 修改 package.json,更新构建命令,添加 postbuild 脚本以复制 init.sql 文件。 - 新增 scripts/build-api.mjs,使用 esbuild 构建 API 代码。 - 新增 scripts/copy-init-sql.mjs,复制数据库初始化 SQL 文件到构建输出目录。 - 在 SubjectSelectionPage 组件中添加 totalScore 属性,增加历史最高分状态显示功能。 - 在 ExamSubjectPage 和 QuestionManagePage 中优化判断题答案处理逻辑。 - 在 OptionList 组件中将判断题选项文本从 'T' 和 'F' 改为 '对' 和 '错'。 - 在 QuizFooter 组件中调整样式,增加按钮和文本的可读性。 - 新增用户默认组测试用例,验证新用户创建后自动加入“全体用户”系统组。 - 新增 tsconfig.api.json,配置 API 相关 TypeScript 编译选项。 - 移除 vite.config.ts 中的 global 定义。
This commit is contained in:
@@ -263,7 +263,9 @@ export class AdminUserController {
|
||||
continue;
|
||||
}
|
||||
|
||||
await UserModel.create({ name, phone, password });
|
||||
const user = await UserModel.create({ name, phone, password });
|
||||
// 统一规则:新用户默认加入“全体用户”系统组
|
||||
await UserGroupModel.updateUserGroups(user.id, []);
|
||||
imported++;
|
||||
} catch (error: any) {
|
||||
if (error.message === '手机号已存在') {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Request, Response } from 'express';
|
||||
import * as XLSX from 'xlsx';
|
||||
import { UserModel, QuestionModel, QuizModel } from '../models';
|
||||
import { UserModel, QuestionModel, QuizModel, UserGroupModel } from '../models';
|
||||
|
||||
export class BackupController {
|
||||
// 导出用户数据
|
||||
@@ -129,10 +129,12 @@ export class BackupController {
|
||||
if (users && users.length > 0) {
|
||||
for (const user of users) {
|
||||
try {
|
||||
await UserModel.create({
|
||||
const createdUser = await UserModel.create({
|
||||
name: user.姓名 || user.name,
|
||||
phone: user.手机号 || user.phone
|
||||
});
|
||||
// 统一规则:恢复数据创建的用户也必须加入“全体用户”系统组
|
||||
await UserGroupModel.updateUserGroups(createdUser.id, []);
|
||||
restoredCount.users++;
|
||||
} catch (error) {
|
||||
console.log('用户已存在,跳过:', user.手机号 || user.phone);
|
||||
|
||||
@@ -155,7 +155,19 @@ export class QuizController {
|
||||
answer.isCorrect = false;
|
||||
}
|
||||
} else if (question.type === 'single' || question.type === 'judgment') {
|
||||
const isCorrect = answer.userAnswer === question.answer;
|
||||
const normalizeJudgment = (raw: unknown) => {
|
||||
const v = String(raw ?? '').trim();
|
||||
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
|
||||
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
|
||||
if (yes.has(v)) return '正确';
|
||||
if (no.has(v)) return '错误';
|
||||
return v;
|
||||
};
|
||||
|
||||
const isCorrect =
|
||||
question.type === 'judgment'
|
||||
? normalizeJudgment(answer.userAnswer) === normalizeJudgment(question.answer)
|
||||
: answer.userAnswer === question.answer;
|
||||
answer.score = isCorrect ? question.score : 0;
|
||||
answer.isCorrect = isCorrect;
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ export class UserController {
|
||||
|
||||
const existingUser = await UserModel.findByPhone(phone);
|
||||
|
||||
if (existingUser) {
|
||||
if (existingUser) {
|
||||
if (existingUser.password && existingUser.password !== password) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
@@ -118,6 +118,11 @@ export class UserController {
|
||||
});
|
||||
} else {
|
||||
const newUser = await UserModel.create({ name, phone, password });
|
||||
// 自动加入"全体用户"组
|
||||
const allUsersGroup = await UserGroupModel.getSystemGroup();
|
||||
if (allUsersGroup) {
|
||||
await UserGroupModel.addMember(allUsersGroup.id, newUser.id);
|
||||
}
|
||||
res.json({
|
||||
success: true,
|
||||
data: newUser
|
||||
|
||||
@@ -2,6 +2,7 @@ import path from 'path';
|
||||
import fs from 'fs';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { createRequire } from 'module';
|
||||
import { fileURLToPath } from 'url';
|
||||
|
||||
// 在ES模块中创建require函数,用于兼容CommonJS模块
|
||||
const require = createRequire(import.meta.url);
|
||||
@@ -98,6 +99,71 @@ const ensureIndex = async (createIndexSql: string) => {
|
||||
await exec(createIndexSql);
|
||||
};
|
||||
|
||||
const ensureUserGroupSchemaAndAllUsersMembership = async () => {
|
||||
// 1) Ensure tables
|
||||
await ensureTable(`
|
||||
CREATE TABLE IF NOT EXISTS user_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
is_system BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`);
|
||||
|
||||
await ensureTable(`
|
||||
CREATE TABLE IF NOT EXISTS user_group_members (
|
||||
group_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
|
||||
// 2) Ensure indexes
|
||||
await ensureIndex(
|
||||
`CREATE INDEX IF NOT EXISTS idx_user_group_members_group_id ON user_group_members(group_id);`,
|
||||
);
|
||||
await ensureIndex(
|
||||
`CREATE INDEX IF NOT EXISTS idx_user_group_members_user_id ON user_group_members(user_id);`,
|
||||
);
|
||||
|
||||
// 3) Ensure system group exists
|
||||
const existingSystemGroup = await get(
|
||||
`SELECT id FROM user_groups WHERE is_system = 1 ORDER BY created_at ASC LIMIT 1`,
|
||||
);
|
||||
|
||||
let systemGroupId = existingSystemGroup?.id as string | undefined;
|
||||
if (!systemGroupId) {
|
||||
const preferredId = 'all-users';
|
||||
try {
|
||||
await run(
|
||||
`INSERT INTO user_groups (id, name, description, is_system) VALUES (?, ?, ?, 1)`,
|
||||
[preferredId, '全体用户', '系统内置:新用户自动加入'],
|
||||
);
|
||||
systemGroupId = preferredId;
|
||||
} catch {
|
||||
const fallbackId = uuidv4();
|
||||
await run(
|
||||
`INSERT INTO user_groups (id, name, description, is_system) VALUES (?, ?, ?, 1)`,
|
||||
[fallbackId, '全体用户', '系统内置:新用户自动加入'],
|
||||
);
|
||||
systemGroupId = fallbackId;
|
||||
}
|
||||
}
|
||||
|
||||
// 4) Backfill membership: ensure all existing users are in the system group
|
||||
if (systemGroupId) {
|
||||
await run(
|
||||
`INSERT OR IGNORE INTO user_group_members (group_id, user_id)
|
||||
SELECT ?, u.id FROM users u`,
|
||||
[systemGroupId],
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const migrateDatabase = async () => {
|
||||
// 跳过迁移,因为数据库连接可能未初始化
|
||||
console.log('跳过数据库迁移');
|
||||
@@ -114,16 +180,22 @@ export const initDatabase = async () => {
|
||||
|
||||
if (!usersTableExists) {
|
||||
// 读取并执行初始化SQL文件
|
||||
const initSqlPath = path.join(path.dirname(import.meta.url.replace('file:///', '')), 'init.sql');
|
||||
const initSqlPath = fileURLToPath(new URL('./init.sql', import.meta.url));
|
||||
const initSql = fs.readFileSync(initSqlPath, 'utf8');
|
||||
|
||||
await exec(initSql);
|
||||
console.log('数据库初始化成功');
|
||||
|
||||
// 用户组(含“全体用户”系统组)
|
||||
await ensureUserGroupSchemaAndAllUsersMembership();
|
||||
} else {
|
||||
console.log('数据库表已存在,跳过初始化');
|
||||
await ensureColumn('questions', "analysis TEXT NOT NULL DEFAULT ''", 'analysis');
|
||||
await ensureColumn('quiz_records', "score_percentage REAL", 'score_percentage');
|
||||
await ensureColumn('quiz_records', "status TEXT", 'status');
|
||||
|
||||
// 用户组(含“全体用户”系统组)
|
||||
await ensureUserGroupSchemaAndAllUsersMembership();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('数据库初始化失败:', error);
|
||||
|
||||
@@ -11,6 +11,32 @@ CREATE TABLE users (
|
||||
CREATE INDEX idx_users_phone ON users(phone);
|
||||
CREATE INDEX idx_users_created_at ON users(created_at);
|
||||
|
||||
-- 用户组表
|
||||
CREATE TABLE user_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT UNIQUE NOT NULL,
|
||||
description TEXT NOT NULL DEFAULT '',
|
||||
is_system BOOLEAN NOT NULL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 用户组成员表
|
||||
CREATE TABLE user_group_members (
|
||||
group_id TEXT NOT NULL,
|
||||
user_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (group_id, user_id),
|
||||
FOREIGN KEY (group_id) REFERENCES user_groups(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE INDEX idx_user_group_members_group_id ON user_group_members(group_id);
|
||||
CREATE INDEX idx_user_group_members_user_id ON user_group_members(user_id);
|
||||
|
||||
-- 内置系统用户组:全体用户
|
||||
INSERT OR IGNORE INTO user_groups (id, name, description, is_system)
|
||||
VALUES ('all-users', '全体用户', '系统内置:新用户自动加入', 1);
|
||||
|
||||
-- 题目表
|
||||
CREATE TABLE questions (
|
||||
id TEXT PRIMARY KEY,
|
||||
|
||||
@@ -34,11 +34,28 @@ export interface ExcelQuestionData {
|
||||
}
|
||||
|
||||
export class QuestionModel {
|
||||
private static normalizeJudgmentAnswer(raw: unknown): string {
|
||||
const v = String(raw ?? '').trim();
|
||||
if (!v) return v;
|
||||
|
||||
// 兼容历史存储与导入:A/B、T/F、true/false、1/0 等
|
||||
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
|
||||
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
|
||||
|
||||
if (yes.has(v)) return '正确';
|
||||
if (no.has(v)) return '错误';
|
||||
return v;
|
||||
}
|
||||
|
||||
// 创建题目
|
||||
static async create(data: CreateQuestionData): Promise<Question> {
|
||||
const id = uuidv4();
|
||||
const optionsStr = data.options ? JSON.stringify(data.options) : null;
|
||||
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
|
||||
const normalizedAnswer =
|
||||
data.type === 'judgment' && !Array.isArray(data.answer)
|
||||
? this.normalizeJudgmentAnswer(data.answer)
|
||||
: data.answer;
|
||||
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
|
||||
const category = data.category && data.category.trim() ? data.category.trim() : '通用';
|
||||
const analysis = String(data.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
@@ -71,7 +88,11 @@ export class QuestionModel {
|
||||
const question = questions[i];
|
||||
const id = uuidv4();
|
||||
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
||||
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
||||
const normalizedAnswer =
|
||||
question.type === 'judgment' && !Array.isArray(question.answer)
|
||||
? this.normalizeJudgmentAnswer(question.answer)
|
||||
: question.answer;
|
||||
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
|
||||
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
||||
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
@@ -132,7 +153,11 @@ export class QuestionModel {
|
||||
const question = questions[i];
|
||||
try {
|
||||
const optionsStr = question.options ? JSON.stringify(question.options) : null;
|
||||
const answerStr = Array.isArray(question.answer) ? JSON.stringify(question.answer) : question.answer;
|
||||
const normalizedAnswer =
|
||||
question.type === 'judgment' && !Array.isArray(question.answer)
|
||||
? this.normalizeJudgmentAnswer(question.answer)
|
||||
: question.answer;
|
||||
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
|
||||
const category = question.category && question.category.trim() ? question.category.trim() : '通用';
|
||||
const analysis = String(question.analysis ?? '').trim().slice(0, 255);
|
||||
|
||||
@@ -291,7 +316,11 @@ export class QuestionModel {
|
||||
}
|
||||
|
||||
if (data.answer !== undefined) {
|
||||
const answerStr = Array.isArray(data.answer) ? JSON.stringify(data.answer) : data.answer;
|
||||
const normalizedAnswer =
|
||||
data.type === 'judgment' && !Array.isArray(data.answer)
|
||||
? this.normalizeJudgmentAnswer(data.answer)
|
||||
: data.answer;
|
||||
const answerStr = Array.isArray(normalizedAnswer) ? JSON.stringify(normalizedAnswer) : normalizedAnswer;
|
||||
fields.push('answer = ?');
|
||||
values.push(answerStr);
|
||||
}
|
||||
@@ -353,6 +382,9 @@ export class QuestionModel {
|
||||
return answerStr;
|
||||
}
|
||||
}
|
||||
if (type === 'judgment') {
|
||||
return this.normalizeJudgmentAnswer(answerStr);
|
||||
}
|
||||
return answerStr;
|
||||
}
|
||||
|
||||
|
||||
@@ -329,6 +329,14 @@ export class QuizModel {
|
||||
return answer;
|
||||
}
|
||||
}
|
||||
if (type === 'judgment') {
|
||||
const v = String(answer ?? '').trim();
|
||||
const yes = new Set(['A', 'T', 'TRUE', 'True', 'true', '1', '正确', '对', '是', 'Y', 'y', 'YES', 'yes']);
|
||||
const no = new Set(['B', 'F', 'FALSE', 'False', 'false', '0', '错误', '错', '否', '不是', 'N', 'n', 'NO', 'no']);
|
||||
if (yes.has(v)) return '正确';
|
||||
if (no.has(v)) return '错误';
|
||||
return v;
|
||||
}
|
||||
return answer;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user