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 + + + + + + + + + + + + + + + + + + + + + + + + + \ 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