diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..5ff6309
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,38 @@
+target/
+!.mvn/wrapper/maven-wrapper.jar
+!**/src/main/**/target/
+!**/src/test/**/target/
+
+### IntelliJ IDEA ###
+.idea/modules.xml
+.idea/jarRepositories.xml
+.idea/compiler.xml
+.idea/libraries/
+*.iws
+*.iml
+*.ipr
+
+### Eclipse ###
+.apt_generated
+.classpath
+.factorypath
+.project
+.settings
+.springBeans
+.sts4-cache
+
+### NetBeans ###
+/nbproject/private/
+/nbbuild/
+/dist/
+/nbdist/
+/.nb-gradle/
+build/
+!**/src/main/**/build/
+!**/src/test/**/build/
+
+### VS Code ###
+.vscode/
+
+### Mac OS ###
+.DS_Store
\ No newline at end of file
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..6851aea
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,216 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {}
+ {
+ "isMigrated": true
+}
+
+
+
+ {
+ "customColor": "",
+ "associatedIndex": 1
+}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1759546098925
+
+
+ 1759546098925
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1759588559366
+
+
+
+ 1759588559366
+
+
+
+ 1759684484435
+
+
+
+ 1759684484435
+
+
+
+ 1759749578708
+
+
+
+ 1759749578708
+
+
+
+ 1759758970278
+
+
+
+ 1759758970278
+
+
+
+ 1759820089275
+
+
+
+ 1759820089275
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/META-INF/MANIFEST.MF b/META-INF/MANIFEST.MF
new file mode 100644
index 0000000..0010825
--- /dev/null
+++ b/META-INF/MANIFEST.MF
@@ -0,0 +1,3 @@
+Manifest-Version: 1.0
+Main-Class: com.pair.Test
+
diff --git a/data/current_user.json b/data/current_user.json
new file mode 100644
index 0000000..32c4470
--- /dev/null
+++ b/data/current_user.json
@@ -0,0 +1,10 @@
+{
+ "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/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/dependency-reduced-pom.xml b/dependency-reduced-pom.xml
new file mode 100644
index 0000000..85a40a2
--- /dev/null
+++ b/dependency-reduced-pom.xml
@@ -0,0 +1,102 @@
+
+
+ 4.0.0
+ com.mathquiz
+ MathQuizApp
+ Math Quiz Application
+ 1.04
+ 小初高数学学习软件- JavaFX版本
+
+
+
+ maven-compiler-plugin
+ 3.11.0
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ UTF-8
+
+
+
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+
+
+
+ maven-dependency-plugin
+ 3.6.1
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/dependencies
+ false
+ false
+ true
+
+
+
+
+
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ com.pair.Test
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+ MathQuizApp
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+ com.pair.Test
+
+ --add-modules
+ javafx.controls,javafx.fxml,javafx.graphics,javafx.base
+
+
+
+
+
+
+
+ jitpack.io
+ https://jitpack.io
+
+
+
+ 21
+ 21
+ UTF-8
+ 21.0.2
+
+
diff --git a/doc/测试文档/发行版1.03测试.md b/doc/测试文档/发行版1.03测试.md
new file mode 100644
index 0000000..e23cdbb
--- /dev/null
+++ b/doc/测试文档/发行版1.03测试.md
@@ -0,0 +1,15 @@
+# 发行版1.03测试
+
+## 新增功能:
+- 可发送真实邮箱
+
+## 待实现功能或待修改bug:
+- 点击注册时窗口界面会卡顿,感觉可以将发送动作放到后台
+- 增加发送注册码后60s后才可继续发送
+- 注册码发送提示还是错误图标,新建一个弹窗提示,不混用错误弹窗函数,提示改为“已发送到邮箱,10分钟内有效”
+- 生成题目页面选择初中高中学段生成题目,做完题出来学段选择又变回小学了,页面初始化得根据用户学段调整
+- 得分的%去掉,得分是按百分比计算,不是将百分比当作得分
+- 注册码简单一点,6为随机数字
+- 个人信息窗口,直接退出回到登录界面也会保存修改的用户名
+- 密码修改成功后窗口提示
+- 登录bug,目前只能用用户名登录
\ No newline at end of file
diff --git a/doc/测试文档/发行版1.04测试.md b/doc/测试文档/发行版1.04测试.md
new file mode 100644
index 0000000..aad0112
--- /dev/null
+++ b/doc/测试文档/发行版1.04测试.md
@@ -0,0 +1,7 @@
+# 发行版1.04测试
+
+## 新增功能
+- 修复登录只能用用户名登录的bug
+- 新增初中高中题型
+- 新增生成注册码冷却时间60s
+- 优化动作提示
\ No newline at end of file
diff --git a/doc/设计文档/设计文档第一版.md b/doc/设计文档/设计文档第一版.md
new file mode 100644
index 0000000..b2b7efa
--- /dev/null
+++ b/doc/设计文档/设计文档第一版.md
@@ -0,0 +1,101 @@
+# 数学题库生成系统详细设计文档
+
+## 项目概述
+
+### 项目目标
+为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
+
+### 技术栈
+
+| 类型 | 技术 | 说明 |
+| -------- | -------------------------- | ---------------------------------------- |
+| 开发语言 | Java 11+ | 保证跨平台兼容性,支持 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/
+├── src/
+│ └── main/
+│ └── java/
+│ └── com/
+│ └── mathquiz/
+│ ├── Main.java # 程序入口
+│ │
+│ ├── model/ # 数据模型
+│ │ ├── User.java
+│ │ ├── Grade.java
+│ │ └── ChoiceQuestion.java
+│ │
+│ ├── service/ # 后端逻辑(无GUI)
+│ │ ├── UserService.java
+│ │ ├── QuestionGenerator.java
+│ │ ├── FileIOService.java
+│ │ └── QuizService.java
+│ │
+│ ├── ui/ # 前端 GUI
+│ │ ├── MainWindow.java
+│ │ ├── RegisterPanel.java
+│ │ ├── LoginPanel.java
+| | ├── PasswordModifyPanel.java
+│ │ ├── GradeSelectPanel.java
+│ │ ├── QuizPanel.java
+│ │ └── ResultPanel.java
+│ │
+│ └── util/ # 工具类
+│ ├── PasswordValidator.java
+| ├── EmailUtil.java
+│ ├── RandomUtils.java
+│ └── FileUtils.java
+│
+├── data/ # 运行时生成(不提交)
+│ ├── users/ # 用户信息 JSON
+│ └── temp_codes/ # 临时注册码文件
+│
+├── pom.xml # Maven 依赖(含 JavaFX)
+└── README.md
+
+## 详细模块设计
+
+### 模型层设计
+
+#### User类
+- String name // 用户ID
+- String email // 用户邮箱
+- String encryptedPwd // 加密后的用户密码
+- Grade grade //学段
+
+#### ChoiceQuestion
+String questionId; // 题目唯一ID(UUID生成)
+Grade grade; // 所属学段
+String questionContent; // 题干
+List options; // 选项列表(固定4个,顺序随机)
+String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D")
+
+#### Grade 枚举
+PRIMARY("小学", 1)
+JUNIOR("初中", 2)
+SENIOR("高中", 3)
+
+### 工具层设计
+
+#### PasswordValidator
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ |
+| isValid | String rawPwd | boolean | 校验规则:1. 长度 6-10 位;2. 包含至少 1 个大写字母;3. 包含至少 1 个小写字母;4. 包含至少 1 个数字 |
+| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) |
+| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配(BCrypt 自带校验逻辑) |
+
+#### EmailUtil
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ |
+| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器(主机、端口、SSL);2. 设置发件人账号 / 授权码;3. 构建邮件内容;4. 发送并返回结果 |
\ No newline at end of file
diff --git a/doc/设计文档/设计文档第三版.md b/doc/设计文档/设计文档第三版.md
new file mode 100644
index 0000000..33e458f
--- /dev/null
+++ b/doc/设计文档/设计文档第三版.md
@@ -0,0 +1,219 @@
+# 数学题库生成系统详细设计文档
+
+## 项目概述
+
+### 项目目标
+为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
+
+### 技术栈
+
+| 类型 | 技术 | 说明 |
+| -------- | -------------------------- | ---------------------------------------- |
+| 开发语言 | 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/
+├── src/
+│ └── main/
+│ └── java/
+│ └── com/
+│ └── mathquiz/
+│ ├── Main.java # 程序入口
+│ │
+│ ├── model/ # 数据模型
+│ │ ├── User.java
+│ │ ├── Grade.java
+│ │ └── ChoiceQuestion.java
+│ │
+│ ├── service/ # 后端逻辑(无GUI)
+│ │ ├── UserService.java
+│ │ ├── QuestionGenerator.java
+│ │ ├── FileIOService.java
+│ │ └── QuizService.java
+│ │
+│ ├── ui/ # 前端 GUI
+│ │ ├── MainWindow.java
+│ │ ├── RegisterPanel.java
+│ │ ├── LoginPanel.java
+| | ├── PasswordModifyPanel.java
+│ │ ├── GradeSelectPanel.java
+│ │ ├── QuizPanel.java
+│ │ └── ResultPanel.java
+│ │
+│ └── util/ # 工具类
+│ ├── PasswordValidator.java
+| ├── EmailUtil.java
+│ ├── RandomUtils.java
+│ └── FileUtils.java
+│
+├── data/ # 运行时生成(不提交)
+│ ├── users/ # 用户信息 JSON
+│ └── temp_codes/ # 临时注册码文件
+│
+├── pom.xml # Maven 依赖(含 JavaFX)
+└── README.md
+
+## 详细模块设计
+
+### 模型层设计
+
+#### User类
+- String name // 用户ID
+- String email // 用户邮箱
+- String encryptedPwd // 加密后的用户密码
+- Grade grade //学段
+
+#### ChoiceQuestion
+String questionId; // 题目唯一ID(UUID生成)
+Grade grade; // 所属学段
+String questionContent; // 题干
+List options; // 选项列表(固定4个,顺序随机)
+String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D")
+
+#### Grade 枚举
+PRIMARY("小学", 1)
+JUNIOR("初中", 2)
+SENIOR("高中", 3)
+
+### 工具层设计
+
+#### PasswordValidator
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------------------- | ------------------------------------------------ | --------- | ------------------------------------------------------------ |
+| isValid | String password | boolean | 校验密码:**6–10位**,**必须同时包含大写字母、小写字母和数字** |
+| encrypt | String password | String | 使用 **SHA-256** 对密码哈希,返回 64 位十六进制字符串 |
+| matches | String plainPassword, String encryptedPassword | boolean | 对明文密码加密后与存储的密文比对,判断是否匹配 |
+| generateRegistrationCode | — | String | 生成 **6–10位** 随机字符串,**保证至少含1字母+1数字**,并打乱顺序 |
+| shuffleString | String str | String | (私有)对字符串字符顺序进行 Fisher-Yates 打乱 |
+
+#### 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,}$ 验证邮箱格式是否合法 |
+
+#### 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 | 删除文件,若文件不存在也返回 true,异常时返回 false 并打印堆栈 |
+| listFiles | String dirPath | File[] | 返回目录下所有文件(不含子目录),若路径无效返回空数组 |
+| appendToFile | String filePath, String content | void | 以 UTF-8 编码追加内容到文件末尾,自动创建文件(若不存在) |
+| copyFile | String sourcePath, String targetPath | void | 复制文件,目标文件存在则覆盖,失败抛出 IOException |
+| getFileSize | String filePath | long | 返回文件大小(字节),失败抛出 IOException |
+
+#### RandomUtils
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------- | ------------------------ | --------- | ---------------------------------------------------------- |
+| nextInt | int min, int max | int | 生成 [min, max] 范围内的随机整数(含边界) |
+| randomChoice | T[] array | T | 从非空数组中随机返回一个元素 |
+| randomChoice | List list | T | 从非空列表中随机返回一个元素 |
+| shuffle | List list | void | 原地打乱列表顺序(使用 Collections.shuffle) |
+| nextDouble | double min, double max | double | 生成 [min, max) 范围内的随机双精度浮点数 |
+| nextBoolean | — | boolean | 返回 true 或 false(各 50% 概率) |
+| probability | double probability | boolean | 按指定概率返回 true(如 0.7 表示 70% 概率返回 true) |
+
+### 服务层设计
+
+#### UserService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ |
+| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt |
+| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt,校验注册码是否匹配 |
+| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则(6–10位,含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json,成功后删除临时注册码文件 |
+| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null |
+| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 |
+
+#### QuestionGenerator (抽象类)
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ------------- | ------------------- | ------------------------------------------------------------ |
+| create | Grade grade | QuestionGenerator | 静态工厂方法,根据年级返回对应子类实例(PrimaryGenerator / MiddleGenerator / SeniorGenerator) |
+| generateOne | — | ChoiceQuestion | 抽象方法,由子类实现,生成一道符合年级要求的选择题(含题干、4选项、正确答案) |
+
+#### FileIOService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ |
+| saveUser | User user | void | 将用户序列化为 JSON,保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json |
+| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null |
+| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt |
+| loadCode | String email | String | 读取 data/temp_codes/{email}.txt,返回注册码或 null |
+| loadUsedQuestionStems | String email | Set | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件,提取题干(每行以“1.”、“2.”开头的内容),用于查重 |
+
+#### QuizService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ |
+| generateQuestions | String email, int count | List | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set) |
+| calculateScore | List questions, List userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) |
+| savePaper | String email, List questions | void | 生成时间戳文件名(yyyy-MM-dd-HH-mm-ss.txt),保存题干到 data/users/{email}/papers/(目录自动创建) |
+
+### UI层接口设计
+
+#### MainWindow
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- |
+| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 |
+| showLoginPanel | — | void | 创建并显示 LoginPanel |
+| showRegisterPanel | — | void | 创建并显示 RegisterPanel |
+| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel |
+| showQuizPanel | List questions, QuizService quizService | void | 创建并显示 QuizPanel |
+| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel |
+
+#### RegisterPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 |
+| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” |
+| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 |
+
+#### LoginPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ----------------------- | ------ | ----------------------------------------------------------- |
+| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 |
+| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 |
+
+#### GradeSelectPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------- | ----------------------- | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 |
+| startQuiz | Grade grade | void | 弹出数量输入对话框(10–30),调用 QuizService.generateQuestions(),跳转到答题页 |
+
+#### QuizPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow, List questions, QuizService quizService | — | 显示第1题 |
+| showQuestion | int index | void | 渲染当前题干和4个选项(RadioButton),绑定“提交”按钮 |
+| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 |
+
+#### ResultPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- |
+| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 |
+| exitAction | — | void | 跳转到登录页 |
+| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 |
\ No newline at end of file
diff --git a/doc/设计文档/设计文档第二版.md b/doc/设计文档/设计文档第二版.md
new file mode 100644
index 0000000..a6ada6e
--- /dev/null
+++ b/doc/设计文档/设计文档第二版.md
@@ -0,0 +1,189 @@
+# 数学题库生成系统详细设计文档
+
+## 项目概述
+
+### 项目目标
+为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
+
+### 技术栈
+
+| 类型 | 技术 | 说明 |
+| -------- | -------------------------- | ---------------------------------------- |
+| 开发语言 | 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/
+├── src/
+│ └── main/
+│ └── java/
+│ └── com/
+│ └── mathquiz/
+│ ├── Main.java # 程序入口
+│ │
+│ ├── model/ # 数据模型
+│ │ ├── User.java
+│ │ ├── Grade.java
+│ │ └── ChoiceQuestion.java
+│ │
+│ ├── service/ # 后端逻辑(无GUI)
+│ │ ├── UserService.java
+│ │ ├── QuestionGenerator.java
+│ │ ├── FileIOService.java
+│ │ └── QuizService.java
+│ │
+│ ├── ui/ # 前端 GUI
+│ │ ├── MainWindow.java
+│ │ ├── RegisterPanel.java
+│ │ ├── LoginPanel.java
+| | ├── PasswordModifyPanel.java
+│ │ ├── GradeSelectPanel.java
+│ │ ├── QuizPanel.java
+│ │ └── ResultPanel.java
+│ │
+│ └── util/ # 工具类
+│ ├── PasswordValidator.java
+| ├── EmailUtil.java
+│ ├── RandomUtils.java
+│ └── FileUtils.java
+│
+├── data/ # 运行时生成(不提交)
+│ ├── users/ # 用户信息 JSON
+│ └── temp_codes/ # 临时注册码文件
+│
+├── pom.xml # Maven 依赖(含 JavaFX)
+└── README.md
+
+## 详细模块设计
+
+### 模型层设计
+
+#### User类
+- String name // 用户ID
+- String email // 用户邮箱
+- String encryptedPwd // 加密后的用户密码
+- Grade grade //学段
+
+#### ChoiceQuestion
+String questionId; // 题目唯一ID(UUID生成)
+Grade grade; // 所属学段
+String questionContent; // 题干
+List options; // 选项列表(固定4个,顺序随机)
+String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D")
+
+#### Grade 枚举
+PRIMARY("小学", 1)
+JUNIOR("初中", 2)
+SENIOR("高中", 3)
+
+### 工具层设计
+
+#### PasswordValidator
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ |
+| isValid | String rawPwd | boolean | 校验规则:1. 长度 6-10 位;2. 包含至少 1 个大写字母;3. 包含至少 1 个小写字母;4. 包含至少 1 个数字 |
+| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) |
+| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配(BCrypt 自带校验逻辑) |
+
+#### EmailUtil
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ |
+| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器(主机、端口、SSL);2. 设置发件人账号 / 授权码;3. 构建邮件内容;4. 发送并返回结果 |
+
+### 服务层设计
+
+#### UserService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ |
+| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt |
+| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt,校验注册码是否匹配 |
+| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则(6–10位,含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json,成功后删除临时注册码文件 |
+| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null |
+| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 |
+
+#### QuestionGenerator (抽象类)
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ------------- | ------------------- | ------------------------------------------------------------ |
+| create | Grade grade | QuestionGenerator | 静态工厂方法,根据年级返回对应子类实例(PrimaryGenerator / MiddleGenerator / SeniorGenerator) |
+| generateOne | — | ChoiceQuestion | 抽象方法,由子类实现,生成一道符合年级要求的选择题(含题干、4选项、正确答案) |
+
+#### FileIOService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ |
+| saveUser | User user | void | 将用户序列化为 JSON,保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json |
+| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null |
+| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt |
+| loadCode | String email | String | 读取 data/temp_codes/{email}.txt,返回注册码或 null |
+| loadUsedQuestionStems | String email | Set | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件,提取题干(每行以“1.”、“2.”开头的内容),用于查重 |
+
+#### QuizService
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ |
+| generateQuestions | String email, int count | List | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set) |
+| calculateScore | List questions, List userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) |
+| savePaper | String email, List questions | void | 生成时间戳文件名(yyyy-MM-dd-HH-mm-ss.txt),保存题干到 data/users/{email}/papers/(目录自动创建) |
+
+### UI层接口设计
+
+#### MainWindow
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- |
+| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 |
+| showLoginPanel | — | void | 创建并显示 LoginPanel |
+| showRegisterPanel | — | void | 创建并显示 RegisterPanel |
+| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel |
+| showQuizPanel | List questions, QuizService quizService | void | 创建并显示 QuizPanel |
+| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel |
+
+#### RegisterPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 |
+| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” |
+| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 |
+
+#### LoginPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ------------- | ----------------------- | ------ | ----------------------------------------------------------- |
+| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 |
+| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 |
+
+#### GradeSelectPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ----------- | ----------------------- | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 |
+| startQuiz | Grade grade | void | 弹出数量输入对话框(10–30),调用 QuizService.generateQuestions(),跳转到答题页 |
+
+#### QuizPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ |
+| 构造函数 | MainWindow mainWindow, List questions, QuizService quizService | — | 显示第1题 |
+| showQuestion | int index | void | 渲染当前题干和4个选项(RadioButton),绑定“提交”按钮 |
+| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 |
+
+#### ResultPanel
+
+| 方法名 | 参数列表 | 返回值 | 逻辑描述 |
+| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- |
+| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 |
+| exitAction | — | void | 跳转到登录页 |
+| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 |
\ 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/doc/需求分析/需求分析第一版.md b/doc/需求分析/需求分析第一版.md
new file mode 100644
index 0000000..8876858
--- /dev/null
+++ b/doc/需求分析/需求分析第一版.md
@@ -0,0 +1,74 @@
+# 数学题库生成系统需求分析文档
+## 项目概述
+为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数
+
+## 用户:
+小学、初中和高中学生。
+要求界面交互简洁直观、易于上手
+
+## 功能需求:
+
+### 用户注册与登录
+#### 注册流程
+- 用户输入邮箱地址,点击"获取注册码"后,软件发送邮箱注册码
+- 用户输入邮箱里的注册码完成验证
+- 验证通过后,设置密码(6-10位,必须包含大小写字母和数字),要重新确认密码
+- 密码设置完成即完成注册,自动进入学段选择界面
+
+#### 登录功能
+- 已注册用户通过"邮箱 + 密码"或"用户名 + 密码"登录
+- 登录成功后进入学段选择界面
+
+#### 密码管理
+- 登录状态下,用户可以发起修改密码操作
+- 修改时需先输入旧密码,然后输入两次新密码(6-10位,必须包含大小写字母和数字)
+
+### 学段选择
+- 注册成功后,跳转到界面选择界面("小学","初中","高中"),点击后可修改用户的学段
+- 登录后,点击"切换学段"可跳转界面选择界面
+
+### 试卷生成
+- 用户输入题目数量(10-30)
+- 系统根据用户选择的学段,生成对应难度的数字选择题试卷
+- 同一用户不能生成重复题目
+- 每个题目包含题干和4个选择(包含1个正确题目)
+
+### 答题流程
+- 试卷生成后,界面依次显示题目
+- 每题显示题干和4个选项,用户选择一个选项后点击"下一题",自动跳转至下一题
+- 直到完成最后一题提高后,自动进入分数展示界面
+
+### 分数计算与展示
+- 分数计算:Z = 答对/总题数 x 100%
+- 显示内容:"您答对了X/Y题,得分:Z%"
+- 分数界面提供两个操作选项:"继续答题"返回答题界面,"退出"返回登录界面
+
+## 非功能需求
+
+### 界面要求
+- 除了输入题目数量,其他所有功能均通过图形化界面操作,界面简洁、直观,符合中小学生使用习惯
+- 操作流程清晰,每个步骤有明确的引导提示("请输入注册码","密码格式错误","题目数量不合法"等)
+
+### 运行环境
+- 桌面应用程序,支持Windows
+
+### 数据储存约束
+- 不使用数据库,用户信息(邮箱、用户名、密码、学段)、注册码等数据通过json文件形式储存
+- 需保证用户数据的安全性(如密码加密储存)
+
+### 题目内容约束
+- 小学:只能有+,-,*./和()
+- 初中:题目中至少有一个平方或开根号的运算符
+- 高中:题目中至少有一个sin,cos或tan的运算符
+
+## 业务流程说明
+
+### 注册登录流程
+新用户:输入邮箱->获取注册码->验证注册码->设置密码->完成注册->选择学段->登录
+旧用户:输入邮箱(或用户码)和密码->登录
+
+### 做题流程
+输入题目数量->生成试卷->依次答题->完成最后一题->显示分数->选择"继续做题"或"退出"
+
+### 密码修改流程
+登录状态->发起修改密码->输入原密码->输入两次新密码->通过密码验证->密码更新
diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..b7ea40a
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,174 @@
+
+
+ 4.0.0
+
+ com.mathquiz
+ MathQuizApp
+ 1.04
+ jar
+
+ Math Quiz Application
+ 小初高数学学习软件- 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}
+
+
+
+
+
+ com.sun.mail
+ javax.mail
+ 1.6.2
+
+
+
+
+ com.sun.activation
+ javax.activation
+ 1.2.0
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+ 3.11.0
+
+ ${maven.compiler.source}
+ ${maven.compiler.target}
+ UTF-8
+
+
+
+
+ org.apache.maven.plugins
+ maven-resources-plugin
+ 3.3.1
+
+ UTF-8
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.6.1
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/dependencies
+ false
+ false
+ true
+
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-shade-plugin
+ 3.5.1
+
+
+ package
+
+ shade
+
+
+
+
+ com.pair.Test
+
+
+
+
+ *:*
+
+ META-INF/*.SF
+ META-INF/*.DSA
+ META-INF/*.RSA
+
+
+
+ MathQuizApp
+
+
+
+
+
+
+
+ org.codehaus.mojo
+ exec-maven-plugin
+ 3.1.0
+
+
+ com.pair.Test
+
+
+ --add-modules
+ javafx.controls,javafx.fxml,javafx.graphics,javafx.base
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/com/pair/Test.java b/src/main/java/com/pair/Test.java
new file mode 100644
index 0000000..fa68257
--- /dev/null
+++ b/src/main/java/com/pair/Test.java
@@ -0,0 +1,38 @@
+package com.pair;// src/main/java/com/pair/Test.java
+
+
+import com.pair.ui.MainWindow;
+import javafx.application.Application;
+import javafx.scene.Scene;
+import javafx.stage.Stage;
+
+import java.io.IOException;
+
+public class Test extends Application {
+
+ @Override
+ public void start(Stage primaryStage) {
+ try {
+ System.out.println("✅ 正在初始化 MainWindow...");
+ MainWindow mainWindow = new MainWindow(primaryStage);
+ Scene scene = new Scene(mainWindow, 1366, 786);
+ primaryStage.setTitle("中小学数学答题系统");
+ primaryStage.setScene(scene);
+ primaryStage.setResizable(true);
+ primaryStage.show();
+ System.out.println("✅ 应用启动成功!");
+ } catch (Throwable e) { // 捕获所有错误,包括 NoClassDefFoundError
+ System.err.println("❌ 启动失败,异常信息:");
+ e.printStackTrace();
+ // 暂停 15 秒,防止窗口关闭
+ try {
+ Thread.sleep(15000);
+ } catch (InterruptedException ignored) {}
+ System.exit(1);
+ }
+ }
+
+ public static void main(String[] args) {
+ launch(args);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/model/ChoiceQuestion.java b/src/main/java/com/pair/model/ChoiceQuestion.java
new file mode 100644
index 0000000..d18b867
--- /dev/null
+++ b/src/main/java/com/pair/model/ChoiceQuestion.java
@@ -0,0 +1,74 @@
+package com.pair.model;
+
+import java.util.List;
+
+//选择题
+public class ChoiceQuestion {
+
+ private String questionText; // 题目文本
+ private Object correctAnswer; // 正确答案
+ private List> options; // 选项列表
+ private Grade grade; // 所属学段
+
+
+
+ public ChoiceQuestion(String questionText, double correctAnswer,
+ List options, Grade grade) {
+ this.questionText = questionText;
+ this.correctAnswer = correctAnswer;
+ this.options = options;
+ this.grade = grade;
+ }
+
+ public ChoiceQuestion(String questionText, String correctAnswer,
+ List options, Grade grade) {
+ this.questionText = questionText;
+ this.correctAnswer = correctAnswer;
+ this.options = options;
+ this.grade = grade;
+ }
+
+
+
+ public String getQuestionText() {
+ return questionText;
+ }
+
+ public void setQuestionText(String questionText) {
+ this.questionText = questionText;
+ }
+
+ public Object getCorrectAnswer() {
+ return correctAnswer;
+ }
+
+ public void setCorrectAnswer(Object correctAnswer) {
+ this.correctAnswer = correctAnswer;
+ }
+
+ public List> getOptions() {
+ return options;
+ }
+
+ public void setOptions(List> options) {
+ this.options = options;
+ }
+
+ public Grade getGrade() {
+ return grade;
+ }
+
+ public void setGrade(Grade grade) {
+ this.grade = grade;
+ }
+
+ @Override
+ public String toString() {
+ return "ChoiceQuestion{" +
+ "questionText='" + questionText + '\'' +
+ ", correctAnswer=" + correctAnswer +
+ ", options=" + options +
+ ", grade=" + grade +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/model/Grade.java b/src/main/java/com/pair/model/Grade.java
new file mode 100644
index 0000000..65bcee9
--- /dev/null
+++ b/src/main/java/com/pair/model/Grade.java
@@ -0,0 +1,29 @@
+package com.pair.model;
+
+
+public enum Grade {
+ // 枚举常量,初始化时传入对应的中文描述
+ 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/pair/model/QuizHistory.java b/src/main/java/com/pair/model/QuizHistory.java
new file mode 100644
index 0000000..98b7558
--- /dev/null
+++ b/src/main/java/com/pair/model/QuizHistory.java
@@ -0,0 +1,79 @@
+package com.pair.model;
+
+
+import java.util.Date;
+import java.util.List;
+
+//答题历史记录模型
+public class QuizHistory {
+
+ private String username; // 用户名
+ private Date timestamp; // 答题时间
+ private List questions; // 题目列表
+ private List userAnswers; // 用户答案列表
+ private int score; // 得分
+
+
+ public QuizHistory(String username, Date timestamp,
+ List questions,
+ List userAnswers,
+ int score) {
+ this.username = username;
+ this.timestamp = timestamp;
+ this.questions = questions;
+ this.userAnswers = userAnswers;
+ this.score = score;
+ }
+
+
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username) {
+ this.username = username;
+ }
+
+ public Date getTimestamp() {
+ return timestamp;
+ }
+
+ public void setTimestamp(Date timestamp) {
+ this.timestamp = timestamp;
+ }
+
+ public List getQuestions() {
+ return questions;
+ }
+
+ public void setQuestions(List questions) {
+ this.questions = questions;
+ }
+
+ public List getUserAnswers() {
+ return userAnswers;
+ }
+
+ public void setUserAnswers(List userAnswers) {
+ this.userAnswers = userAnswers;
+ }
+
+ public int getScore() {
+ return score;
+ }
+
+ public void setScore(int score) {
+ this.score = score;
+ }
+
+ @Override
+ public String toString() {
+ return "QuizHistory{" +
+ "username='" + username + '\'' +
+ ", timestamp=" + timestamp +
+ ", questions=" + (questions != null ? questions.size() : 0) +
+ ", score=" + score +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/model/QuizResult.java b/src/main/java/com/pair/model/QuizResult.java
new file mode 100644
index 0000000..04dbf76
--- /dev/null
+++ b/src/main/java/com/pair/model/QuizResult.java
@@ -0,0 +1,58 @@
+package com.pair.model;
+
+
+//答题结果
+public class QuizResult {
+
+ private int totalQuestions; // 总题数
+ private int correctCount; // 正确题数
+ private int wrongCount; // 错误题数
+ private int score; // 得分
+
+
+ public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) {
+ this.totalQuestions = totalQuestions;
+ this.correctCount = correctCount;
+ this.wrongCount = wrongCount;
+ this.score = score;
+ }
+
+
+ public int getTotalQuestions() {
+ return totalQuestions;
+ }
+
+ public void setTotalQuestions(int totalQuestions) {
+ this.totalQuestions = totalQuestions;
+ }
+
+ public int getCorrectCount() {
+ return correctCount;
+ }
+
+ public void setCorrectCount(int correctCount) {
+ this.correctCount = correctCount;
+ }
+
+ public int getWrongCount() {
+ return wrongCount;
+ }
+
+ public void setWrongCount(int wrongCount) {
+ this.wrongCount = wrongCount;
+ }
+
+ public int getScore() {
+ return score;
+ }
+
+ public void setScore(int score) {
+ this.score = score;
+ }
+
+ @Override
+ public String toString() {
+ int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
+ return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/model/User.java b/src/main/java/com/pair/model/User.java
new file mode 100644
index 0000000..cd93200
--- /dev/null
+++ b/src/main/java/com/pair/model/User.java
@@ -0,0 +1,121 @@
+package com.pair.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; // 邮箱
+ private Grade grade; // 学段
+ private int totalQuizzes; // 总答题次数
+ private double averageScore; // 平均分
+ private Date registrationDate; // 注册时间
+
+
+
+ /**
+ * 完整构造方法(用于从文件加载)
+ */
+ 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;
+ this.grade = grade;
+ this.totalQuizzes = totalQuizzes;
+ this.averageScore = averageScore;
+ this.registrationDate = registrationDate;
+ }
+
+ /**
+ * 简化构造方法(用于新用户注册)
+ */
+ public User(String username, String password, String email, Grade grade) {
+ this.userId = UUID.randomUUID().toString();
+ this.username = username;
+ this.password = password;
+ this.email = email;
+ this.grade = grade;
+ this.totalQuizzes = 0;
+ this.averageScore = 0.0;
+ this.registrationDate = new Date();
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public String getUsername() {
+ return username;
+ }
+
+ public void setUsername(String username){
+ this.username = username;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String password) {
+ this.password = password;
+ }
+
+ public String getEmail() {
+ return email;
+ }
+
+ public void setEmail(String email) {
+ this.email = email;
+ }
+
+ public Grade getGrade() {
+ return grade;
+ }
+
+ public void setGrade(Grade grade) {
+ this.grade = grade;
+ }
+
+ public int getTotalQuizzes() {
+ return totalQuizzes;
+ }
+
+ public void setTotalQuizzes(int totalQuizzes) {
+ this.totalQuizzes = totalQuizzes;
+ }
+
+ public double getAverageScore() {
+ return averageScore;
+ }
+
+ public void setAverageScore(double averageScore) {
+ this.averageScore = averageScore;
+ }
+
+ public Date getRegistrationDate() {
+ return registrationDate;
+ }
+
+ public void setRegistrationDate(Date registrationDate) {
+ this.registrationDate = registrationDate;
+ }
+
+ @Override
+ public String toString() {
+ return "User{" +
+ "username='" + username + '\'' +
+ ", email='" + email + '\'' +
+ ", grade=" + grade +
+ ", totalQuizzes=" + totalQuizzes +
+ ", averageScore=" + String.format("%.1f", averageScore) +
+ ", registrationDate=" + registrationDate +
+ '}';
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/FileIOService.java b/src/main/java/com/pair/service/FileIOService.java
new file mode 100644
index 0000000..932a6bc
--- /dev/null
+++ b/src/main/java/com/pair/service/FileIOService.java
@@ -0,0 +1,213 @@
+package com.pair.service;
+
+
+import com.pair.model.*;
+import com.pair.util.AppDataDirectory;
+import com.pair.util.FileUtils;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.reflect.TypeToken;
+
+import java.io.File;
+import java.io.IOException;
+import java.lang.reflect.Type;
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+/**
+ * 文件IO服务
+ * 负责所有数据的读写操作
+ */
+public class FileIOService {
+
+ private static final String DATA_DIR = AppDataDirectory.getFullPath("data");
+ private static final String USERS_DIR = AppDataDirectory.getFullPath("data/users");
+ private static final String HISTORY_DIR = AppDataDirectory.getFullPath("data/history");
+
+ private static final String REGISTRATION_CODES_FILE = AppDataDirectory.getFullPath("data/registration_codes.json");
+ private static final String USERS_FILE = AppDataDirectory.getFullPath("data/users.json");
+ private static final String CURRENT_USER_FILE = AppDataDirectory.getFullPath("data/current_user.json");
+
+ private static final Gson gson = new GsonBuilder()
+ .setPrettyPrinting()
+ .setDateFormat("yyyy-MM-dd HH:mm:ss")
+ .create();
+
+ // ==================== 初始化 ====================
+
+ public FileIOService() throws IOException {
+ initDataDirectory();
+ }
+
+ public void initDataDirectory() throws IOException {
+ FileUtils.createDirectoryIfNotExists(DATA_DIR);
+ FileUtils.createDirectoryIfNotExists(USERS_DIR);
+ FileUtils.createDirectoryIfNotExists(HISTORY_DIR);
+ FileUtils.ensureFileExists(REGISTRATION_CODES_FILE);
+
+ if (!FileUtils.exists(USERS_FILE)) {
+ Map> data = new HashMap<>();
+ data.put("users", new ArrayList<>());
+ FileUtils.saveAsJson(data, USERS_FILE);
+ }
+
+ System.out.println("✓ 数据目录初始化完成");
+ }
+
+ // ==================== 用户操作 ====================
+
+ public void saveUser(User user) throws IOException {
+ Type type = new TypeToken>>(){}.getType();
+ Map> data = FileUtils.readJsonToObject(USERS_FILE, type);
+
+ List users = data.get("users");
+
+ boolean found = false;
+ for (int i = 0; i < users.size(); i++) {
+ if (users.get(i).getUserId().equals(user.getUserId())) {
+ users.set(i, user);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ users.add(user);
+ }
+
+ FileUtils.saveAsJson(data, USERS_FILE);
+ }
+
+ public List loadAllUsers() throws IOException {
+ if (!FileUtils.exists(USERS_FILE)) {
+ return new ArrayList<>();
+ }
+
+ Type type = new TypeToken>>(){}.getType();
+ Map> data = FileUtils.readJsonToObject(USERS_FILE, type);
+
+ return data.get("users");
+ }
+
+ public User findUserByUsername(String username) throws IOException {
+ List users = loadAllUsers();
+
+ for (User user : users) {
+ if (user.getUsername().equals(username)) {
+ return user;
+ }
+ }
+
+ 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);
+ }
+
+ public User loadCurrentUser() throws IOException {
+ if (!FileUtils.exists(CURRENT_USER_FILE)) {
+ return null;
+ }
+ return FileUtils.readJsonToObject(CURRENT_USER_FILE, User.class);
+ }
+
+ public void clearCurrentUser() {
+ FileUtils.deleteFile(CURRENT_USER_FILE);
+ }
+
+
+ public List getHistoryQuestions() throws IOException {
+ List historyQuestions = new ArrayList<>();
+ File[] files = FileUtils.listFiles(HISTORY_DIR);
+
+ Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
+
+ int count = 0;
+ for (File file : files) {
+ if (count++ >= 20) break;
+
+ try {
+ String content = FileUtils.readFileToString(file.getAbsolutePath());
+ String[] lines = content.split("\n");
+
+ for (int i = 0; i < lines.length; i++) {
+ if (lines[i].startsWith("【题目")) {
+ if (i + 1 < lines.length) {
+ String questionText = lines[i + 1].trim();
+ if (!questionText.isEmpty()) {
+ historyQuestions.add(questionText);
+ }
+ }
+ }
+ }
+ } catch (IOException e) {
+ System.err.println("读取历史文件失败: " + file.getName());
+ }
+ }
+
+ return historyQuestions;
+ }
+
+ // ==================== 业务逻辑方法(从 Model 移过来)====================
+
+ /**
+ * 计算答题历史的正确数
+ */
+ private int calculateCorrectCount(QuizHistory history) {
+ int count = 0;
+ List questions = history.getQuestions();
+ List userAnswers = history.getUserAnswers();
+
+ for (int i = 0; i < questions.size(); i++) {
+ ChoiceQuestion question = questions.get(i);
+ Integer userAnswer = userAnswers.get(i);
+
+ if (userAnswer != null && userAnswer == getCorrectAnswerIndex(question)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * 获取题目的正确答案索引
+ */
+ private int getCorrectAnswerIndex(ChoiceQuestion question) {
+ return question.getOptions().indexOf(question.getCorrectAnswer());
+ }
+
+ // ==================== 工具方法 ====================
+
+ private String sanitizeFilename(String filename) {
+ if (filename == null) {
+ return "unknown";
+ }
+ return filename.replaceAll("[\\\\/:*?\"<>|]", "_");
+ }
+
+ public String getRegistrationCodesFilePath() {
+ return REGISTRATION_CODES_FILE;
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/QuizService.java b/src/main/java/com/pair/service/QuizService.java
new file mode 100644
index 0000000..0d69359
--- /dev/null
+++ b/src/main/java/com/pair/service/QuizService.java
@@ -0,0 +1,427 @@
+package com.pair.service;
+
+import com.pair.model.*;
+import com.pair.service.question_generator.QuestionFactoryManager;
+import java.io.IOException;
+import java.util.*;
+
+/**
+ * 答题服务(包含所有答题相关业务逻辑)
+ */
+public class QuizService {
+
+ private final FileIOService fileIOService;
+ private final UserService userService;
+
+ private List currentQuestions;
+ private List userAnswers;
+ private int currentQuestionIndex;
+ private int answerNumber;
+
+ // ==================== 构造方法 ====================
+
+ public QuizService() throws IOException {
+ this.fileIOService = new FileIOService();
+ this.userService = new UserService(fileIOService);
+ this.currentQuestions = new ArrayList<>();
+ this.userAnswers = new ArrayList<>();
+ this.currentQuestionIndex = 0;
+ }
+
+ public QuizService(FileIOService fileIOService, UserService userService) {
+ this.fileIOService = fileIOService;
+ this.userService = userService;
+ this.currentQuestions = new ArrayList<>();
+ this.userAnswers = new ArrayList<>();
+ this.currentQuestionIndex = 0;
+ }
+
+ // ==================== 答题会话管理 ====================
+
+ //开始生成
+ public void startNewQuiz(User user, int questionCount) throws IOException {
+ currentQuestions.clear();
+ userAnswers.clear();
+ currentQuestionIndex = 0;
+
+ Set historyQuestions = getRecentHistoryQuestions();
+
+ Grade grade = user.getGrade();
+ currentQuestions = QuestionFactoryManager.generateQuestions(
+ grade, questionCount, historyQuestions
+ );
+
+ for (int i = 0; i < currentQuestions.size(); i++) {
+ userAnswers.add(null);
+ }
+
+ System.out.println("✓ 已生成 " + currentQuestions.size() + " 道 " + grade + " 题目");
+ }
+
+ private Set getRecentHistoryQuestions() throws IOException {
+ List historyList = fileIOService.getHistoryQuestions();
+ return new HashSet<>(historyList);
+ }
+
+ // ==================== 题目访问 ====================
+
+ public ChoiceQuestion getCurrentQuestion() {
+ if (currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.size()) {
+ return currentQuestions.get(currentQuestionIndex);
+ }
+ return null;
+ }
+
+ public ChoiceQuestion getQuestion(int index) {
+ if (index >= 0 && index < currentQuestions.size()) {
+ return currentQuestions.get(index);
+ }
+ return null;
+ }
+
+ public List getAllQuestions() {
+ return new ArrayList<>(currentQuestions);
+ }
+
+ public boolean nextQuestion() {
+ if (currentQuestionIndex < currentQuestions.size() - 1) {
+ currentQuestionIndex++;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean previousQuestion() {
+ if (currentQuestionIndex > 0) {
+ currentQuestionIndex--;
+ return true;
+ }
+ return false;
+ }
+
+ public boolean goToQuestion(int index) {
+ if (index >= 0 && index < currentQuestions.size()) {
+ currentQuestionIndex = index;
+ return true;
+ }
+ return false;
+ }
+
+ public int getCurrentQuestionIndex() {
+ return currentQuestionIndex;
+ }
+
+ public int getTotalQuestions() {
+ return currentQuestions.size();
+ }
+
+ public boolean isFirstQuestion() {
+ return currentQuestionIndex == 0;
+ }
+
+ public boolean isLastQuestion() {
+ return currentQuestionIndex == currentQuestions.size() - 1;
+ }
+
+ // ==================== 答题操作 ====================
+
+ public boolean submitAnswer(int questionIndex, int optionIndex) {
+ if (questionIndex < 0 || questionIndex >= currentQuestions.size()) {
+ throw new IllegalArgumentException("题目索引无效: " + questionIndex);
+ }
+
+ ChoiceQuestion question = currentQuestions.get(questionIndex);
+
+ if (optionIndex < 0 || optionIndex >= question.getOptions().size()) {
+ throw new IllegalArgumentException("选项索引无效: " + optionIndex);
+ }
+
+ userAnswers.set(questionIndex, optionIndex);
+
+ return checkAnswer(question, optionIndex);
+ }
+
+ public boolean submitCurrentAnswer(int optionIndex) {
+ return submitAnswer(currentQuestionIndex, optionIndex);
+ }
+
+ public Integer getUserAnswer(int questionIndex) {
+ if (questionIndex >= 0 && questionIndex < userAnswers.size()) {
+ return userAnswers.get(questionIndex);
+ }
+ return null;
+ }
+
+ public List getAllUserAnswers() {
+ return new ArrayList<>(userAnswers);
+ }
+
+ public boolean isAllAnswered() {
+ for (Integer answer : userAnswers) {
+ if (answer == null) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ public int getAnsweredCount() {
+ int count = 0;
+ for (Integer answer : userAnswers) {
+ if (answer != null) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ public boolean isAnswered(int questionIndex) {
+ return userAnswers.get(questionIndex) != null ;
+ }
+
+ // ==================== 成绩计算 ====================
+
+ public QuizResult calculateResult() {
+ int correctCount = 0;
+ int totalQuestions = currentQuestions.size();
+
+ for (int i = 0; i < totalQuestions; i++) {
+ ChoiceQuestion question = currentQuestions.get(i);
+ Integer userAnswer = userAnswers.get(i);
+
+ if (userAnswer != null && checkAnswer(question, userAnswer)) {
+ correctCount++;
+ }
+ }
+
+ int wrongCount = totalQuestions - correctCount;
+ int score = totalQuestions > 0 ? (correctCount * 100) / totalQuestions : 0;
+
+ return new QuizResult(totalQuestions, correctCount, wrongCount, score);
+ }
+
+ public List getCorrectQuestionIndices() {
+ List correctIndices = new ArrayList<>();
+
+ for (int i = 0; i < currentQuestions.size(); i++) {
+ ChoiceQuestion question = currentQuestions.get(i);
+ Integer userAnswer = userAnswers.get(i);
+
+ if (userAnswer != null && checkAnswer(question, userAnswer)) {
+ correctIndices.add(i);
+ }
+ }
+
+ return correctIndices;
+ }
+
+ public List getWrongQuestionIndices() {
+ List wrongIndices = new ArrayList<>();
+
+ for (int i = 0; i < currentQuestions.size(); i++) {
+ ChoiceQuestion question = currentQuestions.get(i);
+ Integer userAnswer = userAnswers.get(i);
+
+ if (userAnswer != null && !checkAnswer(question, userAnswer)) {
+ wrongIndices.add(i);
+ }
+ }
+
+ return wrongIndices;
+ }
+
+ public List getUnansweredQuestionIndices() {
+ List unansweredIndices = new ArrayList<>();
+
+ for (int i = 0; i < userAnswers.size(); i++) {
+ if (userAnswers.get(i) == null) {
+ unansweredIndices.add(i);
+ }
+ }
+
+ return unansweredIndices;
+ }
+
+ // ==================== 业务逻辑方法(从 Model 移过来)====================
+
+ /**
+ * 检查答案是否正确
+ */
+ public boolean checkAnswer(ChoiceQuestion question, int userAnswerIndex) {
+ if (userAnswerIndex < 0 || userAnswerIndex >= question.getOptions().size()) {
+ return false;
+ }
+
+ Object userAnswer = question.getOptions().get(userAnswerIndex);
+ return question.getCorrectAnswer().equals(userAnswer);
+ }
+
+ /**
+ * 获取题目的正确答案索引
+ */
+ public int getCorrectAnswerIndex(ChoiceQuestion question) {
+ return question.getOptions().indexOf(question.getCorrectAnswer());
+ }
+
+ /**
+ * 获取正确答案的字母形式
+ */
+ public String getCorrectAnswerLetter(ChoiceQuestion question) {
+ int index = getCorrectAnswerIndex(question);
+ if (index >= 0 && index < 4) {
+ return String.valueOf((char)('A' + index));
+ }
+ return "未知";
+ }
+
+ /**
+ * 获取答题结果的正确率
+ */
+ public double getAccuracy(QuizResult result) {
+ if (result.getTotalQuestions() == 0) {
+ return 0.0;
+ }
+ return (result.getCorrectCount() * 100.0) / result.getTotalQuestions();
+ }
+
+ /**
+ * 判断是否及格
+ */
+ public boolean isPassed(QuizResult result) {
+ return result.getScore() >= 60;
+ }
+
+ /**
+ * 获取评级
+ */
+ public String getGrade(QuizResult result) {
+ int score = result.getScore();
+ if (score >= 90) return "优秀";
+ if (score >= 80) return "良好";
+ if (score >= 70) return "中等";
+ if (score >= 60) return "及格";
+ return "不及格";
+ }
+
+ /**
+ * 计算答题历史的正确数
+ */
+ public int getCorrectCount(QuizHistory history) {
+ int count = 0;
+ List questions = history.getQuestions();
+ List userAnswers = history.getUserAnswers();
+
+ for (int i = 0; i < questions.size(); i++) {
+ ChoiceQuestion question = questions.get(i);
+ Integer userAnswer = userAnswers.get(i);
+
+ if (userAnswer != null && checkAnswer(question, userAnswer)) {
+ count++;
+ }
+ }
+ return count;
+ }
+
+ /**
+ * 计算答题历史的错误数
+ */
+ public int getWrongCount(QuizHistory history) {
+ return history.getQuestions().size() - getCorrectCount(history);
+ }
+
+ // ==================== 格式化输出 ====================
+
+ public String formatQuestion(ChoiceQuestion question) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(question.getQuestionText()).append("\n");
+
+ List> options = question.getOptions();
+ for (int i = 0; i < options.size(); i++) {
+ sb.append((char)('A' + i)).append(". ").append(options.get(i));
+
+ if (i % 2 == 1) {
+ sb.append("\n");
+ } else {
+ sb.append(" ");
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public String formatCurrentQuestion() {
+ ChoiceQuestion question = getCurrentQuestion();
+ if (question == null) {
+ return "没有可用的题目";
+ }
+
+ StringBuilder sb = new StringBuilder();
+ sb.append("第 ").append(currentQuestionIndex + 1)
+ .append(" / ").append(currentQuestions.size()).append(" 题\n");
+ sb.append(formatQuestion(question));
+
+ return sb.toString();
+ }
+
+ public String formatQuestionWithAnswer(ChoiceQuestion question, Integer userAnswerIndex) {
+ StringBuilder sb = new StringBuilder();
+ sb.append(question.getQuestionText()).append("\n");
+
+ List> options = question.getOptions();
+ int correctIndex = getCorrectAnswerIndex(question);
+
+ for (int i = 0; i < options.size(); i++) {
+ sb.append((char)('A' + i)).append(". ").append(options.get(i));
+
+ if (i == correctIndex) {
+ sb.append(" ✓");
+ }
+
+ if (userAnswerIndex != null && i == userAnswerIndex) {
+ boolean isCorrect = checkAnswer(question, userAnswerIndex);
+ sb.append(isCorrect ? " [您的答案:正确]" : " [您的答案:错误]");
+ }
+
+ if (i % 2 == 1) {
+ sb.append("\n");
+ } else {
+ sb.append(" ");
+ }
+ }
+
+ return sb.toString();
+ }
+
+ public String formatResult(QuizResult result) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("\n========== 答题结束 ==========\n");
+ sb.append("总题数:").append(result.getTotalQuestions()).append(" 题\n");
+ sb.append("正确:").append(result.getCorrectCount()).append(" 题\n");
+ sb.append("错误:").append(result.getWrongCount()).append(" 题\n");
+ sb.append("得分:").append(result.getScore()).append(" 分\n");
+ sb.append("正确率:").append(String.format("%.1f%%", getAccuracy(result))).append("\n");
+ sb.append("评级:").append(getGrade(result)).append("\n");
+ sb.append("===============================\n");
+
+ return sb.toString();
+ }
+
+
+ // ==================== Getters ====================
+
+ public int getAnswerNumber() {
+ return answerNumber;
+ }
+
+ public void setAnswerNumber(int answerNumber) {
+ this.answerNumber = answerNumber;
+ }
+
+ public List getCurrentQuestions() {
+ return new ArrayList<>(currentQuestions);
+ }
+
+ public List getUserAnswers() {
+ return new ArrayList<>(userAnswers);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/UserService.java b/src/main/java/com/pair/service/UserService.java
new file mode 100644
index 0000000..d11d0d2
--- /dev/null
+++ b/src/main/java/com/pair/service/UserService.java
@@ -0,0 +1,517 @@
+package com.pair.service;
+
+
+import com.pair.model.Grade;
+import com.pair.model.User;
+import com.pair.util.EmailUtil;
+import com.pair.util.FileUtils;
+import com.pair.util.PasswordValidator;
+
+import java.io.File;
+import java.io.IOException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static com.pair.util.EmailUtil.validateEmail;
+
+/**
+ * 用户服务(包含所有用户相关业务逻辑)
+ */
+public class UserService {
+
+ private final FileIOService fileIOService;
+ private User currentUser;
+
+ // ==================== 构造方法 ====================
+
+ public UserService() throws IOException {
+ this.fileIOService = new FileIOService();
+ this.currentUser = null;
+ }
+
+ public UserService(FileIOService fileIOService) {
+ this.fileIOService = fileIOService;
+ this.currentUser = null;
+ }
+
+
+ // 注册码有效期(毫秒)
+ private static final long CODE_EXPIRY_TIME = 10 * 60 * 1000; // 10分钟
+
+ // ==================== 注册码管理 ====================
+
+ /**
+ * 生成并保存注册码到文件
+ *
+ * @param email 邮箱
+ * @return 生成的注册码
+ */
+ public String generateRegistrationCode(String email) throws IOException {
+ if (!validateEmail(email)) {
+ throw new IllegalArgumentException("邮箱格式错误!");
+ }
+
+ // 生成6位注册码
+ String code = PasswordValidator.generateRegistrationCode();
+ long expiryTime = System.currentTimeMillis() + CODE_EXPIRY_TIME;
+
+ // 保存到文件
+ System.out.println(expiryTime);
+ saveRegistrationCodeToFile(email, code, expiryTime);
+
+ //发送注册码邮件
+ boolean isEmailSent = EmailUtil.sendRegistrationCode(email, code);
+ if (!isEmailSent) {
+ throw new IllegalArgumentException("邮箱发送失败,请重试");
+ }
+
+ // 打印注册码(实际项目中可以发邮件)
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+ System.out.println("========================================");
+ System.out.println("【注册码】");
+ System.out.println("邮箱: " + email);
+ System.out.println("注册码: " + code);
+ System.out.println("过期时间: " + sdf.format(new Date(expiryTime)));
+ System.out.println("========================================");
+
+ return code;
+ }
+
+ /**
+ * 保存注册码到文件
+ */
+ private void saveRegistrationCodeToFile(String email, String code, long expiryTime) throws IOException {
+ // 读取现有的注册码
+ Map codes = loadRegistrationCodesFromFile();
+
+ // 添加或更新
+ codes.put(email, new RegistrationCode(code, expiryTime));
+
+ // 保存到文件
+ StringBuilder content = new StringBuilder();
+ content.append("# 注册码记录文件\n");
+ content.append("# 格式: 邮箱|注册码|过期时间戳\n");
+ content.append("# 过期时间格式: yyyy-MM-dd HH:mm:ss\n\n");
+
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ for (Map.Entry entry : codes.entrySet()) {
+ String emailKey = entry.getKey();
+ RegistrationCode regCode = entry.getValue();
+
+ content.append(emailKey).append("|")
+ .append(regCode.code).append("|")
+ .append(regCode.expiryTime).append("|")
+ .append(sdf.format(new Date(regCode.expiryTime)))
+ .append("\n");
+ }
+
+ FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
+ }
+
+ /**
+ * 初始化数据目录和文件
+ */
+ private void initializeDataDirectory() {
+ try {
+ File dataDir = new File("data");
+ if (!dataDir.exists()) {
+ dataDir.mkdirs();
+ System.out.println("✓ 已创建 data 目录");
+ }
+
+ // 确保注册码文件存在(即使是空的也创建)
+ File codesFile = new File(fileIOService.getRegistrationCodesFilePath());
+ if (!codesFile.exists()) {
+ StringBuilder initialContent = new StringBuilder();
+ initialContent.append("# 注册码记录文件\n");
+ initialContent.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n");
+
+ FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), initialContent.toString());
+ System.out.println("✓ 已创建注册码文件");
+ }
+ } catch (IOException e) {
+ System.err.println(" 初始化数据目录失败: " + e.getMessage());
+ }
+ }
+
+ /**
+ * 从文件加载注册码
+ */
+ private Map loadRegistrationCodesFromFile() throws IOException {
+ Map codes = new HashMap<>();
+ System.out.println(fileIOService.getRegistrationCodesFilePath());
+ if (!FileUtils.exists(fileIOService.getRegistrationCodesFilePath())) {
+ throw new IOException("目录不存在");
+ }
+
+ String content = FileUtils.readFileToString(fileIOService.getRegistrationCodesFilePath());
+ String[] lines = content.split("\n");
+
+ for (String line : lines) {
+ line = line.trim();
+
+ // 跳过注释和空行
+ if (line.isEmpty() || line.startsWith("#")) {
+ continue;
+ }
+
+ String[] parts = line.split("\\|");
+ if (parts.length >= 3) {
+ String email = parts[0].trim();
+ String code = parts[1].trim();
+ long expiryTime = Long.parseLong(parts[2].trim());
+
+ codes.put(email, new RegistrationCode(code, expiryTime));
+ }
+ }
+
+ return codes;
+ }
+
+ /**
+ * 验证注册码
+ *
+ * @param email 邮箱
+ * @param code 用户输入的注册码
+ * @return true表示验证通过
+ */
+ public boolean verifyRegistrationCode(String email, String code) throws IOException {
+ Map codes = loadRegistrationCodesFromFile();
+
+ RegistrationCode regCode = codes.get(email);
+
+ if (regCode == null) {
+ throw new IllegalArgumentException("未找到该邮箱的注册码,请先获取注册码!");
+ }
+
+ // 检查是否过期
+ if (System.currentTimeMillis() > regCode.expiryTime) {
+ // 删除过期的注册码
+ codes.remove(email);
+ saveAllRegistrationCodes(codes);
+ throw new IllegalArgumentException("注册码已过期,请重新获取!");
+ }
+
+ // 验证注册码
+ boolean isValid = regCode.code.equals(code);
+
+ // 验证成功后删除注册码(一次性使用)
+ if (isValid) {
+ codes.remove(email);
+ saveAllRegistrationCodes(codes);
+ }
+
+ return isValid;
+ }
+
+ /**
+ * 保存所有注册码到文件
+ */
+ private void saveAllRegistrationCodes(Map codes) throws IOException {
+ StringBuilder content = new StringBuilder();
+ content.append("# 注册码记录文件\n");
+ content.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n");
+
+ SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
+
+ for (Map.Entry entry : codes.entrySet()) {
+ content.append(entry.getKey()).append("|")
+ .append(entry.getValue().code).append("|")
+ .append(entry.getValue().expiryTime).append("|")
+ .append(sdf.format(new Date(entry.getValue().expiryTime)))
+ .append("\n");
+ }
+
+ FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
+ }
+
+ /**
+ * 清理过期的注册码
+ */
+ public void cleanExpiredCodes() throws IOException {
+ Map codes = loadRegistrationCodesFromFile();
+ long now = System.currentTimeMillis();
+
+ // 移除过期的
+ codes.entrySet().removeIf(entry -> now > entry.getValue().expiryTime);
+
+ // 保存回文件
+ saveAllRegistrationCodes(codes);
+
+ System.out.println("✓ 已清理过期的注册码");
+ }
+
+ // ==================== 注册码内部类 ====================
+
+ private static class RegistrationCode {
+ String code;
+ long expiryTime;
+
+ RegistrationCode(String code, long expiryTime) {
+ this.code = code;
+ this.expiryTime = expiryTime;
+ }
+ }
+
+
+ // ==================== 用户注册====================
+
+ /**
+ * 用户注册(需要验证码)
+ */
+ public User register(String password, String email, String verificationCode) throws IOException {
+ // 1. 验证注册码
+ if (!verifyRegistrationCode(email, verificationCode)) {
+ throw new IllegalArgumentException("注册码错误!");
+ }
+ //2.验证邮箱格式
+ if (!validateEmail(email)) {
+ throw new IllegalArgumentException("邮箱格式错误!");
+ }
+
+ // 3. 验证用户名是否已存在
+ if (fileIOService.isEmailExists(email)) {
+ throw new IllegalArgumentException("邮箱已经注册!");
+ }
+
+ // 5. 验证密码格式
+ try {
+ PasswordValidator.validatePassword(password);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException("密码格式错误!");
+ }
+
+ // 6. 初始化为小学学段
+ Grade grade = Grade.ELEMENTARY;
+
+ // 7. 加密密码
+ String hashedPassword = PasswordValidator.encrypt(password);
+
+ // 8. 创建用户对象
+ User user = new User(email, hashedPassword, email, grade);
+
+ this.setCurrentUser(user);
+ // 9. 保存到文件
+ fileIOService.saveUser(user);
+
+ return user;
+ }
+
+ public void setCurrentUser(User user) throws IOException {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+
+ // ==================== 用户登录 ====================
+
+ public User login(String username, String password) throws IOException {
+ User user;
+ if (EmailUtil.validateEmail(username)) {
+ user = fileIOService.findUserByEmail(username);
+ } else {
+ user = fileIOService.findUserByUsername(username);
+ }
+
+ if (user == null) {
+ throw new IllegalArgumentException("用户名或邮箱不存在!");
+ }
+
+ String hashedPassword = PasswordValidator.encrypt(password);
+ if (!user.getPassword().equals(hashedPassword)) {
+ throw new IllegalArgumentException("密码错误!");
+ }
+
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+
+ // System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "(" + getGradeDisplayName(user) + ")");
+ return user;
+ }
+
+ public User autoLogin() throws IOException {
+ User user = fileIOService.loadCurrentUser();
+
+ if (user != null) {
+ this.currentUser = user;
+ // System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
+ }
+
+ return user;
+ }
+
+ public void logout() {
+ if (currentUser != null) {
+ // System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
+ this.currentUser = null;
+ fileIOService.clearCurrentUser();
+ }
+ }
+
+ public User getCurrentUser() {
+ return currentUser;
+ }
+
+ public boolean isLoggedIn() {
+ return currentUser != null;
+ }
+
+ // ==================== 密码管理 ====================
+
+ 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("旧密码错误!");
+ }
+
+ try {
+ PasswordValidator.validatePassword(newPassword);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+
+ String hashedNewPassword = PasswordValidator.encrypt(newPassword);
+ user.setPassword(hashedNewPassword);
+
+ fileIOService.saveUser(user);
+
+ if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+
+ System.out.println("✓ 密码修改成功");
+ return true;
+ }
+
+ public boolean resetPassword(String username, String email, String newPassword) throws IOException {
+ User user = fileIOService.findUserByUsername(username);
+
+ if (user == null) {
+ throw new IllegalArgumentException("用户名不存在!");
+ }
+
+ if (!user.getEmail().equals(email)) {
+ throw new IllegalArgumentException("邮箱验证失败!");
+ }
+
+ try {
+ PasswordValidator.validatePassword(newPassword);
+ } catch (IllegalArgumentException e) {
+ throw new IllegalArgumentException(e.getMessage());
+ }
+
+ String hashedNewPassword = PasswordValidator.encrypt(newPassword);
+ user.setPassword(hashedNewPassword);
+
+ fileIOService.saveUser(user);
+
+ System.out.println("✓ 密码重置成功");
+ return true;
+ }
+
+ // ==================== 用户信息管理 ====================
+
+ public boolean updateEmail(User user, String newEmail) throws IOException {
+ if (!validateEmail(newEmail)) {
+ throw new IllegalArgumentException("邮箱格式错误!");
+ }
+
+ user.setEmail(newEmail);
+ fileIOService.saveUser(user);
+
+ if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
+ this.currentUser = user;
+ fileIOService.saveCurrentUser(user);
+ }
+
+ // 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();
+
+ int newTotal = oldTotal + 1;
+ double newAverage = (oldAverage * oldTotal + score) / newTotal;
+
+ user.setTotalQuizzes(newTotal);
+ user.setAverageScore(newAverage);
+
+ fileIOService.saveUser(user);
+ }
+
+ public List getAllUsers() throws IOException {
+ return fileIOService.loadAllUsers();
+ }
+
+ public User findUser(String username) throws IOException {
+ return fileIOService.findUserByUsername(username);
+ }
+
+ public String getGradeDisplayName(User user) {
+ if (user == null || user.getGrade() == null) {
+ return "未知";
+ }
+
+ switch (user.getGrade()) {
+ case ELEMENTARY:
+ return "小学";
+ case MIDDLE:
+ return "初中";
+ case HIGH:
+ return "高中";
+ default:
+ return "未知";
+ }
+ }
+
+ /**
+ * 获取用户统计信息
+ */
+ public String getUserStatistics(User user) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("========== 用户统计 ==========\n");
+ sb.append("用户名:").append(user.getUsername()).append("\n");
+ sb.append("学段:").append(getGradeDisplayName(user)).append("\n");
+ sb.append("邮箱:").append(user.getEmail()).append("\n");
+ sb.append("总答题次数:").append(user.getTotalQuizzes()).append(" 次\n");
+ sb.append("平均分:").append(String.format("%.1f", user.getAverageScore())).append(" 分\n");
+ sb.append("注册时间:").append(user.getRegistrationDate()).append("\n");
+ sb.append("=============================\n");
+
+ return sb.toString();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/QuestionFactoryManager.java b/src/main/java/com/pair/service/question_generator/QuestionFactoryManager.java
new file mode 100644
index 0000000..e6abdbc
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/QuestionFactoryManager.java
@@ -0,0 +1,68 @@
+package com.pair.service.question_generator;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.factory.ElementaryQuestionFactory;
+import com.pair.service.question_generator.factory.HighQuestionFactory;
+import com.pair.service.question_generator.factory.MiddleQuestionFactory;
+import com.pair.service.question_generator.factory.QuestionFactory;
+
+import java.util.*;
+
+/**
+ * 题目工厂管理器
+ */
+public class QuestionFactoryManager {
+
+ private static final Map factories = new HashMap<>();
+
+ static {
+ factories.put(Grade.ELEMENTARY, new ElementaryQuestionFactory());
+ factories.put(Grade.MIDDLE, new MiddleQuestionFactory());
+ factories.put(Grade.HIGH, new HighQuestionFactory());
+ }
+
+ /**
+ * 生成题目(带去重功能)
+ *
+ * @param grade 学段
+ * @param count 题目数量
+ * @param historyQuestions 历史题目(用于去重,传 null 或空集合则不去重)
+ * @return 题目列表
+ */
+ public static List generateQuestions(
+ Grade grade, int count, Set historyQuestions) {
+
+ List questions = new ArrayList<>();
+ Set usedQuestions = historyQuestions != null ?
+ new HashSet<>(historyQuestions) :
+ new HashSet<>();
+
+ int maxAttempts = count * 10;
+ int attempts = 0;
+
+ QuestionFactory factory = factories.get(grade);
+ if (factory == null) {
+ throw new IllegalArgumentException("不支持的学段: " + grade);
+ }
+
+ while (questions.size() < count && attempts < maxAttempts) {
+ ChoiceQuestion question = factory.createQuestion();
+ String questionText = question.getQuestionText();
+
+ if (!usedQuestions.contains(questionText)) {
+ questions.add(question);
+ usedQuestions.add(questionText);
+ }
+
+ attempts++;
+ }
+
+ if (questions.size() < count) {
+ System.out.println("⚠ 警告:只生成了 " + questions.size() +
+ " 道题,未达到要求的 " + count + " 道");
+ }
+
+ return questions;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/factory/ElementaryQuestionFactory.java b/src/main/java/com/pair/service/question_generator/factory/ElementaryQuestionFactory.java
new file mode 100644
index 0000000..16b4629
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/factory/ElementaryQuestionFactory.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.factory;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.elementary.*;
+import com.pair.util.RandomUtils;
+import com.pair.service.question_generator.strategy.*;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 小学题目工厂
+ */
+public class ElementaryQuestionFactory implements QuestionFactory {
+
+ private final List strategies;
+
+ public ElementaryQuestionFactory() {
+ strategies = new ArrayList<>();
+ // 注册所有小学题目生成策略
+ strategies.add(new AdditionStrategy());
+ strategies.add(new SubtractionStrategy());
+ strategies.add(new MultiplicationStrategy());
+ strategies.add(new DivisionStrategy());
+ strategies.add(new ParenthesesAddStrategy());
+ strategies.add(new ParenthesesMultiplyStrategy());
+ }
+
+ //重载接口方法
+ @Override
+ public ChoiceQuestion createQuestion() {
+ // 从六个题型list中随机选择一个生成题目
+ QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
+ return strategy.generate();
+ }
+
+ @Override
+ public Grade getSupportedGrade() {
+ return Grade.ELEMENTARY;
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/factory/HighQuestionFactory.java b/src/main/java/com/pair/service/question_generator/factory/HighQuestionFactory.java
new file mode 100644
index 0000000..f3cd5ad
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/factory/HighQuestionFactory.java
@@ -0,0 +1,43 @@
+package com.pair.service.question_generator.factory;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.QuestionStrategy;
+import com.pair.service.question_generator.strategy.high.*;
+import com.pair.util.RandomUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 高中题目工厂
+ */
+public class HighQuestionFactory implements QuestionFactory {
+
+ private final List strategies;
+
+ public HighQuestionFactory() {
+ strategies = new ArrayList<>();
+ // 注册所有高中题目生成策略
+ strategies.add(new SinStrategy());
+ strategies.add(new CosStrategy());
+ strategies.add(new TanStrategy());
+ strategies.add(new TrigIdentityStrategy());
+ strategies.add(new DerivativeStrategy());
+ strategies.add(new ArithmeticSequenceSumStrategy());
+ strategies.add(new LogarithmStrategy());
+ strategies.add(new ProbabilityStrategy());
+ strategies.add(new FunctionExtremeStrategy());
+ }
+
+ @Override
+ public ChoiceQuestion createQuestion() {
+ QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
+ return strategy.generate();
+ }
+
+ @Override
+ public Grade getSupportedGrade() {
+ return Grade.HIGH;
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/factory/MiddleQuestionFactory.java b/src/main/java/com/pair/service/question_generator/factory/MiddleQuestionFactory.java
new file mode 100644
index 0000000..205254f
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/factory/MiddleQuestionFactory.java
@@ -0,0 +1,50 @@
+package com.pair.service.question_generator.factory;
+
+
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.elementary.ParenthesesAddStrategy;
+import com.pair.service.question_generator.strategy.elementary.ParenthesesMultiplyStrategy;
+import com.pair.service.question_generator.strategy.middle.*;
+import com.pair.util.RandomUtils;
+import com.pair.service.question_generator.strategy.QuestionStrategy;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 初中题目工厂
+ */
+public class MiddleQuestionFactory implements QuestionFactory {
+
+ private final List strategies;
+
+ public MiddleQuestionFactory() {
+ strategies = new ArrayList<>();
+ // 注册所有初中题目生成策略
+ strategies.add(new SquareStrategy());
+ strategies.add(new SquareAddStrategy());
+ strategies.add(new SqrtStrategy());
+ strategies.add(new SqrtAddStrategy());
+ strategies.add(new MixedSquareSqrtStrategy());
+ strategies.add(new ParenthesesAddStrategy());
+ strategies.add(new ParenthesesMultiplyStrategy());
+ strategies.add(new LinearEquationStrategy());
+ strategies.add(new QuadraticEquationStrategy());
+ strategies.add(new TriangleAreaStrategy());
+ strategies.add(new CircleAreaStrategy());
+ strategies.add(new LinearFunctionStrategy());
+ }
+
+ @Override
+ public ChoiceQuestion createQuestion() {
+ QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
+ return strategy.generate();
+ }
+
+ @Override
+ public Grade getSupportedGrade() {
+ return Grade.MIDDLE;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/factory/QuestionFactory.java b/src/main/java/com/pair/service/question_generator/factory/QuestionFactory.java
new file mode 100644
index 0000000..3311f6c
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/factory/QuestionFactory.java
@@ -0,0 +1,17 @@
+package com.pair.service.question_generator.factory;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+
+/**
+ * 题目工厂接口
+ */
+public interface QuestionFactory {
+
+ //创建题目
+ ChoiceQuestion createQuestion();
+
+ //获取工厂支持的学段
+ Grade getSupportedGrade();
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/AbstractQuestionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/AbstractQuestionStrategy.java
new file mode 100644
index 0000000..b376e19
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/AbstractQuestionStrategy.java
@@ -0,0 +1,138 @@
+package com.pair.service.question_generator.strategy;
+
+import com.pair.model.Grade;
+import com.pair.util.RandomUtils;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 抽象题目生成策略基类
+ * 提供通用的选项生成方法,减少重复代码
+ */
+public abstract class AbstractQuestionStrategy implements QuestionStrategy {
+
+ protected final Grade grade;
+
+ /**
+ * 构造方法
+ * @param grade 学段
+ */
+ protected AbstractQuestionStrategy(Grade grade) {
+ this.grade = grade;
+ }
+
+ /**
+ * 生成数值型选项(包含正确答案和3个干扰项)
+ * @param correctAnswer 正确答案
+ * @return 打乱顺序后的选项列表
+ */
+ protected List generateNumericOptions(double correctAnswer) {
+ List options = new ArrayList<>();
+ options.add(correctAnswer);
+
+ // 根据答案大小动态调整干扰项范围
+ int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3);
+
+ // 生成3个干扰项
+ for (int i = 0; i < 3; i++) {
+ double distractor;
+ int attempts = 0;
+
+ do {
+ int offset = RandomUtils.nextInt(-range, range);
+ if (offset == 0) offset = (i + 1) * 3;
+ distractor = correctAnswer + offset;
+ attempts++;
+ } while ((options.contains(distractor) || distractor < 0) && attempts < 20);
+
+ if (attempts >= 20) {
+ distractor = correctAnswer + (i + 1) * 5;
+ }
+
+ options.add(distractor);
+ }
+
+ // 确保有4个选项
+ while (options.size() < 4) {
+ options.add(correctAnswer + RandomUtils.nextInt(5, 15));
+ }
+
+ Collections.shuffle(options);
+ return options;
+ }
+
+ /**
+ * 生成数值型选项(包含一个常见错误答案)
+ * @param correctAnswer 正确答案
+ * @param commonError 常见错误答案
+ * @return 打乱顺序后的选项列表
+ */
+ protected List generateNumericOptionsWithCommonError(
+ double correctAnswer, double commonError) {
+
+ List options = new ArrayList<>();
+ options.add(correctAnswer);
+
+ // 添加常见错误项(如果有效)
+ if (commonError != correctAnswer && commonError > 0 && !Double.isNaN(commonError)) {
+ options.add(commonError);
+ }
+
+ int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3);
+
+ // 填充其他干扰项
+ while (options.size() < 4) {
+ double distractor;
+ int attempts = 0;
+
+ do {
+ int offset = RandomUtils.nextInt(-range, range);
+ if (offset == 0) offset = 5;
+ distractor = correctAnswer + offset;
+ attempts++;
+ } while ((options.contains(distractor) || distractor < 0) && attempts < 20);
+
+ if (attempts < 20) {
+ options.add(distractor);
+ } else {
+ options.add(correctAnswer + options.size() * 3);
+ }
+ }
+
+ Collections.shuffle(options);
+ return options;
+ }
+
+ /**
+ * 生成字符串型选项(用于三角函数等)
+ * @param correctAnswer 正确答案
+ * @param allPossibleValues 所有可能的值
+ * @return 打乱顺序后的选项列表
+ */
+ protected List generateStringOptions(
+ String correctAnswer, List allPossibleValues) {
+
+ List options = new ArrayList<>();
+ options.add(correctAnswer);
+
+ List availableValues = new ArrayList<>(allPossibleValues);
+ availableValues.remove(correctAnswer);
+
+ // 随机选择3个干扰项
+ while (options.size() < 4 && !availableValues.isEmpty()) {
+ int randomIndex = RandomUtils.nextInt(0, availableValues.size() - 1);
+ String distractor = availableValues.get(randomIndex);
+ options.add(distractor);
+ availableValues.remove(randomIndex);
+ }
+
+ while (options.size() < 4) {
+ options.add("未知");
+ }
+
+ Collections.shuffle(options);
+ return options;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/QuestionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/QuestionStrategy.java
new file mode 100644
index 0000000..baa8cb8
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/QuestionStrategy.java
@@ -0,0 +1,12 @@
+package com.pair.service.question_generator.strategy;
+import com.pair.model.ChoiceQuestion;
+
+//题目生成题型接口
+public interface QuestionStrategy {
+
+ //生成题目
+ ChoiceQuestion generate();
+
+ //题型
+ String getStrategyName();
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/AdditionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/AdditionStrategy.java
new file mode 100644
index 0000000..574898d
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/AdditionStrategy.java
@@ -0,0 +1,37 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 加法策略
+ */
+public class AdditionStrategy extends AbstractQuestionStrategy {
+
+ public AdditionStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int num1 = RandomUtils.nextInt(1, 30);
+ int num2 = RandomUtils.nextInt(1, 30);
+
+ String questionText = num1 + " + " + num2;
+ double answer = num1 + num2;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "加法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/DivisionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/DivisionStrategy.java
new file mode 100644
index 0000000..2ff79f0
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/DivisionStrategy.java
@@ -0,0 +1,39 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 除法策略(确保整除)
+ */
+public class DivisionStrategy extends AbstractQuestionStrategy {
+
+ public DivisionStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int divisor = RandomUtils.nextInt(2, 10);
+ int quotient = RandomUtils.nextInt(1, 10);
+ int dividend = divisor * quotient;
+
+ String questionText = dividend + " ÷ " + divisor;
+ double answer = quotient;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "除法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/MultiplicationStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/MultiplicationStrategy.java
new file mode 100644
index 0000000..697b853
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/MultiplicationStrategy.java
@@ -0,0 +1,37 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 乘法策略
+ */
+public class MultiplicationStrategy extends AbstractQuestionStrategy {
+
+ public MultiplicationStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int factor1 = RandomUtils.nextInt(1, 12);
+ int factor2 = RandomUtils.nextInt(1, 12);
+
+ String questionText = factor1 + " × " + factor2;
+ double answer = factor1 * factor2;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "乘法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java
new file mode 100644
index 0000000..9e7daa4
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java
@@ -0,0 +1,40 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+
+import java.util.List;
+
+/**
+ * 带括号的加法乘法混合运算策略
+ * 例如:(a + b) × c
+ */
+public class ParenthesesAddStrategy extends AbstractQuestionStrategy {
+
+ public ParenthesesAddStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int num1 = RandomUtils.nextInt(1, 20);
+ int num2 = RandomUtils.nextInt(1, 20);
+ int num3 = RandomUtils.nextInt(2, 10);
+
+ String questionText = "(" + num1 + " + " + num2 + ") × " + num3;
+ double answer = (num1 + num2) * num3;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "括号加法乘法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java
new file mode 100644
index 0000000..d70c4b9
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+/**
+ * 带括号的减法乘法混合运算策略
+ * 例如:(a - b) × c
+ */
+public class ParenthesesMultiplyStrategy extends AbstractQuestionStrategy {
+
+ public ParenthesesMultiplyStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int num1 = RandomUtils.nextInt(1, 20);
+ int num2 = RandomUtils.nextInt(1, 20);
+ int num3 = RandomUtils.nextInt(2, 10);
+
+ int larger = Math.max(num1, num2);
+ int smaller = Math.min(num1, num2);
+
+ String questionText = "(" + larger + " - " + smaller + ") × " + num3;
+ double answer = (larger - smaller) * num3;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "括号减法乘法";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/elementary/SubtractionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/elementary/SubtractionStrategy.java
new file mode 100644
index 0000000..87f83f7
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/elementary/SubtractionStrategy.java
@@ -0,0 +1,40 @@
+package com.pair.service.question_generator.strategy.elementary;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 减法策略
+ */
+public class SubtractionStrategy extends AbstractQuestionStrategy {
+
+ public SubtractionStrategy() {
+ super(Grade.ELEMENTARY);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int num1 = RandomUtils.nextInt(1, 30);
+ int num2 = RandomUtils.nextInt(1, 30);
+
+ // 确保结果为正数
+ int larger = Math.max(num1, num2);
+ int smaller = Math.min(num1, num2);
+
+ String questionText = larger + " - " + smaller;
+ double answer = larger - smaller;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "减法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/ArithmeticSequenceSumStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/ArithmeticSequenceSumStrategy.java
new file mode 100644
index 0000000..f703f08
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/ArithmeticSequenceSumStrategy.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 等差数列求和策略
+ * 例如:首项 3,公差 2,求前 10 项和
+ */
+public class ArithmeticSequenceSumStrategy extends AbstractQuestionStrategy {
+
+ public ArithmeticSequenceSumStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int a1 = RandomUtils.nextInt(1, 10); // 首项
+ int d = RandomUtils.nextInt(1, 5); // 公差
+ int n = RandomUtils.nextInt(5, 15); // 项数
+
+ String questionText = "等差数列首项为 " + a1 + ",公差为 " + d +
+ ",求前 " + n + " 项和";
+
+ // Sn = n * a1 + n(n-1)/2 * d
+ double answer = n * a1 + n * (n - 1) / 2.0 * d;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "等差数列求和";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/CosStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/CosStrategy.java
new file mode 100644
index 0000000..2d744f4
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/CosStrategy.java
@@ -0,0 +1,57 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+import java.util.*;
+
+/**
+ * 余弦函数策略
+ * 例如:cos(45°)
+ */
+public class CosStrategy extends AbstractQuestionStrategy {
+
+ // 特殊角的余弦值表
+ private static final Map COS_VALUES = new HashMap<>();
+
+ static {
+ COS_VALUES.put(0, "1");
+ COS_VALUES.put(30, "√3/2");
+ COS_VALUES.put(45, "√2/2");
+ COS_VALUES.put(60, "1/2");
+ COS_VALUES.put(90, "0");
+ COS_VALUES.put(120, "-1/2");
+ COS_VALUES.put(135, "-√2/2");
+ COS_VALUES.put(150, "-√3/2");
+ COS_VALUES.put(180, "-1");
+ }
+
+ public CosStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ List angles = new ArrayList<>(COS_VALUES.keySet());
+ int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
+ int angle = angles.get(randomIndex);
+
+ String questionText = "cos(" + angle + "°) = ?";
+ String correctAnswer = COS_VALUES.get(angle);
+
+ List allValues = new ArrayList<>(COS_VALUES.values());
+ List options = generateStringOptions(correctAnswer, allValues);
+
+ return new ChoiceQuestion(questionText, correctAnswer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "余弦函数";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/DerivativeStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/DerivativeStrategy.java
new file mode 100644
index 0000000..d2161cc
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/DerivativeStrategy.java
@@ -0,0 +1,40 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 导数计算策略
+ * 例如:f(x) = 3x² + 2x,求 f'(2)
+ */
+public class DerivativeStrategy extends AbstractQuestionStrategy {
+
+ public DerivativeStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int a = RandomUtils.nextInt(2, 8);
+ int b = RandomUtils.nextInt(1, 6);
+ int x = RandomUtils.nextInt(1, 5);
+
+ String questionText = "f(x) = " + a + "x² + " + b + "x,求 f'(" + x + ")";
+
+ // f'(x) = 2ax + b
+ double answer = 2 * a * x + b;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "导数计算";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/FunctionExtremeStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/FunctionExtremeStrategy.java
new file mode 100644
index 0000000..e094caa
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/FunctionExtremeStrategy.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 函数极值策略
+ * 例如:f(x) = -x² + 4x + 1,求最大值
+ */
+public class FunctionExtremeStrategy extends AbstractQuestionStrategy {
+
+ public FunctionExtremeStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int a = -1; // 开口向下
+ int b = RandomUtils.nextInt(2, 8) * 2; // 偶数,方便计算
+ int c = RandomUtils.nextInt(1, 10);
+
+ String questionText = "f(x) = -x² + " + b + "x + " + c + ",求最大值";
+
+ // 顶点坐标 x = -b/(2a), y = (4ac - b²)/(4a)
+ double xVertex = -b / (2.0 * a);
+ double answer = (4.0 * a * c - b * b) / (4.0 * a);
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "函数极值";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/LogarithmStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/LogarithmStrategy.java
new file mode 100644
index 0000000..2fad434
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/LogarithmStrategy.java
@@ -0,0 +1,55 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 对数运算策略
+ * 例如:log₂8 + log₃9
+ */
+public class LogarithmStrategy extends AbstractQuestionStrategy {
+
+ private static final int[][] LOG_PAIRS = {
+ {2, 4, 2}, // log₂4 = 2
+ {2, 8, 3}, // log₂8 = 3
+ {2, 16, 4}, // log₂16 = 4
+ {3, 9, 2}, // log₃9 = 2
+ {3, 27, 3}, // log₃27 = 3
+ {5, 25, 2}, // log₅25 = 2
+ {10, 100, 2} // log₁₀100 = 2
+ };
+
+ public LogarithmStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int index1 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
+ int index2 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
+
+ int base1 = LOG_PAIRS[index1][0];
+ int num1 = LOG_PAIRS[index1][1];
+ int result1 = LOG_PAIRS[index1][2];
+
+ int base2 = LOG_PAIRS[index2][0];
+ int num2 = LOG_PAIRS[index2][1];
+ int result2 = LOG_PAIRS[index2][2];
+
+ String questionText = "log₍" + base1 + "₎" + num1 + " + log₍" + base2 + "₎" + num2;
+ double answer = result1 + result2;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "对数运算";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/ProbabilityStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/ProbabilityStrategy.java
new file mode 100644
index 0000000..0c47aef
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/ProbabilityStrategy.java
@@ -0,0 +1,40 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 概率计算策略
+ * 例如:袋中有 5 个红球,3 个白球,随机抽一个,抽到红球的概率是多少?
+ */
+public class ProbabilityStrategy extends AbstractQuestionStrategy {
+
+ public ProbabilityStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int red = RandomUtils.nextInt(3, 8);
+ int white = RandomUtils.nextInt(2, 6);
+ int total = red + white;
+
+ String questionText = "袋中有 " + red + " 个红球," + white +
+ " 个白球,随机抽一个,抽到红球的概率是多少?(保留两位小数)";
+
+ double answer = Math.round((double) red / total * 100.0) / 100.0;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "概率计算";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/SinStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/SinStrategy.java
new file mode 100644
index 0000000..f510c62
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/SinStrategy.java
@@ -0,0 +1,56 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+import java.util.*;
+
+import java.util.List;
+
+/**
+ * 正弦函数策略
+ * 例如:sin(30°)
+ */
+public class SinStrategy extends AbstractQuestionStrategy {
+
+ // 特殊角的正弦值表
+ private static final Map SIN_VALUES = new HashMap<>();
+
+ static {
+ SIN_VALUES.put(0, "0");
+ SIN_VALUES.put(30, "1/2");
+ SIN_VALUES.put(45, "√2/2");
+ SIN_VALUES.put(60, "√3/2");
+ SIN_VALUES.put(90, "1");
+ SIN_VALUES.put(120, "√3/2");
+ SIN_VALUES.put(135, "√2/2");
+ SIN_VALUES.put(150, "1/2");
+ SIN_VALUES.put(180, "0");
+ }
+
+ public SinStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ List angles = new ArrayList<>(SIN_VALUES.keySet());
+ int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
+ int angle = angles.get(randomIndex);
+
+ String questionText = "sin(" + angle + "°) = ?";
+ String correctAnswer = SIN_VALUES.get(angle);
+
+ List allValues = new ArrayList<>(SIN_VALUES.values());
+ List options = generateStringOptions(correctAnswer, allValues);
+
+ return new ChoiceQuestion(questionText, correctAnswer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "正弦函数";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/TanStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/TanStrategy.java
new file mode 100644
index 0000000..8c6890e
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/TanStrategy.java
@@ -0,0 +1,56 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+import java.util.*;
+
+/**
+ * 正切函数策略
+ * 例如:tan(45°)
+ */
+public class TanStrategy extends AbstractQuestionStrategy {
+
+ // 特殊角的正切值表
+ private static final Map TAN_VALUES = new HashMap<>();
+
+ static {
+ TAN_VALUES.put(0, "0");
+ TAN_VALUES.put(30, "√3/3");
+ TAN_VALUES.put(45, "1");
+ TAN_VALUES.put(60, "√3");
+ TAN_VALUES.put(120, "-√3");
+ TAN_VALUES.put(135, "-1");
+ TAN_VALUES.put(150, "-√3/3");
+ TAN_VALUES.put(180, "0");
+ }
+
+ public TanStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ List angles = new ArrayList<>(TAN_VALUES.keySet());
+ int randomIndex = RandomUtils.nextInt(0, angles.size() - 1);
+ int angle = angles.get(randomIndex);
+
+ String questionText = "tan(" + angle + "°) = ?";
+ String correctAnswer = TAN_VALUES.get(angle);
+
+ List allValues = new ArrayList<>(TAN_VALUES.values());
+ List options = generateStringOptions(correctAnswer, allValues);
+
+ return new ChoiceQuestion(questionText, correctAnswer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "正切函数";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/high/TrigIdentityStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/high/TrigIdentityStrategy.java
new file mode 100644
index 0000000..05232d1
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/high/TrigIdentityStrategy.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.strategy.high;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+import java.util.*;
+
+/**
+ * 三角恒等式策略
+ * 例如:sin²(θ) + cos²(θ) = ?
+ */
+public class TrigIdentityStrategy extends AbstractQuestionStrategy {
+
+ public TrigIdentityStrategy() {
+ super(Grade.HIGH);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int[] angles = {30, 45, 60, 90};
+ int angle = angles[RandomUtils.nextInt(0, angles.length - 1)];
+
+ String questionText = "sin²(" + angle + "°) + cos²(" + angle + "°) = ?";
+ String correctAnswer = "1"; // 三角恒等式,永远等于1
+
+ List allValues = Arrays.asList("1", "0", "2", "√2", "1/2");
+ List options = generateStringOptions(correctAnswer, allValues);
+
+ return new ChoiceQuestion(questionText, correctAnswer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "三角恒等式";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/CircleAreaStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/CircleAreaStrategy.java
new file mode 100644
index 0000000..136438c
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/CircleAreaStrategy.java
@@ -0,0 +1,36 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 圆的面积策略
+ * 例如:半径为 5cm 的圆,面积是多少?(π取3.14)
+ */
+public class CircleAreaStrategy extends AbstractQuestionStrategy {
+
+ public CircleAreaStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int radius = RandomUtils.nextInt(3, 10);
+
+ String questionText = "半径为 " + radius + "cm 的圆,面积是多少?(π取3.14)";
+ double answer = 3.14 * radius * radius;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "圆的面积";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/LinearEquationStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/LinearEquationStrategy.java
new file mode 100644
index 0000000..0453906
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/LinearEquationStrategy.java
@@ -0,0 +1,39 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 一元一次方程策略
+ * 例如:2x + 5 = 13,求 x
+ */
+public class LinearEquationStrategy extends AbstractQuestionStrategy {
+
+ public LinearEquationStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int a = RandomUtils.nextInt(2, 10); // 系数
+ int x = RandomUtils.nextInt(1, 10); // 真实的 x 值
+ int b = RandomUtils.nextInt(1, 15); // 常数项
+ int result = a * x + b;
+
+ String questionText = a + "x + " + b + " = " + result + ",求 x";
+ double answer = x;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "一元一次方程";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/LinearFunctionStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/LinearFunctionStrategy.java
new file mode 100644
index 0000000..b15aeb3
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/LinearFunctionStrategy.java
@@ -0,0 +1,41 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 一次函数求值策略
+ * 例如:函数 y = 2x + 3,当 x = 5 时,y 的值是多少?
+ */
+public class LinearFunctionStrategy extends AbstractQuestionStrategy {
+
+ public LinearFunctionStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int k = RandomUtils.nextInt(2, 8);
+ int b = RandomUtils.nextInt(-5, 10);
+ int x = RandomUtils.nextInt(1, 10);
+
+ String questionText = "函数 y = " + k + "x " +
+ (b >= 0 ? "+ " : "") + b +
+ ",当 x = " + x + " 时,y 的值是多少?";
+
+ double answer = k * x + b;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "一次函数求值";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java
new file mode 100644
index 0000000..95163cb
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java
@@ -0,0 +1,40 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 平方开方混合运算策略
+ * 例如:√49 + 3²
+ */
+public class MixedSquareSqrtStrategy extends AbstractQuestionStrategy {
+
+ public MixedSquareSqrtStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int sqrtRoot = RandomUtils.nextInt(2, 8);
+ int sqrtNum = sqrtRoot * sqrtRoot;
+
+ int squareBase = RandomUtils.nextInt(2, 6);
+
+ String questionText = "√" + sqrtNum + " + " + squareBase + "²";
+ double answer = sqrtRoot + (squareBase * squareBase);
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "平方开方混合";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/QuadraticEquationStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/QuadraticEquationStrategy.java
new file mode 100644
index 0000000..45745e8
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/QuadraticEquationStrategy.java
@@ -0,0 +1,44 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 一元二次方程策略(求较大根)
+ * 例如:x² - 5x + 6 = 0,求较大的根
+ */
+public class QuadraticEquationStrategy extends AbstractQuestionStrategy {
+
+ public QuadraticEquationStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ // 生成两个根
+ int root1 = RandomUtils.nextInt(1, 6);
+ int root2 = RandomUtils.nextInt(1, 6);
+
+ // 根据韦达定理:x² - (root1+root2)x + (root1*root2) = 0
+ int b = -(root1 + root2);
+ int c = root1 * root2;
+
+ String questionText = "x² " + (b >= 0 ? "+ " : "") + b + "x " +
+ (c >= 0 ? "+ " : "") + c + " = 0,求较大的根";
+
+ double answer = Math.max(root1, root2);
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "一元二次方程";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtAddStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtAddStrategy.java
new file mode 100644
index 0000000..880eeb5
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtAddStrategy.java
@@ -0,0 +1,39 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 开方加法混合策略
+ * 例如:√49 + 5
+ */
+public class SqrtAddStrategy extends AbstractQuestionStrategy {
+
+ public SqrtAddStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int root = RandomUtils.nextInt(2, 10);
+ int num = root * root;
+ int add = RandomUtils.nextInt(1, 20);
+
+ String questionText = "√" + num + " + " + add;
+ double answer = root + add;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "开方加法";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtStrategy.java
new file mode 100644
index 0000000..c567201
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/SqrtStrategy.java
@@ -0,0 +1,39 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+/**
+ * 开方运算策略(完全平方数)
+ * 例如:√49
+ */
+public class SqrtStrategy extends AbstractQuestionStrategy {
+
+ public SqrtStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int root = RandomUtils.nextInt(2, 12);
+ int num = root * root; // 完全平方数
+
+ String questionText = "√" + num;
+ double answer = root;
+
+ // 常见错误:把开方当成除以2
+ double commonError = num / 2.0;
+ List options = generateNumericOptionsWithCommonError(answer, commonError);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "开方";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/SquareAddStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/SquareAddStrategy.java
new file mode 100644
index 0000000..d0e8dd8
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/SquareAddStrategy.java
@@ -0,0 +1,37 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+/**
+ * 平方加法混合策略
+ * 例如:5² + 10
+ */
+public class SquareAddStrategy extends AbstractQuestionStrategy {
+
+ public SquareAddStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int base = RandomUtils.nextInt(2, 10);
+ int add = RandomUtils.nextInt(1, 20);
+
+ String questionText = base + "² + " + add;
+ double answer = base * base + add;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "平方加法";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/SquareStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/SquareStrategy.java
new file mode 100644
index 0000000..f84c01a
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/SquareStrategy.java
@@ -0,0 +1,39 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 平方运算策略
+ * 例如:5²
+ */
+public class SquareStrategy extends AbstractQuestionStrategy {
+
+ public SquareStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int num = RandomUtils.nextInt(1, 15);
+
+ String questionText = num + "²";
+ double answer = num * num;
+
+ // 常见错误:把平方当成乘以2
+ double commonError = num * 2;
+ List options = generateNumericOptionsWithCommonError(answer, commonError);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "平方";
+ }
+}
diff --git a/src/main/java/com/pair/service/question_generator/strategy/middle/TriangleAreaStrategy.java b/src/main/java/com/pair/service/question_generator/strategy/middle/TriangleAreaStrategy.java
new file mode 100644
index 0000000..b01c675
--- /dev/null
+++ b/src/main/java/com/pair/service/question_generator/strategy/middle/TriangleAreaStrategy.java
@@ -0,0 +1,37 @@
+package com.pair.service.question_generator.strategy.middle;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.model.Grade;
+import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
+import com.pair.util.RandomUtils;
+
+import java.util.List;
+
+/**
+ * 三角形面积计算策略
+ * 例如:底 6cm,高 8cm 的三角形面积
+ */
+public class TriangleAreaStrategy extends AbstractQuestionStrategy {
+
+ public TriangleAreaStrategy() {
+ super(Grade.MIDDLE);
+ }
+
+ @Override
+ public ChoiceQuestion generate() {
+ int base = RandomUtils.nextInt(4, 15);
+ int height = RandomUtils.nextInt(4, 15);
+
+ String questionText = "底为 " + base + "cm,高为 " + height + "cm 的三角形面积是多少?";
+ double answer = (base * height) / 2.0;
+
+ List options = generateNumericOptions(answer);
+
+ return new ChoiceQuestion(questionText, answer, options, grade);
+ }
+
+ @Override
+ public String getStrategyName() {
+ return "三角形面积";
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/ui/InfGenPage.java b/src/main/java/com/pair/ui/InfGenPage.java
new file mode 100644
index 0000000..3b7dca1
--- /dev/null
+++ b/src/main/java/com/pair/ui/InfGenPage.java
@@ -0,0 +1,140 @@
+// com/ui/InfGenPage.java
+package com.pair.ui;
+
+import com.pair.model.Grade;
+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("生成题目");
+ private final Button modifyUsernameButton = new Button("修改用户名"); // 新增
+ private final Button modifyEmailButton = new Button("修改邮箱"); // 新增
+
+ public InfGenPage(Runnable onBack, String currentUsername, String currentEmail, Grade currentGrade) {
+ super(onBack);
+ initializeContent();
+ usernameField.setText(currentUsername);
+ emailField.setText(currentEmail);
+ gradeChoice.getSelectionModel().select(currentGrade.ordinal());
+ }
+
+ @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, modifyUsernameButton);
+ HBox emailRow = createFormRow("邮箱:", emailField, modifyEmailButton);
+ HBox passwordRow = createFormRow("密码:", passwordLabel, passwordModifyButton);
+ 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);
+
+ // 新增按钮样式
+ modifyUsernameButton.setStyle(UIConstants.BUTTON_STYLE);
+ modifyUsernameButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ modifyUsernameButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+
+ modifyEmailButton.setStyle(UIConstants.BUTTON_STYLE);
+ modifyEmailButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT);
+ modifyEmailButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE));
+
+ // ========== 添加到表单 ==========
+ 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); // 间距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; }
+ public Button getModifyUsernameButton() { return modifyUsernameButton; } // 新增
+ public Button getModifyEmailButton() { return modifyEmailButton; } // 新增
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/ui/LoginPage.java b/src/main/java/com/pair/ui/LoginPage.java
new file mode 100644
index 0000000..0f5d9d8
--- /dev/null
+++ b/src/main/java/com/pair/ui/LoginPage.java
@@ -0,0 +1,58 @@
+// com/ui/LoginPage.java
+package com.pair.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;
+
+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/pair/ui/MainWindow.java b/src/main/java/com/pair/ui/MainWindow.java
new file mode 100644
index 0000000..f44a851
--- /dev/null
+++ b/src/main/java/com/pair/ui/MainWindow.java
@@ -0,0 +1,296 @@
+// com/ui/MainWindow.java
+package com.pair.ui;
+
+import com.pair.model.User;
+import com.pair.service.QuizService;
+import com.pair.service.UserService;
+import com.pair.util.AsyncRegistrationHelper;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+import javafx.scene.Scene;
+import javafx.scene.control.Button;
+import javafx.scene.control.Toggle;
+import javafx.scene.layout.BorderPane;
+import javafx.stage.Stage;
+import java.io.IOException;
+
+public class MainWindow extends BorderPane {
+ 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) throws IOException {
+ this.primaryStage = primaryStage;
+ showStartPage();
+ }
+
+ 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;
+ }
+
+ // 封装登录页面初始化逻辑
+ 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);
+ }
+
+ // 封装登录核心逻辑(从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());
+ }
+ }
+
+ // 其他页面的初始化方法(保持原有逻辑,统一格式)
+ 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);
+ }
+
+
+ private void handleSendCodeAction(RegisterPage registerPage) {
+ String email = registerPage.getEmailField().getText().trim();
+
+ Button btn = registerPage.getSendCodeButton();
+
+ AsyncRegistrationHelper.sendRegistrationCode(
+ email,
+ userService,
+ () -> {
+ btn.setDisable(true);
+ btn.setText("发送中...");
+ },
+ text -> btn.setText(text), // 倒计时更新
+ () -> {
+ btn.setText("获取注册码");
+ btn.setDisable(false);
+ },
+ msg -> NavigablePanel.showInfoAlert("成功", msg),
+ msg -> NavigablePanel.showErrorAlert("获取注册码失败", msg)
+ );
+ }
+
+ 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();
+
+ 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(), userService.getCurrentUser().getGrade());
+ infGenPage.getGenerateButton().setOnAction(e -> {
+ 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 -> {
+ navigateTo(Panel.PASSWORDMODIFY);
+ });
+ infGenPage.getModifyUsernameButton().setOnAction(e -> handleUsernameModifyAction(infGenPage));
+ infGenPage.getModifyEmailButton().setOnAction(e -> handleEmailModifyAction(infGenPage));
+
+ 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());
+ NavigablePanel.showInfoAlert("成功", "用户名修改成功");
+ } 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());
+ NavigablePanel.showInfoAlert("成功","邮箱修改成功");
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
+ }
+ }
+
+ private void initPasswordModifyPage() {
+ PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
+ pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
+ this.setCenter(pwdPage);
+ }
+
+ 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);
+ NavigablePanel.showInfoAlert("成功", "密码修改成功");
+ navigateTo(Panel.INF_GEN);
+ } catch (IllegalArgumentException ex) {
+ NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
+ return;
+ } catch (IOException ex) {
+ NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
+ return;
+ }
+ }
+
+ 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);
+ });
+
+ this.setCenter(quizPage);
+ }
+
+ 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);
+ }
+
+ // getter和启动方法(保持不变)
+ public Panel getCurrentPanel() {
+ return currentPanel;
+ }
+
+ public static void start(Stage stage) throws IOException {
+ 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/pair/ui/NavigablePanel.java b/src/main/java/com/pair/ui/NavigablePanel.java
new file mode 100644
index 0000000..75ee27d
--- /dev/null
+++ b/src/main/java/com/pair/ui/NavigablePanel.java
@@ -0,0 +1,60 @@
+// com/ui/NavigablePanel.java
+package com.pair.ui;
+
+import javafx.application.Platform;
+import javafx.geometry.Pos;
+import javafx.scene.control.Alert;
+import javafx.scene.control.Button;
+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 static void showInfoAlert(String title, String message) {
+ Platform.runLater(() -> {
+ Alert alert = new Alert(Alert.AlertType.INFORMATION);
+ 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/pair/ui/Panel.java b/src/main/java/com/pair/ui/Panel.java
new file mode 100644
index 0000000..84ed60e
--- /dev/null
+++ b/src/main/java/com/pair/ui/Panel.java
@@ -0,0 +1,11 @@
+package com.pair.ui;
+
+public enum Panel {
+ START, // 开始页面
+ LOGIN, // 登录页面
+ REGISTER, // 注册页面
+ INF_GEN, // 个人信息+生成题目页面
+ PASSWORDMODIFY, // 修改密码页面
+ QUIZ, // 答题页面
+ RESULT // 得分页面
+}
diff --git a/src/main/java/com/pair/ui/PasswordModifyPage.java b/src/main/java/com/pair/ui/PasswordModifyPage.java
new file mode 100644
index 0000000..0288673
--- /dev/null
+++ b/src/main/java/com/pair/ui/PasswordModifyPage.java
@@ -0,0 +1,52 @@
+// com/ui/PasswordModifyPage.java
+package com.pair.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/pair/ui/QuizPage.java b/src/main/java/com/pair/ui/QuizPage.java
new file mode 100644
index 0000000..f4ce292
--- /dev/null
+++ b/src/main/java/com/pair/ui/QuizPage.java
@@ -0,0 +1,314 @@
+// com/ui/QuizPage.java
+package com.pair.ui;
+
+import com.pair.model.ChoiceQuestion;
+import com.pair.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.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/pair/ui/RegisterPage.java b/src/main/java/com/pair/ui/RegisterPage.java
new file mode 100644
index 0000000..f8ff1a1
--- /dev/null
+++ b/src/main/java/com/pair/ui/RegisterPage.java
@@ -0,0 +1,63 @@
+// com/ui/RegisterPage.java
+package com.pair.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/pair/ui/ResultPage.java b/src/main/java/com/pair/ui/ResultPage.java
new file mode 100644
index 0000000..c82b922
--- /dev/null
+++ b/src/main/java/com/pair/ui/ResultPage.java
@@ -0,0 +1,63 @@
+// com/ui/ResultPage.java
+package com.pair.ui;
+
+import com.pair.model.QuizResult;
+import com.pair.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/pair/ui/StartPage.java b/src/main/java/com/pair/ui/StartPage.java
new file mode 100644
index 0000000..e0c34ab
--- /dev/null
+++ b/src/main/java/com/pair/ui/StartPage.java
@@ -0,0 +1,37 @@
+// com/ui/StartPage.java
+package com.pair.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/pair/ui/UIConstants.java b/src/main/java/com/pair/ui/UIConstants.java
new file mode 100644
index 0000000..75527b4
--- /dev/null
+++ b/src/main/java/com/pair/ui/UIConstants.java
@@ -0,0 +1,69 @@
+// UIConstants.java
+package com.pair.ui;
+
+import javafx.geometry.Insets;
+
+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/pair/util/AppDataDirectory.java b/src/main/java/com/pair/util/AppDataDirectory.java
new file mode 100644
index 0000000..5b20b1a
--- /dev/null
+++ b/src/main/java/com/pair/util/AppDataDirectory.java
@@ -0,0 +1,43 @@
+package com.pair.util;
+
+/**
+ * 应用数据目录管理器
+ * 根据不同操作系统返回合适的应用数据存储路径
+ */
+public class AppDataDirectory {
+ private static final String APP_NAME = "Math-Quiz-App"; // 替换为你的应用名
+
+ /**
+ * 获取应用数据根目录
+ */
+ public static String getApplicationDataDirectory() {
+ String os = System.getProperty("os.name").toLowerCase();
+ String basePath;
+
+ if (os.contains("win")) {
+ // Windows
+ String appData = System.getenv("APPDATA");
+ basePath = (appData != null) ? appData : System.getProperty("user.home") + "/AppData/Roaming";
+ } else if (os.contains("mac")) {
+ // macOS
+ basePath = System.getProperty("user.home") + "/Library/Application Support";
+ } else {
+ // Linux/Unix
+ String xdgDataHome = System.getenv("XDG_DATA_HOME");
+ if (xdgDataHome == null) {
+ xdgDataHome = System.getProperty("user.home") + "/.local/share";
+ }
+ basePath = xdgDataHome;
+ }
+
+ return basePath + "/" + APP_NAME;
+ }
+
+ /**
+ * 获取完整的应用数据路径
+ */
+ public static String getFullPath(String relativePath) {
+ String appDataDir = getApplicationDataDirectory();
+ return appDataDir + "/" + relativePath;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/util/AsyncRegistrationHelper.java b/src/main/java/com/pair/util/AsyncRegistrationHelper.java
new file mode 100644
index 0000000..7c865b7
--- /dev/null
+++ b/src/main/java/com/pair/util/AsyncRegistrationHelper.java
@@ -0,0 +1,68 @@
+// AsyncRegistrationHelper.java
+package com.pair.util;
+
+import com.pair.service.UserService;
+import javafx.application.Platform;
+import javafx.concurrent.Task;
+
+import java.io.IOException;
+import java.util.function.Consumer;
+
+public class
+AsyncRegistrationHelper {
+
+ private static final int COUNTDOWN_SECONDS = 60;
+
+ public static void sendRegistrationCode(
+ String email,
+ UserService userService,
+ Runnable onPrepare,
+ Consumer onCountdown,
+ Runnable onComplete,
+ Consumer onSuccess,
+ Consumer onFailure) {
+
+ Platform.runLater(onPrepare);
+
+ Task task = new Task<>() {
+ @Override
+ protected Void call() throws Exception {
+ userService.generateRegistrationCode(email);
+ return null;
+ }
+ };
+
+ task.setOnSucceeded(e -> {
+ onSuccess.accept("注册码已发送到邮箱,10分钟内有效");
+
+ new Thread(() -> {
+ try {
+ for (int i = COUNTDOWN_SECONDS; i >= 0; i--) {
+ int sec = i;
+ Platform.runLater(() -> {
+ if (sec == 0) onComplete.run();
+ else onCountdown.accept(sec + "秒后重试");
+ });
+ Thread.sleep(1000);
+ }
+ } catch (InterruptedException ignored) {
+ Platform.runLater(onComplete);
+ Thread.currentThread().interrupt();
+ }
+ }).start();
+ });
+
+ task.setOnFailed(e -> {
+ Throwable ex = task.getException();
+ String msg = switch (ex) {
+ case IllegalArgumentException iae -> iae.getMessage();
+ case IOException ioe -> "系统错误:" + ioe.getMessage();
+ default -> "发送失败,请检查网络或稍后重试";
+ };
+ onFailure.accept(msg);
+ Platform.runLater(onComplete);
+ });
+
+ new Thread(task).start();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/util/EmailUtil.java b/src/main/java/com/pair/util/EmailUtil.java
new file mode 100644
index 0000000..ffe9a37
--- /dev/null
+++ b/src/main/java/com/pair/util/EmailUtil.java
@@ -0,0 +1,103 @@
+package com.pair.util;
+
+import javax.mail.*;
+import javax.mail.internet.AddressException;
+import javax.mail.internet.InternetAddress;
+import javax.mail.internet.MimeMessage;
+import java.util.Date;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class EmailUtil {
+
+
+ // [用户名]@[域名主体].[顶级域名]
+ public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
+
+ //验证邮箱格式
+ public static boolean validateEmail(String email) {
+ if (email == null || email.trim().isEmpty()) {
+ return false;
+ }
+
+ Matcher matcher = EMAIL_PATTERN.matcher(email);
+ return matcher.matches();
+ }
+
+
+ // ==================== 邮箱配置 ====================
+ private static final String SENDER_EMAIL = "2936213174@qq.com"; // 我的QQ邮箱
+ private static final String SENDER_PASSWORD = "gxfjdzqviasuddci"; // QQ邮箱授权码
+ private static final String SMTP_HOST = "smtp.qq.com"; // QQ邮箱SMTP服务器
+ private static final String SMTP_PORT = "587"; // 端口
+
+ /**
+ * 发送注册码到指定邮箱
+ * @param toEmail 收件人邮箱
+ * @param registrationCode 注册码
+ * @return true表示发送成功,false表示发送失败
+ */
+ public static boolean sendRegistrationCode(String toEmail, String registrationCode) {
+ try {
+ // 1. 配置邮件服务器
+ Properties props = new Properties();
+ props.put("mail.smtp.auth", "true");
+ props.put("mail.smtp.starttls.enable", "true");
+ props.put("mail.smtp.host", SMTP_HOST);
+ props.put("mail.smtp.port", SMTP_PORT);
+
+ // 2. 创建会话
+ Session session = Session.getInstance(props, new Authenticator() {
+ @Override
+ protected PasswordAuthentication getPasswordAuthentication() {
+ return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD);
+ }
+ });
+
+ // 3. 创建邮件
+ Message message = new MimeMessage(session);
+ message.setFrom(new InternetAddress(SENDER_EMAIL));
+ message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
+ message.setSubject("注册验证码");
+
+ // 邮件内容
+ String content = "您的注册验证码是:" + registrationCode + "\n" +
+ "验证码有效期为10分钟,请尽快使用。\n" +
+ "如非本人操作,请忽略此邮件。";
+ message.setText(content);
+
+ // 4. 发送邮件
+ Transport.send(message);
+
+ System.out.println("✓ 验证码已发送到:" + toEmail);
+ return true;
+
+ } catch (Exception e) {
+ System.err.println("✗ 邮件发送失败:" + e.getMessage());
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+
+ /**
+ * 发送密码重置邮件(预留接口)
+ *
+ * @param toEmail 收件人邮箱
+ * @param newPassword 新密码
+ * @return true表示发送成功
+ */
+ public static boolean sendPasswordReset(String toEmail, String newPassword) {
+ // TODO: 将来如果需要"找回密码"功能,可以在这里实现
+
+ System.out.println("【模拟】发送密码重置邮件");
+ System.out.println("收件人: " + toEmail);
+ System.out.println("新密码: " + newPassword);
+
+ return true;
+ }
+
+}
+
+
diff --git a/src/main/java/com/pair/util/FileUtils.java b/src/main/java/com/pair/util/FileUtils.java
new file mode 100644
index 0000000..0b9d560
--- /dev/null
+++ b/src/main/java/com/pair/util/FileUtils.java
@@ -0,0 +1,210 @@
+package com.pair.util;
+
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import java.io.*;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.*;
+import java.lang.reflect.Type;
+
+/**
+ * 文件操作工具类
+ * 提供文件读写、目录创建、JSON序列化等常用操作
+ */
+public class FileUtils {
+
+ private static final Gson gson = new GsonBuilder()
+ .setPrettyPrinting() // 格式化输出JSON
+ .create();
+
+ /**
+ * 读取文件内容为字符串
+ * @param filePath 文件路径
+ * @return 文件内容
+ * @throws IOException 读取失败时抛出
+ */
+ public static String readFileToString(String filePath) throws IOException {
+ return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
+ }
+
+ /**
+ * 写入字符串到文件
+ * @param filePath 文件路径
+ * @param content 要写入的内容
+ * @throws IOException 写入失败时抛出
+ */
+ public static void writeStringToFile(String filePath, String content) throws IOException {
+ File file = new File(filePath);
+
+ // 确保父目录存在
+ File parentDir = file.getParentFile();
+ if (parentDir != null && !parentDir.exists()) {
+ parentDir.mkdirs();
+ }
+
+ // 写入文件
+ try (BufferedWriter writer = new BufferedWriter(
+ new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
+ writer.write(content);
+ }
+ }
+
+
+ /**
+ * 创建目录(如果不存在)
+ * @param dirPath 目录路径
+ * @throws IOException 创建失败时抛出
+ */
+ public static void createDirectoryIfNotExists(String dirPath) throws IOException {
+ Path path = Paths.get(dirPath);
+ if (!Files.exists(path)) {
+ System.out.println(1);
+ Files.createDirectories(path);
+ System.out.println(2);
+ }
+ }
+
+ public static void ensureFileExists(String filePath) throws IOException {
+ if (!FileUtils.exists(filePath)) {
+ // 自动创建父目录
+ String parentDir = Paths.get(filePath).getParent().toString();
+ FileUtils.createDirectoryIfNotExists(parentDir);
+
+ // 创建空文件
+ FileUtils.writeStringToFile(filePath, "");
+ }
+ }
+
+ /**
+ * 检查文件是否存在
+ * @param filePath 文件路径
+ * @return true表示存在
+ */
+ public static boolean exists(String filePath) {
+ return Files.exists(Paths.get(filePath));
+ }
+
+ /**
+ * 删除文件
+ * @param filePath 文件路径
+ * @return true表示删除成功
+ */
+ public static boolean deleteFile(String filePath) {
+ try {
+ return Files.deleteIfExists(Paths.get(filePath));
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ }
+ }
+
+ /**
+ * 获取目录下所有文件
+ * @param dirPath 目录路径
+ * @return 文件数组
+ */
+ public static File[] listFiles(String dirPath) {
+ File dir = new File(dirPath);
+ if (dir.exists() && dir.isDirectory()) {
+ return dir.listFiles();
+ }
+ return new File[0];
+ }
+
+ /**
+ * 追加内容到文件末尾
+ * @param filePath 文件路径
+ * @param content 要追加的内容
+ * @throws IOException 追加失败时抛出
+ */
+ public static void appendToFile(String filePath, String content) throws IOException {
+ Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8,
+ StandardOpenOption.CREATE, StandardOpenOption.APPEND);
+ }
+
+ /**
+ * 复制文件
+ * @param sourcePath 源文件路径
+ * @param targetPath 目标文件路径
+ * @throws IOException 复制失败时抛出
+ */
+ public static void copyFile(String sourcePath, String targetPath) throws IOException {
+ Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING);
+ }
+
+ /**
+ * 获取文件大小(字节)
+ * @param filePath 文件路径
+ * @return 文件大小
+ * @throws IOException 获取失败时抛出
+ */
+ public static long getFileSize(String filePath) throws IOException {
+ return Files.size(Paths.get(filePath));
+ }
+
+ // ==================== JSON 操作方法 ====================
+
+ /**
+ * 将对象保存为JSON文件
+ * @param data 要保存的对象
+ * @param filePath 文件路径
+ * @throws IOException 保存失败时抛出
+ */
+ public static void saveAsJson(Object data, String filePath) throws IOException {
+ String json = gson.toJson(data);
+ writeStringToFile(filePath, json);
+ }
+
+ /**
+ * 从JSON文件读取对象
+ * @param filePath 文件路径
+ * @param classOfT 对象的Class类型
+ * @return 反序列化后的对象
+ * @throws IOException 读取失败时抛出
+ */
+ public static T readJsonToObject(String filePath, Class classOfT) throws IOException {
+ String json = readFileToString(filePath);
+ return gson.fromJson(json, classOfT);
+ }
+
+ /**
+ * 从JSON文件读取对象(支持泛型)
+ * @param filePath 文件路径
+ * @param typeOfT 对象的Type类型(用于泛型)
+ * @return 反序列化后的对象
+ * @throws IOException 读取失败时抛出
+ */
+ public static T readJsonToObject(String filePath, Type typeOfT) throws IOException {
+ String json = readFileToString(filePath);
+ return gson.fromJson(json, typeOfT);
+ }
+
+ /**
+ * 将对象转换为JSON字符串
+ * @param data 要转换的对象
+ * @return JSON字符串
+ */
+ public static String toJson(Object data) {
+ return gson.toJson(data);
+ }
+
+ /**
+ * 将JSON字符串转换为对象
+ * @param json JSON字符串
+ * @param classOfT 对象的Class类型
+ * @return 反序列化后的对象
+ */
+ public static T fromJson(String json, Class classOfT) {
+ return gson.fromJson(json, classOfT);
+ }
+
+ /**
+ * 将JSON字符串转换为对象(支持泛型)
+ * @param json JSON字符串
+ * @param typeOfT 对象的Type类型
+ * @return 反序列化后的对象
+ */
+ public static T fromJson(String json, Type typeOfT) {
+ return gson.fromJson(json, typeOfT);
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/util/PasswordValidator.java b/src/main/java/com/pair/util/PasswordValidator.java
new file mode 100644
index 0000000..ebf8c7b
--- /dev/null
+++ b/src/main/java/com/pair/util/PasswordValidator.java
@@ -0,0 +1,146 @@
+package com.pair.util;
+
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+/**
+ * 密码验证和加密工具类
+ * 提供密码格式验证、加密、匹配等功能
+ */
+public class PasswordValidator {
+
+ // 密码长度限制
+ private static final int CODE_LENGTH = 6;
+
+ private static final int MIN_LENGTH = 6;
+ private static final int MAX_LENGTH = 10;
+
+ // 用于生成随机注册码的字符集
+ private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+ private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
+ private static final String DIGITS = "0123456789";
+ private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
+
+
+ // ==================== 密码验证 ====================
+
+ /**
+ * 验证密码格式(返回详细错误信息)
+ *
+ * @param password 待验证的密码
+ * @return 如果密码有效返回 null,否则返回错误信息
+ */
+ public static void validatePassword(String password) {
+ if (password == null || password.isEmpty()) {
+ throw new IllegalArgumentException("密码不能为空!");
+ }
+
+ if (password.length() < MIN_LENGTH) {
+ throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH + " 位!");
+ }
+
+ if (password.length() > MAX_LENGTH) {
+ throw new IllegalArgumentException("密码长度不能超过 " + MAX_LENGTH + " 位!");
+ }
+
+ if (password.contains(" ")) {
+ throw new IllegalArgumentException("密码不能包含空格!");
+ }
+
+ // 检查是否包含小写字母
+ boolean hasLowerLetter = password.matches(".*[a-z].*");
+ if (!hasLowerLetter) {
+ throw new IllegalArgumentException("必须包含小写字母!");
+ }
+
+ // 检查是否包含大写字母
+ boolean hasUpperLetter = password.matches(".*[A-Z].*");
+ if (!hasUpperLetter) {
+ throw new IllegalArgumentException("必须包含大写字母!");
+ }
+
+ // 检查是否包含数字
+ boolean hasDigit = password.matches(".*\\d.*");
+ if (!hasDigit) {
+ throw new IllegalArgumentException("密码必须包含数字!");
+ }
+ }
+
+
+ // ==================== 密码加密 ====================
+
+ /**
+ * 使用SHA-256加密密码
+ *
+ * @param password 明文密码
+ * @return 加密后的密码(16进制字符串)
+ */
+ public static String encrypt(String password) {
+ if (password == null) {
+ throw new IllegalArgumentException("密码不能为null");
+ }
+
+ 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("SHA-256算法不可用", e);
+ }
+ }
+
+ /**
+ * 验证密码是否匹配
+ *
+ * @param plainPassword 明文密码
+ * @param encryptedPassword 加密后的密码
+ * @return true表示匹配
+ */
+ public static boolean matches(String plainPassword, String encryptedPassword) {
+ if (plainPassword == null || encryptedPassword == null) {
+ return false;
+ }
+
+ return encrypt(plainPassword).equals(encryptedPassword);
+ }
+
+ // ==================== 注册码生成 ====================
+
+ /**
+ * 生成6-10位随机注册码(包含字母和数字)
+ *
+ * @return 注册码
+ */
+ public static String generateRegistrationCode() {
+ return generateRegistrationCode(CODE_LENGTH);
+ }
+
+ /**
+ * 生成指定长度的随机注册码
+ *
+ * @param @注册码长度
+ * @return 注册码
+ */
+ public static String generateRegistrationCode(int codeLength) {
+
+ StringBuilder code = new StringBuilder(codeLength);
+
+ // 填充剩余字符
+ for (int i = 0; i < codeLength; i++) {
+ code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
+ }
+
+ // 打乱字符顺序
+ return RandomUtils.shuffleString(code.toString());
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/pair/util/RandomUtils.java b/src/main/java/com/pair/util/RandomUtils.java
new file mode 100644
index 0000000..dd87f32
--- /dev/null
+++ b/src/main/java/com/pair/util/RandomUtils.java
@@ -0,0 +1,91 @@
+package com.pair.util;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Random;
+
+//各种随机数生成
+public class RandomUtils {
+ private static final Random random = new Random();
+
+
+ //生成[min, max]范围内的随机整数(包含边界)
+ public static int nextInt(int min, int max) {
+ if (min > max) {
+ throw new IllegalArgumentException("min不能大于max");
+ }
+ return min + random.nextInt(max - min + 1);
+ }
+
+
+ //从数组中随机选择一个元素(模板类)
+ public static T randomChoice(T[] array) {
+ if (array == null || array.length == 0) {
+ throw new IllegalArgumentException("数组不能为空");
+ }
+ return array[random.nextInt(array.length)];
+ }
+
+
+ //从列表中随机选择一个元素
+ public static T randomChoice(List list) {
+ if (list == null || list.isEmpty()) {
+ throw new IllegalArgumentException("列表不能为空");
+ }
+ return list.get(random.nextInt(list.size()));
+ }
+
+ /**
+ * 打乱列表顺序
+ * @param list 要打乱的列表
+ */
+ public static void shuffle(List list) {
+ 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) {
+ if (min > max) {
+ throw new IllegalArgumentException("min不能大于max");
+ }
+ return min + (max - min) * random.nextDouble();
+ }
+
+
+ //生成随机布尔值
+ public static boolean nextBoolean() {
+ return random.nextBoolean();
+ }
+
+ //按概率返回true(题目生成概率)
+ //示例:probability(0.7) 有70%概率返回true
+ public static boolean probability(double probability) {
+ if (probability < 0.0 || probability > 1.0) {
+ throw new IllegalArgumentException("概率必须在0.0-1.0之间");
+ }
+ return random.nextDouble() < probability;
+ }
+}
\ No newline at end of file