diff --git a/.idea/artifacts/MathQuizApp_jar.xml b/.idea/artifacts/MathQuizApp_jar.xml
new file mode 100644
index 0000000..5561b0e
--- /dev/null
+++ b/.idea/artifacts/MathQuizApp_jar.xml
@@ -0,0 +1,20 @@
+
+
+ $PROJECT_DIR$/out/artifacts/MathQuizApp_jar
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/encodings.xml b/.idea/encodings.xml
new file mode 100644
index 0000000..aa00ffa
--- /dev/null
+++ b/.idea/encodings.xml
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..fdc35ea
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,14 @@
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml
new file mode 100644
index 0000000..2b63946
--- /dev/null
+++ b/.idea/uiDesigner.xml
@@ -0,0 +1,124 @@
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+ -
+
+
+
+
+ -
+
+
+ -
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..94a25f7
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..d9fb858
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,195 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {}
+ {
+ "isMigrated": true
+}
+
+
+
+
+
+
+
+
+
+
+
+ {
+ "customColor": "",
+ "associatedIndex": 1
+}
+
+
+
+
+
+ {
+ "keyToString": {
+ "Application.Test.executor": "Run",
+ "Application.TestMain.executor": "Run",
+ "RunOnceActivity.ShowReadmeOnStart": "true",
+ "RunOnceActivity.git.unshallow": "true",
+ "SHARE_PROJECT_CONFIGURATION_FILES": "true",
+ "git-widget-placeholder": "LiangJunYaoBranch",
+ "kotlin-language-version-configured": "true",
+ "node.js.detected.package.eslint": "true",
+ "node.js.detected.package.tslint": "true",
+ "node.js.selected.package.eslint": "(autodetect)",
+ "node.js.selected.package.tslint": "(autodetect)",
+ "nodejs_package_manager_path": "npm",
+ "project.structure.last.edited": "Artifacts",
+ "project.structure.proportion": "0.0",
+ "project.structure.side.proportion": "0.0",
+ "settings.editor.selected.configurable": "MavenSettings",
+ "vue.rearranger.settings.migration": "true"
+ }
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1759546098925
+
+
+ 1759546098925
+
+
+
+
+
+
+
+
+
+ 1759588559366
+
+
+
+ 1759588559366
+
+
+
+ 1759684484435
+
+
+
+ 1759684484435
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..996d6dc
--- /dev/null
+++ b/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.Test
+
diff --git a/data/history/初中-王五_1759552247819.txt b/data/history/初中-王五_1759552247819.txt
new file mode 100644
index 0000000..0881453
--- /dev/null
+++ b/data/history/初中-王五_1759552247819.txt
@@ -0,0 +1,77 @@
+========== 答题记录 ==========
+用户:初中-王五
+时间:2025-10-04 12:30:47
+总分:100 分
+正确:10 题 错误:0 题
+=============================
+
+【题目 1】
+√4 + 3²
+A. 20.0 B. 14.0 C. 6.0 D. 11.0
+正确答案:D
+用户答案:D
+结果:✓ 正确
+
+【题目 2】
+2²
+A. 6.0 B. 8.0 C. 4.0 D. 9.0
+正确答案:C
+用户答案:C
+结果:✓ 正确
+
+【题目 3】
+√100 + 15
+A. 24.0 B. 26.0 C. 25.0 D. 21.0
+正确答案:C
+用户答案:C
+结果:✓ 正确
+
+【题目 4】
+√36
+A. 6.0 B. 8.0 C. 7.0 D. 18.0
+正确答案:A
+用户答案:A
+结果:✓ 正确
+
+【题目 5】
+8²
+A. 64.0 B. 71.0 C. 16.0 D. 73.0
+正确答案:A
+用户答案:A
+结果:✓ 正确
+
+【题目 6】
+√16
+A. 4.0 B. 8.0 C. 13.0 D. 6.0
+正确答案:A
+用户答案:A
+结果:✓ 正确
+
+【题目 7】
+12²
+A. 144.0 B. 165.0 C. 149.0 D. 24.0
+正确答案:A
+用户答案:A
+结果:✓ 正确
+
+【题目 8】
+√16 + 2
+A. 11.0 B. 1.0 C. 12.0 D. 6.0
+正确答案:D
+用户答案:D
+结果:✓ 正确
+
+【题目 9】
+√81
+A. 14.0 B. 40.5 C. 9.0 D. 2.0
+正确答案:C
+用户答案:C
+结果:✓ 正确
+
+【题目 10】
+3² + 16
+A. 31.0 B. 25.0 C. 30.0 D. 20.0
+正确答案:B
+用户答案:B
+结果:✓ 正确
+
diff --git a/data/registration_codes.txt b/data/registration_codes.txt
new file mode 100644
index 0000000..93f4751
--- /dev/null
+++ b/data/registration_codes.txt
@@ -0,0 +1,4 @@
+# 注册码记录文件
+# 格式: 邮箱|注册码|过期时间戳|过期时间
+
+test002@example.com|4R8uZYiQ|1759552847711|2025-10-04 12:40:47
diff --git a/data/users.json b/data/users.json
new file mode 100644
index 0000000..9c5d1fb
--- /dev/null
+++ b/data/users.json
@@ -0,0 +1,104 @@
+{
+ "users": [
+ {
+ "userId": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv",
+ "username": "小学-测试",
+ "password": "encrypted123",
+ "email": "test@test.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "b2c3d4e5-6789-01fg-hijk-lmnopqrstuvw",
+ "username": "小学-张三测试",
+ "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263",
+ "email": "test001@example.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "c3d4e5f6-7890-12hi-jklm-nopqrstuvwxy",
+ "username": "初中-李明",
+ "password": "222711cc1da343bafd214b51a33d189a425e801b5d45774a941bbf68a1116d5c",
+ "email": "middle@test.com",
+ "grade": "MIDDLE",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "d4e5f6g7-8901-23jk-lmno-pqrstuvwxyz",
+ "username": "高中-王华",
+ "password": "9f3abee248c95d9ed301ee5a5b71318c0838c994ac5f66e70bfc9ee7ecad0150",
+ "email": "high@test.com",
+ "grade": "HIGH",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "e5f6g7h8-9012-34lm-mnop-qrstuvwxyza",
+ "username": "小学-重载测试",
+ "password": "bd3910fa48dc018fb9884e1d78649396d96882f6fcd8cbcd87b3e0c8bfc86e15",
+ "email": "reload@test.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "f6g7h8i9-0123-45no-opqr-stuvwxyzabc",
+ "username": "小学-李四",
+ "password": "encrypted",
+ "email": "lisi@test.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "g7h8i9j0-1234-56op-pqrs-tuvwxyzabcd",
+ "username": "初中-王五",
+ "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263",
+ "email": "wangwu@test.com",
+ "grade": "MIDDLE",
+ "totalQuizzes": 1,
+ "averageScore": 100.0,
+ "registrationDate": "Oct 4, 2025, 12:30:47 PM"
+ },
+ {
+ "userId": "h8i9j0k1-2345-67pq-qrst-uvwxyzabcde",
+ "username": "2803234009@qq.com",
+ "password": "ef6d562d372a2978b827d204932668d860691727821e9a36f6f532df6fb581bd",
+ "email": "2803234009@qq.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 5, 2025, 7:12:50 PM"
+ },
+ {
+ "userId": "3fff6ec1-76f9-4ce6-9880-4a1859b4f5a3",
+ "username": "111",
+ "password": "950d32ffc1f6079e12d28efdc0e8db995129e30a9dd4f91eae31a81f13389caa",
+ "email": "ljy.sbp@gmail.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 5, 2025, 9:35:09 PM"
+ },
+ {
+ "userId": "75fd5144-97f1-44eb-bf34-d339a0cffd1c",
+ "username": "1111",
+ "password": "b6e4e03a35a862ae38516e7b6d0e0ae8c0a6444bd3d33713e7d212e8ecd5531a",
+ "email": "111@123.com",
+ "grade": "ELEMENTARY",
+ "totalQuizzes": 0,
+ "averageScore": 0.0,
+ "registrationDate": "Oct 6, 2025, 1:35:39 PM"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/doc/设计文档/设计文档第四版.md b/doc/设计文档/设计文档第四版.md
new file mode 100644
index 0000000..34c5d88
--- /dev/null
+++ b/doc/设计文档/设计文档第四版.md
@@ -0,0 +1,436 @@
+# 数学题库生成系统详细设计文档
+
+## 项目概述
+
+### 项目目标
+为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
+
+### 技术栈
+
+| 类型 | 技术 | 说明 |
+| -------- | -------------------------- | ---------------------------------------- |
+| 开发语言 | Java 21+ | 保证跨平台兼容性,支持 JavaFX |
+| GUI框架 | JavaFX 17+ | 实现桌面图形化界面,提供组件化开发能力 |
+| 依赖管理 | Maven | 管理 JavaFX、JSON 解析等依赖 |
+| JSON解析 | Gson 2.10+ | 序列化 / 反序列化用户数据、题目数据 |
+| 邮件发送 | JavaMail API + 第三方 SMTP | 发送注册码(如 QQ 邮箱 / 网易邮箱 SMTP) |
+| 数据加密 | BCrypt 0.9.0+ | 加密存储用户密码,避免明文泄露 |
+| 构建打包 | Maven Shade Plugin | 打包成可执行 JAR,支持直接运行 |
+
+### 总体设计
+
+#### 项目目录结构
+
+MathQuizApp/
+├── .gitignore
+├── pom.xml
+├── README.md
+│
+├── doc/
+│ ├── 需求分析/
+│ │ └── 需求分析第一版.md
+│ └── 设计文档/
+│ ├── 设计文档第一版.md
+│ ├── 设计文档第二版.md
+│ ├── 设计文档第三版.md
+│ └── 设计文档第四版.md
+│
+├── data/ # 运行时数据(不提交)
+│ ├── users/ # 用户 JSON 文件(按邮箱哈希命名)
+│ ├── history/ # 答题记录(如:初中-王五_1759552247819.txt)
+│ ├── temp_codes/ # 【建议新增】临时注册码目录(替代 registration_codes.txt)
+│ └── users.json # 【建议移除】单文件存储易冲突,应按用户分文件
+│
+└── src/
+ └── main/
+ └── java/
+ └── com/
+ └── mathquiz/ # ← 包名应全小写
+ ├── Main.java # 程序入口
+ │
+ ├── model/ # 数据模型
+ │ ├── User.java
+ │ ├── Grade.java
+ │ ├── ChoiceQuestion.java
+ │ ├── QuizResult.java # 答题结果(含分数、时间等)
+ │ └── QuizHistory.java # 历史记录(可选)
+ │
+ ├── service/ # 业务逻辑
+ │ ├── UserService.java
+ │ ├── QuizService.java
+ │ ├── FileIOService.java
+ │ │
+ │ └── question_generator/
+ │ ├── QuestionFactoryManager.java
+ │ │
+ │ ├── factory/ # 工厂类(按年级)
+ │ │ ├── QuestionFactory.java (interface)
+ │ │ ├── ElementaryQuestionFactory.java
+ │ │ ├── MiddleQuestionFactory.java
+ │ │ └── HighQuestionFactory.java
+ │ │
+ │ └── strategy/ # 策略类(按运算类型)
+ │ ├── QuestionStrategy.java (interface)
+ │ ├── AbstractQuestionStrategy.java
+ │ │
+ │ ├── elementary/
+ │ │ ├── AdditionStrategy.java
+ │ │ ├── SubtractionStrategy.java
+ │ │ ├── MultiplicationStrategy.java
+ │ │ ├── DivisionStrategy.java
+ │ │ ├── ParenthesesAddStrategy.java
+ │ │ └── ParenthesesMultiplyStrategy.java
+ │ │
+ │ ├── middle/
+ │ │ ├── SquareStrategy.java
+ │ │ ├── SqrtStrategy.java
+ │ │ ├── SquareAddStrategy.java
+ │ │ ├── SqrtAddStrategy.java
+ │ │ └── MixedSquareSqrtStrategy.java
+ │ │
+ │ └── high/
+ │ ├── SinStrategy.java
+ │ ├── CosStrategy.java
+ │ ├── TanStrategy.java
+ │ └── TrigIdentityStrategy.java
+ │
+ ├── ui/ # JavaFX 界面
+ │ ├── MainWindow.java
+ │ ├── RegisterPanel.java
+ │ ├── LoginPanel.java
+ │ ├── PasswordModifyPanel.java
+ │ ├── GradeSelectPanel.java
+ │ ├── QuizPanel.java
+ │ └── ResultPanel.java
+ │
+ └── util/ # 工具类
+ ├── PasswordValidator.java
+ ├── EmailUtil.java
+ ├── RandomUtils.java
+ └── FileUtils.java
+
+## 详细模块设计
+
+### 模型层设计
+
+#### User类
+- String username
+- String password // 加密后的密码
+- String email
+- Grade grade
+- int totalQuizzes // 总答题次数
+- double averageScore // 平均分
+- Date registrationDate // 注册时间
+
+#### ChoiceQuestion
+- Sting questionText // 题目文本
+- Object correctAnswer // 正确答案
+- List> options // 选项列表
+- Grade grade // 所属学段
+
+#### Grade 枚举
+- ELEMENTARY // 小学
+- MIDDLE // 初中
+- HIGH // 高中
+
+#### QuizResult
+
+- int totalQuestions; // 总题数
+- int correctCount; // 正确题数
+- int wrongCount; // 错误题数
+- int score; // 得分
+
+#### QuizHistory
+
+- String username; // 用户名
+- Date timestamp; // 答题时间
+- List questions; // 题目列表
+- List userAnswers; // 用户答案列表
+- int score; // 得分
+
+### 工具层设计
+
+#### EmailUtil
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------------- | --------------------------------------- | ------- | ------------------------------------------------------------ |
+| sendRegistrationCode | String toEmail, String registrationCode | boolean | 模拟发送注册码邮件(预留接口),打印收件人邮箱和注册码,返回 true。 |
+| sendPasswordReset | String toEmail, String newPassword | boolean | 模拟发送密码重置邮件(预留接口),打印收件人邮箱和新密码,返回 true。 |
+| isValidEmail | String email | boolean | 验证邮箱格式,使用正则表达式`^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$`检查,为空或不匹配则返回 false。 |
+
+#### RandomUtils
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------ | ---------------------- | ------- | ------------------------------------------------------------ |
+| nextInt | int min, int max | int | 生成`[min, max]`范围内的随机整数,若 min > max 则抛 IllegalArgumentException。 |
+| randomChoice | T[] array | T | 从数组中随机选择一个元素,数组为空则抛 IllegalArgumentException。 |
+| randomChoice | List list | T | 从列表中随机选择一个元素,列表为空则抛 IllegalArgumentException。 |
+| shuffle | List list | void | 打乱列表元素顺序(使用 Collections.shuffle)。 |
+| nextDouble | double min, double max | double | 生成`[min, max)`范围内的随机双精度浮点数,若 min > max 则抛异常。 |
+| nextBoolean | 无参数 | boolean | 生成随机布尔值。 |
+| probability | double probability | boolean | 按给定概率(0.0-1.0)返回 true,概率超出范围则抛异常。 |
+
+#### PasswordValidator
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------------------ | ---------------------------------------------- | ------- | ------------------------------------------------------------ |
+| validatePassword | String password | String | 验证密码格式,返回错误信息(null 表示有效)。检查非空、长度(6-20 位)、无空格、包含字母和数字。 |
+| isValid | String password | boolean | 调用 validatePassword,返回密码是否有效(错误信息为 null 则返回 true)。 |
+| getPasswordStrength | String password | String | 评估密码强度(弱 / 中 / 强),基于长度、大小写字母、数字、特殊字符等评分。 |
+| encrypt | String password | String | 使用 SHA-256 加密密码,返回 16 进制字符串,密码为 null 则抛异常。 |
+| matches | String plainPassword, String encryptedPassword | boolean | 验证明文密码加密后是否与加密密码匹配。 |
+| generateRegistrationCode | 无参数 | String | 生成 6-10 位随机注册码(包含大小写字母和数字)。 |
+| generateRegistrationCode | int minLen, int maxLen | String | 生成指定长度范围的注册码,确保至少包含 1 个大写、1 个小写字母和 1 个数字,最终打乱顺序。 |
+| generateRandomPassword | int length, boolean includeSpecialChars | String | 生成固定长度随机密码,可包含特殊字符,确保至少 1 个字母和 1 个数字,最终打乱顺序。 |
+| shuffleString | String str | String | 私有方法,使用 Fisher-Yates 算法打乱字符串顺序。 |
+| isWeakPassword | String password | boolean | 检查密码是否为常见弱密码(如 123456)或连续字符(如 abcdef)。 |
+| getPasswordSuggestion | String password | String | 根据密码情况返回建议(错误信息或强度提升建议)。 |
+
+#### FileUtils
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------------------- | ------------------------------------ | ------- | ------------------------------------------------------------ |
+| readFileToString | String filePath | String | 读取文件内容为字符串(UTF-8 编码),失败则抛 IOException。 |
+| writeStringToFile | String filePath, String content | void | 将字符串写入文件(UTF-8 编码),失败则抛 IOException。 |
+| createDirectoryIfNotExists | String dirPath | void | 若目录不存在则创建(包括父目录),失败则抛 IOException。 |
+| exists | String filePath | boolean | 检查文件是否存在。 |
+| deleteFile | String filePath | boolean | 删除文件,返回是否成功(失败打印异常)。 |
+| listFiles | String dirPath | File[] | 获取目录下所有文件,目录不存在或非目录则返回空数组。 |
+| appendToFile | String filePath, String content | void | 追加内容到文件末尾(UTF-8 编码),失败则抛 IOException。 |
+| copyFile | String sourcePath, String targetPath | void | 复制文件(覆盖目标文件),失败则抛 IOException。 |
+| getFileSize | String filePath | long | 获取文件大小(字节),失败则抛 IOException。 |
+| saveAsJson | Object data, String filePath | void | 将对象序列化为 JSON 并保存到文件,失败则抛 IOException。 |
+| readJsonToObject | String filePath, Class classOfT | T | 从 JSON 文件反序列化为指定类型对象,失败则抛 IOException。 |
+| readJsonToObject | String filePath, Type typeOfT | T | 从 JSON 文件反序列化为泛型对象(支持泛型),失败则抛 IOException。 |
+| toJson | Object data | String | 将对象转换为 JSON 字符串。 |
+| fromJson | String json, Class classOfT | T | 将 JSON 字符串反序列化为指定类型对象。 |
+| fromJson | String json, Type typeOfT | T | 将 JSON 字符串反序列化为泛型对象(支持泛型)。 |
+
+### 服务层设计
+
+#### UserService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------------------------- | ------------------------------------------------------------ | ----------------------------- | ------------------------------------------------------------ |
+| generateRegistrationCode | String email | String | 验证邮箱格式,生成 6-10 位注册码及过期时间(10 分钟),保存到文件并返回注册码 |
+| saveRegistrationCodeToFile | String email, String code, long expiryTime | void | 读取现有注册码,添加 / 更新当前邮箱的注册码,保存到文件 |
+| loadRegistrationCodesFromFile | 无 | Map | 从文件读取注册码,解析为邮箱 - 注册码映射返回 |
+| verifyRegistrationCode | String email, String code | boolean | 验证邮箱对应的注册码是否有效(未过期且匹配),有效则删除注册码 |
+| saveAllRegistrationCodes | Map codes | void | 将所有注册码保存到文件 |
+| cleanExpiredCodes | 无 | void | 清理过期的注册码并保存到文件 |
+| register | String username, String password, String email, String verificationCode | User | 验证注册码、用户名格式、用户名唯一性、密码强度、邮箱格式,提取学段,加密密码,创建并保存用户 |
+| login | String username, String password | User | 验证用户名存在性及密码正确性,设置当前用户并保存 |
+| autoLogin | 无 | User | 从文件加载当前用户并返回(未完全展示) |
+
+#### QuizService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------------------- | -------------------------------------------- | -------------- | ------------------------------------------------------------ |
+| startNewQuiz | User user, int questionCount | void | 初始化答题会话(清空题目、答案,重置索引),获取用户历史题目,根据用户学段生成指定数量的新题目(排除历史题目) |
+| getRecentHistoryQuestions | 无 | Set | 从文件读取历史题目,转换为 Set 返回 |
+| getCurrentQuestion | 无 | ChoiceQuestion | 返回当前索引对应的题目,索引无效则返回 null |
+| getQuestion | int index | ChoiceQuestion | 返回指定索引的题目,索引无效则返回 null |
+| getAllQuestions | 无 | List | 返回当前所有题目的副本 |
+| nextQuestion | 无 | boolean | 当前索引加 1(移至下一题),成功返回 true,否则 false |
+| previousQuestion | 无 | boolean | 当前索引减 1(移至上一题),成功返回 true,否则 false |
+| goToQuestion | int index | boolean | 跳至指定索引题目,成功返回 true,否则 false |
+| getCurrentQuestionIndex | 无 | int | 返回当前题目索引 |
+| getTotalQuestions | 无 | int | 返回总题目数量 |
+| isFirstQuestion | 无 | boolean | 判断当前是否为第一题 |
+| isLastQuestion | 无 | boolean | 判断当前是否为最后一题 |
+| submitAnswer | int questionIndex, int optionIndex | boolean | 提交指定题目的答案,验证索引有效性后保存答案,返回答案是否正确 |
+| submitCurrentAnswer | int optionIndex | boolean | 提交当前题目的答案,调用 submitAnswer (currentQuestionIndex, optionIndex) |
+| getUserAnswer | int questionIndex | Integer | 返回指定题目的用户答案,索引无效返回 null |
+| getAllUserAnswers | 无 | List | 返回所有用户答案的副本 |
+| isAllAnswered | 无 | boolean | 判断所有题目是否都已作答 |
+| getAnsweredCount | 无 | int | 返回已作答的题目数量 |
+| calculateResult | 无 | QuizResult | 计算答题结果(总题数、正确数、错误数、分数)并返回 |
+| getCorrectQuestionIndices | 无 | List | 返回所有回答正确的题目索引 |
+| getWrongQuestionIndices | 无 | List | 返回所有回答错误的题目索引 |
+| getUnansweredQuestionIndices | 无 | List | 返回所有未作答的题目索引 |
+| checkAnswer | ChoiceQuestion question, int userAnswerIndex | boolean | 验证用户答案是否正确(比较选项与正确答案) |
+| getCorrectAnswerIndex | ChoiceQuestion question | int | 返回题目的正确答案在选项中的索引 |
+| getCorrectAnswerLetter | ChoiceQuestion question | String | 返回正确答案的字母形式(A/B/C/D) |
+| getAccuracy | QuizResult result | double | 计算答题正确率(正确数 / 总数 ×100%) |
+| isPassed | QuizResult result | boolean | 判断是否及格(分数≥60) |
+| getGrade | QuizResult result | String | 根据分数返回评级(优秀 / 良好 / 中等 / 及格 / 不及格) |
+| getCorrectCount | QuizHistory history | int | 计算历史记录中回答正确的题目数量 |
+| getWrongCount | QuizHistory history | int | 计算历史记录中回答错误的题目数量 |
+| formatQuestion | ChoiceQuestion question | String | 格式化题目文本及选项(未完全展示) |
+
+#### FileIOService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| --------------------- | ----------------------- | ------- | ------------------------------------------------------------ |
+| initDataDirectory | 无 | void | 初始化数据目录(data、users、history),若用户文件不存在则创建空文件 |
+| saveUser | User user | void | 读取用户列表,若用户已存在则更新,否则添加,保存回文件 |
+| loadAllUsers | 无 | List | 从文件读取所有用户并返回 |
+| findUserByUsername | String username | User | 查找并返回指定用户名的用户,不存在则返回 null |
+| isUsernameExists | String username | boolean | 判断用户名是否已存在 |
+| saveCurrentUser | User user | void | 将当前用户保存到文件 |
+| loadCurrentUser | 无 | User | 从文件加载当前用户,不存在则返回 null |
+| clearCurrentUser | 无 | void | 删除当前用户文件 |
+| saveQuizHistory | QuizHistory history | void | 将答题历史格式化并保存到文件(包含题目、答案、结果等) |
+| getHistoryQuestions | 无 | List | 读取最近 20 个历史文件,提取题目文本并返回 |
+| calculateCorrectCount | QuizHistory history | int | 计算历史记录中正确的题目数量 |
+| getCorrectAnswerIndex | ChoiceQuestion question | int | 返回题目的正确答案在选项中的索引 |
+| sanitizeFilename | String filename | String | 清理文件名中的特殊字符(替换为下划线) |
+
+#### QuestionFactoryManager
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------------- | -------------------------------------------- | ------ | ------------------------------------------------------------ |
+| generateQuestions | Grade grade, int count, Set historyQuestions | List | 根据学段获取对应工厂,生成指定数量的题目(排除历史题目),最多尝试 count×10 次,返回生成的题目列表 |
+
+#### QuestionFactory(接口)
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------------- | -------- | -------------- | ---------------------------------------------- |
+| createQuestion | 无 | ChoiceQuestion | 生成并返回一道选择题(接口方法,由实现类实现) |
+| getSupportedGrade | 无 | Grade | 返回工厂支持的学段(接口方法,由实现类实现) |
+
+#### ElementaryQuestionFactory
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------- | -------- | -------------- | ------------------------------------------------------------ |
+| createQuestion | 无 | ChoiceQuestion | 从策略列表(strategies)中随机选择一个题目生成策略(QuestionStrategy),调用其 generate () 方法生成并返回选择题 |
+
+#### MiddleQuestionFactory
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------------- | -------- | -------------- | ------------------------------------------------------------ |
+| HighQuestionFactory | 无 | 无(构造方法) | 初始化策略列表`strategies`,并注册所有高中题目生成策略(`SinStrategy`、`CosStrategy`、`TanStrategy`、`TrigIdentityStrategy`) |
+| createQuestion | 无 | ChoiceQuestion | 通过`RandomUtils`从策略列表中随机选择一个题目生成策略,调用该策略的`generate()`方法生成并返回选择题 |
+| getSupportedGrade | 无 | Grade | 返回`Grade.HIGH`,表示该工厂支持高中年级的题目生成 |
+
+#### HighQuestionFactory
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| --------------------- | -------- | -------------- | ------------------------------------------------------------ |
+| MiddleQuestionFactory | 无 | 无(构造方法) | 初始化策略列表`strategies`,并注册所有初中题目生成策略(`SquareStrategy`、`SquareAddStrategy`、`SqrtStrategy`、`SqrtAddStrategy`、`MixedSquareSqrtStrategy`) |
+| createQuestion | 无 | ChoiceQuestion | 通过`RandomUtils`从策略列表中随机选择一个题目生成策略,调用该策略的`generate()`方法生成并返回选择题 |
+| getSupportedGrade | 无 | Grade | 返回`Grade.MIDDLE`,表示该工厂支持初中年级的题目生成 |
+
+#### QuestionStrategy(接口)
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | -------------------------- |
+| generate | 无 | ChoiceQuestion | 定义生成题目的接口方法 |
+| getStrategyName | 无 | String | 定义获取策略名称的接口方法 |
+
+#### AbstractQuestionStrategy
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------------------------------- | ---------------------------------------------------- | ------------ | ------------------------------------------------------------ |
+| generateNumericOptions | double correctAnswer | List | 生成包含正确答案和 3 个干扰项的数值选项,打乱顺序后返回 |
+| generateNumericOptionsWithCommonError | double correctAnswer, double commonError | List | 生成包含正确答案、常见错误答案和其他干扰项的数值选项,打乱顺序后返回 |
+| generateStringOptions | String correctAnswer, List allPossibleValues | List | 生成包含正确答案和 3 个干扰项的字符串选项(如三角函数值),打乱顺序后返回 |
+
+#### AdditionStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成加法题(如 3 + 5),随机生成两个加数(1-30),计算和作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “加法” |
+
+#### DivisionStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成除法题(确保整除,如 8 ÷ 2),随机生成除数(2-10)和商(1-10),计算被除数(除数 × 商),生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “除法” |
+
+#### MultiplicationStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成乘法题(如 3 × 4),随机生成两个因数(1-12),计算乘积作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “乘法” |
+
+#### ParenthesesAddStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成带括号的加法乘法混合题(如 (a + b) × c),随机生成两个加数(1-20)和乘数(2-10),计算和与乘数的积作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “括号加法乘法” |
+
+#### ParenthesesMultiplyStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成带括号的减法乘法混合题(如 (a - b) × c),随机生成两个数(1-20)和乘数(2-10),取大数减小数的差与乘数相乘作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “括号减法乘法” |
+
+#### SubtractionStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成减法题(如 5 - 3),随机生成两个数(1-30),取大数减小数确保结果为正,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “减法” |
+
+#### MixedSquareSqrtStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成平方与开方混合题(如√49 + 3²),随机生成开方根值(2-8)和平方底数(2-6),计算开方值与平方值的和作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “平方开方混合” |
+
+#### SqrtAddStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成开方与加法混合题(如√49 + 5),随机生成开方根值(2-10)和加数(1-20),计算根值与加数的和作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “开方加法” |
+
+#### SqrtStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成开方运算题(如√49),随机生成根值(2-12),计算开方结果作为答案,选项包含常见错误(被开方数 ÷2) |
+| getStrategyName | 无 | String | 返回策略名称 “开方” |
+
+#### SquareAddStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成平方与加法混合题(如 5² + 10),随机生成平方底数(2-10)和加数(1-20),计算平方值与加数的和作为答案,生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “平方加法” |
+
+#### SquareStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成平方运算题(如 5²),随机生成底数(1-15),计算平方值作为答案,选项包含常见错误(底数 ×2) |
+| getStrategyName | 无 | String | 返回策略名称 “平方” |
+
+#### CosStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成余弦函数题(如 cos (45°)),从预设特殊角(0°、30° 等)中随机选择,答案为对应角的余弦值,从所有余弦值中生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “余弦函数” |
+
+#### SinStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成正弦函数题(如 sin (30°)),从预设特殊角(0°、30° 等)中随机选择,答案为对应角的正弦值,从所有正弦值中生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “正弦函数” |
+
+#### TanStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成正切函数题(如 tan (45°)),从预设特殊角(0°、30° 等)中随机选择,答案为对应角的正切值,从所有正切值中生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “正切函数” |
+
+#### TrigIdentityStrategy
+
+| 方法名 | 参数列表 | 返回值类型 | 逻辑描述 |
+| --------------- | -------- | -------------- | ------------------------------------------------------------ |
+| generate | 无 | ChoiceQuestion | 生成三角恒等式题(如 sin²(30°) + cos²(30°) = ?),从 30°、45° 等特殊角中选择,答案固定为 1,从给定值列表生成选项 |
+| getStrategyName | 无 | String | 返回策略名称 “三角恒等式” |
+
+### UI层接口设计
+
+####
diff --git a/pom.xml b/pom.xml
index dc0b01f..b719a82 100644
--- a/pom.xml
+++ b/pom.xml
@@ -2,7 +2,7 @@
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
com.mathquiz
@@ -11,74 +11,100 @@
jar
Math Quiz Application
- 小初高数学学习软件 - Swing版本
+ 小初高数学学习软件 - JavaFX版本
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
UTF-8
21
21
+ 21.0.2
-
com.google.code.gson
gson
2.10.1
+
+
+ org.openjfx
+ javafx-base
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-controls
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-fxml
+ ${javafx.version}
+
+
+ org.openjfx
+ javafx-graphics
+ ${javafx.version}
+
-
org.apache.maven.plugins
maven-compiler-plugin
3.11.0
- 17
- 17
+ ${maven.compiler.source}
+ ${maven.compiler.target}
UTF-8
-
org.apache.maven.plugins
- maven-jar-plugin
- 3.3.0
+ maven-resources-plugin
+ 3.3.1
-
-
- com.mathquiz.Main
- true
-
-
+ UTF-8
-
- org.apache.maven.plugins
- maven-shade-plugin
- 3.5.1
+ org.openjfx
+ javafx-maven-plugin
+ 0.0.8
+
+ com.mathquiz.MathQuizApp/com.Test
+
+
+ false
+ ${project.name}
+ com.mathquiz
+ ${project.version}
+ true
+ true
+
+
- package
+ default-cli
- shade
+ jpackage
-
-
-
- com.mathquiz.Main
-
-
- MathQuizApp
-
+
\ No newline at end of file
diff --git a/src/main/java/com/Test.java b/src/main/java/com/Test.java
new file mode 100644
index 0000000..58a542c
--- /dev/null
+++ b/src/main/java/com/Test.java
@@ -0,0 +1,24 @@
+// src/main/java/com/Main.java
+package com;
+
+import com.ui.MainWindow;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+public class Test extends Application {
+
+ @Override
+ public void start(Stage primaryStage) {
+ MainWindow mainWindow = new MainWindow(primaryStage);
+ Scene scene = new Scene(mainWindow, 1366, 786);
+ primaryStage.setTitle("中小学数学答题系统");
+ primaryStage.setScene(scene);
+ primaryStage.setResizable(true);
+ primaryStage.show();
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/model/Grade.java b/src/main/java/com/model/Grade.java
index afa5077..9d8be2e 100644
--- a/src/main/java/com/model/Grade.java
+++ b/src/main/java/com/model/Grade.java
@@ -1,9 +1,29 @@
package com.model;
-//学段
public enum Grade {
- ELEMENTARY, // 小学
- MIDDLE, // 初中
- HIGH // 高中
-}
\ No newline at end of file
+ // 枚举常量,初始化时传入对应的中文描述
+ ELEMENTARY("小学"),
+ MIDDLE("初中"),
+ HIGH("高中");
+
+ private final String chineseName;
+
+ Grade(String chineseName) {
+ this.chineseName = chineseName;
+ }
+
+ public String getChineseName() {
+ return chineseName;
+ }
+
+ public static Grade valueOfChinese(String chineseName) {
+ // 遍历所有枚举常量,匹配中文描述
+ for (Grade grade : Grade.values()) {
+ if (grade.chineseName.equals(chineseName)) {
+ return grade;
+ }
+ }
+ throw new IllegalArgumentException("不存在对应的年级:" + chineseName);
+ }
+}
diff --git a/src/main/java/com/model/QuizResult.java b/src/main/java/com/model/QuizResult.java
index eb70db5..6153306 100644
--- a/src/main/java/com/model/QuizResult.java
+++ b/src/main/java/com/model/QuizResult.java
@@ -52,11 +52,7 @@ public class QuizResult {
@Override
public String toString() {
- return "QuizResult{" +
- "totalQuestions=" + totalQuestions +
- ", correctCount=" + correctCount +
- ", wrongCount=" + wrongCount +
- ", score=" + score +
- '}';
+ int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
+ return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent + "%";
}
}
\ No newline at end of file
diff --git a/src/main/java/com/model/User.java b/src/main/java/com/model/User.java
index 78d530a..9a0d3d3 100644
--- a/src/main/java/com/model/User.java
+++ b/src/main/java/com/model/User.java
@@ -1,11 +1,13 @@
package com.model;
import java.util.Date;
+import java.util.UUID;
//用户
public class User {
+ private final String userId; // 不可变,账号唯一标识
private String username; // 用户名
private String password; // 密码(加密后)
private String email; // 邮箱
@@ -19,8 +21,9 @@ public class User {
/**
* 完整构造方法(用于从文件加载)
*/
- public User(String username, String password, String email, Grade grade,
+ public User(String userId, String username, String password, String email, Grade grade,
int totalQuizzes, double averageScore, Date registrationDate) {
+ this.userId = userId;
this.username = username;
this.password = password;
this.email = email;
@@ -34,6 +37,7 @@ public class User {
* 简化构造方法(用于新用户注册)
*/
public User(String username, String password, String email, Grade grade) {
+ this.userId = UUID.randomUUID().toString();
this.username = username;
this.password = password;
this.email = email;
@@ -43,13 +47,15 @@ public class User {
this.registrationDate = new Date();
}
-
+ public String getUserId() {
+ return userId;
+ }
public String getUsername() {
return username;
}
- public void setUsername(String username) {
+ public void setUsername(String username){
this.username = username;
}
diff --git a/src/main/java/com/service/FileIOService.java b/src/main/java/com/service/FileIOService.java
index 4bfa68e..3a2a56e 100644
--- a/src/main/java/com/service/FileIOService.java
+++ b/src/main/java/com/service/FileIOService.java
@@ -57,7 +57,7 @@ public class FileIOService {
boolean found = false;
for (int i = 0; i < users.size(); i++) {
- if (users.get(i).getUsername().equals(user.getUsername())) {
+ if (users.get(i).getUserId().equals(user.getUserId())) {
users.set(i, user);
found = true;
break;
@@ -94,10 +94,26 @@ public class FileIOService {
return null;
}
+ public User findUserByEmail(String email) throws IOException {
+ List users = loadAllUsers();
+
+ for (User user : users) {
+ if (user.getEmail().equals(email)) {
+ return user;
+ }
+ }
+
+ return null;
+ }
+
public boolean isUsernameExists(String username) throws IOException {
return findUserByUsername(username) != null;
}
+ public boolean isEmailExists(String email) throws IOException {
+ return findUserByEmail(email) != null;
+ }
+
public void saveCurrentUser(User user) throws IOException {
FileUtils.saveAsJson(user, CURRENT_USER_FILE);
}
diff --git a/src/main/java/com/service/QuizService.java b/src/main/java/com/service/QuizService.java
index 1ce5243..2b766a5 100644
--- a/src/main/java/com/service/QuizService.java
+++ b/src/main/java/com/service/QuizService.java
@@ -16,6 +16,7 @@ public class QuizService {
private List currentQuestions;
private List userAnswers;
private int currentQuestionIndex;
+ private int answerNumber;
// ==================== 构造方法 ====================
@@ -173,6 +174,10 @@ public class QuizService {
return count;
}
+ public boolean isAnswered(int questionIndex) {
+ return userAnswers.get(questionIndex) != null ;
+ }
+
// ==================== 成绩计算 ====================
public QuizResult calculateResult() {
@@ -422,6 +427,14 @@ public class QuizService {
// ==================== Getters ====================
+ public int getAnswerNumber() {
+ return answerNumber;
+ }
+
+ public void setAnswerNumber(int answerNumber) {
+ this.answerNumber = answerNumber;
+ }
+
public List getCurrentQuestions() {
return new ArrayList<>(currentQuestions);
}
diff --git a/src/main/java/com/service/UserService.java b/src/main/java/com/service/UserService.java
index 06c78bb..ed75153 100644
--- a/src/main/java/com/service/UserService.java
+++ b/src/main/java/com/service/UserService.java
@@ -25,7 +25,10 @@ public class UserService {
private final FileIOService fileIOService;
private User currentUser;
- private static final Pattern USERNAME_PATTERN = Pattern.compile("^(小学|初中|高中)-([\\u4e00-\\u9fa5a-zA-Z]+)$");
+ // [学段]-[姓名]
+ // private static final Pattern USERNAME_PATTERN = Pattern.compile("^(小学|初中|高中)-([\\u4e00-\\u9fa5a-zA-Z]+)$");
+
+ // [用户名]@[域名主体].[顶级域名]
private static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
// ==================== 构造方法 ====================
@@ -233,27 +236,27 @@ public class UserService {
/**
* 用户注册(需要验证码)
*/
- public User register(String username, String password, String email, String verificationCode) throws IOException {
+ public User register(String password, String email, String verificationCode) throws IOException {
// 1. 验证注册码
if (!verifyRegistrationCode(email, verificationCode)) {
throw new IllegalArgumentException("注册码错误!");
}
- // 2. 验证用户名格式
- if (!validateUsername(username)) {
- throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)");
- }
+// // 2. 验证用户名格式
+// if (!validateUsername(username)) {
+// throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)");
+// }
// 3. 验证用户名是否已存在
- if (fileIOService.isUsernameExists(username)) {
- throw new IllegalArgumentException("用户名已存在!");
+ if (fileIOService.isEmailExists(email)) {
+ throw new IllegalArgumentException("邮箱已经注册!");
}
- // 4. 验证密码强度
- String passwordError = PasswordValidator.validatePassword(password);
- if (passwordError != null) {
- throw new IllegalArgumentException(passwordError);
- }
+// // 4. 验证密码强度
+// String passwordError = PasswordValidator.validatePassword(password);
+// if (passwordError != null) {
+// throw new IllegalArgumentException(passwordError);
+// }
// 5. 验证邮箱格式
if (!validateEmail(email)) {
@@ -261,21 +264,28 @@ public class UserService {
}
// 6. 从用户名中提取学段
- Grade grade = extractGradeFromUsername(username);
+// Grade grade = extractGradeFromUsername(username);
+ Grade grade = Grade.ELEMENTARY;
// 7. 加密密码
- String hashedPassword = hashPassword(password);
+ String hashedPassword = PasswordValidator.encrypt(password);
// 8. 创建用户对象
- User user = new User(username, hashedPassword, email, grade);
+ User user = new User(email, hashedPassword, email, grade);
+ this.setCurrentUser(user);
// 9. 保存到文件
fileIOService.saveUser(user);
- System.out.println("✓ 用户注册成功:" + username);
+ // System.out.println("✓ 用户注册成功:" + email);
return user;
}
+ public void setCurrentUser(User user) throws IOException {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+
// ==================== 用户登录 ====================
public User login(String username, String password) throws IOException {
@@ -285,7 +295,7 @@ public class UserService {
throw new IllegalArgumentException("用户名不存在!");
}
- String hashedPassword = hashPassword(password);
+ String hashedPassword = PasswordValidator.encrypt(password);
if (!user.getPassword().equals(hashedPassword)) {
throw new IllegalArgumentException("密码错误!");
}
@@ -293,7 +303,7 @@ public class UserService {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
- System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "(" + getGradeDisplayName(user) + ")");
+ // System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "(" + getGradeDisplayName(user) + ")");
return user;
}
@@ -302,7 +312,7 @@ public class UserService {
if (user != null) {
this.currentUser = user;
- System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
+ // System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
}
return user;
@@ -310,7 +320,7 @@ public class UserService {
public void logout() {
if (currentUser != null) {
- System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
+ // System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
this.currentUser = null;
fileIOService.clearCurrentUser();
}
@@ -326,8 +336,8 @@ public class UserService {
// ==================== 密码管理 ====================
- public boolean changePassword(User user, String oldPassword, String newPassword) throws IOException {
- String hashedOldPassword = hashPassword(oldPassword);
+ public boolean changePassword(User user, String oldPassword, String newPassword, String confirmPassword) throws IOException {
+ String hashedOldPassword = PasswordValidator.encrypt(oldPassword);
if (!user.getPassword().equals(hashedOldPassword)) {
throw new IllegalArgumentException("旧密码错误!");
}
@@ -341,7 +351,11 @@ public class UserService {
throw new IllegalArgumentException("新密码不能与旧密码相同!");
}
- String hashedNewPassword = hashPassword(newPassword);
+ if (!oldPassword.equals(confirmPassword)) {
+ throw new IllegalArgumentException("两次新密码不同!");
+ }
+
+ String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
@@ -371,7 +385,7 @@ public class UserService {
throw new IllegalArgumentException(passwordError);
}
- String hashedNewPassword = hashPassword(newPassword);
+ String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
@@ -395,10 +409,38 @@ public class UserService {
fileIOService.saveCurrentUser(user);
}
- System.out.println("✓ 邮箱更新成功");
+ // System.out.println("✓ 邮箱更新成功");
return true;
}
+ public void updateUsername(User user, String newUsername) throws IOException {
+ if (newUsername.isEmpty()) {
+ throw new IllegalArgumentException("用户名不为空!");
+ }
+ user.setUsername(newUsername);
+ fileIOService.saveUser(user);
+
+ if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+ }
+
+ public void updateGrade(User user, String chinesename) throws IOException {
+ if (chinesename.isEmpty()) {
+ throw new IllegalArgumentException("学段中文名为空");
+ }
+ Grade newGrade = Grade.valueOfChinese(chinesename);
+
+ user.setGrade(newGrade);
+ fileIOService.saveUser(user);
+
+ if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+ }
+
public void updateUserStatistics(User user, int score) throws IOException {
int oldTotal = user.getTotalQuizzes();
double oldAverage = user.getAverageScore();
@@ -422,23 +464,23 @@ public class UserService {
// ==================== 业务逻辑方法====================
- /**
- * 从用户名提取真实姓名
- */
- public String getRealName(User user) {
- if (user == null || user.getUsername() == null) {
- return "";
- }
-
- String username = user.getUsername();
- int dashIndex = username.indexOf('-');
-
- if (dashIndex > 0 && dashIndex < username.length() - 1) {
- return username.substring(dashIndex + 1);
- }
-
- return username;
- }
+// /**
+// * 从用户名提取真实姓名
+// */
+// public String getRealName(User user) {
+// if (user == null || user.getUsername() == null) {
+// return "";
+// }
+//
+// String username = user.getUsername();
+// int dashIndex = username.indexOf('-');
+//
+// if (dashIndex > 0 && dashIndex < username.length() - 1) {
+// return username.substring(dashIndex + 1);
+// }
+//
+// return username;
+// }
/**
* 获取学段中文名称
@@ -467,7 +509,6 @@ public class UserService {
StringBuilder sb = new StringBuilder();
sb.append("========== 用户统计 ==========\n");
sb.append("用户名:").append(user.getUsername()).append("\n");
- sb.append("真实姓名:").append(getRealName(user)).append("\n");
sb.append("学段:").append(getGradeDisplayName(user)).append("\n");
sb.append("邮箱:").append(user.getEmail()).append("\n");
sb.append("总答题次数:").append(user.getTotalQuizzes()).append(" 次\n");
@@ -480,14 +521,14 @@ public class UserService {
// ==================== 验证工具方法 ====================
- private boolean validateUsername(String username) {
- if (username == null || username.trim().isEmpty()) {
- return false;
- }
-
- Matcher matcher = USERNAME_PATTERN.matcher(username);
- return matcher.matches();
- }
+// private boolean validateUsername(String username) {
+// if (username == null || username.trim().isEmpty()) {
+// return false;
+// }
+//
+// Matcher matcher = USERNAME_PATTERN.matcher(username);
+// return matcher.matches();
+// }
private boolean validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
@@ -498,35 +539,15 @@ public class UserService {
return matcher.matches();
}
- private Grade extractGradeFromUsername(String username) {
- if (username.startsWith("小学-")) {
- return Grade.ELEMENTARY;
- } else if (username.startsWith("初中-")) {
- return Grade.MIDDLE;
- } else if (username.startsWith("高中-")) {
- return Grade.HIGH;
- }
-
- throw new IllegalArgumentException("无法识别的学段");
- }
-
- private String hashPassword(String password) {
- try {
- MessageDigest digest = MessageDigest.getInstance("SHA-256");
- byte[] hash = digest.digest(password.getBytes());
-
- StringBuilder hexString = new StringBuilder();
- for (byte b : hash) {
- String hex = Integer.toHexString(0xff & b);
- if (hex.length() == 1) {
- hexString.append('0');
- }
- hexString.append(hex);
- }
-
- return hexString.toString();
- } catch (NoSuchAlgorithmException e) {
- throw new RuntimeException("密码加密失败", e);
- }
- }
+// private Grade extractGradeFromUsername(String username) {
+// if (username.startsWith("小学-")) {
+// return Grade.ELEMENTARY;
+// } else if (username.startsWith("初中-")) {
+// return Grade.MIDDLE;
+// } else if (username.startsWith("高中-")) {
+// return Grade.HIGH;
+// }
+//
+// throw new IllegalArgumentException("无法识别的学段");
+// }
}
\ No newline at end of file
diff --git a/src/main/java/com/ui/GradeSelectPanel.java b/src/main/java/com/ui/GradeSelectPanel.java
index f210860..391a409 100644
--- a/src/main/java/com/ui/GradeSelectPanel.java
+++ b/src/main/java/com/ui/GradeSelectPanel.java
@@ -1,7 +1,7 @@
-package com.mathquiz.ui;
+package com.ui;
-import com.mathquiz.model.Grade;
-import com.mathquiz.service.QuizService;
+import com.model.Grade;
+import com.service.QuizService;
import javafx.geometry.Insets;
import javafx.scene.control.Button;
import javafx.scene.control.TextInputDialog;
@@ -9,50 +9,4 @@ import javafx.scene.layout.VBox;
public class GradeSelectPanel extends VBox {
- public GradeSelectPanel(MainWindow mainWindow) {
- setPadding(new Insets(20));
- setSpacing(20);
-
- getChildren().addAll(
- new Button("小学") {{
- setOnAction(e -> startQuiz(mainWindow, Grade.PRIMARY));
- }},
- new Button("初中") {{
- setOnAction(e -> startQuiz(mainWindow, Grade.JUNIOR));
- }},
- new Button("高中") {{
- setOnAction(e -> startQuiz(mainWindow, Grade.SENIOR));
- }},
- new Button("修改密码") {{
- setOnAction(e -> mainWindow.showPasswordModifyPanel());
- }}
- );
- }
-
- private void startQuiz(MainWindow mainWindow, Grade grade) {
- TextInputDialog dialog = new TextInputDialog("20");
- dialog.setTitle("题目数量");
- dialog.setHeaderText("请输入题目数量(10-30)");
- dialog.setContentText("数量:");
-
- dialog.showAndWait().ifPresent(input -> {
- try {
- int count = Integer.parseInt(input);
- if (count < 10 || count > 30) {
- throw new NumberFormatException();
- }
-
- // 创建 QuizService(未来可注入)
- QuizService quizService = new QuizService(grade, mainWindow.getFileIOService());
- var questions = quizService.generateQuestions(
- mainWindow.getCurrentUser().getEmail(), count
- );
-
- mainWindow.showQuizPanel(questions, quizService);
-
- } catch (NumberFormatException e) {
- new Alert(Alert.AlertType.ERROR, "请输入10-30之间的整数").showAndWait();
- }
- });
- }
}
\ No newline at end of file
diff --git a/src/main/java/com/ui/InfGenPage.java b/src/main/java/com/ui/InfGenPage.java
new file mode 100644
index 0000000..98ca26e
--- /dev/null
+++ b/src/main/java/com/ui/InfGenPage.java
@@ -0,0 +1,126 @@
+// com/ui/InfGenPage.java
+package com.ui;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+import javafx.scene.text.TextAlignment;
+
+public class InfGenPage extends NavigablePanel {
+
+ private final TextField usernameField = new TextField();
+ private final TextField emailField = new TextField();
+ private final Label passwordLabel = new Label("******");
+ private final ChoiceBox gradeChoice = new ChoiceBox<>();
+ private final Spinner questionCountSpinner = new Spinner<>(10, 30, 10);
+ private final Button passwordModifyButton = new Button("修改密码");
+ private final Button generateButton = new Button("生成题目");
+
+ public InfGenPage(Runnable onBack, String currentUsername, String currentEmail) {
+ super(onBack);
+ initializeContent();
+ usernameField.setText(currentUsername);
+ emailField.setText(currentEmail);
+ }
+
+ @Override
+ protected void buildContent() {
+ // 外层容器:VBox 居中,带内边距和圆角阴影
+ VBox form = new VBox(UIConstants.DEFAULT_SPACING * 1.5);
+ form.setAlignment(Pos.CENTER);
+ form.setPadding(UIConstants.DEFAULT_PADDING);
+ form.setStyle(UIConstants.FORM_STYLE);
+ form.setMaxWidth(500);
+
+ // 标题居中
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+ titleLabel.setTextAlignment(TextAlignment.CENTER);
+ titleLabel.setMaxWidth(Double.MAX_VALUE);
+
+ // ========== 创建表单项行(关键:标签左对齐 + 固定宽度)==========
+ HBox usernameRow = createFormRow("用户名:", usernameField, null);
+ HBox passwordRow = createFormRow("密码:", passwordLabel, passwordModifyButton);
+ HBox emailRow = createFormRow("邮箱:", emailField, null);
+ HBox gradeRow = createFormRow("学段选择:", gradeChoice, null);
+ HBox countRow = createFormRow("题目数量:", questionCountSpinner, generateButton);
+
+ // ========== 配置控件样式 ==========
+ // 用户名输入框
+ usernameField.setStyle(UIConstants.INPUT_STYLE);
+ usernameField.setPrefWidth(200);
+ usernameField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
+
+ // 密码标签
+ passwordLabel.setStyle("-fx-font-size: " + UIConstants.INPUT_FONT_SIZE + "px;");
+
+ // 邮箱输入框
+ emailField.setStyle(UIConstants.INPUT_STYLE);
+ emailField.setPrefWidth(200);
+ emailField.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
+
+ // 学段选择框
+ gradeChoice.getItems().addAll("小学", "初中", "高中");
+ gradeChoice.setValue("小学");
+ gradeChoice.setStyle(UIConstants.INPUT_STYLE);
+ gradeChoice.setPrefWidth(200);
+
+ // 题目数量Spinner
+ questionCountSpinner.setEditable(true);
+ questionCountSpinner.setPrefWidth(200);
+ questionCountSpinner.getEditor().setStyle(UIConstants.INPUT_STYLE);
+ questionCountSpinner.getEditor().setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.INPUT_FONT_SIZE));
+
+ // 按钮样式统一
+ passwordModifyButton.setStyle(UIConstants.BUTTON_STYLE);
+ generateButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ generateButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ generateButton.setStyle(UIConstants.BUTTON_STYLE);
+
+ // ========== 添加到表单 ==========
+ form.getChildren().addAll(
+ titleLabel,
+ usernameRow,
+ emailRow,
+ passwordRow,
+ gradeRow,
+ countRow
+ );
+
+ this.setCenter(form);
+ }
+
+ /**
+ * 创建表单项行(标签左对齐,固定宽度,避免偏移)
+ */
+ private HBox createFormRow(String labelText, Control content, Button rightButton) {
+ HBox row = new HBox(15);
+ row.setAlignment(Pos.CENTER_LEFT); // ← 关键:让整行左对齐
+ row.setMaxWidth(Double.MAX_VALUE);
+
+ // 标签:左对齐,固定宽度,字体统一
+ Label label = new Label(labelText);
+ label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.NORMAL, UIConstants.LABEL_ITEM_TITLE_SIZE));
+ label.setTextAlignment(TextAlignment.LEFT); // ← 关键:标签文字左对齐
+ label.setPrefWidth(120); // 固定宽度,确保所有标签对齐
+
+ row.getChildren().addAll(label, content);
+ if (rightButton != null) {
+ row.getChildren().add(rightButton);
+ }
+
+ return row;
+ }
+
+ // Getters...
+ public TextField getUsernameField() { return usernameField; }
+ public TextField getEmailField() { return emailField; }
+ public ChoiceBox getGradeChoice() { return gradeChoice; }
+ public Spinner getQuestionCountSpinner() { return questionCountSpinner; }
+ public Button getGenerateButton() { return generateButton; }
+ public Button getPasswordModifyButton() { return passwordModifyButton; }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/LoginPage.java b/src/main/java/com/ui/LoginPage.java
new file mode 100644
index 0000000..e10a539
--- /dev/null
+++ b/src/main/java/com/ui/LoginPage.java
@@ -0,0 +1,61 @@
+// com/ui/LoginPage.java
+package com.ui;
+
+import com.model.User;
+import com.service.UserService;
+import javafx.concurrent.Task;
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.HBox;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+public class LoginPage extends NavigablePanel {
+
+ private final TextField usernameOrEmailField = new TextField();
+ private final PasswordField passwordField = new PasswordField();
+ private final Button loginButton = new Button("登录");
+ private final Hyperlink registerLink = new Hyperlink("注册");
+
+ public LoginPage(Runnable onBack) {
+ super(onBack);
+ initializeContent(); // 字段初始化
+ }
+
+ @Override
+ protected void buildContent() {
+ VBox form = new VBox(UIConstants.DEFAULT_SPACING);
+ form.setAlignment(Pos.CENTER);
+ form.setPadding(UIConstants.DEFAULT_PADDING);
+ form.setStyle(UIConstants.FORM_STYLE);
+
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+
+ usernameOrEmailField.setPromptText("邮箱/用户名");
+ usernameOrEmailField.setStyle(UIConstants.INPUT_STYLE);
+
+ passwordField.setPromptText("密码(6-10位,含大小写字母和数字)");
+ passwordField.setStyle(UIConstants.INPUT_STYLE);
+
+ loginButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ loginButton.setStyle(UIConstants.BUTTON_STYLE);
+ loginButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ loginButton.setOnMouseEntered(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
+ loginButton.setOnMouseExited(e -> loginButton.setStyle(UIConstants.BUTTON_STYLE));
+
+ registerLink.setStyle("-fx-text-fill: " + UIConstants.COLOR_ACCENT + ";");
+
+ HBox linkBox = new HBox(registerLink);
+ linkBox.setAlignment(Pos.CENTER);
+
+ form.getChildren().addAll(titleLabel, usernameOrEmailField, passwordField, loginButton, linkBox);
+ this.setCenter(form);
+ }
+
+ public TextField getUsernameOrEmailField() { return usernameOrEmailField; }
+ public PasswordField getPasswordField() { return passwordField; }
+ public Button getLoginButton() { return loginButton; }
+ public Hyperlink getRegisterLink() { return registerLink; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/LoginPanel.java b/src/main/java/com/ui/LoginPanel.java
deleted file mode 100644
index 5c06144..0000000
--- a/src/main/java/com/ui/LoginPanel.java
+++ /dev/null
@@ -1,44 +0,0 @@
-package com.mathquiz.ui;
-
-import com.mathquiz.model.User;
-import javafx.geometry.Insets;
-import javafx.scene.control.*;
-import javafx.scene.layout.VBox;
-
-public class LoginPanel extends VBox {
-
- private final TextField emailField = new TextField();
- private final PasswordField pwdField = new PasswordField();
-
- public LoginPanel(MainWindow mainWindow) {
- setPadding(new Insets(20));
- setSpacing(10);
-
- getChildren().addAll(
- new Label("登录"),
- new Label("邮箱:"),
- emailField,
- new Label("密码:"),
- pwdField,
- new Button("登录") {{
- setOnAction(e -> loginAction(mainWindow));
- }},
- new Hyperlink("没有账号?去注册") {{
- setOnAction(e -> mainWindow.showRegisterPanel());
- }}
- );
- }
-
- private void loginAction(MainWindow mainWindow) {
- String email = emailField.getText().trim();
- String password = pwdField.getText();
-
- User user = mainWindow.getUserService().login(email, password);
- if (user != null) {
- mainWindow.setCurrentUser(user);
- mainWindow.showGradeSelectPanel();
- } else {
- new Alert(Alert.AlertType.ERROR, "邮箱或密码错误").showAndWait();
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ui/MainWindow.java b/src/main/java/com/ui/MainWindow.java
index 52875e9..73d4e5d 100644
--- a/src/main/java/com/ui/MainWindow.java
+++ b/src/main/java/com/ui/MainWindow.java
@@ -1,77 +1,286 @@
-package com.mathquiz.ui;
+// com/ui/MainWindow.java
+package com.ui;
-import com.mathquiz.model.User;
-import com.mathquiz.service.*;
+import com.model.ChoiceQuestion;
+import com.model.User;
+import com.service.QuizService;
+import com.service.UserService;
+import javafx.application.Platform;
+import javafx.scene.Scene;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Toggle;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
+import java.io.IOException;
+import java.util.List;
-/**
- * 主窗口控制器,持有所有服务引用和当前用户状态
- * 所有 UI 面板通过此窗口切换
- */
public class MainWindow extends BorderPane {
-
- // 服务层引用(便于后期替换实现)
- private final UserService userService;
- private final FileIOService fileIOService;
+ private final UserService userService = new UserService();
+ private final QuizService quizService = new QuizService();
private User currentUser;
+ private final Stage primaryStage;
+ private Panel currentPanel;
+
public MainWindow(Stage primaryStage) {
- // 初始化服务(未来可替换为 DI 容器)
- this.fileIOService = new FileIOService();
- this.userService = new UserService(fileIOService);
+ this.primaryStage = primaryStage;
+ showStartPage();
+ }
- // 默认显示登录页
- showLoginPanel();
+ private void showStartPage() {
+ StartPage startPage = new StartPage(() -> navigateTo(Panel.LOGIN));
+ this.setCenter(startPage);
+ currentPanel = Panel.START;
}
- // ---------------- 公共方法:面板切换 ----------------
+ private void navigateTo(Panel target) {
+ switch (target) {
+ case START -> showStartPage();
+ case LOGIN -> initLoginPage();
+ case REGISTER -> initRegisterPage();
+ case INF_GEN -> initInfGenPage();
+ case PASSWORDMODIFY -> initPasswordModifyPage();
+ case QUIZ -> initQuizPage();
+ case RESULT -> initResultPage();
+ }
+ currentPanel = target;
+ }
- public void showPanel(javafx.scene.layout.Pane panel) {
- this.setCenter(panel);
+ // 封装登录页面初始化逻辑
+ private void initLoginPage() {
+ LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START));
+ // 注册链接跳转
+ loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER));
+ // 登录按钮点击事件(绑定封装的登录处理方法)
+ loginPage.getLoginButton().setOnAction(e -> handleLoginAction(loginPage));
+ this.setCenter(loginPage);
}
- public void showLoginPanel() {
- showPanel(new LoginPanel(this));
+ // 封装登录核心逻辑(从UI获取数据→调用服务层→处理结果)
+ private void handleLoginAction(LoginPage loginPage) {
+ String username = loginPage.getUsernameOrEmailField().getText().trim();
+ String password = loginPage.getPasswordField().getText().trim();
+
+ if (username.isEmpty()) {
+ NavigablePanel.showErrorAlert("输入错误", "请输入用户名");
+ return ;
+ }
+ if (password.isEmpty()) {
+ NavigablePanel.showErrorAlert("输入错误", "请输入密码");
+ return ;
+ }
+ try {
+ this.currentUser = userService.login(username, password);; // 保存当前用户
+ navigateTo(Panel.INF_GEN); // 登录成功跳转
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("登录失败", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", "登录过程中发生错误:" + ex.getMessage());
+ }
}
- public void showRegisterPanel() {
- showPanel(new RegisterPanel(this));
+ // 其他页面的初始化方法(保持原有逻辑,统一格式)
+ private void initRegisterPage() {
+ RegisterPage registerPage = new RegisterPage(() -> navigateTo(Panel.LOGIN));
+ registerPage.getSendCodeButton().setOnAction(e -> handleSendCodeAction(registerPage));
+ registerPage.getRegisterButton().setOnAction(e -> handleRegisterAction(registerPage));
+ this.setCenter(registerPage);
}
- public void showPasswordModifyPanel() {
- if (currentUser != null) {
- showPanel(new PasswordModifyPanel(this, currentUser.getEmail()));
+
+ private void handleSendCodeAction(RegisterPage registerPage) {
+ String email = registerPage.getEmailField().getText().trim();
+
+ try {
+ userService.generateRegistrationCode(email);
+ NavigablePanel.showErrorAlert("成功", "注册码已生成,10分钟内有效");
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("获取注册码失败", ex.getMessage());
+ return;
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ return;
}
}
+ private void handleRegisterAction(RegisterPage registerPage) {
+ String email = registerPage.getEmailField().getText().trim();
+ String code = registerPage.getCodeField().getText().trim();
+ String password = registerPage.getPasswordField().getText().trim();
+ String confirmPassword = registerPage.getConfirmPasswordField().getText().trim();
- public void showGradeSelectPanel() {
- showPanel(new GradeSelectPanel(this));
+ if (password.isEmpty()) {
+ NavigablePanel.showErrorAlert("密码错误","密码不能为空");
+ return ;
+ }
+ if (confirmPassword.isEmpty()) {
+ NavigablePanel.showErrorAlert("密码错误", "请重复密码");
+ return ;
+ }
+ if (!password.equals(confirmPassword)) {
+ NavigablePanel.showErrorAlert("密码错误", "两次密码不同");
+ return ;
+ }
+ try {
+ this.currentUser = userService.register(password, email, code);
+ navigateTo(Panel.INF_GEN);
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("注册失败", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ }
+ }
+
+ private void initInfGenPage() {
+ InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN), userService.getCurrentUser().getUsername(),
+ userService.getCurrentUser().getEmail());
+ infGenPage.getGenerateButton().setOnAction(e -> {
+ handleUsernameModifyAction(infGenPage);
+ handleEmailModifyAction(infGenPage);
+ int count = infGenPage.getQuestionCountSpinner().getValue();
+ try {
+ userService.updateGrade(userService.getCurrentUser(), infGenPage.getGradeChoice().getValue());
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("学段错误", ex.getMessage());
+ return;
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ }
+ if (count < 10 || count > 30) {
+ NavigablePanel.showErrorAlert("输入错误", "题数必须为10-30");
+ return;
+ }
+ quizService.setAnswerNumber(count);
+ navigateTo(Panel.QUIZ);
+ });
+ infGenPage.getPasswordModifyButton().setOnAction(e -> {
+ handleUsernameModifyAction(infGenPage);
+ handleEmailModifyAction(infGenPage);
+ navigateTo(Panel.PASSWORDMODIFY);
+ });
+ this.setCenter(infGenPage);
+ this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
+ }
+ private void handleUsernameModifyAction(InfGenPage infGenPage) {
+ try {
+ userService.updateUsername(userService.getCurrentUser(), infGenPage.getUsernameField().getText().trim());
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("用户名错误", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ }
+ }
+ private void handleEmailModifyAction(InfGenPage infGenPage) {
+ try {
+ userService.updateEmail(userService.getCurrentUser(), infGenPage.getEmailField().getText().trim());
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ }
}
- public void showQuizPanel(java.util.List questions, QuizService quizService) {
- showPanel(new QuizPanel(this, questions, quizService));
+ private void initPasswordModifyPage() {
+ PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
+ pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
+ this.setCenter(pwdPage);
}
- public void showResultPanel(int score, Runnable onContinue) {
- showPanel(new ResultPanel(this, score, onContinue));
+ private void handlePasswordModify(PasswordModifyPage pwdPage) {
+ String oldPassword = pwdPage.getOldPasswordField().getText().trim();
+ String newPassword = pwdPage.getNewPasswordField().getText().trim();
+ String confirmPassword = pwdPage.getConfirmNewPasswordField().getText().trim();
+
+ try {
+ userService.changePassword(userService.getCurrentUser(),oldPassword, newPassword, confirmPassword);
+ navigateTo(Panel.INF_GEN);
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
+ return;
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
+ return;
+ }
}
- // ---------------- Getter ----------------
+ private void initQuizPage() {
+ try {
+ quizService.startNewQuiz(currentUser, quizService.getAnswerNumber());
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("生成题目错误", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ navigateTo(Panel.INF_GEN);
+ return;
+ }
+ QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN), quizService);
+ quizPage.setTotalQuestions(quizService.getTotalQuestions());
+ quizPage.goToQuestion(0);
+ quizPage.getPrevButton().setOnAction(e -> {
+ if (quizService.previousQuestion()) {
+ quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
+ }
+ });
+ quizPage.getNextButton().setOnAction(e -> {
+ // 获取用户当前选择(可能为 null)
+ Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
+
+ // 如果有选择,则提交答案
+ if (selected != null) {
+ int selectedIndex = -1;
+ for (int i = 0; i < 4; i++) {
+ if (quizPage.getOptions()[i] == selected) {
+ selectedIndex = i;
+ break;
+ }
+ }
+ quizService.submitCurrentAnswer(selectedIndex);
+ }
+ // 无论是否选择,都尝试跳转到下一题
+ if (quizService.nextQuestion()) {
+ quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
+ } else {
+ // 已是最后一题,显示“交卷”按钮
+ quizPage.getSubmitButton().setVisible(true);
+ quizPage.getNextButton().setVisible(false);
+ }
+ });
+ quizPage.getSubmitButton().setOnAction(e -> {
+ Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
+ if (selected != null) {
+ int selectedIndex = -1;
+ for (int i = 0; i < 4; i++) {
+ if (quizPage.getOptions()[i] == selected) {
+ selectedIndex = i;
+ break;
+ }
+ }
+ quizService.submitCurrentAnswer(selectedIndex);
+ }
+ // 直接交卷,不强制所有题都作答
+ navigateTo(Panel.RESULT);
+ });
- public UserService getUserService() {
- return userService;
+ this.setCenter(quizPage);
}
- public FileIOService getFileIOService() {
- return fileIOService;
+ private void initResultPage() {
+ ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN), quizService);
+ resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START));
+ resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN));
+ resultPage.updateResult();
+ this.setCenter(resultPage);
}
- public User getCurrentUser() {
- return currentUser;
+ // getter和启动方法(保持不变)
+ public Panel getCurrentPanel() {
+ return currentPanel;
}
- public void setCurrentUser(User user) {
- this.currentUser = user;
+ public static void start(Stage stage) {
+ MainWindow mainWindow = new MainWindow(stage);
+ Scene scene = new Scene(mainWindow, 800, 600);
+ stage.setScene(scene);
+ stage.setTitle("中小学数学答题系统");
+ stage.show();
}
}
\ No newline at end of file
diff --git a/src/main/java/com/ui/NavigablePanel.java b/src/main/java/com/ui/NavigablePanel.java
new file mode 100644
index 0000000..dc2e4af
--- /dev/null
+++ b/src/main/java/com/ui/NavigablePanel.java
@@ -0,0 +1,52 @@
+// com/ui/NavigablePanel.java
+package com.ui;
+
+import javafx.application.Platform;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.BorderPane;
+import javafx.scene.layout.HBox;
+import javafx.scene.text.Font;
+
+public abstract class NavigablePanel extends BorderPane {
+
+
+ public NavigablePanel(Runnable onBack) {
+ Button backButton = new Button("←");
+ backButton.setOnAction(e -> onBack.run());
+ backButton.setPrefSize(UIConstants.BACK_BUTTON_WIDTH, UIConstants.BACK_BUTTON_HEIGHT);
+ backButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ backButton.setStyle(
+ "-fx-background-radius: 50; " +
+ "-fx-background-color: " + UIConstants.COLOR_ACCENT + "; " +
+ "-fx-text-fill: white; " +
+ "-fx-font-weight: bold;"
+ );
+
+ HBox topBar = new HBox(10);
+ topBar.setPadding(UIConstants.TOP_BAR_PADDING);
+ topBar.setAlignment(Pos.CENTER_LEFT);
+ topBar.getChildren().add(backButton);
+
+ this.setTop(topBar);
+ }
+
+ protected static void showErrorAlert(String title, String message) {
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.ERROR);
+ alert.setTitle(title);
+ alert.setHeaderText(null);
+ alert.setContentText(message);
+ alert.showAndWait();
+ });
+ }
+
+ // 子类构造函数中调用
+ protected final void initializeContent() {
+ buildContent();
+ }
+ protected abstract void buildContent();
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/Panel.java b/src/main/java/com/ui/Panel.java
new file mode 100644
index 0000000..e3dfbe7
--- /dev/null
+++ b/src/main/java/com/ui/Panel.java
@@ -0,0 +1,11 @@
+package com.ui;
+
+public enum Panel {
+ START, // 开始页面
+ LOGIN, // 登录页面
+ REGISTER, // 注册页面
+ INF_GEN, // 个人信息+生成题目页面
+ PASSWORDMODIFY, // 修改密码页面
+ QUIZ, // 答题页面
+ RESULT // 得分页面
+}
diff --git a/src/main/java/com/ui/PasswordModifyPage.java b/src/main/java/com/ui/PasswordModifyPage.java
new file mode 100644
index 0000000..06197fe
--- /dev/null
+++ b/src/main/java/com/ui/PasswordModifyPage.java
@@ -0,0 +1,52 @@
+// com/ui/PasswordModifyPage.java
+package com.ui;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+public class PasswordModifyPage extends NavigablePanel {
+
+ private final PasswordField oldPasswordField = new PasswordField();
+ private final PasswordField newPasswordField = new PasswordField();
+ private final PasswordField confirmNewPasswordField = new PasswordField();
+ private final Button modifyButton = new Button("修改");
+
+ public PasswordModifyPage(Runnable onBack) {
+ super(onBack);
+ initializeContent();
+ }
+
+ @Override
+ protected void buildContent() {
+ VBox form = new VBox(UIConstants.DEFAULT_SPACING);
+ form.setAlignment(Pos.CENTER);
+ form.setPadding(UIConstants.DEFAULT_PADDING);
+ form.setStyle(UIConstants.FORM_STYLE);
+
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+
+ oldPasswordField.setPromptText("旧密码");
+ oldPasswordField.setStyle(UIConstants.INPUT_STYLE);
+ newPasswordField.setPromptText("新密码(6-10位)");
+ newPasswordField.setStyle(UIConstants.INPUT_STYLE);
+ confirmNewPasswordField.setPromptText("确认新密码");
+ confirmNewPasswordField.setStyle(UIConstants.INPUT_STYLE);
+ modifyButton.setStyle(UIConstants.BUTTON_STYLE);
+ modifyButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ modifyButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+
+ form.getChildren().addAll(
+ titleLabel, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton
+ );
+ this.setCenter(form);
+ }
+
+ public PasswordField getOldPasswordField() { return oldPasswordField; }
+ public PasswordField getNewPasswordField() { return newPasswordField; }
+ public PasswordField getConfirmNewPasswordField() { return confirmNewPasswordField; }
+ public Button getModifyButton() { return modifyButton; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/PasswordModifyPanel.java b/src/main/java/com/ui/PasswordModifyPanel.java
deleted file mode 100644
index bea9dd0..0000000
--- a/src/main/java/com/ui/PasswordModifyPanel.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package com.mathquiz.ui;
-
-import javafx.geometry.Insets;
-import javafx.scene.control.*;
-import javafx.scene.layout.VBox;
-
-public class PasswordModifyPanel extends VBox {
-
- private final String email;
- private final PasswordField oldPwdField = new PasswordField();
- private final PasswordField newPwd1Field = new PasswordField();
- private final PasswordField newPwd2Field = new PasswordField();
-
- public PasswordModifyPanel(MainWindow mainWindow, String email) {
- this.email = email;
- setPadding(new Insets(20));
- setSpacing(10);
-
- getChildren().addAll(
- new Label("修改密码"),
- new Label("原密码:"),
- oldPwdField,
- new Label("新密码(6-10位,含大小写+数字):"),
- newPwd1Field,
- new Label("确认新密码:"),
- newPwd2Field,
- new Button("确认修改") {{
- setOnAction(e -> changePassword(mainWindow));
- }},
- new Button("返回") {{
- setOnAction(e -> mainWindow.showGradeSelectPanel());
- }}
- );
- }
-
- private void changePassword(MainWindow mainWindow) {
- boolean success = mainWindow.getUserService()
- .changePassword(email, oldPwdField.getText(), newPwd1Field.getText(), newPwd2Field.getText());
- if (success) {
- new Alert(Alert.AlertType.INFORMATION, "密码修改成功!").showAndWait();
- mainWindow.showGradeSelectPanel();
- } else {
- new Alert(Alert.AlertType.ERROR, "原密码错误或新密码不符合要求").showAndWait();
- }
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ui/QuizPage.java b/src/main/java/com/ui/QuizPage.java
new file mode 100644
index 0000000..8acfbe8
--- /dev/null
+++ b/src/main/java/com/ui/QuizPage.java
@@ -0,0 +1,315 @@
+// com/ui/QuizPage.java
+package com.ui;
+
+import com.model.ChoiceQuestion;
+import com.service.QuizService;
+import javafx.geometry.Insets;
+import javafx.geometry.Pos;
+import javafx.scene.Node;
+import javafx.scene.control.*;
+import javafx.scene.layout.*;
+import javafx.scene.paint.Color;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+import java.util.List;
+
+public class QuizPage extends NavigablePanel {
+
+ private final QuizService quizService;
+
+ private final Label titleLabel = new Label("中小学数学答题系统");
+ private final Label progressLabel = new Label("完成 0/10");
+ private final Label questionLabel = new Label("题目加载中...");
+ private final ToggleGroup optionGroup = new ToggleGroup();
+ private final RadioButton[] options = new RadioButton[4];
+ private final Button prevButton = new Button("上一题");
+ private final Button nextButton = new Button("下一题");
+ private final Button submitButton = new Button("交卷");
+
+ // 题目导航矩阵容器
+ private final GridPane questionNavGrid = new GridPane();
+ private int totalQuestions = 10; // 默认10题,由外部设置
+ private int currentQuestionIndex = 0; // 当前题号(0-based)
+
+ public QuizPage(Runnable onBack, QuizService quizService) {
+ super(onBack);
+ this.quizService = quizService;
+ initializeContent();
+ }
+
+ @Override
+ protected void buildContent() {
+ // 设置整体布局:BorderPane
+ this.setPadding(new Insets(20));
+ this.setStyle("-fx-background-color: " + UIConstants.COLOR_BACKGROUND + ";");
+
+ // 顶部标题栏
+ HBox topBar = createTopBar();
+ this.setTop(topBar);
+
+ // 主体内容:左右分栏
+ HBox mainContent = new HBox(20);
+ mainContent.setAlignment(Pos.CENTER);
+ mainContent.setPadding(new Insets(10));
+
+ // 左侧:题目内容区
+ VBox leftPanel = createLeftPanel();
+ leftPanel.setMaxWidth(600);
+
+ // 右侧:题目导航矩阵
+ VBox rightPanel = createRightPanel();
+ rightPanel.setMaxWidth(300);
+ rightPanel.setMinWidth(280);
+
+ HBox.setHgrow(leftPanel, Priority.ALWAYS);
+ HBox.setHgrow(rightPanel, Priority.NEVER);
+ mainContent.getChildren().addAll(leftPanel, rightPanel);
+ this.setCenter(mainContent);
+
+ // 底部按钮
+ VBox bottomBarContainer = new VBox(10);
+ bottomBarContainer.setAlignment(Pos.CENTER);
+ bottomBarContainer.setPadding(new Insets(10));
+ nextButton.setStyle(UIConstants.BUTTON_STYLE);
+ submitButton.setStyle(UIConstants.BUTTON_STYLE + "-fx-background-color: " + UIConstants.COLOR_ERROR + ";");
+ prevButton.setStyle(UIConstants.BUTTON_STYLE);
+ nextButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ submitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ prevButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+
+ HBox buttonRow = new HBox(20);
+ buttonRow.setAlignment(Pos.CENTER);
+ buttonRow.getChildren().addAll(prevButton, nextButton, submitButton);
+
+ bottomBarContainer.getChildren().add(buttonRow);
+
+ this.setBottom(bottomBarContainer);
+
+ // 初始化按钮状态
+ updateButtonVisibility();
+ }
+
+ /**
+ * 创建顶部标题栏
+ */
+ private HBox createTopBar() {
+ HBox topBar = new HBox(20);
+ topBar.setAlignment(Pos.CENTER_LEFT);
+ topBar.setPadding(new Insets(10));
+ topBar.setStyle("-fx-background-color: white; -fx-border-width: 0 0 1 0; -fx-border-color: #bdc3c7;");
+
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+ progressLabel.setStyle("-fx-font-size: " + UIConstants.HINT_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_HINT + ";");
+
+ topBar.getChildren().addAll(titleLabel, progressLabel);
+ return topBar;
+ }
+
+ /**
+ * 创建左侧题目内容面板
+ */
+ private VBox createLeftPanel() {
+ // 直接使用带样式的 VBox 作为内容容器
+ VBox content = new VBox(UIConstants.DEFAULT_SPACING);
+ content.setPadding(new Insets(20));
+ content.setStyle(UIConstants.FORM_STYLE);
+ content.setPrefWidth(550);
+ content.setMinWidth(550);
+ content.setMaxWidth(550);
+ content.setPrefHeight(400);
+ content.setMinHeight(400);
+ content.setMaxHeight(400);
+
+ questionLabel.setWrapText(true);
+ questionLabel.setPrefWidth(500);
+ questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.QUIZ_TITLE_FONT_SIZE));
+ questionLabel.setStyle("-fx-font-size: " + UIConstants.QUIZ_TITLE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_PRIMARY + ";");
+
+ VBox optionsBox = new VBox(10);
+ optionsBox.setAlignment(Pos.CENTER_LEFT);
+ for (int i = 0; i < 4; i++) {
+ options[i] = new RadioButton("选项 " + (char)('A' + i));
+ options[i].setToggleGroup(optionGroup);
+ options[i].setStyle("-fx-font-size: " + UIConstants.LABEL_FONT_SIZE + "px;");
+ optionsBox.getChildren().add(options[i]);
+ }
+
+ content.getChildren().addAll(questionLabel, optionsBox);
+ return content;
+ }
+
+ /**
+ * 创建右侧题目导航矩阵面板
+ */
+ private VBox createRightPanel() {
+ VBox rightPanel = new VBox(10);
+ rightPanel.setAlignment(Pos.TOP_CENTER);
+ rightPanel.setPadding(new Insets(20));
+ rightPanel.setStyle(UIConstants.FORM_STYLE);
+
+ Label navTitle = new Label("题目导航");
+ navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE));
+ navTitle.setAlignment(Pos.CENTER);
+
+ // 初始化题目导航矩阵
+ initQuestionNavGrid();
+
+ rightPanel.getChildren().addAll(navTitle, questionNavGrid);
+ return rightPanel;
+ }
+
+ /**
+ * 初始化题目导航网格
+ */
+ private void initQuestionNavGrid() {
+ questionNavGrid.getChildren().clear();
+ questionNavGrid.setHgap(5);
+ questionNavGrid.setVgap(5);
+ questionNavGrid.setAlignment(Pos.CENTER);
+
+ int cols = 5;
+ int rows = (totalQuestions + cols - 1) / cols;
+
+ for (int i = 0; i < totalQuestions; i++) {
+ int row = i / cols;
+ int col = i % cols;
+
+ String text = String.valueOf(i + 1);
+ Button btn = new Button(text);
+ btn.setPrefSize(50, 40);
+ btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.LABEL_FONT_SIZE));
+ btn.setStyle(getButtonStyleForStatus(i));
+
+ final int index = i;
+ btn.setOnAction(e -> goToQuestion(index));
+
+ questionNavGrid.add(btn, col, row);
+ }
+ }
+
+ /**
+ * 根据题目状态返回按钮样式
+ * @param index 题号(0-based)
+ * @return CSS样式字符串
+ */
+ private String getButtonStyleForStatus(int index) {
+ if (index == currentQuestionIndex) {
+ // 当前题
+ return "-fx-background-color: " + UIConstants.COLOR_ACCENT + "; -fx-text-fill: white; -fx-font-weight: bold;";
+ } else if (quizService.isAnswered(index)) {
+ // 已作答
+ return "-fx-background-color: #2ecc71; -fx-text-fill: white;";
+ } else {
+ // 未作答
+ return "-fx-background-color: #ecf0f1; -fx-text-fill: #2c3e50;";
+ }
+ }
+
+
+ /**
+ * 更新按钮可见性(最后一题显示“交卷”,否则显示“下一题”)
+ */
+ private void updateButtonVisibility() {
+ if (currentQuestionIndex == totalQuestions - 1) {
+ nextButton.setVisible(false);
+ submitButton.setVisible(true);
+ } else {
+ nextButton.setVisible(true);
+ submitButton.setVisible(false);
+ }
+ }
+
+ /**
+ * 跳转到指定题目
+ * @param index 题号(0-based)
+ */
+ public void goToQuestion(int index) {
+ currentQuestionIndex = index;
+ quizService.goToQuestion(index);
+ updateProgressLabel();
+ updateQuestionNavButtons();
+ updateButtonVisibility();
+ loadQuestion(index);
+ }
+
+ /**
+ * 更新进度标签
+ */
+ private void updateProgressLabel() {
+ int answeredCount = quizService.getAnsweredCount();
+ progressLabel.setText("完成 " + answeredCount + "/" + totalQuestions + " 题");
+ }
+
+ /**
+ * 更新题目导航按钮样式
+ */
+ private void updateQuestionNavButtons() {
+ for (Node node : questionNavGrid.getChildren()) {
+ if (node instanceof Button) {
+ Button btn = (Button) node;
+ int index = Integer.parseInt(btn.getText()) - 1; // 转换为0-based
+ btn.setStyle(getButtonStyleForStatus(index));
+ }
+ }
+ }
+
+ /**
+ * 加载题目(从 QuizService 获取)
+ * @param index 题号
+ */
+ private void loadQuestion(int index) {
+ System.out.println("🔄 加载题目 " + index + ", options[0]=" + options[0]);
+ if (options[0] == null) {
+ System.err.println("⚠️ RadioButton 未初始化,跳过题目加载");
+ return; // 防止 NPE
+ }
+ ChoiceQuestion question = quizService.getQuestion(index);
+ if (question == null) return;
+
+ // 显示题目
+ questionLabel.setText("第 " + (index + 1) + " 题:\n" + question.getQuestionText());
+
+ List> optionsList = question.getOptions();
+ for (int i = 0; i < 4; i++) {
+ Object option = optionsList.get(i);
+ String optionText = option != null ? option.toString() : "未知";
+ options[i].setText((char)('A' + i) + ". " + optionText);
+ }
+
+ Integer userAnswer = quizService.getUserAnswer(index);
+ if (userAnswer != null && userAnswer >= 0 && userAnswer < 4) {
+ optionGroup.selectToggle(options[userAnswer]);
+ } else {
+ optionGroup.selectToggle(null);
+ }
+
+ // ✅ 强制刷新 UI(可选,通常不需要)
+ // Platform.runLater(() -> {
+ // this.requestLayout(); // 触发重新布局
+ // });
+ }
+
+ // ========== Getter 方法 ==========
+ public Label getProgressLabel() { return progressLabel; }
+ public Label getQuestionLabel() { return questionLabel; }
+ public RadioButton[] getOptions() { return options; }
+ public ToggleGroup getOptionGroup() { return optionGroup; }
+ public Button getNextButton() { return nextButton; }
+ public Button getPrevButton() { return prevButton; }
+ public Button getSubmitButton() { return submitButton; }
+
+ // ========== 设置器方法 ==========
+ public void setTotalQuestions(int total) {
+ this.totalQuestions = total;
+ initQuestionNavGrid(); // 重新初始化导航矩阵
+ updateProgressLabel();
+ }
+
+ public void setCurrentQuestionIndex(int index) {
+ this.currentQuestionIndex = index;
+ updateQuestionNavButtons();
+ updateButtonVisibility();
+ loadQuestion(index);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/QuizPanel.java b/src/main/java/com/ui/QuizPanel.java
deleted file mode 100644
index 9100c25..0000000
--- a/src/main/java/com/ui/QuizPanel.java
+++ /dev/null
@@ -1,67 +0,0 @@
-package com.mathquiz.ui;
-
-import com.mathquiz.model.ChoiceQuestion;
-import com.mathquiz.service.QuizService;
-import javafx.geometry.Insets;
-import javafx.scene.control.*;
-import javafx.scene.layout.VBox;
-
-import java.util.ArrayList;
-import java.util.List;
-
-public class QuizPanel extends VBox {
-
- private final List questions;
- private final List userAnswers = new ArrayList<>();
- private final QuizService quizService;
- private final MainWindow mainWindow;
- private int currentIndex = 0;
-
- public QuizPanel(MainWindow mainWindow, List questions, QuizService quizService) {
- this.mainWindow = mainWindow;
- this.questions = questions;
- this.quizService = quizService;
- setPadding(new Insets(20));
- showQuestion(currentIndex);
- }
-
- private void showQuestion(int index) {
- getChildren().clear();
- ChoiceQuestion q = questions.get(index);
-
- getChildren().add(new Label("第 " + (index + 1) + " 题 / " + questions.size()));
- getChildren().add(new Label(q.getQuestionContent()));
-
- ToggleGroup group = new ToggleGroup();
- for (int i = 0; i < 4; i++) {
- RadioButton rb = new RadioButton(
- (char)('A' + i) + ". " + q.getOptions().get(i)
- );
- rb.setToggleGroup(group);
- getChildren().add(rb);
- }
-
- Button submitBtn = new Button("提交");
- submitBtn.setOnAction(e -> {
- RadioButton selected = (RadioButton) group.getSelectedToggle();
- if (selected != null) {
- String answer = selected.getText().substring(0, 1); // "A"
- userAnswers.add(answer);
-
- if (index + 1 < questions.size()) {
- showQuestion(index + 1);
- } else {
- int score = quizService.calculateScore(questions, userAnswers);
- Runnable onContinue = () -> {
- quizService.savePaper(mainWindow.getCurrentUser().getEmail(), questions);
- };
- mainWindow.showResultPanel(score, onContinue);
- }
- } else {
- new Alert(Alert.AlertType.WARNING, "请选择一个选项").showAndWait();
- }
- });
-
- getChildren().add(submitBtn);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ui/RegisterPage.java b/src/main/java/com/ui/RegisterPage.java
new file mode 100644
index 0000000..df3585a
--- /dev/null
+++ b/src/main/java/com/ui/RegisterPage.java
@@ -0,0 +1,63 @@
+// com/ui/RegisterPage.java
+package com.ui;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.*;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+public class RegisterPage extends NavigablePanel {
+
+ private final TextField emailField = new TextField();
+ private final PasswordField passwordField = new PasswordField();
+ private final PasswordField confirmPasswordField = new PasswordField();
+ private final TextField codeField = new TextField();
+ private final Button sendCodeButton = new Button("获取注册码");
+ private final Button registerButton = new Button("注册");
+
+ public RegisterPage(Runnable onBack) {
+ super(onBack);
+ initializeContent();
+ }
+
+ @Override
+ protected void buildContent() {
+ VBox form = new VBox(UIConstants.DEFAULT_SPACING);
+ form.setAlignment(Pos.CENTER);
+ form.setPadding(UIConstants.DEFAULT_PADDING);
+ form.setStyle(UIConstants.FORM_STYLE);
+
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+
+ emailField.setPromptText("邮箱");
+ emailField.setStyle(UIConstants.INPUT_STYLE);
+ codeField.setPromptText("注册码");
+ codeField.setStyle(UIConstants.INPUT_STYLE);
+ passwordField.setPromptText("密码(6-10位)");
+ passwordField.setStyle(UIConstants.INPUT_STYLE);
+ confirmPasswordField.setPromptText("确认密码");
+ confirmPasswordField.setStyle(UIConstants.INPUT_STYLE);
+ sendCodeButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ sendCodeButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ sendCodeButton.setStyle(UIConstants.BUTTON_STYLE);
+ registerButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ registerButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ registerButton.setStyle(UIConstants.BUTTON_STYLE);
+
+ form.getChildren().addAll(
+ titleLabel, emailField, sendCodeButton, codeField,
+ passwordField, confirmPasswordField, registerButton
+ );
+ this.setCenter(form);
+ }
+
+ // getters for controller
+ public TextField getEmailField() { return emailField; }
+ public TextField getCodeField() { return codeField; }
+ public PasswordField getPasswordField() { return passwordField; }
+ public PasswordField getConfirmPasswordField() { return confirmPasswordField; }
+ public Button getSendCodeButton() { return sendCodeButton; }
+ public Button getRegisterButton() { return registerButton; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/RegisterPanel.java b/src/main/java/com/ui/RegisterPanel.java
deleted file mode 100644
index 27ae653..0000000
--- a/src/main/java/com/ui/RegisterPanel.java
+++ /dev/null
@@ -1,80 +0,0 @@
-package com.mathquiz.ui;
-
-import com.mathquiz.util.EmailUtil;
-import javafx.geometry.Insets;
-import javafx.scene.control.*;
-import javafx.scene.layout.VBox;
-
-/**
- * 注册面板
- */
-public class RegisterPanel extends VBox {
-
- private final TextField emailField = new TextField();
- private final TextField codeField = new TextField();
- private final PasswordField pwd1Field = new PasswordField();
- private final PasswordField pwd2Field = new PasswordField();
-
- public RegisterPanel(MainWindow mainWindow) {
- setPadding(new Insets(20));
- setSpacing(10);
-
- getChildren().addAll(
- new Label("注册"),
- new Label("邮箱:"),
- emailField,
- new Button("发送注册码") {{
- setOnAction(e -> sendCodeAction(mainWindow));
- }},
- new Label("注册码:"),
- codeField,
- new Label("密码(6-10位,含大小写+数字):"),
- pwd1Field,
- new Label("确认密码:"),
- pwd2Field,
- new Button("完成注册") {{
- setOnAction(e -> registerAction(mainWindow));
- }},
- new Hyperlink("已有账号?去登录") {{
- setOnAction(e -> mainWindow.showLoginPanel());
- }}
- );
- }
-
- private void sendCodeAction(MainWindow mainWindow) {
- String email = emailField.getText().trim();
- if (email.isEmpty() || !EmailUtil.isValidEmail(email)) {
- showAlert("请输入有效的邮箱地址");
- return;
- }
- // 调用服务层
- boolean sent = mainWindow.getUserService().sendRegistrationCode(email);
- if (sent) {
- showAlert("注册码已发送(模拟)");
- }
- }
-
- private void registerAction(MainWindow mainWindow) {
- String email = emailField.getText().trim();
- String code = codeField.getText().trim();
- String pwd1 = pwd1Field.getText();
- String pwd2 = pwd2Field.getText();
-
- if (!mainWindow.getUserService().verifyCode(email, code)) {
- showAlert("注册码错误");
- return;
- }
-
- boolean success = mainWindow.getUserService().setPassword(email, pwd1, pwd2);
- if (success) {
- showAlert("注册成功!");
- mainWindow.showGradeSelectPanel();
- } else {
- showAlert("密码不符合要求(6-10位,含大小写字母和数字)");
- }
- }
-
- private void showAlert(String message) {
- new Alert(Alert.AlertType.INFORMATION, message).showAndWait();
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ui/ResultPage.java b/src/main/java/com/ui/ResultPage.java
new file mode 100644
index 0000000..8417f0a
--- /dev/null
+++ b/src/main/java/com/ui/ResultPage.java
@@ -0,0 +1,63 @@
+// com/ui/ResultPage.java
+package com.ui;
+
+import com.model.QuizResult;
+import com.service.QuizService;
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+public class ResultPage extends NavigablePanel {
+
+ private final QuizService quizService;
+ private final Label resultLabel = new Label("您答对了 8/10 题,得分:80%");
+ private final Label gradeLabel = new Label("评级:优秀");
+ private final Button continueButton = new Button("继续答题");
+ private final Button exitButton = new Button("退出");
+
+ public ResultPage(Runnable onBack, QuizService quizService) {
+ super(onBack);
+ initializeContent();
+ this.quizService = quizService;
+ }
+
+ @Override
+ protected void buildContent() {
+ VBox form = new VBox(UIConstants.DEFAULT_SPACING);
+ form.setAlignment(Pos.CENTER);
+ form.setPadding(UIConstants.DEFAULT_PADDING);
+ form.setStyle(UIConstants.FORM_STYLE);
+
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+
+ resultLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.TITLE_FONT_SIZE - 4));
+ resultLabel.setStyle("-fx-font-size: " + UIConstants.SCORE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_ACCENT + ";");
+ gradeLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE));
+ gradeLabel.setStyle("-fx-font-size: " + UIConstants.SUBTITLE_FONT_SIZE + "px; -fx-text-fill: " + UIConstants.COLOR_PRIMARY + ";");
+
+ continueButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ continueButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ continueButton.setStyle(UIConstants.BUTTON_STYLE);
+ exitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ exitButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ exitButton.setStyle(UIConstants.BUTTON_STYLE);
+
+ form.getChildren().addAll(titleLabel, resultLabel, gradeLabel, continueButton, exitButton);
+ this.setCenter(form);
+ }
+
+ public void updateResult() {
+ QuizResult result = quizService.calculateResult();
+ resultLabel.setText(result.toString());
+ gradeLabel.setText(quizService.getGrade(result));
+ }
+
+ public Label getResultLabel() { return resultLabel; }
+ public Label getGradeLabel() { return gradeLabel; }
+ public Button getContinueButton() { return continueButton; }
+ public Button getExitButton() { return exitButton; }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/ResultPanel.java b/src/main/java/com/ui/ResultPanel.java
deleted file mode 100644
index b94f202..0000000
--- a/src/main/java/com/ui/ResultPanel.java
+++ /dev/null
@@ -1,32 +0,0 @@
-package com.mathquiz.ui;
-
-import javafx.geometry.Insets;
-import javafx.scene.control.Alert;
-import javafx.scene.control.Button;
-import javafx.scene.control.Label;
-import javafx.scene.layout.VBox;
-
-public class ResultPanel extends VBox {
-
- public ResultPanel(MainWindow mainWindow, int score, Runnable onContinue) {
- setPadding(new Insets(20));
- setSpacing(20);
-
- getChildren().addAll(
- new Label("答题结束!"),
- new Label("您的得分: " + score + " 分"),
- new Button("继续做题") {{
- setOnAction(e -> {
- onContinue.run(); // 保存试卷
- mainWindow.showGradeSelectPanel();
- });
- }},
- new Button("退出") {{
- setOnAction(e -> {
- mainWindow.setCurrentUser(null);
- mainWindow.showLoginPanel();
- });
- }}
- );
- }
-}
\ No newline at end of file
diff --git a/src/main/java/com/ui/StartPage.java b/src/main/java/com/ui/StartPage.java
new file mode 100644
index 0000000..d340334
--- /dev/null
+++ b/src/main/java/com/ui/StartPage.java
@@ -0,0 +1,37 @@
+// com/ui/StartPage.java
+package com.ui;
+
+import javafx.geometry.Pos;
+import javafx.scene.control.Button;
+import javafx.scene.control.Label;
+import javafx.scene.layout.VBox;
+import javafx.scene.text.Font;
+import javafx.scene.text.FontWeight;
+
+public class StartPage extends VBox {
+
+ private final Button startButton;
+
+ public StartPage(Runnable onStart) {
+ this.setAlignment(Pos.CENTER);
+ this.setSpacing(UIConstants.DEFAULT_SPACING);
+ this.setPadding(UIConstants.DEFAULT_PADDING);
+
+ Label titleLabel = new Label("中小学数学答题系统");
+ titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
+
+ Label subtitleLabel = new Label("HNU@梁峻耀 吴佰轩");
+ subtitleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUBTITLE_FONT_SIZE));
+ subtitleLabel.setStyle("-fx-text-fill: gray;");
+
+ startButton = new Button("开始");
+ startButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ startButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+ startButton.setOnAction(e -> onStart.run());
+ startButton.setStyle(UIConstants.BUTTON_STYLE);
+ startButton.setOnMouseEntered(e -> startButton.setStyle(UIConstants.BUTTON_STYLE + UIConstants.BUTTON_HOVER_STYLE));
+ startButton.setOnMouseExited(e -> startButton.setStyle(UIConstants.BUTTON_STYLE));
+
+ this.getChildren().addAll(titleLabel, subtitleLabel, startButton);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ui/UIConstants.java b/src/main/java/com/ui/UIConstants.java
new file mode 100644
index 0000000..d41f9f0
--- /dev/null
+++ b/src/main/java/com/ui/UIConstants.java
@@ -0,0 +1,70 @@
+// UIConstants.java
+package com.ui;
+
+import javafx.geometry.Insets;
+import javafx.scene.paint.Color;
+
+public final class UIConstants {
+ private UIConstants() {}
+
+ public static final double LABEL_ITEM_TITLE_SIZE = 16.0;
+
+ // 间距与边距
+ public static final double DEFAULT_SPACING = 15.0;
+ public static final Insets DEFAULT_PADDING = new Insets(40);
+ public static final Insets SMALL_PADDING = new Insets(20);
+ public static final Insets TOP_BAR_PADDING = new Insets(10);
+
+ // 字体
+ public static final String FONT_FAMILY = "Microsoft YaHei";
+ public static final double TITLE_FONT_SIZE = 26.0;
+ public static final double SUBTITLE_FONT_SIZE = 16.0;
+ public static final double BUTTON_FONT_SIZE = 15.0;
+ public static final double LABEL_FONT_SIZE = 14.0;
+ public static final double INPUT_FONT_SIZE = 14.0;
+ public static final double HINT_FONT_SIZE = 12.0;
+ public static final double ERROR_FONT_SIZE = 12.0;
+ public static final double QUIZ_TITLE_FONT_SIZE = 20.0;
+ public static final double SCORE_FONT_SIZE = 32.0;
+
+ // 按钮尺寸
+ public static final double BUTTON_WIDTH = 140.0;
+ public static final double BUTTON_HEIGHT = 40.0;
+ public static final double BACK_BUTTON_WIDTH = 80.0;
+ public static final double BACK_BUTTON_HEIGHT = 30.0;
+
+ // 颜色
+ public static final String COLOR_PRIMARY = "#2c3e50";
+ public static final String COLOR_ACCENT = "#3498db";
+ public static final String COLOR_ERROR = "#e74c3c";
+ public static final String COLOR_HINT = "#7f8c8d";
+ public static final String COLOR_BACKGROUND = "#ecf0f1";
+
+ // 按钮样式
+ public static final String BUTTON_STYLE =
+ "-fx-background-color: " + COLOR_ACCENT + "; " +
+ "-fx-text-fill: white; " +
+ "-fx-background-radius: 8; " +
+ "-fx-font-size: " + BUTTON_FONT_SIZE + "px; " +
+ "-fx-font-family: '" + FONT_FAMILY + "'; " +
+ "-fx-cursor: hand;";
+
+ public static final String BUTTON_HOVER_STYLE =
+ "-fx-background-color: #2980b9;";
+
+ // 输入框样式
+ public static final String INPUT_STYLE =
+ "-fx-background-radius: 8; " +
+ "-fx-border-radius: 8; " +
+ "-fx-border-color: #bdc3c7; " +
+ "-fx-padding: 8; " +
+ "-fx-font-size: " + INPUT_FONT_SIZE + "px; " +
+ "-fx-font-family: '" + FONT_FAMILY + "';";
+
+ // 表单容器样式
+ public static final String FORM_STYLE =
+ "-fx-background-color: white; " +
+ "-fx-background-radius: 12; " +
+ "-fx-border-radius: 12; " +
+ "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.1), 10, 0, 0, 5);";
+}
\ No newline at end of file
diff --git a/src/main/java/com/util/PasswordValidator.java b/src/main/java/com/util/PasswordValidator.java
index 1ab48e7..7c167df 100644
--- a/src/main/java/com/util/PasswordValidator.java
+++ b/src/main/java/com/util/PasswordValidator.java
@@ -12,7 +12,7 @@ public class PasswordValidator {
// 密码长度限制
private static final int MIN_LENGTH = 6;
- private static final int MAX_LENGTH = 20; // 改为20位,更安全
+ private static final int MAX_LENGTH = 10;
// 用于生成随机注册码的字符集
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
@@ -20,8 +20,6 @@ public class PasswordValidator {
private static final String DIGITS = "0123456789";
private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
- // 使用 SecureRandom 替代 Math.random(),更安全
- private static final SecureRandom random = new SecureRandom();
// ==================== 密码验证 ====================
@@ -48,16 +46,20 @@ public class PasswordValidator {
return "密码不能包含空格!";
}
- // 检查是否包含字母
- boolean hasLetter = password.matches(".*[a-zA-Z].*");
-
- // 检查是否包含数字
- boolean hasDigit = password.matches(".*\\d.*");
+ // 检查是否包含小写字母
+ boolean hasLowerLetter = password.matches(".*[a-z].*");
+ if (!hasLowerLetter) {
+ return "必须包含小写字母!";
+ }
- if (!hasLetter) {
- return "密码必须包含字母!";
+ // 检查是否包含大写字母
+ boolean hasUpperLetter = password.matches(".*[A-Z].*");
+ if (!hasUpperLetter) {
+ return "必须包含大写字母!";
}
+ // 检查是否包含数字
+ boolean hasDigit = password.matches(".*\\d.*");
if (!hasDigit) {
return "密码必须包含数字!";
}
@@ -75,39 +77,39 @@ public class PasswordValidator {
return validatePassword(password) == null;
}
- /**
- * 检查密码强度等级
- *
- * @param password 密码
- * @return 强度等级:弱、中、强
- */
- public static String getPasswordStrength(String password) {
- if (password == null || password.length() < MIN_LENGTH) {
- return "弱";
- }
-
- int score = 0;
-
- // 长度加分
- if (password.length() >= 8) score++;
- if (password.length() >= 12) score++;
-
- // 包含小写字母
- if (password.matches(".*[a-z].*")) score++;
-
- // 包含大写字母
- if (password.matches(".*[A-Z].*")) score++;
-
- // 包含数字
- if (password.matches(".*\\d.*")) score++;
-
- // 包含特殊字符
- if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*")) score++;
-
- if (score <= 2) return "弱";
- if (score <= 4) return "中";
- return "强";
- }
+// /**
+// * 检查密码强度等级
+// *
+// * @param password 密码
+// * @return 强度等级:弱、中、强
+// */
+// public static String getPasswordStrength(String password) {
+// if (password == null || password.length() < MIN_LENGTH) {
+// return "弱";
+// }
+//
+// int score = 0;
+//
+// // 长度加分
+// if (password.length() >= 8) score++;
+// if (password.length() >= 12) score++;
+//
+// // 包含小写字母
+// if (password.matches(".*[a-z].*")) score++;
+//
+// // 包含大写字母
+// if (password.matches(".*[A-Z].*")) score++;
+//
+// // 包含数字
+// if (password.matches(".*\\d.*")) score++;
+//
+// // 包含特殊字符
+// if (password.matches(".*[!@#$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>/?].*")) score++;
+//
+// if (score <= 2) return "弱";
+// if (score <= 4) return "中";
+// return "强";
+// }
// ==================== 密码加密 ====================
@@ -164,7 +166,7 @@ public class PasswordValidator {
* @return 注册码
*/
public static String generateRegistrationCode() {
- return generateRegistrationCode(MIN_LENGTH, 10);
+ return generateRegistrationCode(MIN_LENGTH, MAX_LENGTH);
}
/**
@@ -179,151 +181,151 @@ public class PasswordValidator {
throw new IllegalArgumentException("长度参数无效");
}
- int length = minLen + random.nextInt(maxLen - minLen + 1);
+ int length = RandomUtils.nextInt(minLen, maxLen);
StringBuilder code = new StringBuilder(length);
// 确保至少有一个大写字母
- code.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
+ code.append(UPPERCASE.charAt(RandomUtils.nextInt(0, UPPERCASE.length() - 1)));
// 确保至少有一个小写字母
- code.append(LOWERCASE.charAt(random.nextInt(LOWERCASE.length())));
+ code.append(LOWERCASE.charAt(RandomUtils.nextInt(0, LOWERCASE.length() - 1)));
// 确保至少有一个数字
- code.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
+ code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
// 填充剩余字符
for (int i = 3; i < length; i++) {
- code.append(ALL_CHARS.charAt(random.nextInt(ALL_CHARS.length())));
+ code.append(ALL_CHARS.charAt(RandomUtils.nextInt(0, ALL_CHARS.length() - 1)));
}
// 打乱字符顺序
- return shuffleString(code.toString());
+ return RandomUtils.shuffleString(code.toString());
}
- /**
- * 生成固定长度的随机密码
- *
- * @param length 密码长度
- * @param includeSpecialChars 是否包含特殊字符
- * @return 随机密码
- */
- public static String generateRandomPassword(int length, boolean includeSpecialChars) {
- if (length < MIN_LENGTH) {
- throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH);
- }
-
- String chars = ALL_CHARS;
- if (includeSpecialChars) {
- chars += "!@#$%^&*()_+-=[]{}";
- }
-
- StringBuilder password = new StringBuilder(length);
-
- // 确保至少包含一个字母和一个数字
- password.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
- password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
-
- // 填充剩余字符
- for (int i = 2; i < length; i++) {
- password.append(chars.charAt(random.nextInt(chars.length())));
- }
-
- return shuffleString(password.toString());
- }
+// /**
+// * 生成固定长度的随机密码
+// *
+// * @param length 密码长度
+// * @param includeSpecialChars 是否包含特殊字符
+// * @return 随机密码
+// */
+// public static String generateRandomPassword(int length, boolean includeSpecialChars) {
+// if (length < MIN_LENGTH) {
+// throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH);
+// }
+//
+// String chars = ALL_CHARS;
+// if (includeSpecialChars) {
+// chars += "!@#$%^&*()_+-=[]{}";
+// }
+//
+// StringBuilder password = new StringBuilder(length);
+//
+// // 确保至少包含一个字母和一个数字
+// password.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length())));
+// password.append(DIGITS.charAt(random.nextInt(DIGITS.length())));
+//
+// // 填充剩余字符
+// for (int i = 2; i < length; i++) {
+// password.append(chars.charAt(random.nextInt(chars.length())));
+// }
+//
+// return shuffleString(password.toString());
+// }
// ==================== 工具方法 ====================
-
- /**
- * 打乱字符串(使用 Fisher-Yates 算法)
- *
- * @param str 原字符串
- * @return 打乱后的字符串
- */
- private static String shuffleString(String str) {
- if (str == null || str.length() <= 1) {
- return str;
- }
-
- char[] chars = str.toCharArray();
-
- for (int i = chars.length - 1; i > 0; i--) {
- int j = random.nextInt(i + 1);
-
- // 交换
- char temp = chars[i];
- chars[i] = chars[j];
- chars[j] = temp;
- }
-
- return new String(chars);
- }
-
- /**
- * 检查密码是否包含常见弱密码
- *
- * @param password 密码
- * @return true表示是弱密码
- */
- public static boolean isWeakPassword(String password) {
- if (password == null) {
- return true;
- }
-
- String lowerPassword = password.toLowerCase();
-
- // 常见弱密码列表
- String[] weakPasswords = {
- "123456", "password", "123456789", "12345678", "12345",
- "111111", "1234567", "sunshine", "qwerty", "iloveyou",
- "princess", "admin", "welcome", "666666", "abc123",
- "football", "123123", "monkey", "654321", "!@#$%^&*",
- "charlie", "aa123456", "donald", "password1", "qwerty123"
- };
-
- for (String weak : weakPasswords) {
- if (lowerPassword.equals(weak) || lowerPassword.contains(weak)) {
- return true;
- }
- }
-
- // 检查是否是连续数字或字母
- if (password.matches("^(\\d)\\1+$") || // 全是相同数字
- password.matches("^(.)\\1+$") || // 全是相同字符
- password.matches("^(0123456789|123456789|987654321|abcdefghij|qwertyuiop).*")) { // 连续字符
- return true;
- }
-
- return false;
- }
-
- /**
- * 生成密码建议
- *
- * @param password 密码
- * @return 建议文本
- */
- public static String getPasswordSuggestion(String password) {
- if (password == null || password.isEmpty()) {
- return "请输入密码";
- }
-
- String error = validatePassword(password);
- if (error != null) {
- return error;
- }
-
- String strength = getPasswordStrength(password);
-
- if ("弱".equals(strength)) {
- return "密码强度较弱,建议:\n" +
- "• 使用至少8位字符\n" +
- "• 同时包含大小写字母、数字\n" +
- "• 添加特殊字符";
- } else if ("中".equals(strength)) {
- return "密码强度中等,可以考虑添加特殊字符提高安全性";
- } else {
- return "密码强度良好!";
- }
- }
+//
+// /**
+// * 打乱字符串(使用 Fisher-Yates 算法)
+// *
+// * @param str 原字符串
+// * @return 打乱后的字符串
+// */
+// private static String shuffleString(String str) {
+// if (str == null || str.length() <= 1) {
+// return str;
+// }
+//
+// char[] chars = str.toCharArray();
+//
+// for (int i = chars.length - 1; i > 0; i--) {
+// int j = random.nextInt(i + 1);
+//
+// // 交换
+// char temp = chars[i];
+// chars[i] = chars[j];
+// chars[j] = temp;
+// }
+//
+// return new String(chars);
+// }
+
+// /**
+// * 检查密码是否包含常见弱密码
+// *
+// * @param password 密码
+// * @return true表示是弱密码
+// */
+// public static boolean isWeakPassword(String password) {
+// if (password == null) {
+// return true;
+// }
+//
+// String lowerPassword = password.toLowerCase();
+//
+// // 常见弱密码列表
+// String[] weakPasswords = {
+// "123456", "password", "123456789", "12345678", "12345",
+// "111111", "1234567", "sunshine", "qwerty", "iloveyou",
+// "princess", "admin", "welcome", "666666", "abc123",
+// "football", "123123", "monkey", "654321", "!@#$%^&*",
+// "charlie", "aa123456", "donald", "password1", "qwerty123"
+// };
+//
+// for (String weak : weakPasswords) {
+// if (lowerPassword.equals(weak) || lowerPassword.contains(weak)) {
+// return true;
+// }
+// }
+//
+// // 检查是否是连续数字或字母
+// if (password.matches("^(\\d)\\1+$") || // 全是相同数字
+// password.matches("^(.)\\1+$") || // 全是相同字符
+// password.matches("^(0123456789|123456789|987654321|abcdefghij|qwertyuiop).*")) { // 连续字符
+// return true;
+// }
+//
+// return false;
+// }
+
+// /**
+// * 生成密码建议
+// *
+// * @param password 密码
+// * @return 建议文本
+// */
+// public static String getPasswordSuggestion(String password) {
+// if (password == null || password.isEmpty()) {
+// return "请输入密码";
+// }
+//
+// String error = validatePassword(password);
+// if (error != null) {
+// return error;
+// }
+//
+// String strength = getPasswordStrength(password);
+//
+// if ("弱".equals(strength)) {
+// return "密码强度较弱,建议:\n" +
+// "• 使用至少8位字符\n" +
+// "• 同时包含大小写字母、数字\n" +
+// "• 添加特殊字符";
+// } else if ("中".equals(strength)) {
+// return "密码强度中等,可以考虑添加特殊字符提高安全性";
+// } else {
+// return "密码强度良好!";
+// }
+// }
}
\ No newline at end of file
diff --git a/src/main/java/com/util/RandomUtils.java b/src/main/java/com/util/RandomUtils.java
index f06a7ed..94c4635 100644
--- a/src/main/java/com/util/RandomUtils.java
+++ b/src/main/java/com/util/RandomUtils.java
@@ -43,6 +43,28 @@ public class RandomUtils {
Collections.shuffle(list, random);
}
+ /**
+ * 打乱字符串(使用Fisher-Yates算法)
+ * @param str 原字符串
+ * @return 打乱后的字符串
+ */
+ public static String shuffleString(String str) {
+ if (str == null || str.length() <= 1) {
+ return str;
+ }
+
+ char[] chars = str.toCharArray();
+ for (int i = chars.length - 1; i > 0; i--) {
+ int j = random.nextInt(i + 1);
+
+ // 交换字符
+ char temp = chars[i];
+ chars[i] = chars[j];
+ chars[j] = temp;
+ }
+ return new String(chars);
+ }
+
//生成指定范围内的随机双精度浮点数
public static double nextDouble(double min, double max) {
diff --git a/src/test/java/TestMain.java b/src/test/java/TestMain.java
deleted file mode 100644
index 245b50e..0000000
--- a/src/test/java/TestMain.java
+++ /dev/null
@@ -1,774 +0,0 @@
-
-
-import com.model.*;
-import com.service.*;
-import com.service.question_generator.QuestionFactoryManager;
-import com.util.PasswordValidator;
-
-import java.io.IOException;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/**
- * 完整测试类
- * 测试项目的各个功能模块
- */
-public class TestMain {
-
- private static int testsPassed = 0;
- private static int testsFailed = 0;
-
- public static void main(String[] args) {
- System.out.println("========================================");
- System.out.println(" 数学答题系统 - 完整测试");
- System.out.println("========================================\n");
-
- try {
- // 1. 测试工具类
- testPasswordValidator();
-
- // 2. 测试文件服务
- testFileIOService();
-
- // 3. 测试用户服务
- testUserService();
-
- // 4. 测试题目生成
- testQuestionGeneration();
-
- // 5. 测试答题服务
- testQuizService();
-
- // 6. 测试完整流程
- testCompleteWorkflow();
-
- // 输出测试结果
- printTestSummary();
-
- } catch (Exception e) {
- System.err.println("测试过程中发生错误:" + e.getMessage());
- e.printStackTrace();
- }
- }
-
- // ==================== 1. 测试密码验证工具 ====================
-
- private static void testPasswordValidator() {
- System.out.println("【测试1】密码验证工具");
- System.out.println("----------------------------------------");
-
- // 测试1.1: 有效密码
- test("有效密码验证",
- PasswordValidator.isValid("Abc123456"),
- "密码 'Abc123456' 应该有效");
-
- // 测试1.2: 密码太短
- test("密码太短检测",
- !PasswordValidator.isValid("Abc12"),
- "密码 'Abc12' 应该无效(太短)");
-
- // 测试1.3: 缺少数字
- test("缺少数字检测",
- !PasswordValidator.isValid("Abcdefgh"),
- "密码 'Abcdefgh' 应该无效(缺少数字)");
-
- // 测试1.4: 密码加密
- String encrypted1 = PasswordValidator.encrypt("test123");
- String encrypted2 = PasswordValidator.encrypt("test123");
- test("密码加密一致性",
- encrypted1.equals(encrypted2),
- "相同密码加密结果应该一致");
-
- // 测试1.5: 密码匹配
- test("密码匹配验证",
- PasswordValidator.matches("test123", encrypted1),
- "密码匹配应该成功");
-
- // 测试1.6: 生成注册码
- String code = PasswordValidator.generateRegistrationCode();
- test("注册码生成",
- code.length() >= 6 && code.length() <= 10,
- "注册码长度应该在6-10位之间,实际:" + code.length());
-
- // 测试1.7: 密码强度检测
- String strength = PasswordValidator.getPasswordStrength("Abc123!@#");
- test("密码强度检测",
- strength != null && !strength.isEmpty(),
- "密码强度应该返回有效值,实际:" + strength);
-
- System.out.println();
- }
-
- // ==================== 2. 测试文件IO服务 ====================
-
- private static void testFileIOService() throws IOException {
- System.out.println("【测试2】文件IO服务");
- System.out.println("----------------------------------------");
-
- FileIOService fileService = new FileIOService();
-
- // 测试2.1: 初始化目录
- try {
- fileService.initDataDirectory();
- test("初始化数据目录", true, "数据目录初始化成功");
- } catch (Exception e) {
- test("初始化数据目录", false, "失败:" + e.getMessage());
- }
-
- // 测试2.2: 保存和加载用户
- User testUser = new User("小学-测试", "encrypted123", "test@test.com", Grade.ELEMENTARY);
- try {
- fileService.saveUser(testUser);
- User loaded = fileService.findUserByUsername("小学-测试");
- test("保存和加载用户",
- loaded != null && loaded.getUsername().equals("小学-测试"),
- "用户数据应该正确保存和加载");
- } catch (Exception e) {
- test("保存和加载用户", false, "失败:" + e.getMessage());
- }
-
- // 测试2.3: 检查用户名是否存在
- try {
- boolean exists = fileService.isUsernameExists("小学-测试");
- test("检查用户名存在", exists, "用户名应该存在");
- } catch (Exception e) {
- test("检查用户名存在", false, "失败:" + e.getMessage());
- }
-
- // 测试2.4: 查找不存在的用户
- try {
- User notFound = fileService.findUserByUsername("不存在的用户");
- test("查找不存在的用户",
- notFound == null,
- "不存在的用户应该返回null");
- } catch (Exception e) {
- test("查找不存在的用户", false, "失败:" + e.getMessage());
- }
-
- System.out.println();
- }
-
- // ==================== 3. 测试用户服务====================
-
- private static void testUserService() throws IOException {
- System.out.println("【测试3】用户服务(包含验证码)");
- System.out.println("----------------------------------------");
-
- UserService userService = new UserService();
-
- // ========== 3.1 测试注册码生成和保存 ==========
-
- String testEmail1 = "test001@example.com";
- String registrationCode1 = null;
-
- try {
- registrationCode1 = userService.generateRegistrationCode(testEmail1);
- test("生成注册码",
- registrationCode1 != null && registrationCode1.length() >= 6,
- "注册码:" + registrationCode1);
- } catch (Exception e) {
- test("生成注册码", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.2 测试注册码文件存储 ==========
-
- try {
- boolean fileExists = com.util.FileUtils.exists("data/registration_codes.txt");
- test("注册码文件创建",
- fileExists,
- "注册码应该保存到文件");
- } catch (Exception e) {
- test("注册码文件创建", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.3 测试用户注册(带验证码)==========
-
- String testUsername = "小学-张三测试";
- String testPassword = "Test123456";
-
- try {
- User user = userService.register(testUsername, testPassword, testEmail1, registrationCode1);
- test("用户注册(带验证码)",
- user != null && user.getUsername().equals(testUsername),
- "用户注册成功");
- } catch (Exception e) {
- test("用户注册(带验证码)", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.4 测试注册码一次性使用 ==========
-
- try {
- // 尝试用同一个注册码再次注册
- userService.register("小学-李四", "Test123456", testEmail1, registrationCode1);
- test("注册码一次性使用", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("注册码一次性使用",
- e.getMessage().contains("未找到"),
- "注册码使用后应该被删除");
- } catch (Exception e) {
- test("注册码一次性使用", false, "异常类型错误:" + e.getMessage());
- }
-
- // ========== 3.5 测试错误的注册码 ==========
-
- String testEmail2 = "test002@example.com";
-
- try {
- String code = userService.generateRegistrationCode(testEmail2);
- // 故意使用错误的注册码
- userService.register("小学-王五", "Test123456", testEmail2, "wrongCode123");
- test("错误注册码检测", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("错误注册码检测",
- e.getMessage().contains("注册码错误"),
- "应该检测到错误的注册码");
- } catch (Exception e) {
- test("错误注册码检测", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.6 测试未获取注册码就注册 ==========
-
- try {
- userService.register("小学-赵六", "Test123456", "nocode@test.com", "randomCode");
- test("未获取注册码检测", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("未获取注册码检测",
- e.getMessage().contains("未找到"),
- "应该检测到未获取注册码");
- } catch (Exception e) {
- test("未获取注册码检测", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.7 测试重复注册检测 ==========
-
- String testEmail3 = "test003@example.com";
-
- try {
- String code = userService.generateRegistrationCode(testEmail3);
- userService.register(testUsername, "Test123456", testEmail3, code);
- test("重复注册检测", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("重复注册检测",
- e.getMessage().contains("已存在"),
- "应该检测到用户名已存在");
- } catch (Exception e) {
- test("重复注册检测", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.8 测试用户登录 ==========
-
- try {
- User user = userService.login(testUsername, testPassword);
- test("用户登录",
- user != null && userService.isLoggedIn(),
- "用户登录成功");
- } catch (Exception e) {
- test("用户登录", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.9 测试错误密码登录 ==========
-
- try {
- userService.logout(); // 先退出
- userService.login(testUsername, "WrongPassword123");
- test("错误密码登录", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("错误密码登录",
- e.getMessage().contains("密码错误"),
- "应该检测到密码错误");
- } catch (Exception e) {
- test("错误密码登录", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.10 测试不存在的用户登录 ==========
-
- try {
- userService.login("小学-不存在", "Test123456");
- test("不存在用户登录", false, "应该抛出异常");
- } catch (IllegalArgumentException e) {
- test("不存在用户登录",
- e.getMessage().contains("不存在"),
- "应该检测到用户名不存在");
- } catch (Exception e) {
- test("不存在用户登录", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.11 测试获取当前用户 ==========
-
- try {
- userService.login(testUsername, testPassword);
- User current = userService.getCurrentUser();
- test("获取当前用户",
- current != null && current.getUsername().equals(testUsername),
- "应该返回当前登录用户");
- } catch (Exception e) {
- test("获取当前用户", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.12 测试提取真实姓名 ==========
-
- User user = userService.getCurrentUser();
- String realName = userService.getRealName(user);
- test("提取真实姓名",
- realName.equals("张三测试"),
- "应该正确提取真实姓名,实际:" + realName);
-
- // ========== 3.13 测试获取学段显示名 ==========
-
- String gradeName = userService.getGradeDisplayName(user);
- test("获取学段显示名",
- gradeName.equals("小学"),
- "应该返回'小学',实际:" + gradeName);
-
- // ========== 3.14 测试退出登录 ==========
-
- userService.logout();
- test("退出登录",
- !userService.isLoggedIn(),
- "退出后应该未登录状态");
-
- // ========== 3.15 测试完整注册流程(不同学段)==========
-
- // 初中学生注册
- try {
- String middleEmail = "middle@test.com";
- String middleCode = userService.generateRegistrationCode(middleEmail);
- User middleUser = userService.register("初中-李明", "Middle123", middleEmail, middleCode);
-
- test("初中学生注册",
- middleUser != null && middleUser.getGrade() == Grade.MIDDLE,
- "初中学生注册成功");
- } catch (Exception e) {
- test("初中学生注册", false, "失败:" + e.getMessage());
- }
-
- // 高中学生注册
- try {
- String highEmail = "high@test.com";
- String highCode = userService.generateRegistrationCode(highEmail);
- User highUser = userService.register("高中-王华", "High123456", highEmail, highCode);
-
- test("高中学生注册",
- highUser != null && highUser.getGrade() == Grade.HIGH,
- "高中学生注册成功");
- } catch (Exception e) {
- test("高中学生注册", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.16 测试密码强度验证 ==========
-
- try {
- String weakEmail = "weak@test.com";
- String weakCode = userService.generateRegistrationCode(weakEmail);
- userService.register("小学-弱密码", "123", weakEmail, weakCode);
- test("密码强度验证", false, "应该拒绝弱密码");
- } catch (IllegalArgumentException e) {
- test("密码强度验证",
- e.getMessage().contains("密码"),
- "应该检测到密码不符合要求");
- } catch (Exception e) {
- test("密码强度验证", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.17 测试邮箱格式验证 ==========
-
- try {
- userService.generateRegistrationCode("invalid-email");
- test("邮箱格式验证", false, "应该拒绝无效邮箱");
- } catch (IllegalArgumentException e) {
- test("邮箱格式验证",
- e.getMessage().contains("邮箱"),
- "应该检测到邮箱格式错误");
- } catch (Exception e) {
- test("邮箱格式验证", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.18 测试用户名格式验证 ==========
-
- try {
- String invalidEmail = "invalid@test.com";
- String invalidCode = userService.generateRegistrationCode(invalidEmail);
- userService.register("错误格式", "Test123456", invalidEmail, invalidCode);
- test("用户名格式验证", false, "应该拒绝错误格式的用户名");
- } catch (IllegalArgumentException e) {
- test("用户名格式验证",
- e.getMessage().contains("格式"),
- "应该检测到用户名格式错误");
- } catch (Exception e) {
- test("用户名格式验证", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.19 测试清理过期注册码 ==========
-
- try {
- userService.cleanExpiredCodes();
- test("清理过期注册码", true, "清理操作成功");
- } catch (Exception e) {
- test("清理过期注册码", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.20 测试从文件重新加载注册码 ==========
-
- try {
- String reloadEmail = "reload@test.com";
- String reloadCode = userService.generateRegistrationCode(reloadEmail);
-
- // 创建新的 UserService 实例(模拟重启)
- UserService newUserService = new UserService();
-
- // 使用之前保存的注册码
- User reloadUser = newUserService.register("小学-重载测试", "Reload123", reloadEmail, reloadCode);
-
- test("从文件重载注册码",
- reloadUser != null,
- "应该能从文件读取注册码");
- } catch (Exception e) {
- test("从文件重载注册码", false, "失败:" + e.getMessage());
- }
-
- // ========== 3.21 查看注册码文件内容 ==========
-
- try {
- if (com.util.FileUtils.exists("data/registration_codes.txt")) {
- String fileContent = com.util.FileUtils.readFileToString(
- "data/registration_codes.txt"
- );
-
- System.out.println("\n 【注册码文件内容预览】");
- String[] lines = fileContent.split("\n");
- int lineCount = 0;
- for (String line : lines) {
- if (lineCount++ < 10) { // 显示前10行
- System.out.println(" " + line);
- }
- }
- if (lines.length > 10) {
- System.out.println(" ... (共 " + lines.length + " 行)");
- }
-
- test("注册码文件格式",
- fileContent.contains("#") && fileContent.contains("|"),
- "文件格式正确");
- }
- } catch (Exception e) {
- test("查看文件内容", false, "失败:" + e.getMessage());
- }
-
- System.out.println();
- }
-
- // ==================== 4. 测试题目生成 ====================
-
- private static void testQuestionGeneration() {
- System.out.println("【测试4】题目生成");
- System.out.println("----------------------------------------");
-
- // 测试4.1: 生成小学题目(不去重)
- try {
- List questions = QuestionFactoryManager.generateQuestions(
- Grade.ELEMENTARY, 1, null
- );
-
- test("生成小学题目",
- questions.size() == 1 && questions.get(0).getQuestionText() != null,
- "应该生成1道有效的小学题目");
-
- System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
- } catch (Exception e) {
- test("生成小学题目", false, "失败:" + e.getMessage());
- }
-
- // 测试4.2: 生成初中题目
- try {
- List questions = QuestionFactoryManager.generateQuestions(
- Grade.MIDDLE, 1, null
- );
-
- test("生成初中题目",
- questions.size() == 1,
- "应该生成1道有效的初中题目");
-
- System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
- } catch (Exception e) {
- test("生成初中题目", false, "失败:" + e.getMessage());
- }
-
- // 测试4.3: 生成高中题目
- try {
- List questions = QuestionFactoryManager.generateQuestions(
- Grade.HIGH, 1, null
- );
-
- test("生成高中题目",
- questions.size() == 1,
- "应该生成1道有效的高中题目");
-
- System.out.println(" 示例题目:" + questions.get(0).getQuestionText());
- } catch (Exception e) {
- test("生成高中题目", false, "失败:" + e.getMessage());
- }
-
- // 测试4.4: 批量生成题目
- try {
- List questions = QuestionFactoryManager.generateQuestions(
- Grade.ELEMENTARY, 10, null
- );
-
- test("批量生成题目",
- questions.size() == 10,
- "应该生成10道题目,实际:" + questions.size());
- } catch (Exception e) {
- test("批量生成题目", false, "失败:" + e.getMessage());
- }
-
- // 测试4.5: 题目去重功能
- try {
- // 第一次生成
- List firstBatch = QuestionFactoryManager.generateQuestions(
- Grade.ELEMENTARY, 5, null
- );
-
- // 收集已生成的题目文本
- Set historyQuestions = new HashSet<>();
- for (ChoiceQuestion q : firstBatch) {
- historyQuestions.add(q.getQuestionText());
- }
-
- // 第二次生成(带去重)
- List secondBatch = QuestionFactoryManager.generateQuestions(
- Grade.ELEMENTARY, 5, historyQuestions
- );
-
- // 检查第二次生成的题目是否与第一次重复
- boolean noDuplicate = true;
- for (ChoiceQuestion q : secondBatch) {
- if (historyQuestions.contains(q.getQuestionText())) {
- noDuplicate = false;
- break;
- }
- }
-
- test("题目去重功能",
- noDuplicate,
- "第二次生成的题目不应与第一次重复");
- } catch (Exception e) {
- test("题目去重功能", false, "失败:" + e.getMessage());
- }
-
- System.out.println();
- }
- // ==================== 5. 测试答题服务 ====================
-
- private static void testQuizService() throws IOException {
- System.out.println("【测试5】答题服务");
- System.out.println("----------------------------------------");
-
- FileIOService fileService = new FileIOService();
- UserService userService = new UserService(fileService);
- QuizService quizService = new QuizService(fileService, userService);
-
- // 创建测试用户
- User testUser = new User("小学-李四", "encrypted", "lisi@test.com", Grade.ELEMENTARY);
- fileService.saveUser(testUser);
-
- // 测试5.1: 开始答题
- try {
- quizService.startNewQuiz(testUser, 5);
- test("开始答题会话",
- quizService.getTotalQuestions() == 5,
- "应该生成5道题目");
- } catch (Exception e) {
- test("开始答题会话", false, "失败:" + e.getMessage());
- }
-
- // 测试5.2: 获取当前题目
- ChoiceQuestion current = quizService.getCurrentQuestion();
- test("获取当前题目",
- current != null,
- "应该返回当前题目");
-
- // 测试5.3: 提交答案
- try {
- boolean correct = quizService.submitCurrentAnswer(0);
- test("提交答案",
- true, // 只要不抛异常就算通过
- "提交答案应该成功,结果:" + (correct ? "正确" : "错误"));
- } catch (Exception e) {
- test("提交答案", false, "失败:" + e.getMessage());
- }
-
- // 测试5.4: 题目导航
- boolean canNext = quizService.nextQuestion();
- test("下一题导航",
- canNext,
- "应该能够移动到下一题");
-
- boolean canPrev = quizService.previousQuestion();
- test("上一题导航",
- canPrev,
- "应该能够移动到上一题");
-
- // 测试5.5: 检查答案
- ChoiceQuestion question = quizService.getCurrentQuestion();
- int correctIndex = quizService.getCorrectAnswerIndex(question);
- boolean isCorrect = quizService.checkAnswer(question, correctIndex);
- test("检查正确答案",
- isCorrect,
- "正确答案应该通过验证");
-
- // 测试5.6: 答题进度
- quizService.goToQuestion(0);
- quizService.submitCurrentAnswer(0);
- quizService.nextQuestion();
- quizService.submitCurrentAnswer(1);
-
- int answered = quizService.getAnsweredCount();
- test("答题进度统计",
- answered == 2,
- "应该有2道题已作答,实际:" + answered);
-
- // 测试5.7: 完成所有题目并计算成绩
- for (int i = 0; i < quizService.getTotalQuestions(); i++) {
- quizService.goToQuestion(i);
- quizService.submitCurrentAnswer(0);
- }
-
- QuizResult result = quizService.calculateResult();
- test("计算成绩",
- result.getTotalQuestions() == 5,
- "成绩统计应该正确,总题数:" + result.getTotalQuestions());
-
- System.out.println(" 得分:" + result.getScore());
- System.out.println(" 正确:" + result.getCorrectCount());
- System.out.println(" 错误:" + result.getWrongCount());
-
- // 测试5.8: 格式化输出
- String formatted = quizService.formatResult(result);
- test("格式化结果输出",
- formatted != null && formatted.contains("答题结束"),
- "应该返回格式化的结果文本");
-
- System.out.println();
- }
-
- // ==================== 6. 测试完整流程 ====================
-
- private static void testCompleteWorkflow() throws IOException {
- System.out.println("【测试6】完整答题流程");
- System.out.println("----------------------------------------");
-
- FileIOService fileService = new FileIOService();
- UserService userService = new UserService(fileService);
- QuizService quizService = new QuizService(fileService, userService);
-
- try {
- // ========== 步骤1: 注册新用户 ==========
- System.out.println("步骤1: 注册新用户...");
-
- String username = "初中-王五";
- String password = "Test123456";
- String email = "wangwu@test.com";
-
- // 1.1 生成注册码
- String registrationCode = userService.generateRegistrationCode(email);
- System.out.println(" 获取注册码:" + registrationCode);
-
- // 1.2 使用注册码注册
- User user = userService.register(username, password, email, registrationCode);
- test("完整流程-注册", user != null, "用户注册成功");
-
- // ========== 步骤2: 用户登录 ==========
- System.out.println("步骤2: 用户登录...");
- userService.login(username, password);
- test("完整流程-登录", userService.isLoggedIn(), "用户登录成功");
-
- // ========== 步骤3: 开始答题 ==========
- System.out.println("步骤3: 开始答题(10道题)...");
- quizService.startNewQuiz(user, 10);
- test("完整流程-生成题目",
- quizService.getTotalQuestions() == 10,
- "题目生成成功");
-
- // ========== 步骤4: 答题(模拟全部答对)==========
- System.out.println("步骤4: 模拟答题过程...");
- for (int i = 0; i < 10; i++) {
- quizService.goToQuestion(i);
- ChoiceQuestion q = quizService.getCurrentQuestion();
- int correctIndex = quizService.getCorrectAnswerIndex(q);
- quizService.submitAnswer(i, correctIndex);
- }
- test("完整流程-答题", quizService.isAllAnswered(), "所有题目已作答");
-
- // ========== 步骤5: 计算成绩 ==========
- System.out.println("步骤5: 计算成绩...");
- QuizResult result = quizService.calculateResult();
- test("完整流程-计算成绩",
- result.getScore() == 100,
- "全部答对应该得100分,实际:" + result.getScore());
-
- System.out.println(quizService.formatResult(result));
-
- // ========== 步骤6: 保存记录 ==========
- System.out.println("步骤6: 保存答题记录...");
- quizService.saveQuizHistory(user);
-
- // 验证用户统计是否更新
- User updatedUser = fileService.findUserByUsername(username);
- test("完整流程-保存记录",
- updatedUser.getTotalQuizzes() == 1,
- "用户答题次数应该增加,实际:" + updatedUser.getTotalQuizzes());
-
- test("完整流程-平均分更新",
- updatedUser.getAverageScore() == 100.0,
- "平均分应该更新,实际:" + updatedUser.getAverageScore());
-
- // ========== 步骤7: 退出登录 ==========
- System.out.println("步骤7: 退出登录...");
- userService.logout();
- test("完整流程-退出", !userService.isLoggedIn(), "退出登录成功");
-
- System.out.println("\n✓ 完整流程测试通过!");
-
- } catch (Exception e) {
- test("完整流程", false, "失败:" + e.getMessage());
- e.printStackTrace();
- }
-
- System.out.println();
- }
-
- // ==================== 测试工具方法 ====================
-
- private static void test(String testName, boolean condition, String message) {
- if (condition) {
- System.out.println(" ✓ " + testName + ": 通过");
- if (message != null && !message.isEmpty()) {
- System.out.println(" " + message);
- }
- testsPassed++;
- } else {
- System.out.println(" ✗ " + testName + ": 失败");
- if (message != null && !message.isEmpty()) {
- System.out.println(" " + message);
- }
- testsFailed++;
- }
- }
-
- private static void printTestSummary() {
- System.out.println("========================================");
- System.out.println(" 测试结果汇总");
- System.out.println("========================================");
- System.out.println("总测试数:" + (testsPassed + testsFailed));
- System.out.println("通过:" + testsPassed + " 项");
- System.out.println("失败:" + testsFailed + " 项");
-
- if (testsFailed == 0) {
- System.out.println("\n🎉 所有测试通过!项目功能正常,可以开始开发UI了!");
- } else {
- System.out.println("\n⚠ 有 " + testsFailed + " 项测试失败,请检查并修复问题");
- }
- System.out.println("========================================");
- }
-}
\ No newline at end of file