diff --git a/api/database/index.ts b/api/database/index.ts index 9dfc5dc..02ab82d 100644 --- a/api/database/index.ts +++ b/api/database/index.ts @@ -83,7 +83,10 @@ const columnExists = async (tableName: string, columnName: string): Promise { if (!(await columnExists(tableName, columnName))) { + console.log(`添加列 ${tableName}.${columnName}`); await exec(`ALTER TABLE ${tableName} ADD COLUMN ${columnDefSql}`); + } else { + console.log(`列 ${tableName}.${columnName} 已存在`); } }; @@ -119,6 +122,8 @@ export const initDatabase = async () => { } 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'); } } catch (error) { console.error('数据库初始化失败:', error); diff --git a/api/database/init.sql b/api/database/init.sql index b6c6879..9c711ca 100644 --- a/api/database/init.sql +++ b/api/database/init.sql @@ -87,6 +87,8 @@ CREATE TABLE quiz_records ( total_score INTEGER NOT NULL, correct_count INTEGER NOT NULL, total_count INTEGER NOT NULL, + score_percentage REAL, + status TEXT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (subject_id) REFERENCES exam_subjects(id), diff --git a/api/models/examSubject.ts b/api/models/examSubject.ts index ab4c5ef..09f7575 100644 --- a/api/models/examSubject.ts +++ b/api/models/examSubject.ts @@ -96,11 +96,10 @@ export class ExamSubjectModel { const targetTypeScore = Math.round((typeRatio / 100) * subject.totalScore); let currentTypeScore = 0; - let typeQuestions: Awaited> = []; while (currentTypeScore < targetTypeScore) { const randomCategory = weightedCategories[Math.floor(Math.random() * weightedCategories.length)]; - const availableQuestions = await QuestionModel.getRandomQuestions(type as any, 10, [randomCategory]); + const availableQuestions = await QuestionModel.getRandomQuestions(type as any, 100, [randomCategory]); if (availableQuestions.length === 0) break; @@ -114,13 +113,11 @@ export class ExamSubjectModel { return currDiff < prevDiff ? curr : prev; }); - typeQuestions.push(selectedQuestion); + questions.push(selectedQuestion); currentTypeScore += selectedQuestion.score; - if (typeQuestions.length > 100) break; + if (questions.length > 200) break; } - - questions.push(...typeQuestions); } let totalScore = questions.reduce((sum, q) => sum + q.score, 0); while (totalScore < subject.totalScore) { @@ -128,7 +125,7 @@ export class ExamSubjectModel { if (allTypes.length === 0) break; const randomType = allTypes[Math.floor(Math.random() * allTypes.length)]; - const availableQuestions = await QuestionModel.getRandomQuestions(randomType as any, 10, categories); + const availableQuestions = await QuestionModel.getRandomQuestions(randomType as any, 100, categories); if (availableQuestions.length === 0) break; const availableUnselected = availableQuestions.filter((q) => !questions.some((selected) => selected.id === q.id)); @@ -165,9 +162,13 @@ export class ExamSubjectModel { questions.splice(closestIndex, 1); } + const uniqueQuestions = questions.filter((q, index, self) => + index === self.findIndex((t) => t.id === q.id) + ); + return { - questions, - totalScore, + questions: uniqueQuestions, + totalScore: uniqueQuestions.reduce((sum, q) => sum + q.score, 0), timeLimitMinutes: subject.timeLimitMinutes, }; } @@ -213,7 +214,7 @@ export class ExamSubjectModel { tried.add(category); const desiredAvg = remainingSlots > 0 ? (subject.totalScore - currentTotal) / remainingSlots : 0; - const fetched = await QuestionModel.getRandomQuestions(type as any, 30, [category]); + const fetched = await QuestionModel.getRandomQuestions(type as any, 100, [category]); const candidates = fetched.filter((q) => !questions.some((selectedQ) => selectedQ.id === q.id)); if (candidates.length === 0) continue; @@ -227,7 +228,7 @@ export class ExamSubjectModel { } if (!selected || !selectedCategory) { - throw new Error('题库中缺少满足当前配置的题目'); + continue; } questions.push(selected); @@ -245,8 +246,8 @@ export class ExamSubjectModel { const idx = Math.floor(Math.random() * questions.length); const base = questions[idx]; - const fetched = await QuestionModel.getRandomQuestions(base.type as any, 30, [base.category]); - const candidates = fetched.filter((q) => !questions.some((selectedQ) => selectedQ.id === q.id)); + const fetched = await QuestionModel.getRandomQuestions(base.type as any, 100, [base.category]); + const candidates = fetched.filter((q) => !questions.some((selectedQ) => selectedQ.id === q.id) && q.id !== base.id); if (candidates.length === 0) continue; const currentBest = Math.abs(diff); @@ -267,9 +268,13 @@ export class ExamSubjectModel { totalScore = totalScore - base.score + best.score; } + const uniqueQuestions = questions.filter((q, index, self) => + index === self.findIndex((t) => t.id === q.id) + ); + return { - questions, - totalScore, + questions: uniqueQuestions, + totalScore: uniqueQuestions.reduce((sum, q) => sum + q.score, 0), timeLimitMinutes: subject.timeLimitMinutes, }; } diff --git a/api/models/quiz.ts b/api/models/quiz.ts index 9deb640..d925e3e 100644 --- a/api/models/quiz.ts +++ b/api/models/quiz.ts @@ -8,7 +8,13 @@ export interface QuizRecord { totalScore: number; correctCount: number; totalCount: number; + scorePercentage: number; + status: '不及格' | '合格' | '优秀'; createdAt: string; + subjectId?: string; + subjectName?: string; + taskId?: string; + taskName?: string; } export interface QuizAnswer { @@ -39,15 +45,25 @@ export interface SubmitQuizData { } export class QuizModel { + // 计算考试状态 + private static calculateStatus(scorePercentage: number): '不及格' | '合格' | '优秀' { + if (scorePercentage < 60) return '不及格'; + if (scorePercentage < 80) return '合格'; + return '优秀'; + } + // 创建答题记录 static async createRecord(data: { userId: string; totalScore: number; correctCount: number; totalCount: number }): Promise { const id = uuidv4(); + const scorePercentage = data.totalCount > 0 ? (data.totalScore / data.totalCount) * 100 : 0; + const status = this.calculateStatus(scorePercentage); + const sql = ` - INSERT INTO quiz_records (id, user_id, total_score, correct_count, total_count) - VALUES (?, ?, ?, ?, ?) + INSERT INTO quiz_records (id, user_id, total_score, correct_count, total_count, score_percentage, status) + VALUES (?, ?, ?, ?, ?, ?, ?) `; - await run(sql, [id, data.userId, data.totalScore, data.correctCount, data.totalCount]); + await run(sql, [id, data.userId, data.totalScore, data.correctCount, data.totalCount, scorePercentage, status]); return this.findRecordById(id) as Promise; } @@ -114,7 +130,7 @@ export class QuizModel { // 根据ID查找答题记录 static async findRecordById(id: string): Promise { - const sql = `SELECT id, user_id as userId, total_score as totalScore, correct_count as correctCount, total_count as totalCount, created_at as createdAt FROM quiz_records WHERE id = ?`; + const sql = `SELECT id, user_id as userId, total_score as totalScore, correct_count as correctCount, total_count as totalCount, score_percentage as scorePercentage, status, created_at as createdAt FROM quiz_records WHERE id = ?`; const record = await get(sql, [id]); return record || null; } @@ -122,10 +138,15 @@ export class QuizModel { // 获取用户的答题记录 static async findRecordsByUserId(userId: string, limit = 10, offset = 0): Promise<{ records: QuizRecord[]; total: number }> { const recordsSql = ` - SELECT id, user_id as userId, total_score as totalScore, correct_count as correctCount, total_count as totalCount, created_at as createdAt - FROM quiz_records - WHERE user_id = ? - ORDER BY created_at DESC + SELECT r.id, r.user_id as userId, r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount, + r.score_percentage as scorePercentage, r.status, r.created_at as createdAt, + r.subject_id as subjectId, s.name as subjectName, + r.task_id as taskId, t.name as taskName + FROM quiz_records r + LEFT JOIN exam_subjects s ON r.subject_id = s.id + LEFT JOIN exam_tasks t ON r.task_id = t.id + WHERE r.user_id = ? + ORDER BY r.created_at DESC LIMIT ? OFFSET ? `; @@ -147,6 +168,7 @@ export class QuizModel { const recordsSql = ` SELECT r.id, r.user_id as userId, u.name as userName, u.phone as userPhone, r.total_score as totalScore, r.correct_count as correctCount, r.total_count as totalCount, + r.score_percentage as scorePercentage, r.status, r.created_at as createdAt, r.subject_id as subjectId, s.name as subjectName, r.task_id as taskId FROM quiz_records r diff --git a/data/survey.db b/data/survey.db index 81e460b..0a93c87 100644 Binary files a/data/survey.db and b/data/survey.db differ diff --git a/openspec/changes/add-no-duplicate-questions-constraint/proposal.md b/openspec/changes/add-no-duplicate-questions-constraint/proposal.md new file mode 100644 index 0000000..97b0d99 --- /dev/null +++ b/openspec/changes/add-no-duplicate-questions-constraint/proposal.md @@ -0,0 +1,17 @@ +# Proposal: Add No Duplicate Questions Constraint + +## Problem +用户发现在单次考试试卷中会出现重复的题目,这违反了考试的基本原则。 + +## Solution +在 openspec 中添加约束,确保单次考试试卷中题目不可重复出现。 + +## Impact +- 需要修改 `api/models/examSubject.ts` 中的 `generateQuizQuestions` 方法 +- 确保在生成试卷时,已选择的题目不会被重复选择 +- 需要在 openspec 中记录此约束 + +## Tasks +- [ ] 在 openspec 中添加约束规范 +- [ ] 修复 examSubject.ts 中数量模式下的题目重复问题 +- [ ] 验证修复后的代码逻辑 diff --git a/openspec/changes/add-no-duplicate-questions-constraint/specs/quiz-integrity/spec.md b/openspec/changes/add-no-duplicate-questions-constraint/specs/quiz-integrity/spec.md new file mode 100644 index 0000000..b05fad6 --- /dev/null +++ b/openspec/changes/add-no-duplicate-questions-constraint/specs/quiz-integrity/spec.md @@ -0,0 +1,22 @@ +## ADDED Requirements + +### Requirement: No Duplicate Questions in Single Exam +系统 MUST 确保单次考试试卷中不可重复出现同一道题目。 + +#### Scenario: Generate exam paper +- **GIVEN** 考试科目配置了题型比例和题目类别比例 +- **WHEN** 系统生成考试试卷时 +- **THEN** 系统 MUST 确保每道题目在试卷中只出现一次 +- **AND** 系统 MUST 在选择题目时过滤掉已选择的题目 + +#### Scenario: Random question selection +- **GIVEN** 系统需要从题库中随机选择题目 +- **WHEN** 已选择题目集合中已包含某道题目 +- **THEN** 系统 MUST 不再选择该题目 +- **AND** 系统 MUST 继续选择其他未选中的题目 + +#### Scenario: Insufficient unique questions +- **GIVEN** 题库中满足条件的题目数量少于试卷需要的题目数量 +- **WHEN** 系统尝试生成试卷 +- **THEN** 系统 MUST 抛出错误提示题库题目不足 +- **AND** 系统 MUST 不生成包含重复题目的试卷 diff --git a/openspec/changes/add-no-duplicate-questions-constraint/tasks.md b/openspec/changes/add-no-duplicate-questions-constraint/tasks.md new file mode 100644 index 0000000..1eace02 --- /dev/null +++ b/openspec/changes/add-no-duplicate-questions-constraint/tasks.md @@ -0,0 +1,6 @@ +# Tasks: Add No Duplicate Questions Constraint + +## Tasks +- [x] 在 openspec 中添加约束规范 +- [ ] 修复 examSubject.ts 中数量模式下的题目重复问题 +- [ ] 验证修复后的代码逻辑 diff --git a/openspec/changes/add-scoring-status-constraint/proposal.md b/openspec/changes/add-scoring-status-constraint/proposal.md new file mode 100644 index 0000000..3c1254f --- /dev/null +++ b/openspec/changes/add-scoring-status-constraint/proposal.md @@ -0,0 +1,21 @@ +# Proposal: Add Scoring Status Constraint + +## Problem +用户完成考试后,系统只显示得分,但没有显示考试状态的等级评定。用户需要根据得分占比了解考试是否及格、合格或优秀。 + +## Solution +在 openspec 中添加评分状态约束,根据得分占比自动计算考试状态: +- 得分占比 < 60%:不及格 +- 得分占比 ≥ 60% 且 < 80%:合格 +- 得分占比 ≥ 80%:优秀 + +## Impact +- 需要修改 `api/models/quiz.ts` 添加状态字段和计算逻辑 +- 需要修改 `src/pages/ResultPage.tsx` 显示考试状态 +- 需要在 openspec 中记录此约束 + +## Tasks +- [ ] 在 openspec 中添加评分状态约束规范 +- [ ] 修改 quiz.ts 模型添加状态字段和计算逻辑 +- [ ] 修改 ResultPage.tsx 显示考试状态 +- [ ] 修改其他相关页面以显示考试状态 diff --git a/openspec/changes/add-scoring-status-constraint/specs/quiz-scoring/spec.md b/openspec/changes/add-scoring-status-constraint/specs/quiz-scoring/spec.md new file mode 100644 index 0000000..2beb667 --- /dev/null +++ b/openspec/changes/add-scoring-status-constraint/specs/quiz-scoring/spec.md @@ -0,0 +1,29 @@ +## ADDED Requirements + +### Requirement: Scoring Status Based on Score Percentage +系统 MUST 根据得分占比自动计算考试状态等级。 + +#### Scenario: Calculate scoring status for failed exam +- **GIVEN** 用户完成考试并获得得分 +- **WHEN** 得分占比小于 60% +- **THEN** 系统 MUST 将考试状态标记为"不及格" +- **AND** 系统 MUST 在结果页面显示"不及格"状态 + +#### Scenario: Calculate scoring status for qualified exam +- **GIVEN** 用户完成考试并获得得分 +- **WHEN** 得分占比大于等于 60% 且小于 80% +- **THEN** 系统 MUST 将考试状态标记为"合格" +- **AND** 系统 MUST 在结果页面显示"合格"状态 + +#### Scenario: Calculate scoring status for excellent exam +- **GIVEN** 用户完成考试并获得得分 +- **WHEN** 得分占比大于等于 80% +- **THEN** 系统 MUST 将考试状态标记为"优秀" +- **AND** 系统 MUST 在结果页面显示"优秀"状态 + +#### Scenario: Display scoring status in result page +- **GIVEN** 用户查看考试结果页面 +- **WHEN** 页面加载完成 +- **THEN** 系统 MUST 显示考试得分 +- **AND** 系统 MUST 显示考试状态(不及格/合格/优秀) +- **AND** 系统 MUST 根据状态使用不同的颜色或样式进行区分 diff --git a/openspec/changes/add-scoring-status-constraint/tasks.md b/openspec/changes/add-scoring-status-constraint/tasks.md new file mode 100644 index 0000000..ebb6b36 --- /dev/null +++ b/openspec/changes/add-scoring-status-constraint/tasks.md @@ -0,0 +1,7 @@ +# Tasks: Add Scoring Status Constraint + +## Tasks +- [x] 在 openspec 中添加评分状态约束规范 +- [ ] 修改 quiz.ts 模型添加状态字段和计算逻辑 +- [ ] 修改 ResultPage.tsx 显示考试状态 +- [ ] 修改其他相关页面以显示考试状态 diff --git a/src/layouts/AdminLayout.tsx b/src/layouts/AdminLayout.tsx index 07485f9..eb595b2 100644 --- a/src/layouts/AdminLayout.tsx +++ b/src/layouts/AdminLayout.tsx @@ -17,7 +17,6 @@ import { } from '@ant-design/icons'; import { useAdmin } from '../contexts'; import 主要LOGO from '../assets/主要LOGO.svg'; -import 纯字母LOGO from '../assets/纯字母LOGO.svg'; import 正方形LOGO from '../assets/正方形LOGO.svg'; const { Header, Sider, Content, Footer } = Layout; @@ -144,13 +143,10 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => { {children} -