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