From 2b1353efd8b3fdad9b6c861b80e734a1ea9241d8 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Thu, 2 Oct 2025 16:39:07 +0800 Subject: [PATCH 01/28] add requirement analysis v1 --- doc/需求分析/需求分析第一版.md | 76 +++++++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 doc/需求分析/需求分析第一版.md diff --git a/doc/需求分析/需求分析第一版.md b/doc/需求分析/需求分析第一版.md new file mode 100644 index 0000000..93a2862 --- /dev/null +++ b/doc/需求分析/需求分析第一版.md @@ -0,0 +1,76 @@ +## 用户: +小学、初中和高中学生。 +## 功能: +1所有功能通过图形化界面操作,要求是桌面应用(编程语言和技术不限); +2用户*注册*功能。用户提供邮箱,点击注册将收到一个注册码,用户可使用该注册码完成注册; +3用户完成注册后,界面提示设置密码,用户输入两次密码匹配后设置密码成功。密码6-10位,必须含大小写字母和数字。用户在登录状态下可*修改密码*,输入正确的原密码,再输入两次相同的新密码后修改密码成功; +4密码设置成功后,跳转到*选择界面*,界面显示小学、初中和高中三个选项,用户点击其中之一后,提示用户输入需要生成的题目数量; +5用户输入题目数量后,生成一张试卷(同一张卷子不能有相同题目,题目全部为选择题),*界面*显示第一题的题干和四个选项,用户选择四个选项中的一个后提交,界面显示第二题,...,直至最后一题; +6最后一题提交后,界面显示分数,分数根据答对的百分比计算; +7用户在*分数界面*可选择退出或继续做题; +8小初高数学题目要求见个人项目。 +9不可以使用数据库存储数据。 +## 模块简要 +### 后端 +- 用户模块 +- 题目生成模块 +- 文件交互模块 +- 后端入口 +### 前端 +- 启动页 +- 注册页 +- 登录页 +- 修改密码页 +- 选择年级页 +- 出题页 +- 分数页 + +## 模块功能详细 +### 后端 +#### 用户模块 +##### 功能 +- 用户注册(生成注册码+暂时保存用户输入信息) +- 验证注册码 +- 设置/修改密码(6-16位,含大小写+数字) +- 登录验证 +- 密码修改(要先验证原密码正确) +##### 数据储存 +需要储存注册用户的邮箱,用户名,密码 +用json储存 + +#### 题目生成模块 +- 根据年级生成选择题(包含题干和4个选项,必有1个正确答案) +- 生成卷子(保证每个用户生成过的题目不重复,可用set) + +#### 文件交互模块 +- 读写用户信息(json) +- 保存/读取临时的组成码 + +#### 后端入口 +提供统一接口供前端调用 + +### 前端 +#### 启动页 +- 显示软件名称 +- 点击跳转**登录页** + +#### 登录页 +- 输入邮箱+密码,点击**登录**跳转到**出题页** +- 点击"注册"跳转**注册页** + +#### 注册页 +- 输入邮箱,点击"获取注册码"->发送注册码,显示"已发送注册码到您的邮箱" +- 输入注册码 + 密码 + 确认密码 ->完成注册,跳转到**选择年级页** + +#### 选择年级页 +- 小学、初中、高中三个选项,点击跳转到**出题页** + +#### 出题页 +- 输入数字(10~30)点击出题显示"题号+题干+4个选项",点击提交自动跳转下一题 +- 最后一题答完,自动跳转**分数页** +- 点击切换年级进入**选择年级页** + +#### 分数页 +- 显示:“您答对了 X / Y 题,得分:Z%” +- 点击退出跳转到**登录页** +- 点击继续做题跳转到**出题页** -- 2.34.1 From 7bc11c7c66a57e1e683fd336035c6c35dbd17eb6 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Thu, 2 Oct 2025 20:16:58 +0800 Subject: [PATCH 02/28] add design doc v1 --- doc/设计文档/设计文档第一版.md | 101 +++++++++++++++ doc/需求分析/需求分析第一版.md | 146 +++++++++++----------- 2 files changed, 173 insertions(+), 74 deletions(-) create mode 100644 doc/设计文档/设计文档第一版.md diff --git a/doc/设计文档/设计文档第一版.md b/doc/设计文档/设计文档第一版.md new file mode 100644 index 0000000..b2b7efa --- /dev/null +++ b/doc/设计文档/设计文档第一版.md @@ -0,0 +1,101 @@ +# 数学题库生成系统详细设计文档 + +## 项目概述 + +### 项目目标 +为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数 + +### 技术栈 + +| 类型 | 技术 | 说明 | +| -------- | -------------------------- | ---------------------------------------- | +| 开发语言 | Java 11+ | 保证跨平台兼容性,支持 JavaFX | +| GUI框架 | JavaFX 17+ | 实现桌面图形化界面,提供组件化开发能力 | +| 依赖管理 | Maven | 管理 JavaFX、JSON 解析等依赖 | +| JSON解析 | Gson 2.10+ | 序列化 / 反序列化用户数据、题目数据 | +| 邮件发送 | JavaMail API + 第三方 SMTP | 发送注册码(如 QQ 邮箱 / 网易邮箱 SMTP) | +| 数据加密 | BCrypt 0.9.0+ | 加密存储用户密码,避免明文泄露 | +| 构建打包 | Maven Shade Plugin | 打包成可执行 JAR,支持直接运行 | + +### 总体设计 + +#### 项目目录结构 + +MathQuizApp/ +├── src/ +│ └── main/ +│ └── java/ +│ └── com/ +│ └── mathquiz/ +│ ├── Main.java # 程序入口 +│ │ +│ ├── model/ # 数据模型 +│ │ ├── User.java +│ │ ├── Grade.java +│ │ └── ChoiceQuestion.java +│ │ +│ ├── service/ # 后端逻辑(无GUI) +│ │ ├── UserService.java +│ │ ├── QuestionGenerator.java +│ │ ├── FileIOService.java +│ │ └── QuizService.java +│ │ +│ ├── ui/ # 前端 GUI +│ │ ├── MainWindow.java +│ │ ├── RegisterPanel.java +│ │ ├── LoginPanel.java +| | ├── PasswordModifyPanel.java +│ │ ├── GradeSelectPanel.java +│ │ ├── QuizPanel.java +│ │ └── ResultPanel.java +│ │ +│ └── util/ # 工具类 +│ ├── PasswordValidator.java +| ├── EmailUtil.java +│ ├── RandomUtils.java +│ └── FileUtils.java +│ +├── data/ # 运行时生成(不提交) +│ ├── users/ # 用户信息 JSON +│ └── temp_codes/ # 临时注册码文件 +│ +├── pom.xml # Maven 依赖(含 JavaFX) +└── README.md + +## 详细模块设计 + +### 模型层设计 + +#### User类 +- String name // 用户ID +- String email // 用户邮箱 +- String encryptedPwd // 加密后的用户密码 +- Grade grade //学段 + +#### ChoiceQuestion +String questionId; // 题目唯一ID(UUID生成) +Grade grade; // 所属学段 +String questionContent; // 题干 +List options; // 选项列表(固定4个,顺序随机) +String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D") + +#### Grade 枚举 +PRIMARY("小学", 1) +JUNIOR("初中", 2) +SENIOR("高中", 3) + +### 工具层设计 + +#### PasswordValidator + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ | +| isValid | String rawPwd | boolean | 校验规则:1. 长度 6-10 位;2. 包含至少 1 个大写字母;3. 包含至少 1 个小写字母;4. 包含至少 1 个数字 | +| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) | +| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配(BCrypt 自带校验逻辑) | + +#### EmailUtil + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ | +| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器(主机、端口、SSL);2. 设置发件人账号 / 授权码;3. 构建邮件内容;4. 发送并返回结果 | \ No newline at end of file diff --git a/doc/需求分析/需求分析第一版.md b/doc/需求分析/需求分析第一版.md index 93a2862..8876858 100644 --- a/doc/需求分析/需求分析第一版.md +++ b/doc/需求分析/需求分析第一版.md @@ -1,76 +1,74 @@ +# 数学题库生成系统需求分析文档 +## 项目概述 +为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数 + ## 用户: 小学、初中和高中学生。 -## 功能: -1所有功能通过图形化界面操作,要求是桌面应用(编程语言和技术不限); -2用户*注册*功能。用户提供邮箱,点击注册将收到一个注册码,用户可使用该注册码完成注册; -3用户完成注册后,界面提示设置密码,用户输入两次密码匹配后设置密码成功。密码6-10位,必须含大小写字母和数字。用户在登录状态下可*修改密码*,输入正确的原密码,再输入两次相同的新密码后修改密码成功; -4密码设置成功后,跳转到*选择界面*,界面显示小学、初中和高中三个选项,用户点击其中之一后,提示用户输入需要生成的题目数量; -5用户输入题目数量后,生成一张试卷(同一张卷子不能有相同题目,题目全部为选择题),*界面*显示第一题的题干和四个选项,用户选择四个选项中的一个后提交,界面显示第二题,...,直至最后一题; -6最后一题提交后,界面显示分数,分数根据答对的百分比计算; -7用户在*分数界面*可选择退出或继续做题; -8小初高数学题目要求见个人项目。 -9不可以使用数据库存储数据。 -## 模块简要 -### 后端 -- 用户模块 -- 题目生成模块 -- 文件交互模块 -- 后端入口 -### 前端 -- 启动页 -- 注册页 -- 登录页 -- 修改密码页 -- 选择年级页 -- 出题页 -- 分数页 - -## 模块功能详细 -### 后端 -#### 用户模块 -##### 功能 -- 用户注册(生成注册码+暂时保存用户输入信息) -- 验证注册码 -- 设置/修改密码(6-16位,含大小写+数字) -- 登录验证 -- 密码修改(要先验证原密码正确) -##### 数据储存 -需要储存注册用户的邮箱,用户名,密码 -用json储存 - -#### 题目生成模块 -- 根据年级生成选择题(包含题干和4个选项,必有1个正确答案) -- 生成卷子(保证每个用户生成过的题目不重复,可用set) - -#### 文件交互模块 -- 读写用户信息(json) -- 保存/读取临时的组成码 - -#### 后端入口 -提供统一接口供前端调用 - -### 前端 -#### 启动页 -- 显示软件名称 -- 点击跳转**登录页** - -#### 登录页 -- 输入邮箱+密码,点击**登录**跳转到**出题页** -- 点击"注册"跳转**注册页** - -#### 注册页 -- 输入邮箱,点击"获取注册码"->发送注册码,显示"已发送注册码到您的邮箱" -- 输入注册码 + 密码 + 确认密码 ->完成注册,跳转到**选择年级页** - -#### 选择年级页 -- 小学、初中、高中三个选项,点击跳转到**出题页** - -#### 出题页 -- 输入数字(10~30)点击出题显示"题号+题干+4个选项",点击提交自动跳转下一题 -- 最后一题答完,自动跳转**分数页** -- 点击切换年级进入**选择年级页** - -#### 分数页 -- 显示:“您答对了 X / Y 题,得分:Z%” -- 点击退出跳转到**登录页** -- 点击继续做题跳转到**出题页** +要求界面交互简洁直观、易于上手 + +## 功能需求: + +### 用户注册与登录 +#### 注册流程 +- 用户输入邮箱地址,点击"获取注册码"后,软件发送邮箱注册码 +- 用户输入邮箱里的注册码完成验证 +- 验证通过后,设置密码(6-10位,必须包含大小写字母和数字),要重新确认密码 +- 密码设置完成即完成注册,自动进入学段选择界面 + +#### 登录功能 +- 已注册用户通过"邮箱 + 密码"或"用户名 + 密码"登录 +- 登录成功后进入学段选择界面 + +#### 密码管理 +- 登录状态下,用户可以发起修改密码操作 +- 修改时需先输入旧密码,然后输入两次新密码(6-10位,必须包含大小写字母和数字) + +### 学段选择 +- 注册成功后,跳转到界面选择界面("小学","初中","高中"),点击后可修改用户的学段 +- 登录后,点击"切换学段"可跳转界面选择界面 + +### 试卷生成 +- 用户输入题目数量(10-30) +- 系统根据用户选择的学段,生成对应难度的数字选择题试卷 +- 同一用户不能生成重复题目 +- 每个题目包含题干和4个选择(包含1个正确题目) + +### 答题流程 +- 试卷生成后,界面依次显示题目 +- 每题显示题干和4个选项,用户选择一个选项后点击"下一题",自动跳转至下一题 +- 直到完成最后一题提高后,自动进入分数展示界面 + +### 分数计算与展示 +- 分数计算:Z = 答对/总题数 x 100% +- 显示内容:"您答对了X/Y题,得分:Z%" +- 分数界面提供两个操作选项:"继续答题"返回答题界面,"退出"返回登录界面 + +## 非功能需求 + +### 界面要求 +- 除了输入题目数量,其他所有功能均通过图形化界面操作,界面简洁、直观,符合中小学生使用习惯 +- 操作流程清晰,每个步骤有明确的引导提示("请输入注册码","密码格式错误","题目数量不合法"等) + +### 运行环境 +- 桌面应用程序,支持Windows + +### 数据储存约束 +- 不使用数据库,用户信息(邮箱、用户名、密码、学段)、注册码等数据通过json文件形式储存 +- 需保证用户数据的安全性(如密码加密储存) + +### 题目内容约束 +- 小学:只能有+,-,*./和() +- 初中:题目中至少有一个平方或开根号的运算符 +- 高中:题目中至少有一个sin,cos或tan的运算符 + +## 业务流程说明 + +### 注册登录流程 +新用户:输入邮箱->获取注册码->验证注册码->设置密码->完成注册->选择学段->登录 +旧用户:输入邮箱(或用户码)和密码->登录 + +### 做题流程 +输入题目数量->生成试卷->依次答题->完成最后一题->显示分数->选择"继续做题"或"退出" + +### 密码修改流程 +登录状态->发起修改密码->输入原密码->输入两次新密码->通过密码验证->密码更新 -- 2.34.1 From fa964125bca0cc1fb2e38d88e76c0d958194bccc Mon Sep 17 00:00:00 2001 From: bx <2936213174@qq.com> Date: Fri, 3 Oct 2025 14:16:15 +0800 Subject: [PATCH 03/28] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 38 ++++++ .idea/.gitignore | 8 ++ .idea/encodings.xml | 7 ++ .idea/misc.xml | 14 +++ .idea/vcs.xml | 6 + pom.xml | 84 +++++++++++++ src/main/java/com/bx/MathQuiz/Main.java | 4 + .../java/com/bx/model/ChoiceQuestion.java | 117 ++++++++++++++++++ src/main/java/com/bx/model/Grade.java | 49 ++++++++ src/main/java/com/bx/model/User.java | 95 ++++++++++++++ src/main/java/com/bx/util/RandomUtils.java | 83 +++++++++++++ 11 files changed, 505 insertions(+) create mode 100644 .gitignore create mode 100644 .idea/.gitignore create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/vcs.xml create mode 100644 pom.xml create mode 100644 src/main/java/com/bx/MathQuiz/Main.java create mode 100644 src/main/java/com/bx/model/ChoiceQuestion.java create mode 100644 src/main/java/com/bx/model/Grade.java create mode 100644 src/main/java/com/bx/model/User.java create mode 100644 src/main/java/com/bx/util/RandomUtils.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ff6309 --- /dev/null +++ b/.gitignore @@ -0,0 +1,38 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### IntelliJ IDEA ### +.idea/modules.xml +.idea/jarRepositories.xml +.idea/compiler.xml +.idea/libraries/ +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml 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/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/pom.xml b/pom.xml new file mode 100644 index 0000000..dc0b01f --- /dev/null +++ b/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + com.mathquiz + MathQuizApp + 1.0.0 + jar + + Math Quiz Application + 小初高数学学习软件 - Swing版本 + + + UTF-8 + 21 + 21 + + + + + + com.google.code.gson + gson + 2.10.1 + + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 17 + 17 + UTF-8 + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.3.0 + + + + com.mathquiz.Main + true + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.5.1 + + + package + + shade + + + + + com.mathquiz.Main + + + MathQuizApp + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/bx/MathQuiz/Main.java b/src/main/java/com/bx/MathQuiz/Main.java new file mode 100644 index 0000000..17c5761 --- /dev/null +++ b/src/main/java/com/bx/MathQuiz/Main.java @@ -0,0 +1,4 @@ +package com.bx.MathQuiz; + +public class Main { +} diff --git a/src/main/java/com/bx/model/ChoiceQuestion.java b/src/main/java/com/bx/model/ChoiceQuestion.java new file mode 100644 index 0000000..bc52c92 --- /dev/null +++ b/src/main/java/com/bx/model/ChoiceQuestion.java @@ -0,0 +1,117 @@ +package com.bx.model; + +import java.util.List; + +/** + * 选择题数据模型 + * 包含题目内容、正确答案、四个选项 + */ +public class ChoiceQuestion { + private String questionText; // 题目文本,如 "3 + 5" + private double correctAnswer; // 正确答案 + private List options; // 四个选项(包含正确答案) + private Grade grade; // 所属学段 + + private int correctOptionIndex; // 正确答案在选项中的索引(0-3) + + /** + * 无参构造函数 + */ + public ChoiceQuestion() { + } + + /** + * 完整构造函数 + */ + public ChoiceQuestion(String questionText, double correctAnswer, List options, Grade grade) { + this.questionText = questionText; + this.correctAnswer = correctAnswer; + this.options = options; + this.grade = grade; + + // 自动找到正确答案的索引 + this.correctOptionIndex = findCorrectOptionIndex(); + } + + /** + * 查找正确答案在选项中的位置 + */ + private int findCorrectOptionIndex() { + for (int i = 0; i < options.size(); i++) { + if (Math.abs(options.get(i) - correctAnswer) < 0.01) { + return i; + } + } + return -1; // 理论上不会发生 + } + + /** + * 检查用户选择的选项是否正确 + * @param optionIndex 用户选择的选项索引(0-3) + * @return true表示正确 + */ + public boolean checkAnswer(int optionIndex) { + return optionIndex == correctOptionIndex; + } + + /** + * 检查用户输入的答案值是否正确 + * @param answer 用户输入的答案值 + * @return true表示正确 + */ + public boolean checkAnswerValue(double answer) { + return Math.abs(answer - correctAnswer) < 0.01; + } + + // ========== Getter和Setter方法 ========== + + public String getQuestionText() { + return questionText; + } + + public void setQuestionText(String questionText) { + this.questionText = questionText; + } + + public double getCorrectAnswer() { + return correctAnswer; + } + + public void setCorrectAnswer(double correctAnswer) { + this.correctAnswer = correctAnswer; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + this.correctOptionIndex = findCorrectOptionIndex(); + } + + public Grade getGrade() { + return grade; + } + + public void setGrade(Grade grade) { + this.grade = grade; + } + + public int getCorrectOptionIndex() { + return correctOptionIndex; + } + + public void setCorrectOptionIndex(int correctOptionIndex) { + this.correctOptionIndex = correctOptionIndex; + } + + @Override + public String toString() { + return "ChoiceQuestion{" + + "questionText='" + questionText + '\'' + + ", correctAnswer=" + correctAnswer + + ", grade=" + grade + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/bx/model/Grade.java b/src/main/java/com/bx/model/Grade.java new file mode 100644 index 0000000..8f878b2 --- /dev/null +++ b/src/main/java/com/bx/model/Grade.java @@ -0,0 +1,49 @@ +package com.bx.model; + +/** + * 学段枚举类 + * 定义三个学段:小学、初中、高中 + */ +public enum Grade { + ELEMENTARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + Grade(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + //根据显示名称查找对应的枚举值 + public static Grade fromDisplayName(String displayName) { + for (Grade grade : values()) { + if (grade.displayName.equals(displayName)) { + return grade; + } + } + throw new IllegalArgumentException("未知学段: " + displayName); + } + + /** + * 获取所有学段的显示名称数组 + * @return ["小学", "初中", "高中"] + */ + public static String[] getDisplayNames() { + Grade[] grades = values(); + String[] names = new String[grades.length]; + for (int i = 0; i < grades.length; i++) { + names[i] = grades[i].displayName; + } + return names; + } + + @Override + public String toString() { + return displayName; + } +} \ No newline at end of file diff --git a/src/main/java/com/bx/model/User.java b/src/main/java/com/bx/model/User.java new file mode 100644 index 0000000..1557f2f --- /dev/null +++ b/src/main/java/com/bx/model/User.java @@ -0,0 +1,95 @@ +package com.bx.model; + +public class User { + private String username; // 用户名,格式:小学-张三 + private String password; // 密码(加密后) + private Grade grade; // 学段 + private String email; // 邮箱 + private String registrationCode; // 注册码(明文保存,用于用户查看) + private long createdTime; // 创建时间戳 + private long lastLoginTime; // 最后登录时间戳 + + /** + * 无参构造函数(Gson反序列化需要) + */ + public User() { + } + + /** + * 完整构造函数 + */ + public User(String username, String password, Grade grade, String email) { + this.username = username; + this.password = password; + this.grade = grade; + this.email = email; + this.createdTime = System.currentTimeMillis(); + } + + // ========== Getter和Setter方法 ========== + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Grade getGrade() { + return grade; + } + + public void setGrade(Grade grade) { + this.grade = grade; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getRegistrationCode() { + return registrationCode; + } + + public void setRegistrationCode(String registrationCode) { + this.registrationCode = registrationCode; + } + + public long getCreatedTime() { + return createdTime; + } + + public void setCreatedTime(long createdTime) { + this.createdTime = createdTime; + } + + public long getLastLoginTime() { + return lastLoginTime; + } + + public void setLastLoginTime(long lastLoginTime) { + this.lastLoginTime = lastLoginTime; + } + + @Override + public String toString() { + return "User{" + + "username='" + username + '\'' + + ", grade=" + grade + + ", email='" + email + '\'' + + '}'; + } +} diff --git a/src/main/java/com/bx/util/RandomUtils.java b/src/main/java/com/bx/util/RandomUtils.java new file mode 100644 index 0000000..8735980 --- /dev/null +++ b/src/main/java/com/bx/util/RandomUtils.java @@ -0,0 +1,83 @@ +package com.bx.util; + +import java.util.Collections; +import java.util.List; +import java.util.Random; + +//各种随机数生成 +public class RandomUtils { + private static final Random random = new Random(); + + + //生成[min, max]范围内的随机整数(包含边界) + public static int nextInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min不能大于max"); + } + return min + random.nextInt(max - min + 1); + } + + + //从数组中随机选择一个元素(模板类) + public static T randomChoice(T[] array) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException("数组不能为空"); + } + return array[random.nextInt(array.length)]; + } + + /** + * 从列表中随机选择一个元素 + * @param list 列表 + * @return 随机选中的元素 + */ + public static T randomChoice(List list) { + if (list == null || list.isEmpty()) { + throw new IllegalArgumentException("列表不能为空"); + } + return list.get(random.nextInt(list.size())); + } + + /** + * 打乱列表顺序 + * @param list 要打乱的列表 + */ + public static void shuffle(List list) { + Collections.shuffle(list, random); + } + + /** + * 生成指定范围内的随机双精度浮点数 + * @param min 最小值 + * @param max 最大值 + * @return 随机浮点数 + */ + public static double nextDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException("min不能大于max"); + } + return min + (max - min) * random.nextDouble(); + } + + /** + * 生成随机布尔值 + * @return true或false + */ + public static boolean nextBoolean() { + return random.nextBoolean(); + } + + /** + * 按概率返回true + * @param probability 概率值(0.0 - 1.0) + * @return true或false + * + * 示例:probability(0.7) 有70%概率返回true + */ + public static boolean probability(double probability) { + if (probability < 0.0 || probability > 1.0) { + throw new IllegalArgumentException("概率必须在0.0-1.0之间"); + } + return random.nextDouble() < probability; + } +} \ No newline at end of file -- 2.34.1 From 06a4612ad6066a0a457209013cf4b0d2a3f8661b Mon Sep 17 00:00:00 2001 From: bx <2936213174@qq.com> Date: Fri, 3 Oct 2025 14:54:13 +0800 Subject: [PATCH 04/28] =?UTF-8?q?=E7=AC=AC=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/MathQuiz/Main.java | 4 + src/main/java/com/bx/MathQuiz/Main.java | 4 - .../com/{bx => }/model/ChoiceQuestion.java | 2 +- src/main/java/com/{bx => }/model/Grade.java | 2 +- src/main/java/com/{bx => }/model/User.java | 2 +- src/main/java/com/util/EmailUtil.java | 109 +++++++++++++++++ src/main/java/com/util/FileUtils.java | 111 ++++++++++++++++++ src/main/java/com/util/PasswordValidator.java | 102 ++++++++++++++++ .../java/com/{bx => }/util/RandomUtils.java | 32 ++--- src/test/java/TestMain.java | 2 + 10 files changed, 340 insertions(+), 30 deletions(-) create mode 100644 src/main/java/com/MathQuiz/Main.java delete mode 100644 src/main/java/com/bx/MathQuiz/Main.java rename src/main/java/com/{bx => }/model/ChoiceQuestion.java (99%) rename src/main/java/com/{bx => }/model/Grade.java (98%) rename src/main/java/com/{bx => }/model/User.java (99%) create mode 100644 src/main/java/com/util/EmailUtil.java create mode 100644 src/main/java/com/util/FileUtils.java create mode 100644 src/main/java/com/util/PasswordValidator.java rename src/main/java/com/{bx => }/util/RandomUtils.java (75%) create mode 100644 src/test/java/TestMain.java diff --git a/src/main/java/com/MathQuiz/Main.java b/src/main/java/com/MathQuiz/Main.java new file mode 100644 index 0000000..49ac2f4 --- /dev/null +++ b/src/main/java/com/MathQuiz/Main.java @@ -0,0 +1,4 @@ +package com.MathQuiz; + +public class Main { +} diff --git a/src/main/java/com/bx/MathQuiz/Main.java b/src/main/java/com/bx/MathQuiz/Main.java deleted file mode 100644 index 17c5761..0000000 --- a/src/main/java/com/bx/MathQuiz/Main.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.bx.MathQuiz; - -public class Main { -} diff --git a/src/main/java/com/bx/model/ChoiceQuestion.java b/src/main/java/com/model/ChoiceQuestion.java similarity index 99% rename from src/main/java/com/bx/model/ChoiceQuestion.java rename to src/main/java/com/model/ChoiceQuestion.java index bc52c92..b71c59a 100644 --- a/src/main/java/com/bx/model/ChoiceQuestion.java +++ b/src/main/java/com/model/ChoiceQuestion.java @@ -1,4 +1,4 @@ -package com.bx.model; +package com.model; import java.util.List; diff --git a/src/main/java/com/bx/model/Grade.java b/src/main/java/com/model/Grade.java similarity index 98% rename from src/main/java/com/bx/model/Grade.java rename to src/main/java/com/model/Grade.java index 8f878b2..7de066d 100644 --- a/src/main/java/com/bx/model/Grade.java +++ b/src/main/java/com/model/Grade.java @@ -1,4 +1,4 @@ -package com.bx.model; +package com.model; /** * 学段枚举类 diff --git a/src/main/java/com/bx/model/User.java b/src/main/java/com/model/User.java similarity index 99% rename from src/main/java/com/bx/model/User.java rename to src/main/java/com/model/User.java index 1557f2f..eadfd17 100644 --- a/src/main/java/com/bx/model/User.java +++ b/src/main/java/com/model/User.java @@ -1,4 +1,4 @@ -package com.bx.model; +package com.model; public class User { private String username; // 用户名,格式:小学-张三 diff --git a/src/main/java/com/util/EmailUtil.java b/src/main/java/com/util/EmailUtil.java new file mode 100644 index 0000000..ff915ac --- /dev/null +++ b/src/main/java/com/util/EmailUtil.java @@ -0,0 +1,109 @@ +package com.util; + +/** + * 邮件工具类 + * 用于发送注册码邮件(可选功能) + * 注意:当前项目需求中,注册码是直接显示在界面上的, + * 不需要发送邮件,所以这个类暂时保留为空实现。 + * 如果将来需要发送邮件,可以使用JavaMail库实现。 + */ +public class EmailUtil { + + /** + * 发送注册码邮件(预留接口) + * @param toEmail 收件人邮箱 + * @param registrationCode 注册码 + * @return true表示发送成功 + */ + public static boolean sendRegistrationCode(String toEmail, String registrationCode) { + // TODO: 暂不实现邮件发送功能 + // 原因:项目需求是直接在界面显示注册码,不需要发邮件 + + System.out.println("【模拟】发送注册码邮件"); + System.out.println("收件人: " + toEmail); + System.out.println("注册码: " + registrationCode); + + return true; + } + + /** + * 发送密码重置邮件(预留接口) + * @param toEmail 收件人邮箱 + * @param newPassword 新密码 + * @return true表示发送成功 + */ + public static boolean sendPasswordReset(String toEmail, String newPassword) { + // TODO: 将来如果需要"找回密码"功能,可以在这里实现 + + System.out.println("【模拟】发送密码重置邮件"); + System.out.println("收件人: " + toEmail); + System.out.println("新密码: " + newPassword); + + return true; + } + + /** + * 验证邮箱格式 + * @param email 邮箱地址 + * @return true表示格式正确 + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // 简单的邮箱格式验证 + String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + return email.matches(emailRegex); + } +} + +/* + * 如果将来需要真正实现邮件发送,可以参考以下代码: + * + * 1. 在pom.xml添加依赖: + * + * javax.mail + * javax.mail-api + * 1.6.2 + * + * + * com.sun.mail + * javax.mail + * 1.6.2 + * + * + * 2. 实现代码示例: + * + * import javax.mail.*; + * import javax.mail.internet.*; + * import java.util.Properties; + * + * public static boolean sendEmail(String toEmail, String subject, String content) { + * try { + * Properties props = new Properties(); + * props.put("mail.smtp.auth", "true"); + * props.put("mail.smtp.starttls.enable", "true"); + * props.put("mail.smtp.host", "smtp.qq.com"); + * props.put("mail.smtp.port", "587"); + * + * Session session = Session.getInstance(props, new Authenticator() { + * protected PasswordAuthentication getPasswordAuthentication() { + * return new PasswordAuthentication("your-email@qq.com", "your-password"); + * } + * }); + * + * Message message = new MimeMessage(session); + * message.setFrom(new InternetAddress("your-email@qq.com")); + * message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); + * message.setSubject(subject); + * message.setText(content); + * + * Transport.send(message); + * return true; + * } catch (Exception e) { + * e.printStackTrace(); + * return false; + * } + * } + */ \ No newline at end of file diff --git a/src/main/java/com/util/FileUtils.java b/src/main/java/com/util/FileUtils.java new file mode 100644 index 0000000..d4daf9e --- /dev/null +++ b/src/main/java/com/util/FileUtils.java @@ -0,0 +1,111 @@ +package com.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; + +/** + * 文件操作工具类 + * 提供文件读写、目录创建等常用操作 + */ +public class FileUtils { + + /** + * 读取文件内容为字符串 + * @param filePath 文件路径 + * @return 文件内容 + * @throws IOException 读取失败时抛出 + */ + public static String readFileToString(String filePath) throws IOException { + return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8); + } + + /** + * 写入字符串到文件 + * @param filePath 文件路径 + * @param content 要写入的内容 + * @throws IOException 写入失败时抛出 + */ + public static void writeStringToFile(String filePath, String content) throws IOException { + Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8); + } + + /** + * 创建目录(如果不存在) + * @param dirPath 目录路径 + * @throws IOException 创建失败时抛出 + */ + public static void createDirectoryIfNotExists(String dirPath) throws IOException { + Path path = Paths.get(dirPath); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + /** + * 检查文件是否存在 + * @param filePath 文件路径 + * @return true表示存在 + */ + public static boolean exists(String filePath) { + return Files.exists(Paths.get(filePath)); + } + + /** + * 删除文件 + * @param filePath 文件路径 + * @return true表示删除成功 + */ + public static boolean deleteFile(String filePath) { + try { + return Files.deleteIfExists(Paths.get(filePath)); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 获取目录下所有文件 + * @param dirPath 目录路径 + * @return 文件数组 + */ + public static File[] listFiles(String dirPath) { + File dir = new File(dirPath); + if (dir.exists() && dir.isDirectory()) { + return dir.listFiles(); + } + return new File[0]; + } + + /** + * 追加内容到文件末尾 + * @param filePath 文件路径 + * @param content 要追加的内容 + * @throws IOException 追加失败时抛出 + */ + public static void appendToFile(String filePath, String content) throws IOException { + Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + + /** + * 复制文件 + * @param sourcePath 源文件路径 + * @param targetPath 目标文件路径 + * @throws IOException 复制失败时抛出 + */ + public static void copyFile(String sourcePath, String targetPath) throws IOException { + Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING); + } + + /** + * 获取文件大小(字节) + * @param filePath 文件路径 + * @return 文件大小 + * @throws IOException 获取失败时抛出 + */ + public static long getFileSize(String filePath) throws IOException { + return Files.size(Paths.get(filePath)); + } +} \ No newline at end of file diff --git a/src/main/java/com/util/PasswordValidator.java b/src/main/java/com/util/PasswordValidator.java new file mode 100644 index 0000000..40a26b5 --- /dev/null +++ b/src/main/java/com/util/PasswordValidator.java @@ -0,0 +1,102 @@ +package com.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 密码验证和加密工具类 + * 提供密码格式验证、加密、匹配等功能 + */ +public class PasswordValidator { + + /** + * 验证密码格式:6-10位,包含字母和数字 + * @param password 待验证的密码 + * @return true表示格式正确 + */ + public static boolean isValid(String password) { + if (password == null || password.length() < 6 || password.length() > 10) { + return false; + } + + boolean hasLetter = password.matches("^(?=.*[a-z])(?=.*[A-Z]).*$"); + boolean hasDigit = password.matches(".*\\d.*"); + + return hasLetter && hasDigit; + } + + /** + * 使用SHA-256加密密码 + * @param password 明文密码 + * @return 加密后的密码(16进制字符串) + */ + public static String encrypt(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("SHA-256算法不可用", e); + } + } + + /** + * 验证密码是否匹配 + * @param plainPassword 明文密码 + * @param encryptedPassword 加密后的密码 + * @return true表示匹配 + */ + public static boolean matches(String plainPassword, String encryptedPassword) { + return encrypt(plainPassword).equals(encryptedPassword); + } + + /** + * 生成6-10位随机注册码(包含字母和数字) + * @return 注册码 + */ + public static String generateRegistrationCode() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int length = 6 + (int) (Math.random() * 5); // 6-10位 + + StringBuilder code = new StringBuilder(); + + // 确保至少有一个字母 + code.append(chars.charAt((int) (Math.random() * 52))); + // 确保至少有一个数字 + code.append(chars.charAt(52 + (int) (Math.random() * 10))); + + // 填充剩余字符 + for (int i = 2; i < length; i++) { + code.append(chars.charAt((int) (Math.random() * chars.length()))); + } + + // 打乱字符顺序 + return shuffleString(code.toString()); + } + + /** + * 打乱字符串 + * @param str 原字符串 + * @return 打乱后的字符串 + */ + private static String shuffleString(String str) { + char[] chars = str.toCharArray(); + for (int i = chars.length - 1; i > 0; i--) { + int j = (int) (Math.random() * (i + 1)); + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + } + return new String(chars); + } +} \ No newline at end of file diff --git a/src/main/java/com/bx/util/RandomUtils.java b/src/main/java/com/util/RandomUtils.java similarity index 75% rename from src/main/java/com/bx/util/RandomUtils.java rename to src/main/java/com/util/RandomUtils.java index 8735980..f06a7ed 100644 --- a/src/main/java/com/bx/util/RandomUtils.java +++ b/src/main/java/com/util/RandomUtils.java @@ -1,4 +1,4 @@ -package com.bx.util; +package com.util; import java.util.Collections; import java.util.List; @@ -26,11 +26,8 @@ public class RandomUtils { return array[random.nextInt(array.length)]; } - /** - * 从列表中随机选择一个元素 - * @param list 列表 - * @return 随机选中的元素 - */ + + //从列表中随机选择一个元素 public static T randomChoice(List list) { if (list == null || list.isEmpty()) { throw new IllegalArgumentException("列表不能为空"); @@ -46,12 +43,8 @@ public class RandomUtils { Collections.shuffle(list, random); } - /** - * 生成指定范围内的随机双精度浮点数 - * @param min 最小值 - * @param max 最大值 - * @return 随机浮点数 - */ + + //生成指定范围内的随机双精度浮点数 public static double nextDouble(double min, double max) { if (min > max) { throw new IllegalArgumentException("min不能大于max"); @@ -59,21 +52,14 @@ public class RandomUtils { return min + (max - min) * random.nextDouble(); } - /** - * 生成随机布尔值 - * @return true或false - */ + + //生成随机布尔值 public static boolean nextBoolean() { return random.nextBoolean(); } - /** - * 按概率返回true - * @param probability 概率值(0.0 - 1.0) - * @return true或false - * - * 示例:probability(0.7) 有70%概率返回true - */ + //按概率返回true(题目生成概率) + //示例:probability(0.7) 有70%概率返回true public static boolean probability(double probability) { if (probability < 0.0 || probability > 1.0) { throw new IllegalArgumentException("概率必须在0.0-1.0之间"); diff --git a/src/test/java/TestMain.java b/src/test/java/TestMain.java new file mode 100644 index 0000000..d4c7da5 --- /dev/null +++ b/src/test/java/TestMain.java @@ -0,0 +1,2 @@ +public class TestMain { +} -- 2.34.1 From ab3914fc86efd70b54a5715440aa6e7fbe6d29e8 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 14:57:59 +0800 Subject: [PATCH 05/28] add design doc v2 --- .idea/.gitignore | 8 + .idea/PAIR.iml | 9 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + doc/设计文档/设计文档第二版.md | 189 ++++++++++++++++++ src/main/java/com/mathquiz/Main.java | 0 .../com/mathquiz/model/ChoiceQuestion.java | 0 src/main/java/com/mathquiz/model/Grade.java | 0 src/main/java/com/mathquiz/model/User.java | 0 10 files changed, 226 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/PAIR.iml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 doc/设计文档/设计文档第二版.md create mode 100644 src/main/java/com/mathquiz/Main.java create mode 100644 src/main/java/com/mathquiz/model/ChoiceQuestion.java create mode 100644 src/main/java/com/mathquiz/model/Grade.java create mode 100644 src/main/java/com/mathquiz/model/User.java diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/PAIR.iml b/.idea/PAIR.iml new file mode 100644 index 0000000..d6ebd48 --- /dev/null +++ b/.idea/PAIR.iml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..6f29fee --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..20bffb7 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/doc/设计文档/设计文档第二版.md b/doc/设计文档/设计文档第二版.md new file mode 100644 index 0000000..a6ada6e --- /dev/null +++ b/doc/设计文档/设计文档第二版.md @@ -0,0 +1,189 @@ +# 数学题库生成系统详细设计文档 + +## 项目概述 + +### 项目目标 +为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数 + +### 技术栈 + +| 类型 | 技术 | 说明 | +| -------- | -------------------------- | ---------------------------------------- | +| 开发语言 | Java 21+ | 保证跨平台兼容性,支持 JavaFX | +| GUI框架 | JavaFX 17+ | 实现桌面图形化界面,提供组件化开发能力 | +| 依赖管理 | Maven | 管理 JavaFX、JSON 解析等依赖 | +| JSON解析 | Gson 2.10+ | 序列化 / 反序列化用户数据、题目数据 | +| 邮件发送 | JavaMail API + 第三方 SMTP | 发送注册码(如 QQ 邮箱 / 网易邮箱 SMTP) | +| 数据加密 | BCrypt 0.9.0+ | 加密存储用户密码,避免明文泄露 | +| 构建打包 | Maven Shade Plugin | 打包成可执行 JAR,支持直接运行 | + +### 总体设计 + +#### 项目目录结构 + +MathQuizApp/ +├── src/ +│ └── main/ +│ └── java/ +│ └── com/ +│ └── mathquiz/ +│ ├── Main.java # 程序入口 +│ │ +│ ├── model/ # 数据模型 +│ │ ├── User.java +│ │ ├── Grade.java +│ │ └── ChoiceQuestion.java +│ │ +│ ├── service/ # 后端逻辑(无GUI) +│ │ ├── UserService.java +│ │ ├── QuestionGenerator.java +│ │ ├── FileIOService.java +│ │ └── QuizService.java +│ │ +│ ├── ui/ # 前端 GUI +│ │ ├── MainWindow.java +│ │ ├── RegisterPanel.java +│ │ ├── LoginPanel.java +| | ├── PasswordModifyPanel.java +│ │ ├── GradeSelectPanel.java +│ │ ├── QuizPanel.java +│ │ └── ResultPanel.java +│ │ +│ └── util/ # 工具类 +│ ├── PasswordValidator.java +| ├── EmailUtil.java +│ ├── RandomUtils.java +│ └── FileUtils.java +│ +├── data/ # 运行时生成(不提交) +│ ├── users/ # 用户信息 JSON +│ └── temp_codes/ # 临时注册码文件 +│ +├── pom.xml # Maven 依赖(含 JavaFX) +└── README.md + +## 详细模块设计 + +### 模型层设计 + +#### User类 +- String name // 用户ID +- String email // 用户邮箱 +- String encryptedPwd // 加密后的用户密码 +- Grade grade //学段 + +#### ChoiceQuestion +String questionId; // 题目唯一ID(UUID生成) +Grade grade; // 所属学段 +String questionContent; // 题干 +List options; // 选项列表(固定4个,顺序随机) +String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D") + +#### Grade 枚举 +PRIMARY("小学", 1) +JUNIOR("初中", 2) +SENIOR("高中", 3) + +### 工具层设计 + +#### PasswordValidator + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------- | ---------------------------------- | ------- | ------------------------------------------------------------ | +| isValid | String rawPwd | boolean | 校验规则:1. 长度 6-10 位;2. 包含至少 1 个大写字母;3. 包含至少 1 个小写字母;4. 包含至少 1 个数字 | +| encrypt | String rawPwd | String | 使用 BCrypt 加密密码(自动生成盐值,无需额外存储盐) | +| matches | String rawPwd, String encryptedPwd | boolean | 校验明文密码与加密密码是否匹配(BCrypt 自带校验逻辑) | + +#### EmailUtil + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ----------------------------------------- | ------- | ------------------------------------------------------------ | +| sendTextEmail | String to, String subject, String content | boolean | 1. 配置 SMTP 服务器(主机、端口、SSL);2. 设置发件人账号 / 授权码;3. 构建邮件内容;4. 发送并返回结果 | + +### 服务层设计 + +#### UserService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ | +| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt | +| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt,校验注册码是否匹配 | +| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则(6–10位,含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json,成功后删除临时注册码文件 | +| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null | +| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 | + +#### QuestionGenerator (抽象类) + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ------------- | ------------------- | ------------------------------------------------------------ | +| create | Grade grade | QuestionGenerator | 静态工厂方法,根据年级返回对应子类实例(PrimaryGenerator / MiddleGenerator / SeniorGenerator) | +| generateOne | — | ChoiceQuestion | 抽象方法,由子类实现,生成一道符合年级要求的选择题(含题干、4选项、正确答案) | + +#### FileIOService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ | +| saveUser | User user | void | 将用户序列化为 JSON,保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json | +| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null | +| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt | +| loadCode | String email | String | 读取 data/temp_codes/{email}.txt,返回注册码或 null | +| loadUsedQuestionStems | String email | Set | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件,提取题干(每行以“1.”、“2.”开头的内容),用于查重 | + +#### QuizService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ | +| generateQuestions | String email, int count | List | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set) | +| calculateScore | List questions, List userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) | +| savePaper | String email, List questions | void | 生成时间戳文件名(yyyy-MM-dd-HH-mm-ss.txt),保存题干到 data/users/{email}/papers/(目录自动创建) | + +### UI层接口设计 + +#### MainWindow + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- | +| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 | +| showLoginPanel | — | void | 创建并显示 LoginPanel | +| showRegisterPanel | — | void | 创建并显示 RegisterPanel | +| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel | +| showQuizPanel | List questions, QuizService quizService | void | 创建并显示 QuizPanel | +| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel | + +#### RegisterPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 | +| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” | +| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 | + +#### LoginPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ----------------------- | ------ | ----------------------------------------------------------- | +| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 | +| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 | + +#### GradeSelectPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ----------- | ----------------------- | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 | +| startQuiz | Grade grade | void | 弹出数量输入对话框(10–30),调用 QuizService.generateQuestions(),跳转到答题页 | + +#### QuizPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow, List questions, QuizService quizService | — | 显示第1题 | +| showQuestion | int index | void | 渲染当前题干和4个选项(RadioButton),绑定“提交”按钮 | +| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 | + +#### ResultPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- | +| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 | +| exitAction | — | void | 跳转到登录页 | +| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 | \ No newline at end of file diff --git a/src/main/java/com/mathquiz/Main.java b/src/main/java/com/mathquiz/Main.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/mathquiz/model/ChoiceQuestion.java b/src/main/java/com/mathquiz/model/ChoiceQuestion.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/mathquiz/model/Grade.java b/src/main/java/com/mathquiz/model/Grade.java new file mode 100644 index 0000000..e69de29 diff --git a/src/main/java/com/mathquiz/model/User.java b/src/main/java/com/mathquiz/model/User.java new file mode 100644 index 0000000..e69de29 -- 2.34.1 From 7eb4906c9b832fc55a1ca3663eda4f274e3113e3 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 15:28:43 +0800 Subject: [PATCH 06/28] model change --- .../com/mathquiz/model/ChoiceQuestion.java | 2 + src/main/java/com/mathquiz/model/Grade.java | 27 ++++++++++ src/main/java/com/mathquiz/model/User.java | 52 +++++++++++++++++++ 3 files changed, 81 insertions(+) diff --git a/src/main/java/com/mathquiz/model/ChoiceQuestion.java b/src/main/java/com/mathquiz/model/ChoiceQuestion.java index e69de29..e31c864 100644 --- a/src/main/java/com/mathquiz/model/ChoiceQuestion.java +++ b/src/main/java/com/mathquiz/model/ChoiceQuestion.java @@ -0,0 +1,2 @@ +package com.mathquiz.model; + diff --git a/src/main/java/com/mathquiz/model/Grade.java b/src/main/java/com/mathquiz/model/Grade.java index e69de29..ede720f 100644 --- a/src/main/java/com/mathquiz/model/Grade.java +++ b/src/main/java/com/mathquiz/model/Grade.java @@ -0,0 +1,27 @@ +package com.mathquiz.model; + +/** + * 年级枚举,表示小学、初中、高中三个学段 + */ + +public enum Grade { + PRIMARY("小学", 1), + JUNIOR("初中", 2), + SENIOR("高中", 3); + + private final String displayName; + private final int level; + + Grade(String displayName, int level) { + this.displayName = displayName; + this.level = level; + } + + public String getDisplayName() { + return displayName; + } + + public int getLevel() { + return level; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/model/User.java b/src/main/java/com/mathquiz/model/User.java index e69de29..d519167 100644 --- a/src/main/java/com/mathquiz/model/User.java +++ b/src/main/java/com/mathquiz/model/User.java @@ -0,0 +1,52 @@ +package com.mathquiz.model; + +/** + * 用户类 + */ + +public class User { + private String name; + private String email; + private String encryptedPwd; + private Grade grade; + + public User() {} + + public User(String email, String encryptedPwd) { + this.email = email; + this.encryptedPwd = encryptedPwd; + } + + // Getter & Setter + public java.lang.String getName() { + return name; + } + + public void setName(java.lang.String name) { + this.name = name; + } + + public java.lang.String getEmail() { + return email; + } + + public void setEmail(java.lang.String email) { + this.email = email; + } + + public java.lang.String getEncryptedPwd() { + return encryptedPwd; + } + + public void setEncryptedPwd(java.lang.String encryptedPwd) { + this.encryptedPwd = encryptedPwd; + } + + public Grade getGrade() { + return grade; + } + + public void setGrade(Grade grade) { + this.grade = grade; + } +} \ No newline at end of file -- 2.34.1 From c879f90de1b772b97cd7c1f9eb3c51a504cd3b2c Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 15:51:20 +0800 Subject: [PATCH 07/28] add design doc v3 --- doc/设计文档/设计文档第三版.md | 219 ++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 doc/设计文档/设计文档第三版.md diff --git a/doc/设计文档/设计文档第三版.md b/doc/设计文档/设计文档第三版.md new file mode 100644 index 0000000..33e458f --- /dev/null +++ b/doc/设计文档/设计文档第三版.md @@ -0,0 +1,219 @@ +# 数学题库生成系统详细设计文档 + +## 项目概述 + +### 项目目标 +为小初高学生提供一个本地的桌面端图像化应用,支持用户注册和登录,根据学生年级按规则随机生成数学选择题,完成答题后自动计算分数 + +### 技术栈 + +| 类型 | 技术 | 说明 | +| -------- | -------------------------- | ---------------------------------------- | +| 开发语言 | Java 21+ | 保证跨平台兼容性,支持 JavaFX | +| GUI框架 | JavaFX 17+ | 实现桌面图形化界面,提供组件化开发能力 | +| 依赖管理 | Maven | 管理 JavaFX、JSON 解析等依赖 | +| JSON解析 | Gson 2.10+ | 序列化 / 反序列化用户数据、题目数据 | +| 邮件发送 | JavaMail API + 第三方 SMTP | 发送注册码(如 QQ 邮箱 / 网易邮箱 SMTP) | +| 数据加密 | BCrypt 0.9.0+ | 加密存储用户密码,避免明文泄露 | +| 构建打包 | Maven Shade Plugin | 打包成可执行 JAR,支持直接运行 | + +### 总体设计 + +#### 项目目录结构 + +MathQuizApp/ +├── src/ +│ └── main/ +│ └── java/ +│ └── com/ +│ └── mathquiz/ +│ ├── Main.java # 程序入口 +│ │ +│ ├── model/ # 数据模型 +│ │ ├── User.java +│ │ ├── Grade.java +│ │ └── ChoiceQuestion.java +│ │ +│ ├── service/ # 后端逻辑(无GUI) +│ │ ├── UserService.java +│ │ ├── QuestionGenerator.java +│ │ ├── FileIOService.java +│ │ └── QuizService.java +│ │ +│ ├── ui/ # 前端 GUI +│ │ ├── MainWindow.java +│ │ ├── RegisterPanel.java +│ │ ├── LoginPanel.java +| | ├── PasswordModifyPanel.java +│ │ ├── GradeSelectPanel.java +│ │ ├── QuizPanel.java +│ │ └── ResultPanel.java +│ │ +│ └── util/ # 工具类 +│ ├── PasswordValidator.java +| ├── EmailUtil.java +│ ├── RandomUtils.java +│ └── FileUtils.java +│ +├── data/ # 运行时生成(不提交) +│ ├── users/ # 用户信息 JSON +│ └── temp_codes/ # 临时注册码文件 +│ +├── pom.xml # Maven 依赖(含 JavaFX) +└── README.md + +## 详细模块设计 + +### 模型层设计 + +#### User类 +- String name // 用户ID +- String email // 用户邮箱 +- String encryptedPwd // 加密后的用户密码 +- Grade grade //学段 + +#### ChoiceQuestion +String questionId; // 题目唯一ID(UUID生成) +Grade grade; // 所属学段 +String questionContent; // 题干 +List options; // 选项列表(固定4个,顺序随机) +String correctAnswer; // 正确答案(如"A"/"B"/"C"/"D") + +#### Grade 枚举 +PRIMARY("小学", 1) +JUNIOR("初中", 2) +SENIOR("高中", 3) + +### 工具层设计 + +#### PasswordValidator + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------------------- | ------------------------------------------------ | --------- | ------------------------------------------------------------ | +| isValid | String password | boolean | 校验密码:**6–10位**,**必须同时包含大写字母、小写字母和数字** | +| encrypt | String password | String | 使用 **SHA-256** 对密码哈希,返回 64 位十六进制字符串 | +| matches | String plainPassword, String encryptedPassword | boolean | 对明文密码加密后与存储的密文比对,判断是否匹配 | +| generateRegistrationCode | — | String | 生成 **6–10位** 随机字符串,**保证至少含1字母+1数字**,并打乱顺序 | +| shuffleString | String str | String | (私有)对字符串字符顺序进行 Fisher-Yates 打乱 | + +#### EmailUtil + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------------- | ----------------------------------------- | --------- | ------------------------------------------------------------ | +| sendRegistrationCode | String toEmail, String registrationCode | boolean | **模拟发送注册码邮件**(实际不发邮件),打印收件人和注册码到控制台,始终返回 true | +| sendPasswordReset | String toEmail, String newPassword | boolean | **预留接口**:模拟发送密码重置邮件,打印信息到控制台,始终返回 true | +| isValidEmail | String email | boolean | 使用正则表达式 ^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$ 验证邮箱格式是否合法 | + +#### FileUtils + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------------------- | -------------------------------------- | --------- | ------------------------------------------------------------ | +| readFileToString | String filePath | String | 以 UTF-8 编码读取文件内容为字符串,失败抛出 IOException | +| writeStringToFile | String filePath, String content | void | 以 UTF-8 编码将字符串写入文件(覆盖),失败抛出 IOException | +| createDirectoryIfNotExists | String dirPath | void | 若目录不存在,则递归创建目录,失败抛出 IOException | +| exists | String filePath | boolean | 判断文件或目录是否存在 | +| deleteFile | String filePath | boolean | 删除文件,若文件不存在也返回 true,异常时返回 false 并打印堆栈 | +| listFiles | String dirPath | File[] | 返回目录下所有文件(不含子目录),若路径无效返回空数组 | +| appendToFile | String filePath, String content | void | 以 UTF-8 编码追加内容到文件末尾,自动创建文件(若不存在) | +| copyFile | String sourcePath, String targetPath | void | 复制文件,目标文件存在则覆盖,失败抛出 IOException | +| getFileSize | String filePath | long | 返回文件大小(字节),失败抛出 IOException | + +#### RandomUtils + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------- | ------------------------ | --------- | ---------------------------------------------------------- | +| nextInt | int min, int max | int | 生成 [min, max] 范围内的随机整数(含边界) | +| randomChoice | T[] array | T | 从非空数组中随机返回一个元素 | +| randomChoice | List list | T | 从非空列表中随机返回一个元素 | +| shuffle | List list | void | 原地打乱列表顺序(使用 Collections.shuffle) | +| nextDouble | double min, double max | double | 生成 [min, max) 范围内的随机双精度浮点数 | +| nextBoolean | — | boolean | 返回 true 或 false(各 50% 概率) | +| probability | double probability | boolean | 按指定概率返回 true(如 0.7 表示 70% 概率返回 true) | + +### 服务层设计 + +#### UserService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------------- | ----------------------------------------------------------- | ------- | ------------------------------------------------------------ | +| sendRegistrationCode | String email | boolean | 生成 6 位数字注册码,调用 EmailUtil.sendTextEmail()发送(开发阶段可模拟),并将注册码写入 data/temp_codes/{email}.txt | +| verifyCode | String email, String code | boolean | 读取 data/temp_codes/{email}.txt,校验注册码是否匹配 | +| setPassword | String email, String pwd1, String pwd2 | boolean | 校验两次密码一致且符合规则(6–10位,含大小写+数字),使用 BCrypt加密后保存用户到 data/users/{hash}.json,成功后删除临时注册码文件 | +| login | String email, String password | User | 读取用户文件,用 BCrypt.matches() 验证密码,返回 User 对象或 null | +| changePassword | String email, String oldPwd, String newPwd1, String newPwd2 | boolean | 先验证原密码正确,再校验新密码格式,更新加密密码并保存 | + +#### QuestionGenerator (抽象类) + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ------------- | ------------------- | ------------------------------------------------------------ | +| create | Grade grade | QuestionGenerator | 静态工厂方法,根据年级返回对应子类实例(PrimaryGenerator / MiddleGenerator / SeniorGenerator) | +| generateOne | — | ChoiceQuestion | 抽象方法,由子类实现,生成一道符合年级要求的选择题(含题干、4选项、正确答案) | + +#### FileIOService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ----------------------- | --------------------------- | ------------- | ------------------------------------------------------------ | +| saveUser | User user | void | 将用户序列化为 JSON,保存到 data/users/{BCrypt.hash(email).substring(0,16)}.json | +| loadUserByEmail | String email | User | 遍历 data/users/ 下所有 JSON 文件,反序列化后匹配邮箱,返回 User 或 null | +| saveCode | String email, String code | void | 写入 data/temp_codes/{email}.txt | +| loadCode | String email | String | 读取 data/temp_codes/{email}.txt,返回注册码或 null | +| loadUsedQuestionStems | String email | Set | 遍历 data/users/{email}/papers/(若存在)下所有 .txt 文件,提取题干(每行以“1.”、“2.”开头的内容),用于查重 | + +#### QuizService + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------------- | ---------------------------------------------------------- | ---------------------- | ------------------------------------------------------------ | +| generateQuestions | String email, int count | List | 调用 QuestionGenerator.generateOne() 循环生成,确保题干不重复(使用 loadUsedQuestionStems + 当前卷子 Set) | +| calculateScore | List questions, List userAnswers | int | 比对每题 correctAnswer 与用户答案,计算百分比得分(四舍五入) | +| savePaper | String email, List questions | void | 生成时间戳文件名(yyyy-MM-dd-HH-mm-ss.txt),保存题干到 data/users/{email}/papers/(目录自动创建) | + +### UI层接口设计 + +#### MainWindow + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------------- | --------------------------------------------------------- | ------ | ------------------------------------------------- | +| showPanel | Pane panel | void | 设置 BorderPane.center 为指定面板,实现页面切换 | +| showLoginPanel | — | void | 创建并显示 LoginPanel | +| showRegisterPanel | — | void | 创建并显示 RegisterPanel | +| showGradeSelectPanel | — | void | 创建并显示 GradeSelectPanel | +| showQuizPanel | List questions, QuizService quizService | void | 创建并显示 QuizPanel | +| showResultPanel | int score, Runnable onContinue | void | 创建并显示 ResultPanel | + +#### RegisterPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------- | ----------------------- | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow | — | 初始化邮箱、注册码、密码输入框和按钮,绑定事件 | +| sendCodeAction | — | void | 调用 mainWindow.getUserService().sendRegistrationCode(),提示“注册码已发送(模拟)” | +| registerAction | — | void | 调用 setPassword(),成功则跳转到年级选择页 | + +#### LoginPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ------------- | ----------------------- | ------ | ----------------------------------------------------------- | +| 构造函数 | MainWindow mainWindow | — | 初始化登录表单,绑定“登录”按钮 | +| loginAction | — | void | 调用 login(),成功则设置 currentUser 并跳转到年级选择页 | + +#### GradeSelectPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ----------- | ----------------------- | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow | — | 创建三个年级按钮 | +| startQuiz | Grade grade | void | 弹出数量输入对话框(10–30),调用 QuizService.generateQuestions(),跳转到答题页 | + +#### QuizPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| -------------- | ------------------------------------------------------------ | ------ | ------------------------------------------------------------ | +| 构造函数 | MainWindow mainWindow, List questions, QuizService quizService | — | 显示第1题 | +| showQuestion | int index | void | 渲染当前题干和4个选项(RadioButton),绑定“提交”按钮 | +| submitAction | — | void | 记录答案,若非最后一题则显示下一题,否则计算分数并跳转结果页 | + +#### ResultPanel + +| 方法名 | 参数列表 | 返回值 | 逻辑描述 | +| ---------------- | ------------------------------------------------------- | ------ | ----------------------------------------------------- | +| 构造函数 | MainWindow mainWindow, int score, Runnable onContinue | — | 显示得分,绑定“退出”和“继续做题”按钮 | +| exitAction | — | void | 跳转到登录页 | +| continueAction | — | void | 执行 onContinue.run()(保存试卷),跳转到年级选择页 | \ No newline at end of file -- 2.34.1 From 499661407951dd759f24fc9e573b15756c82d3f2 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 15:52:06 +0800 Subject: [PATCH 08/28] merge utils --- .../java/com/mathquiz/util/EmailUtil.java | 109 +++++++++++++++++ .../java/com/mathquiz/util/FileUtils.java | 111 ++++++++++++++++++ .../com/mathquiz/util/PasswordValidator.java | 102 ++++++++++++++++ .../java/com/mathquiz/util/RandomUtils.java | 69 +++++++++++ 4 files changed, 391 insertions(+) create mode 100644 src/main/java/com/mathquiz/util/EmailUtil.java create mode 100644 src/main/java/com/mathquiz/util/FileUtils.java create mode 100644 src/main/java/com/mathquiz/util/PasswordValidator.java create mode 100644 src/main/java/com/mathquiz/util/RandomUtils.java diff --git a/src/main/java/com/mathquiz/util/EmailUtil.java b/src/main/java/com/mathquiz/util/EmailUtil.java new file mode 100644 index 0000000..ff915ac --- /dev/null +++ b/src/main/java/com/mathquiz/util/EmailUtil.java @@ -0,0 +1,109 @@ +package com.util; + +/** + * 邮件工具类 + * 用于发送注册码邮件(可选功能) + * 注意:当前项目需求中,注册码是直接显示在界面上的, + * 不需要发送邮件,所以这个类暂时保留为空实现。 + * 如果将来需要发送邮件,可以使用JavaMail库实现。 + */ +public class EmailUtil { + + /** + * 发送注册码邮件(预留接口) + * @param toEmail 收件人邮箱 + * @param registrationCode 注册码 + * @return true表示发送成功 + */ + public static boolean sendRegistrationCode(String toEmail, String registrationCode) { + // TODO: 暂不实现邮件发送功能 + // 原因:项目需求是直接在界面显示注册码,不需要发邮件 + + System.out.println("【模拟】发送注册码邮件"); + System.out.println("收件人: " + toEmail); + System.out.println("注册码: " + registrationCode); + + return true; + } + + /** + * 发送密码重置邮件(预留接口) + * @param toEmail 收件人邮箱 + * @param newPassword 新密码 + * @return true表示发送成功 + */ + public static boolean sendPasswordReset(String toEmail, String newPassword) { + // TODO: 将来如果需要"找回密码"功能,可以在这里实现 + + System.out.println("【模拟】发送密码重置邮件"); + System.out.println("收件人: " + toEmail); + System.out.println("新密码: " + newPassword); + + return true; + } + + /** + * 验证邮箱格式 + * @param email 邮箱地址 + * @return true表示格式正确 + */ + public static boolean isValidEmail(String email) { + if (email == null || email.trim().isEmpty()) { + return false; + } + + // 简单的邮箱格式验证 + String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + return email.matches(emailRegex); + } +} + +/* + * 如果将来需要真正实现邮件发送,可以参考以下代码: + * + * 1. 在pom.xml添加依赖: + * + * javax.mail + * javax.mail-api + * 1.6.2 + * + * + * com.sun.mail + * javax.mail + * 1.6.2 + * + * + * 2. 实现代码示例: + * + * import javax.mail.*; + * import javax.mail.internet.*; + * import java.util.Properties; + * + * public static boolean sendEmail(String toEmail, String subject, String content) { + * try { + * Properties props = new Properties(); + * props.put("mail.smtp.auth", "true"); + * props.put("mail.smtp.starttls.enable", "true"); + * props.put("mail.smtp.host", "smtp.qq.com"); + * props.put("mail.smtp.port", "587"); + * + * Session session = Session.getInstance(props, new Authenticator() { + * protected PasswordAuthentication getPasswordAuthentication() { + * return new PasswordAuthentication("your-email@qq.com", "your-password"); + * } + * }); + * + * Message message = new MimeMessage(session); + * message.setFrom(new InternetAddress("your-email@qq.com")); + * message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); + * message.setSubject(subject); + * message.setText(content); + * + * Transport.send(message); + * return true; + * } catch (Exception e) { + * e.printStackTrace(); + * return false; + * } + * } + */ \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/FileUtils.java b/src/main/java/com/mathquiz/util/FileUtils.java new file mode 100644 index 0000000..d4daf9e --- /dev/null +++ b/src/main/java/com/mathquiz/util/FileUtils.java @@ -0,0 +1,111 @@ +package com.util; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.*; + +/** + * 文件操作工具类 + * 提供文件读写、目录创建等常用操作 + */ +public class FileUtils { + + /** + * 读取文件内容为字符串 + * @param filePath 文件路径 + * @return 文件内容 + * @throws IOException 读取失败时抛出 + */ + public static String readFileToString(String filePath) throws IOException { + return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8); + } + + /** + * 写入字符串到文件 + * @param filePath 文件路径 + * @param content 要写入的内容 + * @throws IOException 写入失败时抛出 + */ + public static void writeStringToFile(String filePath, String content) throws IOException { + Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8); + } + + /** + * 创建目录(如果不存在) + * @param dirPath 目录路径 + * @throws IOException 创建失败时抛出 + */ + public static void createDirectoryIfNotExists(String dirPath) throws IOException { + Path path = Paths.get(dirPath); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } + + /** + * 检查文件是否存在 + * @param filePath 文件路径 + * @return true表示存在 + */ + public static boolean exists(String filePath) { + return Files.exists(Paths.get(filePath)); + } + + /** + * 删除文件 + * @param filePath 文件路径 + * @return true表示删除成功 + */ + public static boolean deleteFile(String filePath) { + try { + return Files.deleteIfExists(Paths.get(filePath)); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + } + + /** + * 获取目录下所有文件 + * @param dirPath 目录路径 + * @return 文件数组 + */ + public static File[] listFiles(String dirPath) { + File dir = new File(dirPath); + if (dir.exists() && dir.isDirectory()) { + return dir.listFiles(); + } + return new File[0]; + } + + /** + * 追加内容到文件末尾 + * @param filePath 文件路径 + * @param content 要追加的内容 + * @throws IOException 追加失败时抛出 + */ + public static void appendToFile(String filePath, String content) throws IOException { + Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.APPEND); + } + + /** + * 复制文件 + * @param sourcePath 源文件路径 + * @param targetPath 目标文件路径 + * @throws IOException 复制失败时抛出 + */ + public static void copyFile(String sourcePath, String targetPath) throws IOException { + Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING); + } + + /** + * 获取文件大小(字节) + * @param filePath 文件路径 + * @return 文件大小 + * @throws IOException 获取失败时抛出 + */ + public static long getFileSize(String filePath) throws IOException { + return Files.size(Paths.get(filePath)); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/PasswordValidator.java b/src/main/java/com/mathquiz/util/PasswordValidator.java new file mode 100644 index 0000000..40a26b5 --- /dev/null +++ b/src/main/java/com/mathquiz/util/PasswordValidator.java @@ -0,0 +1,102 @@ +package com.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 密码验证和加密工具类 + * 提供密码格式验证、加密、匹配等功能 + */ +public class PasswordValidator { + + /** + * 验证密码格式:6-10位,包含字母和数字 + * @param password 待验证的密码 + * @return true表示格式正确 + */ + public static boolean isValid(String password) { + if (password == null || password.length() < 6 || password.length() > 10) { + return false; + } + + boolean hasLetter = password.matches("^(?=.*[a-z])(?=.*[A-Z]).*$"); + boolean hasDigit = password.matches(".*\\d.*"); + + return hasLetter && hasDigit; + } + + /** + * 使用SHA-256加密密码 + * @param password 明文密码 + * @return 加密后的密码(16进制字符串) + */ + public static String encrypt(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("SHA-256算法不可用", e); + } + } + + /** + * 验证密码是否匹配 + * @param plainPassword 明文密码 + * @param encryptedPassword 加密后的密码 + * @return true表示匹配 + */ + public static boolean matches(String plainPassword, String encryptedPassword) { + return encrypt(plainPassword).equals(encryptedPassword); + } + + /** + * 生成6-10位随机注册码(包含字母和数字) + * @return 注册码 + */ + public static String generateRegistrationCode() { + String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; + int length = 6 + (int) (Math.random() * 5); // 6-10位 + + StringBuilder code = new StringBuilder(); + + // 确保至少有一个字母 + code.append(chars.charAt((int) (Math.random() * 52))); + // 确保至少有一个数字 + code.append(chars.charAt(52 + (int) (Math.random() * 10))); + + // 填充剩余字符 + for (int i = 2; i < length; i++) { + code.append(chars.charAt((int) (Math.random() * chars.length()))); + } + + // 打乱字符顺序 + return shuffleString(code.toString()); + } + + /** + * 打乱字符串 + * @param str 原字符串 + * @return 打乱后的字符串 + */ + private static String shuffleString(String str) { + char[] chars = str.toCharArray(); + for (int i = chars.length - 1; i > 0; i--) { + int j = (int) (Math.random() * (i + 1)); + char temp = chars[i]; + chars[i] = chars[j]; + chars[j] = temp; + } + return new String(chars); + } +} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/RandomUtils.java b/src/main/java/com/mathquiz/util/RandomUtils.java new file mode 100644 index 0000000..c7351b9 --- /dev/null +++ b/src/main/java/com/mathquiz/util/RandomUtils.java @@ -0,0 +1,69 @@ +package com.util; + +import java.util.Collections; +import java.util.List; +import java.util.Random; + +//各种随机数生成 +public class RandomUtils { + private static final Random random = new Random(); + + + //生成[min, max]范围内的随机整数(包含边界) + public static int nextInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("min不能大于max"); + } + return min + random.nextInt(max - min + 1); + } + + + //从数组中随机选择一个元素(模板类) + public static T randomChoice(T[] array) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException("数组不能为空"); + } + return array[random.nextInt(array.length)]; + } + + + //从列表中随机选择一个元素 + public static T randomChoice(List list) { + if (list == null || list.isEmpty()) { + throw new IllegalArgumentException("列表不能为空"); + } + return list.get(random.nextInt(list.size())); + } + + /** + * 打乱列表顺序 + * @param list 要打乱的列表 + */ + public static void shuffle(List list) { + Collections.shuffle(list, random); + } + + + //生成指定范围内的随机双精度浮点数 + public static double nextDouble(double min, double max) { + if (min > max) { + throw new IllegalArgumentException("min不能大于max"); + } + return min + (max - min) * random.nextDouble(); + } + + + //生成随机布尔值 + public static boolean nextBoolean() { + return random.nextBoolean(); + } + + //按概率返回true(题目生成概率) + //示例:probability(0.7) 有70%概率返回true + public static boolean probability(double probability) { + if (probability < 0.0 || probability > 1.0) { + throw new IllegalArgumentException("概率必须在0.0-1.0之间"); + } + return random.nextDouble() < probability; + } +} \ No newline at end of file -- 2.34.1 From c83015e7d4885bceb5b97034a45ad93ea37f835e Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 15:52:39 +0800 Subject: [PATCH 09/28] add ChoiceQuestion --- .../com/mathquiz/model/ChoiceQuestion.java | 139 ++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/src/main/java/com/mathquiz/model/ChoiceQuestion.java b/src/main/java/com/mathquiz/model/ChoiceQuestion.java index e31c864..e3dcb16 100644 --- a/src/main/java/com/mathquiz/model/ChoiceQuestion.java +++ b/src/main/java/com/mathquiz/model/ChoiceQuestion.java @@ -1,2 +1,141 @@ package com.mathquiz.model; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +/** + * 选择题模型类 + * 表示一道完整的数学选择题,包含题干、选项、正确答案等信息 + */ +public class ChoiceQuestion { + + /** + * 题目唯一ID(UUID生成) + */ + private String questionId; + + /** + * 所属学段(小学 / 初中 / 高中) + */ + private Grade grade; + + /** + * 题干内容,例如:"2 + 3 × (4 - 1) = ?" + */ + private String questionContent; + + /** + * 选项列表,固定4个选项,顺序已随机打乱 + * 格式示例:["11", "10", "13", "9"] + */ + private List options; + + /** + * 正确答案标识,取值为 "A"、"B"、"C" 或 "D" + */ + private String correctAnswer; + + // ---------------- 构造函数 ---------------- + + /** + * 默认构造函数(用于 JSON 反序列化) + */ + public ChoiceQuestion() {} + + /** + * 全参构造函数 + */ + public ChoiceQuestion(String questionId, Grade grade, String questionContent, + List options, String correctAnswer) { + this.questionId = questionId; + this.grade = grade; + this.questionContent = questionContent; + this.options = options; + this.correctAnswer = correctAnswer; + } + + // ---------------- 静态工厂方法 ---------------- + + /** + * 创建一道新题目,自动分配 UUID 作为 questionId + * + * @param grade 年级 + * @param questionContent 题干 + * @param options 4个选项(顺序应已打乱) + * @param correctAnswer 正确答案("A"-"D") + * @return 新的 ChoiceQuestion 实例 + */ + public static ChoiceQuestion of(Grade grade, String questionContent, + List options, String correctAnswer) { + return new ChoiceQuestion(UUID.randomUUID().toString(), grade, questionContent, options, correctAnswer); + } + + // ---------------- Getter & Setter ---------------- + + public String getQuestionId() { + return questionId; + } + + public void setQuestionId(String questionId) { + this.questionId = questionId; + } + + public Grade getGrade() { + return grade; + } + + public void setGrade(Grade grade) { + this.grade = grade; + } + + public String getQuestionContent() { + return questionContent; + } + + public void setQuestionContent(String questionContent) { + this.questionContent = questionContent; + } + + public List getOptions() { + return options; + } + + public void setOptions(List options) { + this.options = options; + } + + public String getCorrectAnswer() { + return correctAnswer; + } + + public void setCorrectAnswer(String correctAnswer) { + this.correctAnswer = correctAnswer; + } + + // ---------------- Object 方法 ---------------- + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ChoiceQuestion that = (ChoiceQuestion) o; + return Objects.equals(questionId, that.questionId); + } + + @Override + public int hashCode() { + return Objects.hash(questionId); + } + + @Override + public String toString() { + return "ChoiceQuestion{" + + "questionId='" + questionId + '\'' + + ", grade=" + grade + + ", questionContent='" + questionContent + '\'' + + ", options=" + options + + ", correctAnswer='" + correctAnswer + '\'' + + '}'; + } +} \ No newline at end of file -- 2.34.1 From c50774c43216d847be71728c1eecf5644be1f838 Mon Sep 17 00:00:00 2001 From: bx <2936213174@qq.com> Date: Fri, 3 Oct 2025 20:06:47 +0800 Subject: [PATCH 10/28] =?UTF-8?q?=E7=AC=AC=E4=BA=8C=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/uiDesigner.xml | 124 +++++ src/main/java/com/model/ChoiceQuestion.java | 81 +-- src/main/java/com/model/Grade.java | 47 +- src/main/java/com/model/QuizHistory.java | 82 +++ src/main/java/com/model/QuizResult.java | 66 +++ src/main/java/com/model/User.java | 89 +-- src/main/java/com/service/FileIOService.java | 242 +++++++++ src/main/java/com/service/QuizService.java | 432 +++++++++++++++ src/main/java/com/service/UserService.java | 321 +++++++++++ .../QuestionFactoryManager.java | 97 ++++ .../factory/ElementaryQuestionFactory.java | 41 ++ .../factory/HighQuestionFactory.java | 41 ++ .../factory/MiddleQuestionFactory.java | 41 ++ .../factory/QuestionFactory.java | 17 + .../strategy/AbstractQuestionStrategy.java | 138 +++++ .../strategy/QuestionStrategy.java | 12 + .../strategy/elementary/AdditionStrategy.java | 37 ++ .../strategy/elementary/DivisionStrategy.java | 39 ++ .../elementary/MultiplicationStrategy.java | 37 ++ .../elementary/ParenthesesAddStrategy.java | 40 ++ .../ParenthesesMultiplyStrategy.java | 41 ++ .../elementary/SubtractionStrategy.java | 40 ++ .../strategy/high/CosStrategy.java | 57 ++ .../strategy/high/SinStrategy.java | 56 ++ .../strategy/high/TanStrategy.java | 56 ++ .../strategy/high/TrigIdentityStrategy.java | 41 ++ .../middle/MixedSquareSqrtStrategy.java | 40 ++ .../strategy/middle/SqrtAddStrategy.java | 39 ++ .../strategy/middle/SqrtStrategy.java | 39 ++ .../strategy/middle/SquareAddStrategy.java | 37 ++ .../strategy/middle/SquareStrategy.java | 39 ++ src/main/java/com/util/FileUtils.java | 75 ++- src/main/java/com/util/PasswordValidator.java | 261 ++++++++- src/test/java/TestMain.java | 507 +++++++++++++++++- 34 files changed, 3195 insertions(+), 157 deletions(-) create mode 100644 .idea/uiDesigner.xml create mode 100644 src/main/java/com/model/QuizHistory.java create mode 100644 src/main/java/com/model/QuizResult.java create mode 100644 src/main/java/com/service/FileIOService.java create mode 100644 src/main/java/com/service/QuizService.java create mode 100644 src/main/java/com/service/UserService.java create mode 100644 src/main/java/com/service/question_generator/QuestionFactoryManager.java create mode 100644 src/main/java/com/service/question_generator/factory/ElementaryQuestionFactory.java create mode 100644 src/main/java/com/service/question_generator/factory/HighQuestionFactory.java create mode 100644 src/main/java/com/service/question_generator/factory/MiddleQuestionFactory.java create mode 100644 src/main/java/com/service/question_generator/factory/QuestionFactory.java create mode 100644 src/main/java/com/service/question_generator/strategy/AbstractQuestionStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/QuestionStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/AdditionStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/DivisionStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/MultiplicationStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/elementary/SubtractionStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/high/CosStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/high/SinStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/high/TanStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/high/TrigIdentityStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/middle/SqrtAddStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/middle/SqrtStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/middle/SquareAddStrategy.java create mode 100644 src/main/java/com/service/question_generator/strategy/middle/SquareStrategy.java 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/src/main/java/com/model/ChoiceQuestion.java b/src/main/java/com/model/ChoiceQuestion.java index b71c59a..897059e 100644 --- a/src/main/java/com/model/ChoiceQuestion.java +++ b/src/main/java/com/model/ChoiceQuestion.java @@ -3,67 +3,34 @@ package com.model; import java.util.List; /** - * 选择题数据模型 - * 包含题目内容、正确答案、四个选项 + * 选择题模型(纯数据) */ public class ChoiceQuestion { - private String questionText; // 题目文本,如 "3 + 5" - private double correctAnswer; // 正确答案 - private List options; // 四个选项(包含正确答案) - private Grade grade; // 所属学段 - private int correctOptionIndex; // 正确答案在选项中的索引(0-3) + private String questionText; // 题目文本 + private Object correctAnswer; // 正确答案 + private List options; // 选项列表 + private Grade grade; // 所属学段 - /** - * 无参构造函数 - */ - public ChoiceQuestion() { - } + // ==================== 构造方法 ==================== - /** - * 完整构造函数 - */ - public ChoiceQuestion(String questionText, double correctAnswer, List options, Grade grade) { + public ChoiceQuestion(String questionText, double correctAnswer, + List options, Grade grade) { this.questionText = questionText; this.correctAnswer = correctAnswer; this.options = options; this.grade = grade; - - // 自动找到正确答案的索引 - this.correctOptionIndex = findCorrectOptionIndex(); - } - - /** - * 查找正确答案在选项中的位置 - */ - private int findCorrectOptionIndex() { - for (int i = 0; i < options.size(); i++) { - if (Math.abs(options.get(i) - correctAnswer) < 0.01) { - return i; - } - } - return -1; // 理论上不会发生 } - /** - * 检查用户选择的选项是否正确 - * @param optionIndex 用户选择的选项索引(0-3) - * @return true表示正确 - */ - public boolean checkAnswer(int optionIndex) { - return optionIndex == correctOptionIndex; - } - - /** - * 检查用户输入的答案值是否正确 - * @param answer 用户输入的答案值 - * @return true表示正确 - */ - public boolean checkAnswerValue(double answer) { - return Math.abs(answer - correctAnswer) < 0.01; + public ChoiceQuestion(String questionText, String correctAnswer, + List options, Grade grade) { + this.questionText = questionText; + this.correctAnswer = correctAnswer; + this.options = options; + this.grade = grade; } - // ========== Getter和Setter方法 ========== + // ==================== Getters & Setters ==================== public String getQuestionText() { return questionText; @@ -73,21 +40,20 @@ public class ChoiceQuestion { this.questionText = questionText; } - public double getCorrectAnswer() { + public Object getCorrectAnswer() { return correctAnswer; } - public void setCorrectAnswer(double correctAnswer) { + public void setCorrectAnswer(Object correctAnswer) { this.correctAnswer = correctAnswer; } - public List getOptions() { + public List getOptions() { return options; } - public void setOptions(List options) { + public void setOptions(List options) { this.options = options; - this.correctOptionIndex = findCorrectOptionIndex(); } public Grade getGrade() { @@ -98,19 +64,12 @@ public class ChoiceQuestion { this.grade = grade; } - public int getCorrectOptionIndex() { - return correctOptionIndex; - } - - public void setCorrectOptionIndex(int correctOptionIndex) { - this.correctOptionIndex = correctOptionIndex; - } - @Override public String toString() { return "ChoiceQuestion{" + "questionText='" + questionText + '\'' + ", correctAnswer=" + correctAnswer + + ", options=" + options + ", grade=" + grade + '}'; } diff --git a/src/main/java/com/model/Grade.java b/src/main/java/com/model/Grade.java index 7de066d..1d2729f 100644 --- a/src/main/java/com/model/Grade.java +++ b/src/main/java/com/model/Grade.java @@ -1,49 +1,10 @@ package com.model; /** - * 学段枚举类 - * 定义三个学段:小学、初中、高中 + * 学段枚举 */ public enum Grade { - ELEMENTARY("小学"), - MIDDLE("初中"), - HIGH("高中"); - - private final String displayName; - - Grade(String displayName) { - this.displayName = displayName; - } - - public String getDisplayName() { - return displayName; - } - - //根据显示名称查找对应的枚举值 - public static Grade fromDisplayName(String displayName) { - for (Grade grade : values()) { - if (grade.displayName.equals(displayName)) { - return grade; - } - } - throw new IllegalArgumentException("未知学段: " + displayName); - } - - /** - * 获取所有学段的显示名称数组 - * @return ["小学", "初中", "高中"] - */ - public static String[] getDisplayNames() { - Grade[] grades = values(); - String[] names = new String[grades.length]; - for (int i = 0; i < grades.length; i++) { - names[i] = grades[i].displayName; - } - return names; - } - - @Override - public String toString() { - return displayName; - } + ELEMENTARY, // 小学 + MIDDLE, // 初中 + HIGH // 高中 } \ No newline at end of file diff --git a/src/main/java/com/model/QuizHistory.java b/src/main/java/com/model/QuizHistory.java new file mode 100644 index 0000000..1d60028 --- /dev/null +++ b/src/main/java/com/model/QuizHistory.java @@ -0,0 +1,82 @@ +package com.model; + + +import java.util.Date; +import java.util.List; + +/** + * 答题历史记录模型(纯数据) + */ +public class QuizHistory { + + private String username; // 用户名 + private Date timestamp; // 答题时间 + private List questions; // 题目列表 + private List userAnswers; // 用户答案列表 + private int score; // 得分 + + // ==================== 构造方法 ==================== + + public QuizHistory(String username, Date timestamp, + List questions, + List userAnswers, + int score) { + this.username = username; + this.timestamp = timestamp; + this.questions = questions; + this.userAnswers = userAnswers; + this.score = score; + } + + // ==================== Getters & Setters ==================== + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Date getTimestamp() { + return timestamp; + } + + public void setTimestamp(Date timestamp) { + this.timestamp = timestamp; + } + + public List getQuestions() { + return questions; + } + + public void setQuestions(List questions) { + this.questions = questions; + } + + public List getUserAnswers() { + return userAnswers; + } + + public void setUserAnswers(List userAnswers) { + this.userAnswers = userAnswers; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + @Override + public String toString() { + return "QuizHistory{" + + "username='" + username + '\'' + + ", timestamp=" + timestamp + + ", questions=" + (questions != null ? questions.size() : 0) + + ", score=" + score + + '}'; + } +} \ No newline at end of file diff --git a/src/main/java/com/model/QuizResult.java b/src/main/java/com/model/QuizResult.java new file mode 100644 index 0000000..7ce4e50 --- /dev/null +++ b/src/main/java/com/model/QuizResult.java @@ -0,0 +1,66 @@ +package com.model; + + +/** + * 答题结果模型(纯数据) + */ +public class QuizResult { + + private int totalQuestions; // 总题数 + private int correctCount; // 正确题数 + private int wrongCount; // 错误题数 + private int score; // 得分 + + // ==================== 构造方法 ==================== + + public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) { + this.totalQuestions = totalQuestions; + this.correctCount = correctCount; + this.wrongCount = wrongCount; + this.score = score; + } + + // ==================== Getters & Setters ==================== + + public int getTotalQuestions() { + return totalQuestions; + } + + public void setTotalQuestions(int totalQuestions) { + this.totalQuestions = totalQuestions; + } + + public int getCorrectCount() { + return correctCount; + } + + public void setCorrectCount(int correctCount) { + this.correctCount = correctCount; + } + + public int getWrongCount() { + return wrongCount; + } + + public void setWrongCount(int wrongCount) { + this.wrongCount = wrongCount; + } + + public int getScore() { + return score; + } + + public void setScore(int score) { + this.score = score; + } + + @Override + public String toString() { + return "QuizResult{" + + "totalQuestions=" + totalQuestions + + ", correctCount=" + correctCount + + ", wrongCount=" + wrongCount + + ", score=" + score + + '}'; + } +} \ 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 eadfd17..288ce9b 100644 --- a/src/main/java/com/model/User.java +++ b/src/main/java/com/model/User.java @@ -1,32 +1,50 @@ package com.model; +import java.util.Date; + +/** + * 用户模型(纯数据) + */ public class User { - private String username; // 用户名,格式:小学-张三 + + private String username; // 用户名 private String password; // 密码(加密后) - private Grade grade; // 学段 private String email; // 邮箱 - private String registrationCode; // 注册码(明文保存,用于用户查看) - private long createdTime; // 创建时间戳 - private long lastLoginTime; // 最后登录时间戳 + private Grade grade; // 学段 + private int totalQuizzes; // 总答题次数 + private double averageScore; // 平均分 + private Date registrationDate; // 注册时间 + + // ==================== 构造方法 ==================== /** - * 无参构造函数(Gson反序列化需要) + * 完整构造方法(用于从文件加载) */ - public User() { + public User(String username, String password, String email, Grade grade, + int totalQuizzes, double averageScore, Date registrationDate) { + this.username = username; + this.password = password; + this.email = email; + this.grade = grade; + this.totalQuizzes = totalQuizzes; + this.averageScore = averageScore; + this.registrationDate = registrationDate; } /** - * 完整构造函数 + * 简化构造方法(用于新用户注册) */ - public User(String username, String password, Grade grade, String email) { + public User(String username, String password, String email, Grade grade) { this.username = username; this.password = password; - this.grade = grade; this.email = email; - this.createdTime = System.currentTimeMillis(); + this.grade = grade; + this.totalQuizzes = 0; + this.averageScore = 0.0; + this.registrationDate = new Date(); } - // ========== Getter和Setter方法 ========== + // ==================== Getters & Setters ==================== public String getUsername() { return username; @@ -44,14 +62,6 @@ public class User { this.password = password; } - public Grade getGrade() { - return grade; - } - - public void setGrade(Grade grade) { - this.grade = grade; - } - public String getEmail() { return email; } @@ -60,36 +70,47 @@ public class User { this.email = email; } - public String getRegistrationCode() { - return registrationCode; + public Grade getGrade() { + return grade; + } + + public void setGrade(Grade grade) { + this.grade = grade; } - public void setRegistrationCode(String registrationCode) { - this.registrationCode = registrationCode; + public int getTotalQuizzes() { + return totalQuizzes; } - public long getCreatedTime() { - return createdTime; + public void setTotalQuizzes(int totalQuizzes) { + this.totalQuizzes = totalQuizzes; } - public void setCreatedTime(long createdTime) { - this.createdTime = createdTime; + public double getAverageScore() { + return averageScore; } - public long getLastLoginTime() { - return lastLoginTime; + public void setAverageScore(double averageScore) { + this.averageScore = averageScore; } - public void setLastLoginTime(long lastLoginTime) { - this.lastLoginTime = lastLoginTime; + public Date getRegistrationDate() { + return registrationDate; + } + + public void setRegistrationDate(Date registrationDate) { + this.registrationDate = registrationDate; } @Override public String toString() { return "User{" + "username='" + username + '\'' + - ", grade=" + grade + ", email='" + email + '\'' + + ", grade=" + grade + + ", totalQuizzes=" + totalQuizzes + + ", averageScore=" + String.format("%.1f", averageScore) + + ", registrationDate=" + registrationDate + '}'; } -} +} \ No newline at end of file diff --git a/src/main/java/com/service/FileIOService.java b/src/main/java/com/service/FileIOService.java new file mode 100644 index 0000000..4bfa68e --- /dev/null +++ b/src/main/java/com/service/FileIOService.java @@ -0,0 +1,242 @@ +package com.service; + + +import com.model.*; +import com.util.FileUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.reflect.TypeToken; + +import java.io.File; +import java.io.IOException; +import java.lang.reflect.Type; +import java.text.SimpleDateFormat; +import java.util.*; + +/** + * 文件IO服务 + * 负责所有数据的读写操作 + */ +public class FileIOService { + + private static final String DATA_DIR = "data"; + private static final String USERS_DIR = DATA_DIR + "/users"; + private static final String HISTORY_DIR = DATA_DIR + "/history"; + + private static final String USERS_FILE = DATA_DIR + "/users.json"; + private static final String CURRENT_USER_FILE = DATA_DIR + "/current_user.json"; + + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() + .setDateFormat("yyyy-MM-dd HH:mm:ss") + .create(); + + // ==================== 初始化 ==================== + + public void initDataDirectory() throws IOException { + FileUtils.createDirectoryIfNotExists(DATA_DIR); + FileUtils.createDirectoryIfNotExists(USERS_DIR); + FileUtils.createDirectoryIfNotExists(HISTORY_DIR); + + if (!FileUtils.exists(USERS_FILE)) { + Map> data = new HashMap<>(); + data.put("users", new ArrayList<>()); + FileUtils.saveAsJson(data, USERS_FILE); + } + + System.out.println("✓ 数据目录初始化完成"); + } + + // ==================== 用户操作 ==================== + + public void saveUser(User user) throws IOException { + Type type = new TypeToken>>(){}.getType(); + Map> data = FileUtils.readJsonToObject(USERS_FILE, type); + + List users = data.get("users"); + + boolean found = false; + for (int i = 0; i < users.size(); i++) { + if (users.get(i).getUsername().equals(user.getUsername())) { + users.set(i, user); + found = true; + break; + } + } + + if (!found) { + users.add(user); + } + + FileUtils.saveAsJson(data, USERS_FILE); + } + + public List loadAllUsers() throws IOException { + if (!FileUtils.exists(USERS_FILE)) { + return new ArrayList<>(); + } + + Type type = new TypeToken>>(){}.getType(); + Map> data = FileUtils.readJsonToObject(USERS_FILE, type); + + return data.get("users"); + } + + public User findUserByUsername(String username) throws IOException { + List users = loadAllUsers(); + + for (User user : users) { + if (user.getUsername().equals(username)) { + return user; + } + } + + return null; + } + + public boolean isUsernameExists(String username) throws IOException { + return findUserByUsername(username) != null; + } + + public void saveCurrentUser(User user) throws IOException { + FileUtils.saveAsJson(user, CURRENT_USER_FILE); + } + + public User loadCurrentUser() throws IOException { + if (!FileUtils.exists(CURRENT_USER_FILE)) { + return null; + } + return FileUtils.readJsonToObject(CURRENT_USER_FILE, User.class); + } + + public void clearCurrentUser() { + FileUtils.deleteFile(CURRENT_USER_FILE); + } + + // ==================== 答题历史操作 ==================== + + public void saveQuizHistory(QuizHistory history) throws IOException { + String filename = HISTORY_DIR + "/" + + sanitizeFilename(history.getUsername()) + "_" + + System.currentTimeMillis() + ".txt"; + + StringBuilder content = new StringBuilder(); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + content.append("========== 答题记录 ==========\n"); + content.append("用户:").append(history.getUsername()).append("\n"); + content.append("时间:").append(dateFormat.format(history.getTimestamp())).append("\n"); + content.append("总分:").append(history.getScore()).append(" 分\n"); + + // 调用 QuizService 的业务方法计算正确数和错误数 + int correctCount = calculateCorrectCount(history); + int wrongCount = history.getQuestions().size() - correctCount; + + content.append("正确:").append(correctCount).append(" 题 "); + content.append("错误:").append(wrongCount).append(" 题\n"); + content.append("=============================\n\n"); + + List questions = history.getQuestions(); + List userAnswers = history.getUserAnswers(); + + for (int i = 0; i < questions.size(); i++) { + ChoiceQuestion q = questions.get(i); + Integer userAnswer = userAnswers.get(i); + + content.append("【题目 ").append(i + 1).append("】\n"); + content.append(q.getQuestionText()).append("\n"); + + List options = q.getOptions(); + for (int j = 0; j < options.size(); j++) { + content.append((char)('A' + j)).append(". ") + .append(options.get(j)).append(" "); + } + content.append("\n"); + + int correctIndex = getCorrectAnswerIndex(q); + content.append("正确答案:").append((char)('A' + correctIndex)).append("\n"); + + content.append("用户答案:"); + if (userAnswer != null) { + content.append((char)('A' + userAnswer)); + } else { + content.append("未作答"); + } + content.append("\n"); + + boolean isCorrect = (userAnswer != null && userAnswer == correctIndex); + content.append("结果:").append(isCorrect ? "✓ 正确" : "✗ 错误").append("\n\n"); + } + + FileUtils.writeStringToFile(filename, content.toString()); + } + + public List getHistoryQuestions() throws IOException { + List historyQuestions = new ArrayList<>(); + File[] files = FileUtils.listFiles(HISTORY_DIR); + + Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified())); + + int count = 0; + for (File file : files) { + if (count++ >= 20) break; + + try { + String content = FileUtils.readFileToString(file.getAbsolutePath()); + String[] lines = content.split("\n"); + + for (int i = 0; i < lines.length; i++) { + if (lines[i].startsWith("【题目")) { + if (i + 1 < lines.length) { + String questionText = lines[i + 1].trim(); + if (!questionText.isEmpty()) { + historyQuestions.add(questionText); + } + } + } + } + } catch (IOException e) { + System.err.println("读取历史文件失败: " + file.getName()); + } + } + + return historyQuestions; + } + + // ==================== 业务逻辑方法(从 Model 移过来)==================== + + /** + * 计算答题历史的正确数 + */ + private int calculateCorrectCount(QuizHistory history) { + int count = 0; + List questions = history.getQuestions(); + List userAnswers = history.getUserAnswers(); + + for (int i = 0; i < questions.size(); i++) { + ChoiceQuestion question = questions.get(i); + Integer userAnswer = userAnswers.get(i); + + if (userAnswer != null && userAnswer == getCorrectAnswerIndex(question)) { + count++; + } + } + return count; + } + + /** + * 获取题目的正确答案索引 + */ + private int getCorrectAnswerIndex(ChoiceQuestion question) { + return question.getOptions().indexOf(question.getCorrectAnswer()); + } + + // ==================== 工具方法 ==================== + + private String sanitizeFilename(String filename) { + if (filename == null) { + return "unknown"; + } + return filename.replaceAll("[\\\\/:*?\"<>|]", "_"); + } +} \ No newline at end of file diff --git a/src/main/java/com/service/QuizService.java b/src/main/java/com/service/QuizService.java new file mode 100644 index 0000000..1ce5243 --- /dev/null +++ b/src/main/java/com/service/QuizService.java @@ -0,0 +1,432 @@ +package com.service; + +import com.model.*; +import com.service.question_generator.QuestionFactoryManager; +import java.io.IOException; +import java.util.*; + +/** + * 答题服务(包含所有答题相关业务逻辑) + */ +public class QuizService { + + private final FileIOService fileIOService; + private final UserService userService; + + private List currentQuestions; + private List userAnswers; + private int currentQuestionIndex; + + // ==================== 构造方法 ==================== + + public QuizService() { + this.fileIOService = new FileIOService(); + this.userService = new UserService(fileIOService); + this.currentQuestions = new ArrayList<>(); + this.userAnswers = new ArrayList<>(); + this.currentQuestionIndex = 0; + } + + public QuizService(FileIOService fileIOService, UserService userService) { + this.fileIOService = fileIOService; + this.userService = userService; + this.currentQuestions = new ArrayList<>(); + this.userAnswers = new ArrayList<>(); + this.currentQuestionIndex = 0; + } + + // ==================== 答题会话管理 ==================== + + public void startNewQuiz(User user, int questionCount) throws IOException { + currentQuestions.clear(); + userAnswers.clear(); + currentQuestionIndex = 0; + + Set historyQuestions = getRecentHistoryQuestions(); + + Grade grade = user.getGrade(); + currentQuestions = QuestionFactoryManager.generateQuestions( + grade, questionCount, historyQuestions + ); + + for (int i = 0; i < currentQuestions.size(); i++) { + userAnswers.add(null); + } + + System.out.println("✓ 已生成 " + currentQuestions.size() + " 道 " + grade + " 题目"); + } + + private Set getRecentHistoryQuestions() throws IOException { + List historyList = fileIOService.getHistoryQuestions(); + return new HashSet<>(historyList); + } + + // ==================== 题目访问 ==================== + + public ChoiceQuestion getCurrentQuestion() { + if (currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.size()) { + return currentQuestions.get(currentQuestionIndex); + } + return null; + } + + public ChoiceQuestion getQuestion(int index) { + if (index >= 0 && index < currentQuestions.size()) { + return currentQuestions.get(index); + } + return null; + } + + public List getAllQuestions() { + return new ArrayList<>(currentQuestions); + } + + public boolean nextQuestion() { + if (currentQuestionIndex < currentQuestions.size() - 1) { + currentQuestionIndex++; + return true; + } + return false; + } + + public boolean previousQuestion() { + if (currentQuestionIndex > 0) { + currentQuestionIndex--; + return true; + } + return false; + } + + public boolean goToQuestion(int index) { + if (index >= 0 && index < currentQuestions.size()) { + currentQuestionIndex = index; + return true; + } + return false; + } + + public int getCurrentQuestionIndex() { + return currentQuestionIndex; + } + + public int getTotalQuestions() { + return currentQuestions.size(); + } + + public boolean isFirstQuestion() { + return currentQuestionIndex == 0; + } + + public boolean isLastQuestion() { + return currentQuestionIndex == currentQuestions.size() - 1; + } + + // ==================== 答题操作 ==================== + + public boolean submitAnswer(int questionIndex, int optionIndex) { + if (questionIndex < 0 || questionIndex >= currentQuestions.size()) { + throw new IllegalArgumentException("题目索引无效: " + questionIndex); + } + + ChoiceQuestion question = currentQuestions.get(questionIndex); + + if (optionIndex < 0 || optionIndex >= question.getOptions().size()) { + throw new IllegalArgumentException("选项索引无效: " + optionIndex); + } + + userAnswers.set(questionIndex, optionIndex); + + return checkAnswer(question, optionIndex); + } + + public boolean submitCurrentAnswer(int optionIndex) { + return submitAnswer(currentQuestionIndex, optionIndex); + } + + public Integer getUserAnswer(int questionIndex) { + if (questionIndex >= 0 && questionIndex < userAnswers.size()) { + return userAnswers.get(questionIndex); + } + return null; + } + + public List getAllUserAnswers() { + return new ArrayList<>(userAnswers); + } + + public boolean isAllAnswered() { + for (Integer answer : userAnswers) { + if (answer == null) { + return false; + } + } + return true; + } + + public int getAnsweredCount() { + int count = 0; + for (Integer answer : userAnswers) { + if (answer != null) { + count++; + } + } + return count; + } + + // ==================== 成绩计算 ==================== + + public QuizResult calculateResult() { + int correctCount = 0; + int totalQuestions = currentQuestions.size(); + + for (int i = 0; i < totalQuestions; i++) { + ChoiceQuestion question = currentQuestions.get(i); + Integer userAnswer = userAnswers.get(i); + + if (userAnswer != null && checkAnswer(question, userAnswer)) { + correctCount++; + } + } + + int wrongCount = totalQuestions - correctCount; + int score = totalQuestions > 0 ? (correctCount * 100) / totalQuestions : 0; + + return new QuizResult(totalQuestions, correctCount, wrongCount, score); + } + + public List getCorrectQuestionIndices() { + List correctIndices = new ArrayList<>(); + + for (int i = 0; i < currentQuestions.size(); i++) { + ChoiceQuestion question = currentQuestions.get(i); + Integer userAnswer = userAnswers.get(i); + + if (userAnswer != null && checkAnswer(question, userAnswer)) { + correctIndices.add(i); + } + } + + return correctIndices; + } + + public List getWrongQuestionIndices() { + List wrongIndices = new ArrayList<>(); + + for (int i = 0; i < currentQuestions.size(); i++) { + ChoiceQuestion question = currentQuestions.get(i); + Integer userAnswer = userAnswers.get(i); + + if (userAnswer != null && !checkAnswer(question, userAnswer)) { + wrongIndices.add(i); + } + } + + return wrongIndices; + } + + public List getUnansweredQuestionIndices() { + List unansweredIndices = new ArrayList<>(); + + for (int i = 0; i < userAnswers.size(); i++) { + if (userAnswers.get(i) == null) { + unansweredIndices.add(i); + } + } + + return unansweredIndices; + } + + // ==================== 业务逻辑方法(从 Model 移过来)==================== + + /** + * 检查答案是否正确 + */ + public boolean checkAnswer(ChoiceQuestion question, int userAnswerIndex) { + if (userAnswerIndex < 0 || userAnswerIndex >= question.getOptions().size()) { + return false; + } + + Object userAnswer = question.getOptions().get(userAnswerIndex); + return question.getCorrectAnswer().equals(userAnswer); + } + + /** + * 获取题目的正确答案索引 + */ + public int getCorrectAnswerIndex(ChoiceQuestion question) { + return question.getOptions().indexOf(question.getCorrectAnswer()); + } + + /** + * 获取正确答案的字母形式 + */ + public String getCorrectAnswerLetter(ChoiceQuestion question) { + int index = getCorrectAnswerIndex(question); + if (index >= 0 && index < 4) { + return String.valueOf((char)('A' + index)); + } + return "未知"; + } + + /** + * 获取答题结果的正确率 + */ + public double getAccuracy(QuizResult result) { + if (result.getTotalQuestions() == 0) { + return 0.0; + } + return (result.getCorrectCount() * 100.0) / result.getTotalQuestions(); + } + + /** + * 判断是否及格 + */ + public boolean isPassed(QuizResult result) { + return result.getScore() >= 60; + } + + /** + * 获取评级 + */ + public String getGrade(QuizResult result) { + int score = result.getScore(); + if (score >= 90) return "优秀"; + if (score >= 80) return "良好"; + if (score >= 70) return "中等"; + if (score >= 60) return "及格"; + return "不及格"; + } + + /** + * 计算答题历史的正确数 + */ + public int getCorrectCount(QuizHistory history) { + int count = 0; + List questions = history.getQuestions(); + List userAnswers = history.getUserAnswers(); + + for (int i = 0; i < questions.size(); i++) { + ChoiceQuestion question = questions.get(i); + Integer userAnswer = userAnswers.get(i); + + if (userAnswer != null && checkAnswer(question, userAnswer)) { + count++; + } + } + return count; + } + + /** + * 计算答题历史的错误数 + */ + public int getWrongCount(QuizHistory history) { + return history.getQuestions().size() - getCorrectCount(history); + } + + // ==================== 格式化输出 ==================== + + public String formatQuestion(ChoiceQuestion question) { + StringBuilder sb = new StringBuilder(); + sb.append(question.getQuestionText()).append("\n"); + + List options = question.getOptions(); + for (int i = 0; i < options.size(); i++) { + sb.append((char)('A' + i)).append(". ").append(options.get(i)); + + if (i % 2 == 1) { + sb.append("\n"); + } else { + sb.append(" "); + } + } + + return sb.toString(); + } + + public String formatCurrentQuestion() { + ChoiceQuestion question = getCurrentQuestion(); + if (question == null) { + return "没有可用的题目"; + } + + StringBuilder sb = new StringBuilder(); + sb.append("第 ").append(currentQuestionIndex + 1) + .append(" / ").append(currentQuestions.size()).append(" 题\n"); + sb.append(formatQuestion(question)); + + return sb.toString(); + } + + public String formatQuestionWithAnswer(ChoiceQuestion question, Integer userAnswerIndex) { + StringBuilder sb = new StringBuilder(); + sb.append(question.getQuestionText()).append("\n"); + + List options = question.getOptions(); + int correctIndex = getCorrectAnswerIndex(question); + + for (int i = 0; i < options.size(); i++) { + sb.append((char)('A' + i)).append(". ").append(options.get(i)); + + if (i == correctIndex) { + sb.append(" ✓"); + } + + if (userAnswerIndex != null && i == userAnswerIndex) { + boolean isCorrect = checkAnswer(question, userAnswerIndex); + sb.append(isCorrect ? " [您的答案:正确]" : " [您的答案:错误]"); + } + + if (i % 2 == 1) { + sb.append("\n"); + } else { + sb.append(" "); + } + } + + return sb.toString(); + } + + public String formatResult(QuizResult result) { + StringBuilder sb = new StringBuilder(); + sb.append("\n========== 答题结束 ==========\n"); + sb.append("总题数:").append(result.getTotalQuestions()).append(" 题\n"); + sb.append("正确:").append(result.getCorrectCount()).append(" 题\n"); + sb.append("错误:").append(result.getWrongCount()).append(" 题\n"); + sb.append("得分:").append(result.getScore()).append(" 分\n"); + sb.append("正确率:").append(String.format("%.1f%%", getAccuracy(result))).append("\n"); + sb.append("评级:").append(getGrade(result)).append("\n"); + sb.append("===============================\n"); + + return sb.toString(); + } + + // ==================== 数据持久化 ==================== + + public void saveQuizHistory(User user) throws IOException { + QuizResult result = calculateResult(); + + QuizHistory history = new QuizHistory( + user.getUsername(), + new Date(), + currentQuestions, + userAnswers, + result.getScore() + ); + + fileIOService.saveQuizHistory(history); + + userService.updateUserStatistics(user, result.getScore()); + + System.out.println("✓ 答题记录已保存"); + } + + // ==================== Getters ==================== + + public List getCurrentQuestions() { + return new ArrayList<>(currentQuestions); + } + + public List getUserAnswers() { + return new ArrayList<>(userAnswers); + } +} \ No newline at end of file diff --git a/src/main/java/com/service/UserService.java b/src/main/java/com/service/UserService.java new file mode 100644 index 0000000..72c39c2 --- /dev/null +++ b/src/main/java/com/service/UserService.java @@ -0,0 +1,321 @@ +package com.service; + + +import com.model.Grade; +import com.model.User; +import com.util.PasswordValidator; + +import java.io.IOException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * 用户服务(包含所有用户相关业务逻辑) + */ +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 EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); + + // ==================== 构造方法 ==================== + + public UserService() { + this.fileIOService = new FileIOService(); + this.currentUser = null; + } + + public UserService(FileIOService fileIOService) { + this.fileIOService = fileIOService; + this.currentUser = null; + } + + // ==================== 用户注册 ==================== + + public User register(String username, String password, String email) throws IOException { + if (!validateUsername(username)) { + throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)"); + } + + if (fileIOService.isUsernameExists(username)) { + throw new IllegalArgumentException("用户名已存在!"); + } + + String passwordError = PasswordValidator.validatePassword(password); + if (passwordError != null) { + throw new IllegalArgumentException(passwordError); + } + + if (!validateEmail(email)) { + throw new IllegalArgumentException("邮箱格式错误!"); + } + + Grade grade = extractGradeFromUsername(username); + String hashedPassword = hashPassword(password); + + User user = new User(username, hashedPassword, email, grade); + fileIOService.saveUser(user); + + System.out.println("✓ 用户注册成功:" + username); + return user; + } + + // ==================== 用户登录 ==================== + + public User login(String username, String password) throws IOException { + User user = fileIOService.findUserByUsername(username); + + if (user == null) { + throw new IllegalArgumentException("用户名不存在!"); + } + + String hashedPassword = hashPassword(password); + if (!user.getPassword().equals(hashedPassword)) { + throw new IllegalArgumentException("密码错误!"); + } + + this.currentUser = user; + fileIOService.saveCurrentUser(user); + + System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "(" + getGradeDisplayName(user) + ")"); + return user; + } + + public User autoLogin() throws IOException { + User user = fileIOService.loadCurrentUser(); + + if (user != null) { + this.currentUser = user; + System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user)); + } + + return user; + } + + public void logout() { + if (currentUser != null) { + System.out.println("✓ " + getRealName(currentUser) + " 已退出登录"); + this.currentUser = null; + fileIOService.clearCurrentUser(); + } + } + + public User getCurrentUser() { + return currentUser; + } + + public boolean isLoggedIn() { + return currentUser != null; + } + + // ==================== 密码管理 ==================== + + public boolean changePassword(User user, String oldPassword, String newPassword) throws IOException { + String hashedOldPassword = hashPassword(oldPassword); + if (!user.getPassword().equals(hashedOldPassword)) { + throw new IllegalArgumentException("旧密码错误!"); + } + + String passwordError = PasswordValidator.validatePassword(newPassword); + if (passwordError != null) { + throw new IllegalArgumentException(passwordError); + } + + if (oldPassword.equals(newPassword)) { + throw new IllegalArgumentException("新密码不能与旧密码相同!"); + } + + String hashedNewPassword = hashPassword(newPassword); + user.setPassword(hashedNewPassword); + + fileIOService.saveUser(user); + + if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) { + this.currentUser = user; + fileIOService.saveCurrentUser(user); + } + + System.out.println("✓ 密码修改成功"); + return true; + } + + public boolean resetPassword(String username, String email, String newPassword) throws IOException { + User user = fileIOService.findUserByUsername(username); + + if (user == null) { + throw new IllegalArgumentException("用户名不存在!"); + } + + if (!user.getEmail().equals(email)) { + throw new IllegalArgumentException("邮箱验证失败!"); + } + + String passwordError = PasswordValidator.validatePassword(newPassword); + if (passwordError != null) { + throw new IllegalArgumentException(passwordError); + } + + String hashedNewPassword = hashPassword(newPassword); + user.setPassword(hashedNewPassword); + + fileIOService.saveUser(user); + + System.out.println("✓ 密码重置成功"); + return true; + } + + // ==================== 用户信息管理 ==================== + + public boolean updateEmail(User user, String newEmail) throws IOException { + if (!validateEmail(newEmail)) { + throw new IllegalArgumentException("邮箱格式错误!"); + } + + user.setEmail(newEmail); + fileIOService.saveUser(user); + + if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) { + this.currentUser = user; + fileIOService.saveCurrentUser(user); + } + + System.out.println("✓ 邮箱更新成功"); + return true; + } + + public void updateUserStatistics(User user, int score) throws IOException { + int oldTotal = user.getTotalQuizzes(); + double oldAverage = user.getAverageScore(); + + int newTotal = oldTotal + 1; + double newAverage = (oldAverage * oldTotal + score) / newTotal; + + user.setTotalQuizzes(newTotal); + user.setAverageScore(newAverage); + + fileIOService.saveUser(user); + } + + public List getAllUsers() throws IOException { + return fileIOService.loadAllUsers(); + } + + public User findUser(String username) throws IOException { + return fileIOService.findUserByUsername(username); + } + + // ==================== 业务逻辑方法(从 Model 移过来)==================== + + /** + * 从用户名提取真实姓名 + */ + 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 getGradeDisplayName(User user) { + if (user == null || user.getGrade() == null) { + return "未知"; + } + + switch (user.getGrade()) { + case ELEMENTARY: + return "小学"; + case MIDDLE: + return "初中"; + case HIGH: + return "高中"; + default: + return "未知"; + } + } + + /** + * 获取用户统计信息 + */ + public String getUserStatistics(User user) { + StringBuilder sb = new StringBuilder(); + sb.append("========== 用户统计 ==========\n"); + sb.append("用户名:").append(user.getUsername()).append("\n"); + sb.append("真实姓名:").append(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"); + sb.append("平均分:").append(String.format("%.1f", user.getAverageScore())).append(" 分\n"); + sb.append("注册时间:").append(user.getRegistrationDate()).append("\n"); + sb.append("=============================\n"); + + return sb.toString(); + } + + // ==================== 验证工具方法 ==================== + + 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()) { + return false; + } + + Matcher matcher = EMAIL_PATTERN.matcher(email); + 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); + } + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/QuestionFactoryManager.java b/src/main/java/com/service/question_generator/QuestionFactoryManager.java new file mode 100644 index 0000000..151c3ae --- /dev/null +++ b/src/main/java/com/service/question_generator/QuestionFactoryManager.java @@ -0,0 +1,97 @@ +package com.service.question_generator; + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.factory.ElementaryQuestionFactory; +import com.service.question_generator.factory.HighQuestionFactory; +import com.service.question_generator.factory.MiddleQuestionFactory; +import com.service.question_generator.factory.QuestionFactory; + +import java.util.*; + +/** + * 题目工厂管理器 + */ +public class QuestionFactoryManager { + + private static final Map factories = new HashMap<>(); + + static { + factories.put(Grade.ELEMENTARY, new ElementaryQuestionFactory()); + factories.put(Grade.MIDDLE, new MiddleQuestionFactory()); + factories.put(Grade.HIGH, new HighQuestionFactory()); + } + + /** + * 生成单个题目 + * @param grade 学段 + * @return 题目对象 + */ + public static ChoiceQuestion generateQuestion(Grade grade) { + QuestionFactory factory = factories.get(grade); + if (factory == null) { + throw new IllegalArgumentException("不支持的学段: " + grade); + } + return factory.createQuestion(); + } + + /** + * 批量生成题目(不去重) + * + * @param grade 学段 + * @param count 题目数量 + * @param historyQuestions + * @return 题目列表 + */ + public static List generateQuestions(Grade grade, int count, Set historyQuestions) { + return generateUniqueQuestions(grade, count, new HashSet<>()); + } + + /** + * 批量生成题目(带历史去重) + * @param grade 学段 + * @param count 题目数量 + * @param historyQuestions 历史题目文本集合 + * @return 题目列表 + */ + public static List generateUniqueQuestions( + Grade grade, int count, Set historyQuestions) { + + List questions = new ArrayList<>(); + Set currentQuestions = new HashSet<>(historyQuestions); + + int maxAttempts = count * 10; + int attempts = 0; + + QuestionFactory factory = factories.get(grade); + if (factory == null) { + throw new IllegalArgumentException("不支持的学段: " + grade); + } + + while (questions.size() < count && attempts < maxAttempts) { + ChoiceQuestion question = factory.createQuestion(); + String questionText = question.getQuestionText(); + + if (!currentQuestions.contains(questionText)) { + questions.add(question); + currentQuestions.add(questionText); + } + + attempts++; + } + + if (questions.size() < count) { + System.out.println("警告:只生成了 " + questions.size() + + " 道题,未达到要求的 " + count + " 道"); + } + + return questions; + } + + /** + * 注册新的工厂(支持扩展) + */ + public static void registerFactory(Grade grade, QuestionFactory factory) { + factories.put(grade, factory); + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/factory/ElementaryQuestionFactory.java b/src/main/java/com/service/question_generator/factory/ElementaryQuestionFactory.java new file mode 100644 index 0000000..40c341f --- /dev/null +++ b/src/main/java/com/service/question_generator/factory/ElementaryQuestionFactory.java @@ -0,0 +1,41 @@ +package com.service.question_generator.factory; + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.strategy.elementary.*; +import com.util.RandomUtils; +import com.service.question_generator.strategy.*; +import java.util.ArrayList; +import java.util.List; + +/** + * 小学题目工厂 + */ +public class ElementaryQuestionFactory implements QuestionFactory { + + private final List strategies; + + public ElementaryQuestionFactory() { + strategies = new ArrayList<>(); + // 注册所有小学题目生成策略 + strategies.add(new AdditionStrategy()); + strategies.add(new SubtractionStrategy()); + strategies.add(new MultiplicationStrategy()); + strategies.add(new DivisionStrategy()); + strategies.add(new ParenthesesAddStrategy()); + strategies.add(new ParenthesesMultiplyStrategy()); + } + + //重载接口方法 + @Override + public ChoiceQuestion createQuestion() { + // 从六个题型list中随机选择一个生成题目 + QuestionStrategy strategy = RandomUtils.randomChoice(strategies); + return strategy.generate(); + } + + @Override + public Grade getSupportedGrade() { + return Grade.ELEMENTARY; + } +} diff --git a/src/main/java/com/service/question_generator/factory/HighQuestionFactory.java b/src/main/java/com/service/question_generator/factory/HighQuestionFactory.java new file mode 100644 index 0000000..4357c0f --- /dev/null +++ b/src/main/java/com/service/question_generator/factory/HighQuestionFactory.java @@ -0,0 +1,41 @@ +package com.service.question_generator.factory; + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.strategy.QuestionStrategy; +import com.service.question_generator.strategy.high.CosStrategy; +import com.service.question_generator.strategy.high.SinStrategy; +import com.service.question_generator.strategy.high.TanStrategy; +import com.service.question_generator.strategy.high.TrigIdentityStrategy; +import com.util.RandomUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * 高中题目工厂 + */ +public class HighQuestionFactory implements QuestionFactory { + + private final List strategies; + + public HighQuestionFactory() { + strategies = new ArrayList<>(); + // 注册所有高中题目生成策略 + strategies.add(new SinStrategy()); + strategies.add(new CosStrategy()); + strategies.add(new TanStrategy()); + strategies.add(new TrigIdentityStrategy()); + } + + @Override + public ChoiceQuestion createQuestion() { + QuestionStrategy strategy = RandomUtils.randomChoice(strategies); + return strategy.generate(); + } + + @Override + public Grade getSupportedGrade() { + return Grade.HIGH; + } +} diff --git a/src/main/java/com/service/question_generator/factory/MiddleQuestionFactory.java b/src/main/java/com/service/question_generator/factory/MiddleQuestionFactory.java new file mode 100644 index 0000000..4352566 --- /dev/null +++ b/src/main/java/com/service/question_generator/factory/MiddleQuestionFactory.java @@ -0,0 +1,41 @@ +package com.service.question_generator.factory; + + + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.strategy.middle.*; +import com.util.RandomUtils; +import com.service.question_generator.strategy.QuestionStrategy; + +import java.util.ArrayList; +import java.util.List; + +/** + * 初中题目工厂 + */ +public class MiddleQuestionFactory implements QuestionFactory { + + private final List strategies; + + public MiddleQuestionFactory() { + strategies = new ArrayList<>(); + // 注册所有初中题目生成策略 + strategies.add(new SquareStrategy()); + strategies.add(new SquareAddStrategy()); + strategies.add(new SqrtStrategy()); + strategies.add(new SqrtAddStrategy()); + strategies.add(new MixedSquareSqrtStrategy()); + } + + @Override + public ChoiceQuestion createQuestion() { + QuestionStrategy strategy = RandomUtils.randomChoice(strategies); + return strategy.generate(); + } + + @Override + public Grade getSupportedGrade() { + return Grade.MIDDLE; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/factory/QuestionFactory.java b/src/main/java/com/service/question_generator/factory/QuestionFactory.java new file mode 100644 index 0000000..4878dc6 --- /dev/null +++ b/src/main/java/com/service/question_generator/factory/QuestionFactory.java @@ -0,0 +1,17 @@ +package com.service.question_generator.factory; + +import com.model.ChoiceQuestion; +import com.model.Grade; + + +/** + * 题目工厂接口 + */ +public interface QuestionFactory { + + //创建题目 + ChoiceQuestion createQuestion(); + + //获取工厂支持的学段 + Grade getSupportedGrade(); +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/AbstractQuestionStrategy.java b/src/main/java/com/service/question_generator/strategy/AbstractQuestionStrategy.java new file mode 100644 index 0000000..d3dc40d --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/AbstractQuestionStrategy.java @@ -0,0 +1,138 @@ +package com.service.question_generator.strategy; + +import com.model.Grade; +import com.util.RandomUtils; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * 抽象题目生成策略基类 + * 提供通用的选项生成方法,减少重复代码 + */ +public abstract class AbstractQuestionStrategy implements QuestionStrategy { + + protected final Grade grade; + + /** + * 构造方法 + * @param grade 学段 + */ + protected AbstractQuestionStrategy(Grade grade) { + this.grade = grade; + } + + /** + * 生成数值型选项(包含正确答案和3个干扰项) + * @param correctAnswer 正确答案 + * @return 打乱顺序后的选项列表 + */ + protected List generateNumericOptions(double correctAnswer) { + List options = new ArrayList<>(); + options.add(correctAnswer); + + // 根据答案大小动态调整干扰项范围 + int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3); + + // 生成3个干扰项 + for (int i = 0; i < 3; i++) { + double distractor; + int attempts = 0; + + do { + int offset = RandomUtils.nextInt(-range, range); + if (offset == 0) offset = (i + 1) * 3; + distractor = correctAnswer + offset; + attempts++; + } while ((options.contains(distractor) || distractor < 0) && attempts < 20); + + if (attempts >= 20) { + distractor = correctAnswer + (i + 1) * 5; + } + + options.add(distractor); + } + + // 确保有4个选项 + while (options.size() < 4) { + options.add(correctAnswer + RandomUtils.nextInt(5, 15)); + } + + Collections.shuffle(options); + return options; + } + + /** + * 生成数值型选项(包含一个常见错误答案) + * @param correctAnswer 正确答案 + * @param commonError 常见错误答案 + * @return 打乱顺序后的选项列表 + */ + protected List generateNumericOptionsWithCommonError( + double correctAnswer, double commonError) { + + List options = new ArrayList<>(); + options.add(correctAnswer); + + // 添加常见错误项(如果有效) + if (commonError != correctAnswer && commonError > 0 && !Double.isNaN(commonError)) { + options.add(commonError); + } + + int range = (int) Math.max(10, Math.abs(correctAnswer) * 0.3); + + // 填充其他干扰项 + while (options.size() < 4) { + double distractor; + int attempts = 0; + + do { + int offset = RandomUtils.nextInt(-range, range); + if (offset == 0) offset = 5; + distractor = correctAnswer + offset; + attempts++; + } while ((options.contains(distractor) || distractor < 0) && attempts < 20); + + if (attempts < 20) { + options.add(distractor); + } else { + options.add(correctAnswer + options.size() * 3); + } + } + + Collections.shuffle(options); + return options; + } + + /** + * 生成字符串型选项(用于三角函数等) + * @param correctAnswer 正确答案 + * @param allPossibleValues 所有可能的值 + * @return 打乱顺序后的选项列表 + */ + protected List generateStringOptions( + String correctAnswer, List allPossibleValues) { + + List options = new ArrayList<>(); + options.add(correctAnswer); + + List availableValues = new ArrayList<>(allPossibleValues); + availableValues.remove(correctAnswer); + + // 随机选择3个干扰项 + while (options.size() < 4 && !availableValues.isEmpty()) { + int randomIndex = RandomUtils.nextInt(0, availableValues.size() - 1); + String distractor = availableValues.get(randomIndex); + options.add(distractor); + availableValues.remove(randomIndex); + } + + while (options.size() < 4) { + options.add("未知"); + } + + Collections.shuffle(options); + return options; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/QuestionStrategy.java b/src/main/java/com/service/question_generator/strategy/QuestionStrategy.java new file mode 100644 index 0000000..e9306d3 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/QuestionStrategy.java @@ -0,0 +1,12 @@ +package com.service.question_generator.strategy; +import com.model.ChoiceQuestion; + +//题目生成题型接口 +public interface QuestionStrategy { + + //生成题目 + ChoiceQuestion generate(); + + //题型 + String getStrategyName(); +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/elementary/AdditionStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/AdditionStrategy.java new file mode 100644 index 0000000..913e104 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/AdditionStrategy.java @@ -0,0 +1,37 @@ +package com.service.question_generator.strategy.elementary; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 加法策略 + */ +public class AdditionStrategy extends AbstractQuestionStrategy { + + public AdditionStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int num1 = RandomUtils.nextInt(1, 30); + int num2 = RandomUtils.nextInt(1, 30); + + String questionText = num1 + " + " + num2; + double answer = num1 + num2; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "加法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/elementary/DivisionStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/DivisionStrategy.java new file mode 100644 index 0000000..f702d43 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/DivisionStrategy.java @@ -0,0 +1,39 @@ +package com.service.question_generator.strategy.elementary; + + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 除法策略(确保整除) + */ +public class DivisionStrategy extends AbstractQuestionStrategy { + + public DivisionStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int divisor = RandomUtils.nextInt(2, 10); + int quotient = RandomUtils.nextInt(1, 10); + int dividend = divisor * quotient; + + String questionText = dividend + " ÷ " + divisor; + double answer = quotient; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "除法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/elementary/MultiplicationStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/MultiplicationStrategy.java new file mode 100644 index 0000000..10a9266 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/MultiplicationStrategy.java @@ -0,0 +1,37 @@ +package com.service.question_generator.strategy.elementary; + + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 乘法策略 + */ +public class MultiplicationStrategy extends AbstractQuestionStrategy { + + public MultiplicationStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int factor1 = RandomUtils.nextInt(1, 12); + int factor2 = RandomUtils.nextInt(1, 12); + + String questionText = factor1 + " × " + factor2; + double answer = factor1 * factor2; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "乘法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java new file mode 100644 index 0000000..8050541 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesAddStrategy.java @@ -0,0 +1,40 @@ +package com.service.question_generator.strategy.elementary; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + + +import java.util.List; + +/** + * 带括号的加法乘法混合运算策略 + * 例如:(a + b) × c + */ +public class ParenthesesAddStrategy extends AbstractQuestionStrategy { + + public ParenthesesAddStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int num1 = RandomUtils.nextInt(1, 20); + int num2 = RandomUtils.nextInt(1, 20); + int num3 = RandomUtils.nextInt(2, 10); + + String questionText = "(" + num1 + " + " + num2 + ") × " + num3; + double answer = (num1 + num2) * num3; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "括号加法乘法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java new file mode 100644 index 0000000..d5e8792 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/ParenthesesMultiplyStrategy.java @@ -0,0 +1,41 @@ +package com.service.question_generator.strategy.elementary; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; +/** + * 带括号的减法乘法混合运算策略 + * 例如:(a - b) × c + */ +public class ParenthesesMultiplyStrategy extends AbstractQuestionStrategy { + + public ParenthesesMultiplyStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int num1 = RandomUtils.nextInt(1, 20); + int num2 = RandomUtils.nextInt(1, 20); + int num3 = RandomUtils.nextInt(2, 10); + + int larger = Math.max(num1, num2); + int smaller = Math.min(num1, num2); + + String questionText = "(" + larger + " - " + smaller + ") × " + num3; + double answer = (larger - smaller) * num3; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "括号减法乘法"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/elementary/SubtractionStrategy.java b/src/main/java/com/service/question_generator/strategy/elementary/SubtractionStrategy.java new file mode 100644 index 0000000..1170b06 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/elementary/SubtractionStrategy.java @@ -0,0 +1,40 @@ +package com.service.question_generator.strategy.elementary; + +import com.model.ChoiceQuestion; +import com.model.Grade; +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 减法策略 + */ +public class SubtractionStrategy extends AbstractQuestionStrategy { + + public SubtractionStrategy() { + super(Grade.ELEMENTARY); + } + + @Override + public ChoiceQuestion generate() { + int num1 = RandomUtils.nextInt(1, 30); + int num2 = RandomUtils.nextInt(1, 30); + + // 确保结果为正数 + int larger = Math.max(num1, num2); + int smaller = Math.min(num1, num2); + + String questionText = larger + " - " + smaller; + double answer = larger - smaller; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "减法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/high/CosStrategy.java b/src/main/java/com/service/question_generator/strategy/high/CosStrategy.java new file mode 100644 index 0000000..6aba6ee --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/high/CosStrategy.java @@ -0,0 +1,57 @@ +package com.service.question_generator.strategy.high; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +import java.util.*; + +/** + * 余弦函数策略 + * 例如:cos(45°) + */ +public class CosStrategy extends AbstractQuestionStrategy { + + // 特殊角的余弦值表 + private static final Map COS_VALUES = new HashMap<>(); + + static { + COS_VALUES.put(0, "1"); + COS_VALUES.put(30, "√3/2"); + COS_VALUES.put(45, "√2/2"); + COS_VALUES.put(60, "1/2"); + COS_VALUES.put(90, "0"); + COS_VALUES.put(120, "-1/2"); + COS_VALUES.put(135, "-√2/2"); + COS_VALUES.put(150, "-√3/2"); + COS_VALUES.put(180, "-1"); + } + + public CosStrategy() { + super(Grade.HIGH); + } + + @Override + public ChoiceQuestion generate() { + List angles = new ArrayList<>(COS_VALUES.keySet()); + int randomIndex = RandomUtils.nextInt(0, angles.size() - 1); + int angle = angles.get(randomIndex); + + String questionText = "cos(" + angle + "°) = ?"; + String correctAnswer = COS_VALUES.get(angle); + + List allValues = new ArrayList<>(COS_VALUES.values()); + List options = generateStringOptions(correctAnswer, allValues); + + return new ChoiceQuestion(questionText, correctAnswer, options, grade); + } + + @Override + public String getStrategyName() { + return "余弦函数"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/high/SinStrategy.java b/src/main/java/com/service/question_generator/strategy/high/SinStrategy.java new file mode 100644 index 0000000..c0fec5f --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/high/SinStrategy.java @@ -0,0 +1,56 @@ +package com.service.question_generator.strategy.high; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; +import java.util.*; + +import java.util.List; + +/** + * 正弦函数策略 + * 例如:sin(30°) + */ +public class SinStrategy extends AbstractQuestionStrategy { + + // 特殊角的正弦值表 + private static final Map SIN_VALUES = new HashMap<>(); + + static { + SIN_VALUES.put(0, "0"); + SIN_VALUES.put(30, "1/2"); + SIN_VALUES.put(45, "√2/2"); + SIN_VALUES.put(60, "√3/2"); + SIN_VALUES.put(90, "1"); + SIN_VALUES.put(120, "√3/2"); + SIN_VALUES.put(135, "√2/2"); + SIN_VALUES.put(150, "1/2"); + SIN_VALUES.put(180, "0"); + } + + public SinStrategy() { + super(Grade.HIGH); + } + + @Override + public ChoiceQuestion generate() { + List angles = new ArrayList<>(SIN_VALUES.keySet()); + int randomIndex = RandomUtils.nextInt(0, angles.size() - 1); + int angle = angles.get(randomIndex); + + String questionText = "sin(" + angle + "°) = ?"; + String correctAnswer = SIN_VALUES.get(angle); + + List allValues = new ArrayList<>(SIN_VALUES.values()); + List options = generateStringOptions(correctAnswer, allValues); + + return new ChoiceQuestion(questionText, correctAnswer, options, grade); + } + + @Override + public String getStrategyName() { + return "正弦函数"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/high/TanStrategy.java b/src/main/java/com/service/question_generator/strategy/high/TanStrategy.java new file mode 100644 index 0000000..4e0a26a --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/high/TanStrategy.java @@ -0,0 +1,56 @@ +package com.service.question_generator.strategy.high; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +import java.util.*; + +/** + * 正切函数策略 + * 例如:tan(45°) + */ +public class TanStrategy extends AbstractQuestionStrategy { + + // 特殊角的正切值表 + private static final Map TAN_VALUES = new HashMap<>(); + + static { + TAN_VALUES.put(0, "0"); + TAN_VALUES.put(30, "√3/3"); + TAN_VALUES.put(45, "1"); + TAN_VALUES.put(60, "√3"); + TAN_VALUES.put(120, "-√3"); + TAN_VALUES.put(135, "-1"); + TAN_VALUES.put(150, "-√3/3"); + TAN_VALUES.put(180, "0"); + } + + public TanStrategy() { + super(Grade.HIGH); + } + + @Override + public ChoiceQuestion generate() { + List angles = new ArrayList<>(TAN_VALUES.keySet()); + int randomIndex = RandomUtils.nextInt(0, angles.size() - 1); + int angle = angles.get(randomIndex); + + String questionText = "tan(" + angle + "°) = ?"; + String correctAnswer = TAN_VALUES.get(angle); + + List allValues = new ArrayList<>(TAN_VALUES.values()); + List options = generateStringOptions(correctAnswer, allValues); + + return new ChoiceQuestion(questionText, correctAnswer, options, grade); + } + + @Override + public String getStrategyName() { + return "正切函数"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/high/TrigIdentityStrategy.java b/src/main/java/com/service/question_generator/strategy/high/TrigIdentityStrategy.java new file mode 100644 index 0000000..0af4b9e --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/high/TrigIdentityStrategy.java @@ -0,0 +1,41 @@ +package com.service.question_generator.strategy.high; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +import java.util.*; + +/** + * 三角恒等式策略 + * 例如:sin²(θ) + cos²(θ) = ? + */ +public class TrigIdentityStrategy extends AbstractQuestionStrategy { + + public TrigIdentityStrategy() { + super(Grade.HIGH); + } + + @Override + public ChoiceQuestion generate() { + int[] angles = {30, 45, 60, 90}; + int angle = angles[RandomUtils.nextInt(0, angles.length - 1)]; + + String questionText = "sin²(" + angle + "°) + cos²(" + angle + "°) = ?"; + String correctAnswer = "1"; // 三角恒等式,永远等于1 + + List allValues = Arrays.asList("1", "0", "2", "√2", "1/2"); + List options = generateStringOptions(correctAnswer, allValues); + + return new ChoiceQuestion(questionText, correctAnswer, options, grade); + } + + @Override + public String getStrategyName() { + return "三角恒等式"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java b/src/main/java/com/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java new file mode 100644 index 0000000..739e2b3 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/middle/MixedSquareSqrtStrategy.java @@ -0,0 +1,40 @@ +package com.service.question_generator.strategy.middle; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 平方开方混合运算策略 + * 例如:√49 + 3² + */ +public class MixedSquareSqrtStrategy extends AbstractQuestionStrategy { + + public MixedSquareSqrtStrategy() { + super(Grade.MIDDLE); + } + + @Override + public ChoiceQuestion generate() { + int sqrtRoot = RandomUtils.nextInt(2, 8); + int sqrtNum = sqrtRoot * sqrtRoot; + + int squareBase = RandomUtils.nextInt(2, 6); + + String questionText = "√" + sqrtNum + " + " + squareBase + "²"; + double answer = sqrtRoot + (squareBase * squareBase); + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "平方开方混合"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/middle/SqrtAddStrategy.java b/src/main/java/com/service/question_generator/strategy/middle/SqrtAddStrategy.java new file mode 100644 index 0000000..ed32748 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/middle/SqrtAddStrategy.java @@ -0,0 +1,39 @@ +package com.service.question_generator.strategy.middle; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 开方加法混合策略 + * 例如:√49 + 5 + */ +public class SqrtAddStrategy extends AbstractQuestionStrategy { + + public SqrtAddStrategy() { + super(Grade.MIDDLE); + } + + @Override + public ChoiceQuestion generate() { + int root = RandomUtils.nextInt(2, 10); + int num = root * root; + int add = RandomUtils.nextInt(1, 20); + + String questionText = "√" + num + " + " + add; + double answer = root + add; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "开方加法"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/middle/SqrtStrategy.java b/src/main/java/com/service/question_generator/strategy/middle/SqrtStrategy.java new file mode 100644 index 0000000..8cc2eff --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/middle/SqrtStrategy.java @@ -0,0 +1,39 @@ +package com.service.question_generator.strategy.middle; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; +/** + * 开方运算策略(完全平方数) + * 例如:√49 + */ +public class SqrtStrategy extends AbstractQuestionStrategy { + + public SqrtStrategy() { + super(Grade.MIDDLE); + } + + @Override + public ChoiceQuestion generate() { + int root = RandomUtils.nextInt(2, 12); + int num = root * root; // 完全平方数 + + String questionText = "√" + num; + double answer = root; + + // 常见错误:把开方当成除以2 + double commonError = num / 2.0; + List options = generateNumericOptionsWithCommonError(answer, commonError); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "开方"; + } +} diff --git a/src/main/java/com/service/question_generator/strategy/middle/SquareAddStrategy.java b/src/main/java/com/service/question_generator/strategy/middle/SquareAddStrategy.java new file mode 100644 index 0000000..51e5516 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/middle/SquareAddStrategy.java @@ -0,0 +1,37 @@ +package com.service.question_generator.strategy.middle; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; +/** + * 平方加法混合策略 + * 例如:5² + 10 + */ +public class SquareAddStrategy extends AbstractQuestionStrategy { + + public SquareAddStrategy() { + super(Grade.MIDDLE); + } + + @Override + public ChoiceQuestion generate() { + int base = RandomUtils.nextInt(2, 10); + int add = RandomUtils.nextInt(1, 20); + + String questionText = base + "² + " + add; + double answer = base * base + add; + + List options = generateNumericOptions(answer); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "平方加法"; + } +} \ No newline at end of file diff --git a/src/main/java/com/service/question_generator/strategy/middle/SquareStrategy.java b/src/main/java/com/service/question_generator/strategy/middle/SquareStrategy.java new file mode 100644 index 0000000..8cf8b05 --- /dev/null +++ b/src/main/java/com/service/question_generator/strategy/middle/SquareStrategy.java @@ -0,0 +1,39 @@ +package com.service.question_generator.strategy.middle; + +import com.model.ChoiceQuestion; +import com.model.Grade; + +import com.service.question_generator.strategy.AbstractQuestionStrategy; +import com.util.RandomUtils; + +import java.util.List; + +/** + * 平方运算策略 + * 例如:5² + */ +public class SquareStrategy extends AbstractQuestionStrategy { + + public SquareStrategy() { + super(Grade.MIDDLE); + } + + @Override + public ChoiceQuestion generate() { + int num = RandomUtils.nextInt(1, 15); + + String questionText = num + "²"; + double answer = num * num; + + // 常见错误:把平方当成乘以2 + double commonError = num * 2; + List options = generateNumericOptionsWithCommonError(answer, commonError); + + return new ChoiceQuestion(questionText, answer, options, grade); + } + + @Override + public String getStrategyName() { + return "平方"; + } +} diff --git a/src/main/java/com/util/FileUtils.java b/src/main/java/com/util/FileUtils.java index d4daf9e..186a2cc 100644 --- a/src/main/java/com/util/FileUtils.java +++ b/src/main/java/com/util/FileUtils.java @@ -1,15 +1,22 @@ package com.util; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; import java.io.*; import java.nio.charset.StandardCharsets; import java.nio.file.*; +import java.lang.reflect.Type; /** * 文件操作工具类 - * 提供文件读写、目录创建等常用操作 + * 提供文件读写、目录创建、JSON序列化等常用操作 */ public class FileUtils { + private static final Gson gson = new GsonBuilder() + .setPrettyPrinting() // 格式化输出JSON + .create(); + /** * 读取文件内容为字符串 * @param filePath 文件路径 @@ -108,4 +115,70 @@ public class FileUtils { public static long getFileSize(String filePath) throws IOException { return Files.size(Paths.get(filePath)); } + + // ==================== JSON 操作方法 ==================== + + /** + * 将对象保存为JSON文件 + * @param data 要保存的对象 + * @param filePath 文件路径 + * @throws IOException 保存失败时抛出 + */ + public static void saveAsJson(Object data, String filePath) throws IOException { + String json = gson.toJson(data); + writeStringToFile(filePath, json); + } + + /** + * 从JSON文件读取对象 + * @param filePath 文件路径 + * @param classOfT 对象的Class类型 + * @return 反序列化后的对象 + * @throws IOException 读取失败时抛出 + */ + public static T readJsonToObject(String filePath, Class classOfT) throws IOException { + String json = readFileToString(filePath); + return gson.fromJson(json, classOfT); + } + + /** + * 从JSON文件读取对象(支持泛型) + * @param filePath 文件路径 + * @param typeOfT 对象的Type类型(用于泛型) + * @return 反序列化后的对象 + * @throws IOException 读取失败时抛出 + */ + public static T readJsonToObject(String filePath, Type typeOfT) throws IOException { + String json = readFileToString(filePath); + return gson.fromJson(json, typeOfT); + } + + /** + * 将对象转换为JSON字符串 + * @param data 要转换的对象 + * @return JSON字符串 + */ + public static String toJson(Object data) { + return gson.toJson(data); + } + + /** + * 将JSON字符串转换为对象 + * @param json JSON字符串 + * @param classOfT 对象的Class类型 + * @return 反序列化后的对象 + */ + public static T fromJson(String json, Class classOfT) { + return gson.fromJson(json, classOfT); + } + + /** + * 将JSON字符串转换为对象(支持泛型) + * @param json JSON字符串 + * @param typeOfT 对象的Type类型 + * @return 反序列化后的对象 + */ + public static T fromJson(String json, Type typeOfT) { + return gson.fromJson(json, typeOfT); + } } \ No newline at end of file diff --git a/src/main/java/com/util/PasswordValidator.java b/src/main/java/com/util/PasswordValidator.java index 40a26b5..1ab48e7 100644 --- a/src/main/java/com/util/PasswordValidator.java +++ b/src/main/java/com/util/PasswordValidator.java @@ -2,6 +2,7 @@ package com.util; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; /** * 密码验证和加密工具类 @@ -9,33 +10,123 @@ import java.security.NoSuchAlgorithmException; */ public class PasswordValidator { + // 密码长度限制 + private static final int MIN_LENGTH = 6; + private static final int MAX_LENGTH = 20; // 改为20位,更安全 + + // 用于生成随机注册码的字符集 + private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ"; + private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz"; + private static final String DIGITS = "0123456789"; + private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS; + + // 使用 SecureRandom 替代 Math.random(),更安全 + private static final SecureRandom random = new SecureRandom(); + + // ==================== 密码验证 ==================== + /** - * 验证密码格式:6-10位,包含字母和数字 + * 验证密码格式(返回详细错误信息) + * + * @param password 待验证的密码 + * @return 如果密码有效返回 null,否则返回错误信息 + */ + public static String validatePassword(String password) { + if (password == null || password.isEmpty()) { + return "密码不能为空!"; + } + + if (password.length() < MIN_LENGTH) { + return "密码长度不能少于 " + MIN_LENGTH + " 位!"; + } + + if (password.length() > MAX_LENGTH) { + return "密码长度不能超过 " + MAX_LENGTH + " 位!"; + } + + if (password.contains(" ")) { + return "密码不能包含空格!"; + } + + // 检查是否包含字母 + boolean hasLetter = password.matches(".*[a-zA-Z].*"); + + // 检查是否包含数字 + boolean hasDigit = password.matches(".*\\d.*"); + + if (!hasLetter) { + return "密码必须包含字母!"; + } + + if (!hasDigit) { + return "密码必须包含数字!"; + } + + return null; // 验证通过 + } + + /** + * 验证密码格式(简单版本) + * * @param password 待验证的密码 * @return true表示格式正确 */ public static boolean isValid(String password) { - if (password == null || password.length() < 6 || password.length() > 10) { - return false; + return validatePassword(password) == null; + } + + /** + * 检查密码强度等级 + * + * @param password 密码 + * @return 强度等级:弱、中、强 + */ + public static String getPasswordStrength(String password) { + if (password == null || password.length() < MIN_LENGTH) { + return "弱"; } - boolean hasLetter = password.matches("^(?=.*[a-z])(?=.*[A-Z]).*$"); - boolean hasDigit = password.matches(".*\\d.*"); + int score = 0; + + // 长度加分 + if (password.length() >= 8) score++; + if (password.length() >= 12) score++; - return hasLetter && hasDigit; + // 包含小写字母 + 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 "强"; } + // ==================== 密码加密 ==================== + /** * 使用SHA-256加密密码 + * * @param password 明文密码 * @return 加密后的密码(16进制字符串) */ public static String encrypt(String password) { + if (password == null) { + throw new IllegalArgumentException("密码不能为null"); + } + try { MessageDigest digest = MessageDigest.getInstance("SHA-256"); byte[] hash = digest.digest(password.getBytes()); - StringBuilder hexString = new StringBuilder(); + StringBuilder hexString = new StringBuilder(); for (byte b : hash) { String hex = Integer.toHexString(0xff & b); if (hex.length() == 1) { @@ -52,32 +143,58 @@ public class PasswordValidator { /** * 验证密码是否匹配 + * * @param plainPassword 明文密码 * @param encryptedPassword 加密后的密码 * @return true表示匹配 */ public static boolean matches(String plainPassword, String encryptedPassword) { + if (plainPassword == null || encryptedPassword == null) { + return false; + } + return encrypt(plainPassword).equals(encryptedPassword); } + // ==================== 注册码生成 ==================== + /** * 生成6-10位随机注册码(包含字母和数字) + * * @return 注册码 */ public static String generateRegistrationCode() { - String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - int length = 6 + (int) (Math.random() * 5); // 6-10位 + return generateRegistrationCode(MIN_LENGTH, 10); + } - StringBuilder code = new StringBuilder(); + /** + * 生成指定长度范围的随机注册码 + * + * @param minLen 最小长度 + * @param maxLen 最大长度 + * @return 注册码 + */ + public static String generateRegistrationCode(int minLen, int maxLen) { + if (minLen < 4 || maxLen < minLen) { + throw new IllegalArgumentException("长度参数无效"); + } + + int length = minLen + random.nextInt(maxLen - minLen + 1); + + StringBuilder code = new StringBuilder(length); + + // 确保至少有一个大写字母 + code.append(UPPERCASE.charAt(random.nextInt(UPPERCASE.length()))); + + // 确保至少有一个小写字母 + code.append(LOWERCASE.charAt(random.nextInt(LOWERCASE.length()))); - // 确保至少有一个字母 - code.append(chars.charAt((int) (Math.random() * 52))); // 确保至少有一个数字 - code.append(chars.charAt(52 + (int) (Math.random() * 10))); + code.append(DIGITS.charAt(random.nextInt(DIGITS.length()))); // 填充剩余字符 - for (int i = 2; i < length; i++) { - code.append(chars.charAt((int) (Math.random() * chars.length()))); + for (int i = 3; i < length; i++) { + code.append(ALL_CHARS.charAt(random.nextInt(ALL_CHARS.length()))); } // 打乱字符顺序 @@ -85,18 +202,128 @@ public class PasswordValidator { } /** - * 打乱字符串 + * 生成固定长度的随机密码 + * + * @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 = (int) (Math.random() * (i + 1)); + 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/test/java/TestMain.java b/src/test/java/TestMain.java index d4c7da5..b002dde 100644 --- a/src/test/java/TestMain.java +++ b/src/test/java/TestMain.java @@ -1,2 +1,507 @@ + + +import com.model.*; +import com.service.*; +import com.service.question_generator.QuestionFactoryManager; +import com.util.PasswordValidator; + +import java.io.IOException; +import java.util.List; + +/** + * 完整测试类 + * 测试项目的各个功能模块 + */ 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(); + String testUsername = "小学-张三测试"; + + // 测试3.1: 用户注册 + try { + User user = userService.register(testUsername, "Test123456", "zhangsan@test.com"); + test("用户注册", + user != null && user.getUsername().equals(testUsername), + "用户注册应该成功"); + } catch (Exception e) { + test("用户注册", false, "失败:" + e.getMessage()); + } + + // 测试3.2: 重复注册检测 + try { + userService.register(testUsername, "Test123456", "test@test.com"); + test("重复注册检测", false, "应该抛出异常"); + } catch (IllegalArgumentException e) { + test("重复注册检测", + e.getMessage().contains("已存在"), + "应该检测到用户名已存在"); + } + + // 测试3.3: 用户登录 + try { + User user = userService.login(testUsername, "Test123456"); + test("用户登录", + user != null && userService.isLoggedIn(), + "用户登录应该成功"); + } catch (Exception e) { + test("用户登录", false, "失败:" + e.getMessage()); + } + + // 测试3.4: 错误密码登录 + try { + userService.logout(); // 先退出 + userService.login(testUsername, "WrongPassword"); + test("错误密码登录", false, "应该抛出异常"); + } catch (IllegalArgumentException e) { + test("错误密码登录", + e.getMessage().contains("密码错误"), + "应该检测到密码错误"); + } + + // 测试3.5: 获取当前用户 + try { + userService.login(testUsername, "Test123456"); + User current = userService.getCurrentUser(); + test("获取当前用户", + current != null && current.getUsername().equals(testUsername), + "应该返回当前登录用户"); + } catch (Exception e) { + test("获取当前用户", false, "失败:" + e.getMessage()); + } + + // 测试3.6: 提取真实姓名 + User user = userService.getCurrentUser(); + String realName = userService.getRealName(user); + test("提取真实姓名", + realName.equals("张三测试"), + "应该正确提取真实姓名,实际:" + realName); + + // 测试3.7: 获取学段显示名 + String gradeName = userService.getGradeDisplayName(user); + test("获取学段显示名", + gradeName.equals("小学"), + "应该返回'小学',实际:" + gradeName); + + // 测试3.8: 退出登录 + userService.logout(); + test("退出登录", + !userService.isLoggedIn(), + "退出后应该未登录状态"); + + System.out.println(); + } + + // ==================== 4. 测试题目生成 ==================== + + private static void testQuestionGeneration() { + System.out.println("【测试4】题目生成"); + System.out.println("----------------------------------------"); + + // 测试4.1: 生成小学题目 + try { + ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); + test("生成小学题目", + q != null && q.getQuestionText() != null && q.getOptions().size() == 4, + "应该生成有效的小学题目"); + + // 显示示例题目 + System.out.println(" 示例题目:" + q.getQuestionText()); + System.out.println(" 选项数量:" + q.getOptions().size()); + } catch (Exception e) { + test("生成小学题目", false, "失败:" + e.getMessage()); + } + + // 测试4.2: 生成初中题目 + try { + ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.MIDDLE); + test("生成初中题目", + q != null && q.getQuestionText() != null, + "应该生成有效的初中题目"); + + System.out.println(" 示例题目:" + q.getQuestionText()); + } catch (Exception e) { + test("生成初中题目", false, "失败:" + e.getMessage()); + } + + // 测试4.3: 生成高中题目 + try { + ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.HIGH); + test("生成高中题目", + q != null && q.getQuestionText() != null, + "应该生成有效的高中题目"); + + System.out.println(" 示例题目:" + q.getQuestionText()); + } catch (Exception e) { + test("生成高中题目", false, "失败:" + e.getMessage()); + } + + // 测试4.4: 批量生成题目 + try { + List questions = QuestionFactoryManager.generateQuestions(Grade.ELEMENTARY, 10, historyQuestions); + test("批量生成题目", + questions.size() == 10, + "应该生成10道题目,实际:" + questions.size()); + } catch (Exception e) { + test("批量生成题目", false, "失败:" + e.getMessage()); + } + + // 测试4.5: 题目去重 + try { + ChoiceQuestion q1 = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); + ChoiceQuestion q2 = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); + + // 理论上不应该完全相同(概率极低) + boolean different = !q1.getQuestionText().equals(q2.getQuestionText()); + test("题目去重机制", + different, + "连续生成的题目应该不同(高概率)"); + } 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: 注册新用户..."); + User user = userService.register("初中-王五", "Test123456", "wangwu@test.com"); + test("完整流程-注册", user != null, "用户注册成功"); + + // 步骤2: 登录 + System.out.println("步骤2: 用户登录..."); + userService.login("初中-王五", "Test123456"); + 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("初中-王五"); + 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 -- 2.34.1 From 6dfb6e29e524694e4dfee80a886cdf3a5bed9e1c Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 21:44:29 +0800 Subject: [PATCH 11/28] for merge --- src/main/java/com/mathquiz/Main.java | 0 .../com/mathquiz/model/ChoiceQuestion.java | 141 ------------------ src/main/java/com/mathquiz/model/Grade.java | 27 ---- src/main/java/com/mathquiz/model/User.java | 52 ------- .../com/mathquiz/ui/GradeSelectPanel.java | 58 +++++++ src/main/java/com/mathquiz/ui/LoginPanel.java | 44 ++++++ src/main/java/com/mathquiz/ui/MainWindow.java | 77 ++++++++++ .../com/mathquiz/ui/PasswordModifyPanel.java | 46 ++++++ src/main/java/com/mathquiz/ui/QuizPanel.java | 67 +++++++++ .../java/com/mathquiz/ui/RegisterPanel.java | 80 ++++++++++ .../java/com/mathquiz/ui/ResultPanel.java | 32 ++++ .../java/com/mathquiz/util/EmailUtil.java | 109 -------------- .../java/com/mathquiz/util/FileUtils.java | 111 -------------- .../com/mathquiz/util/PasswordValidator.java | 102 ------------- .../java/com/mathquiz/util/RandomUtils.java | 69 --------- 15 files changed, 404 insertions(+), 611 deletions(-) delete mode 100644 src/main/java/com/mathquiz/Main.java delete mode 100644 src/main/java/com/mathquiz/model/ChoiceQuestion.java delete mode 100644 src/main/java/com/mathquiz/model/Grade.java delete mode 100644 src/main/java/com/mathquiz/model/User.java create mode 100644 src/main/java/com/mathquiz/ui/GradeSelectPanel.java create mode 100644 src/main/java/com/mathquiz/ui/LoginPanel.java create mode 100644 src/main/java/com/mathquiz/ui/MainWindow.java create mode 100644 src/main/java/com/mathquiz/ui/PasswordModifyPanel.java create mode 100644 src/main/java/com/mathquiz/ui/QuizPanel.java create mode 100644 src/main/java/com/mathquiz/ui/RegisterPanel.java create mode 100644 src/main/java/com/mathquiz/ui/ResultPanel.java delete mode 100644 src/main/java/com/mathquiz/util/EmailUtil.java delete mode 100644 src/main/java/com/mathquiz/util/FileUtils.java delete mode 100644 src/main/java/com/mathquiz/util/PasswordValidator.java delete mode 100644 src/main/java/com/mathquiz/util/RandomUtils.java diff --git a/src/main/java/com/mathquiz/Main.java b/src/main/java/com/mathquiz/Main.java deleted file mode 100644 index e69de29..0000000 diff --git a/src/main/java/com/mathquiz/model/ChoiceQuestion.java b/src/main/java/com/mathquiz/model/ChoiceQuestion.java deleted file mode 100644 index e3dcb16..0000000 --- a/src/main/java/com/mathquiz/model/ChoiceQuestion.java +++ /dev/null @@ -1,141 +0,0 @@ -package com.mathquiz.model; - -import java.util.List; -import java.util.Objects; -import java.util.UUID; - -/** - * 选择题模型类 - * 表示一道完整的数学选择题,包含题干、选项、正确答案等信息 - */ -public class ChoiceQuestion { - - /** - * 题目唯一ID(UUID生成) - */ - private String questionId; - - /** - * 所属学段(小学 / 初中 / 高中) - */ - private Grade grade; - - /** - * 题干内容,例如:"2 + 3 × (4 - 1) = ?" - */ - private String questionContent; - - /** - * 选项列表,固定4个选项,顺序已随机打乱 - * 格式示例:["11", "10", "13", "9"] - */ - private List options; - - /** - * 正确答案标识,取值为 "A"、"B"、"C" 或 "D" - */ - private String correctAnswer; - - // ---------------- 构造函数 ---------------- - - /** - * 默认构造函数(用于 JSON 反序列化) - */ - public ChoiceQuestion() {} - - /** - * 全参构造函数 - */ - public ChoiceQuestion(String questionId, Grade grade, String questionContent, - List options, String correctAnswer) { - this.questionId = questionId; - this.grade = grade; - this.questionContent = questionContent; - this.options = options; - this.correctAnswer = correctAnswer; - } - - // ---------------- 静态工厂方法 ---------------- - - /** - * 创建一道新题目,自动分配 UUID 作为 questionId - * - * @param grade 年级 - * @param questionContent 题干 - * @param options 4个选项(顺序应已打乱) - * @param correctAnswer 正确答案("A"-"D") - * @return 新的 ChoiceQuestion 实例 - */ - public static ChoiceQuestion of(Grade grade, String questionContent, - List options, String correctAnswer) { - return new ChoiceQuestion(UUID.randomUUID().toString(), grade, questionContent, options, correctAnswer); - } - - // ---------------- Getter & Setter ---------------- - - public String getQuestionId() { - return questionId; - } - - public void setQuestionId(String questionId) { - this.questionId = questionId; - } - - public Grade getGrade() { - return grade; - } - - public void setGrade(Grade grade) { - this.grade = grade; - } - - public String getQuestionContent() { - return questionContent; - } - - public void setQuestionContent(String questionContent) { - this.questionContent = questionContent; - } - - public List getOptions() { - return options; - } - - public void setOptions(List options) { - this.options = options; - } - - public String getCorrectAnswer() { - return correctAnswer; - } - - public void setCorrectAnswer(String correctAnswer) { - this.correctAnswer = correctAnswer; - } - - // ---------------- Object 方法 ---------------- - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ChoiceQuestion that = (ChoiceQuestion) o; - return Objects.equals(questionId, that.questionId); - } - - @Override - public int hashCode() { - return Objects.hash(questionId); - } - - @Override - public String toString() { - return "ChoiceQuestion{" + - "questionId='" + questionId + '\'' + - ", grade=" + grade + - ", questionContent='" + questionContent + '\'' + - ", options=" + options + - ", correctAnswer='" + correctAnswer + '\'' + - '}'; - } -} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/model/Grade.java b/src/main/java/com/mathquiz/model/Grade.java deleted file mode 100644 index ede720f..0000000 --- a/src/main/java/com/mathquiz/model/Grade.java +++ /dev/null @@ -1,27 +0,0 @@ -package com.mathquiz.model; - -/** - * 年级枚举,表示小学、初中、高中三个学段 - */ - -public enum Grade { - PRIMARY("小学", 1), - JUNIOR("初中", 2), - SENIOR("高中", 3); - - private final String displayName; - private final int level; - - Grade(String displayName, int level) { - this.displayName = displayName; - this.level = level; - } - - public String getDisplayName() { - return displayName; - } - - public int getLevel() { - return level; - } -} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/model/User.java b/src/main/java/com/mathquiz/model/User.java deleted file mode 100644 index d519167..0000000 --- a/src/main/java/com/mathquiz/model/User.java +++ /dev/null @@ -1,52 +0,0 @@ -package com.mathquiz.model; - -/** - * 用户类 - */ - -public class User { - private String name; - private String email; - private String encryptedPwd; - private Grade grade; - - public User() {} - - public User(String email, String encryptedPwd) { - this.email = email; - this.encryptedPwd = encryptedPwd; - } - - // Getter & Setter - public java.lang.String getName() { - return name; - } - - public void setName(java.lang.String name) { - this.name = name; - } - - public java.lang.String getEmail() { - return email; - } - - public void setEmail(java.lang.String email) { - this.email = email; - } - - public java.lang.String getEncryptedPwd() { - return encryptedPwd; - } - - public void setEncryptedPwd(java.lang.String encryptedPwd) { - this.encryptedPwd = encryptedPwd; - } - - public Grade getGrade() { - return grade; - } - - public void setGrade(Grade grade) { - this.grade = grade; - } -} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/ui/GradeSelectPanel.java b/src/main/java/com/mathquiz/ui/GradeSelectPanel.java new file mode 100644 index 0000000..f210860 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/GradeSelectPanel.java @@ -0,0 +1,58 @@ +package com.mathquiz.ui; + +import com.mathquiz.model.Grade; +import com.mathquiz.service.QuizService; +import javafx.geometry.Insets; +import javafx.scene.control.Button; +import javafx.scene.control.TextInputDialog; +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/mathquiz/ui/LoginPanel.java b/src/main/java/com/mathquiz/ui/LoginPanel.java new file mode 100644 index 0000000..5c06144 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/LoginPanel.java @@ -0,0 +1,44 @@ +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/mathquiz/ui/MainWindow.java b/src/main/java/com/mathquiz/ui/MainWindow.java new file mode 100644 index 0000000..52875e9 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/MainWindow.java @@ -0,0 +1,77 @@ +package com.mathquiz.ui; + +import com.mathquiz.model.User; +import com.mathquiz.service.*; +import javafx.scene.layout.BorderPane; +import javafx.stage.Stage; + +/** + * 主窗口控制器,持有所有服务引用和当前用户状态 + * 所有 UI 面板通过此窗口切换 + */ +public class MainWindow extends BorderPane { + + // 服务层引用(便于后期替换实现) + private final UserService userService; + private final FileIOService fileIOService; + private User currentUser; + + public MainWindow(Stage primaryStage) { + // 初始化服务(未来可替换为 DI 容器) + this.fileIOService = new FileIOService(); + this.userService = new UserService(fileIOService); + + // 默认显示登录页 + showLoginPanel(); + } + + // ---------------- 公共方法:面板切换 ---------------- + + public void showPanel(javafx.scene.layout.Pane panel) { + this.setCenter(panel); + } + + public void showLoginPanel() { + showPanel(new LoginPanel(this)); + } + + public void showRegisterPanel() { + showPanel(new RegisterPanel(this)); + } + + public void showPasswordModifyPanel() { + if (currentUser != null) { + showPanel(new PasswordModifyPanel(this, currentUser.getEmail())); + } + } + + public void showGradeSelectPanel() { + showPanel(new GradeSelectPanel(this)); + } + + public void showQuizPanel(java.util.List questions, QuizService quizService) { + showPanel(new QuizPanel(this, questions, quizService)); + } + + public void showResultPanel(int score, Runnable onContinue) { + showPanel(new ResultPanel(this, score, onContinue)); + } + + // ---------------- Getter ---------------- + + public UserService getUserService() { + return userService; + } + + public FileIOService getFileIOService() { + return fileIOService; + } + + public User getCurrentUser() { + return currentUser; + } + + public void setCurrentUser(User user) { + this.currentUser = user; + } +} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/ui/PasswordModifyPanel.java b/src/main/java/com/mathquiz/ui/PasswordModifyPanel.java new file mode 100644 index 0000000..bea9dd0 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/PasswordModifyPanel.java @@ -0,0 +1,46 @@ +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/mathquiz/ui/QuizPanel.java b/src/main/java/com/mathquiz/ui/QuizPanel.java new file mode 100644 index 0000000..9100c25 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/QuizPanel.java @@ -0,0 +1,67 @@ +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/mathquiz/ui/RegisterPanel.java b/src/main/java/com/mathquiz/ui/RegisterPanel.java new file mode 100644 index 0000000..27ae653 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/RegisterPanel.java @@ -0,0 +1,80 @@ +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/mathquiz/ui/ResultPanel.java b/src/main/java/com/mathquiz/ui/ResultPanel.java new file mode 100644 index 0000000..b94f202 --- /dev/null +++ b/src/main/java/com/mathquiz/ui/ResultPanel.java @@ -0,0 +1,32 @@ +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/mathquiz/util/EmailUtil.java b/src/main/java/com/mathquiz/util/EmailUtil.java deleted file mode 100644 index ff915ac..0000000 --- a/src/main/java/com/mathquiz/util/EmailUtil.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.util; - -/** - * 邮件工具类 - * 用于发送注册码邮件(可选功能) - * 注意:当前项目需求中,注册码是直接显示在界面上的, - * 不需要发送邮件,所以这个类暂时保留为空实现。 - * 如果将来需要发送邮件,可以使用JavaMail库实现。 - */ -public class EmailUtil { - - /** - * 发送注册码邮件(预留接口) - * @param toEmail 收件人邮箱 - * @param registrationCode 注册码 - * @return true表示发送成功 - */ - public static boolean sendRegistrationCode(String toEmail, String registrationCode) { - // TODO: 暂不实现邮件发送功能 - // 原因:项目需求是直接在界面显示注册码,不需要发邮件 - - System.out.println("【模拟】发送注册码邮件"); - System.out.println("收件人: " + toEmail); - System.out.println("注册码: " + registrationCode); - - return true; - } - - /** - * 发送密码重置邮件(预留接口) - * @param toEmail 收件人邮箱 - * @param newPassword 新密码 - * @return true表示发送成功 - */ - public static boolean sendPasswordReset(String toEmail, String newPassword) { - // TODO: 将来如果需要"找回密码"功能,可以在这里实现 - - System.out.println("【模拟】发送密码重置邮件"); - System.out.println("收件人: " + toEmail); - System.out.println("新密码: " + newPassword); - - return true; - } - - /** - * 验证邮箱格式 - * @param email 邮箱地址 - * @return true表示格式正确 - */ - public static boolean isValidEmail(String email) { - if (email == null || email.trim().isEmpty()) { - return false; - } - - // 简单的邮箱格式验证 - String emailRegex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; - return email.matches(emailRegex); - } -} - -/* - * 如果将来需要真正实现邮件发送,可以参考以下代码: - * - * 1. 在pom.xml添加依赖: - * - * javax.mail - * javax.mail-api - * 1.6.2 - * - * - * com.sun.mail - * javax.mail - * 1.6.2 - * - * - * 2. 实现代码示例: - * - * import javax.mail.*; - * import javax.mail.internet.*; - * import java.util.Properties; - * - * public static boolean sendEmail(String toEmail, String subject, String content) { - * try { - * Properties props = new Properties(); - * props.put("mail.smtp.auth", "true"); - * props.put("mail.smtp.starttls.enable", "true"); - * props.put("mail.smtp.host", "smtp.qq.com"); - * props.put("mail.smtp.port", "587"); - * - * Session session = Session.getInstance(props, new Authenticator() { - * protected PasswordAuthentication getPasswordAuthentication() { - * return new PasswordAuthentication("your-email@qq.com", "your-password"); - * } - * }); - * - * Message message = new MimeMessage(session); - * message.setFrom(new InternetAddress("your-email@qq.com")); - * message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); - * message.setSubject(subject); - * message.setText(content); - * - * Transport.send(message); - * return true; - * } catch (Exception e) { - * e.printStackTrace(); - * return false; - * } - * } - */ \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/FileUtils.java b/src/main/java/com/mathquiz/util/FileUtils.java deleted file mode 100644 index d4daf9e..0000000 --- a/src/main/java/com/mathquiz/util/FileUtils.java +++ /dev/null @@ -1,111 +0,0 @@ -package com.util; - -import java.io.*; -import java.nio.charset.StandardCharsets; -import java.nio.file.*; - -/** - * 文件操作工具类 - * 提供文件读写、目录创建等常用操作 - */ -public class FileUtils { - - /** - * 读取文件内容为字符串 - * @param filePath 文件路径 - * @return 文件内容 - * @throws IOException 读取失败时抛出 - */ - public static String readFileToString(String filePath) throws IOException { - return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8); - } - - /** - * 写入字符串到文件 - * @param filePath 文件路径 - * @param content 要写入的内容 - * @throws IOException 写入失败时抛出 - */ - public static void writeStringToFile(String filePath, String content) throws IOException { - Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8); - } - - /** - * 创建目录(如果不存在) - * @param dirPath 目录路径 - * @throws IOException 创建失败时抛出 - */ - public static void createDirectoryIfNotExists(String dirPath) throws IOException { - Path path = Paths.get(dirPath); - if (!Files.exists(path)) { - Files.createDirectories(path); - } - } - - /** - * 检查文件是否存在 - * @param filePath 文件路径 - * @return true表示存在 - */ - public static boolean exists(String filePath) { - return Files.exists(Paths.get(filePath)); - } - - /** - * 删除文件 - * @param filePath 文件路径 - * @return true表示删除成功 - */ - public static boolean deleteFile(String filePath) { - try { - return Files.deleteIfExists(Paths.get(filePath)); - } catch (IOException e) { - e.printStackTrace(); - return false; - } - } - - /** - * 获取目录下所有文件 - * @param dirPath 目录路径 - * @return 文件数组 - */ - public static File[] listFiles(String dirPath) { - File dir = new File(dirPath); - if (dir.exists() && dir.isDirectory()) { - return dir.listFiles(); - } - return new File[0]; - } - - /** - * 追加内容到文件末尾 - * @param filePath 文件路径 - * @param content 要追加的内容 - * @throws IOException 追加失败时抛出 - */ - public static void appendToFile(String filePath, String content) throws IOException { - Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8, - StandardOpenOption.CREATE, StandardOpenOption.APPEND); - } - - /** - * 复制文件 - * @param sourcePath 源文件路径 - * @param targetPath 目标文件路径 - * @throws IOException 复制失败时抛出 - */ - public static void copyFile(String sourcePath, String targetPath) throws IOException { - Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING); - } - - /** - * 获取文件大小(字节) - * @param filePath 文件路径 - * @return 文件大小 - * @throws IOException 获取失败时抛出 - */ - public static long getFileSize(String filePath) throws IOException { - return Files.size(Paths.get(filePath)); - } -} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/PasswordValidator.java b/src/main/java/com/mathquiz/util/PasswordValidator.java deleted file mode 100644 index 40a26b5..0000000 --- a/src/main/java/com/mathquiz/util/PasswordValidator.java +++ /dev/null @@ -1,102 +0,0 @@ -package com.util; - -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; - -/** - * 密码验证和加密工具类 - * 提供密码格式验证、加密、匹配等功能 - */ -public class PasswordValidator { - - /** - * 验证密码格式:6-10位,包含字母和数字 - * @param password 待验证的密码 - * @return true表示格式正确 - */ - public static boolean isValid(String password) { - if (password == null || password.length() < 6 || password.length() > 10) { - return false; - } - - boolean hasLetter = password.matches("^(?=.*[a-z])(?=.*[A-Z]).*$"); - boolean hasDigit = password.matches(".*\\d.*"); - - return hasLetter && hasDigit; - } - - /** - * 使用SHA-256加密密码 - * @param password 明文密码 - * @return 加密后的密码(16进制字符串) - */ - public static String encrypt(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("SHA-256算法不可用", e); - } - } - - /** - * 验证密码是否匹配 - * @param plainPassword 明文密码 - * @param encryptedPassword 加密后的密码 - * @return true表示匹配 - */ - public static boolean matches(String plainPassword, String encryptedPassword) { - return encrypt(plainPassword).equals(encryptedPassword); - } - - /** - * 生成6-10位随机注册码(包含字母和数字) - * @return 注册码 - */ - public static String generateRegistrationCode() { - String chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; - int length = 6 + (int) (Math.random() * 5); // 6-10位 - - StringBuilder code = new StringBuilder(); - - // 确保至少有一个字母 - code.append(chars.charAt((int) (Math.random() * 52))); - // 确保至少有一个数字 - code.append(chars.charAt(52 + (int) (Math.random() * 10))); - - // 填充剩余字符 - for (int i = 2; i < length; i++) { - code.append(chars.charAt((int) (Math.random() * chars.length()))); - } - - // 打乱字符顺序 - return shuffleString(code.toString()); - } - - /** - * 打乱字符串 - * @param str 原字符串 - * @return 打乱后的字符串 - */ - private static String shuffleString(String str) { - char[] chars = str.toCharArray(); - for (int i = chars.length - 1; i > 0; i--) { - int j = (int) (Math.random() * (i + 1)); - char temp = chars[i]; - chars[i] = chars[j]; - chars[j] = temp; - } - return new String(chars); - } -} \ No newline at end of file diff --git a/src/main/java/com/mathquiz/util/RandomUtils.java b/src/main/java/com/mathquiz/util/RandomUtils.java deleted file mode 100644 index c7351b9..0000000 --- a/src/main/java/com/mathquiz/util/RandomUtils.java +++ /dev/null @@ -1,69 +0,0 @@ -package com.util; - -import java.util.Collections; -import java.util.List; -import java.util.Random; - -//各种随机数生成 -public class RandomUtils { - private static final Random random = new Random(); - - - //生成[min, max]范围内的随机整数(包含边界) - public static int nextInt(int min, int max) { - if (min > max) { - throw new IllegalArgumentException("min不能大于max"); - } - return min + random.nextInt(max - min + 1); - } - - - //从数组中随机选择一个元素(模板类) - public static T randomChoice(T[] array) { - if (array == null || array.length == 0) { - throw new IllegalArgumentException("数组不能为空"); - } - return array[random.nextInt(array.length)]; - } - - - //从列表中随机选择一个元素 - public static T randomChoice(List list) { - if (list == null || list.isEmpty()) { - throw new IllegalArgumentException("列表不能为空"); - } - return list.get(random.nextInt(list.size())); - } - - /** - * 打乱列表顺序 - * @param list 要打乱的列表 - */ - public static void shuffle(List list) { - Collections.shuffle(list, random); - } - - - //生成指定范围内的随机双精度浮点数 - public static double nextDouble(double min, double max) { - if (min > max) { - throw new IllegalArgumentException("min不能大于max"); - } - return min + (max - min) * random.nextDouble(); - } - - - //生成随机布尔值 - public static boolean nextBoolean() { - return random.nextBoolean(); - } - - //按概率返回true(题目生成概率) - //示例:probability(0.7) 有70%概率返回true - public static boolean probability(double probability) { - if (probability < 0.0 || probability > 1.0) { - throw new IllegalArgumentException("概率必须在0.0-1.0之间"); - } - return random.nextDouble() < probability; - } -} \ No newline at end of file -- 2.34.1 From 5214d29612a6ec67d0a2f4435ed475fb97cbe218 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Fri, 3 Oct 2025 21:58:00 +0800 Subject: [PATCH 12/28] for merge --- src/main/java/com/{mathquiz => }/ui/GradeSelectPanel.java | 0 src/main/java/com/{mathquiz => }/ui/LoginPanel.java | 0 src/main/java/com/{mathquiz => }/ui/MainWindow.java | 0 src/main/java/com/{mathquiz => }/ui/PasswordModifyPanel.java | 0 src/main/java/com/{mathquiz => }/ui/QuizPanel.java | 0 src/main/java/com/{mathquiz => }/ui/RegisterPanel.java | 0 src/main/java/com/{mathquiz => }/ui/ResultPanel.java | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename src/main/java/com/{mathquiz => }/ui/GradeSelectPanel.java (100%) rename src/main/java/com/{mathquiz => }/ui/LoginPanel.java (100%) rename src/main/java/com/{mathquiz => }/ui/MainWindow.java (100%) rename src/main/java/com/{mathquiz => }/ui/PasswordModifyPanel.java (100%) rename src/main/java/com/{mathquiz => }/ui/QuizPanel.java (100%) rename src/main/java/com/{mathquiz => }/ui/RegisterPanel.java (100%) rename src/main/java/com/{mathquiz => }/ui/ResultPanel.java (100%) diff --git a/src/main/java/com/mathquiz/ui/GradeSelectPanel.java b/src/main/java/com/ui/GradeSelectPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/GradeSelectPanel.java rename to src/main/java/com/ui/GradeSelectPanel.java diff --git a/src/main/java/com/mathquiz/ui/LoginPanel.java b/src/main/java/com/ui/LoginPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/LoginPanel.java rename to src/main/java/com/ui/LoginPanel.java diff --git a/src/main/java/com/mathquiz/ui/MainWindow.java b/src/main/java/com/ui/MainWindow.java similarity index 100% rename from src/main/java/com/mathquiz/ui/MainWindow.java rename to src/main/java/com/ui/MainWindow.java diff --git a/src/main/java/com/mathquiz/ui/PasswordModifyPanel.java b/src/main/java/com/ui/PasswordModifyPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/PasswordModifyPanel.java rename to src/main/java/com/ui/PasswordModifyPanel.java diff --git a/src/main/java/com/mathquiz/ui/QuizPanel.java b/src/main/java/com/ui/QuizPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/QuizPanel.java rename to src/main/java/com/ui/QuizPanel.java diff --git a/src/main/java/com/mathquiz/ui/RegisterPanel.java b/src/main/java/com/ui/RegisterPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/RegisterPanel.java rename to src/main/java/com/ui/RegisterPanel.java diff --git a/src/main/java/com/mathquiz/ui/ResultPanel.java b/src/main/java/com/ui/ResultPanel.java similarity index 100% rename from src/main/java/com/mathquiz/ui/ResultPanel.java rename to src/main/java/com/ui/ResultPanel.java -- 2.34.1 From ce09a0810a19e84392bb3c93eb7a8c5014020f5f Mon Sep 17 00:00:00 2001 From: bx <2936213174@qq.com> Date: Sat, 4 Oct 2025 10:47:50 +0800 Subject: [PATCH 13/28] =?UTF-8?q?=E7=AC=AC=E4=B8=89=E7=89=88=EF=BC=88?= =?UTF-8?q?=E5=B8=A6=E6=B5=8B=E8=AF=95=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/model/ChoiceQuestion.java | 8 +- src/main/java/com/model/Grade.java | 5 +- src/main/java/com/model/QuizHistory.java | 7 +- src/main/java/com/model/QuizResult.java | 6 +- src/main/java/com/model/User.java | 9 +- src/main/java/com/service/UserService.java | 217 +++++++++- .../QuestionFactoryManager.java | 47 +-- src/test/java/TestMain.java | 377 +++++++++++++++--- 8 files changed, 557 insertions(+), 119 deletions(-) diff --git a/src/main/java/com/model/ChoiceQuestion.java b/src/main/java/com/model/ChoiceQuestion.java index 897059e..e4b0b54 100644 --- a/src/main/java/com/model/ChoiceQuestion.java +++ b/src/main/java/com/model/ChoiceQuestion.java @@ -2,9 +2,7 @@ package com.model; import java.util.List; -/** - * 选择题模型(纯数据) - */ +//选择题 public class ChoiceQuestion { private String questionText; // 题目文本 @@ -12,7 +10,7 @@ public class ChoiceQuestion { private List options; // 选项列表 private Grade grade; // 所属学段 - // ==================== 构造方法 ==================== + public ChoiceQuestion(String questionText, double correctAnswer, List options, Grade grade) { @@ -30,7 +28,7 @@ public class ChoiceQuestion { this.grade = grade; } - // ==================== Getters & Setters ==================== + public String getQuestionText() { return questionText; diff --git a/src/main/java/com/model/Grade.java b/src/main/java/com/model/Grade.java index 1d2729f..afa5077 100644 --- a/src/main/java/com/model/Grade.java +++ b/src/main/java/com/model/Grade.java @@ -1,8 +1,7 @@ package com.model; -/** - * 学段枚举 - */ + +//学段 public enum Grade { ELEMENTARY, // 小学 MIDDLE, // 初中 diff --git a/src/main/java/com/model/QuizHistory.java b/src/main/java/com/model/QuizHistory.java index 1d60028..5bad9d0 100644 --- a/src/main/java/com/model/QuizHistory.java +++ b/src/main/java/com/model/QuizHistory.java @@ -4,9 +4,7 @@ package com.model; import java.util.Date; import java.util.List; -/** - * 答题历史记录模型(纯数据) - */ +//答题历史记录模型 public class QuizHistory { private String username; // 用户名 @@ -15,7 +13,6 @@ public class QuizHistory { private List userAnswers; // 用户答案列表 private int score; // 得分 - // ==================== 构造方法 ==================== public QuizHistory(String username, Date timestamp, List questions, @@ -28,7 +25,7 @@ public class QuizHistory { this.score = score; } - // ==================== Getters & Setters ==================== + public String getUsername() { return username; diff --git a/src/main/java/com/model/QuizResult.java b/src/main/java/com/model/QuizResult.java index 7ce4e50..eb70db5 100644 --- a/src/main/java/com/model/QuizResult.java +++ b/src/main/java/com/model/QuizResult.java @@ -1,9 +1,7 @@ package com.model; -/** - * 答题结果模型(纯数据) - */ +//答题结果 public class QuizResult { private int totalQuestions; // 总题数 @@ -11,7 +9,6 @@ public class QuizResult { private int wrongCount; // 错误题数 private int score; // 得分 - // ==================== 构造方法 ==================== public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) { this.totalQuestions = totalQuestions; @@ -20,7 +17,6 @@ public class QuizResult { this.score = score; } - // ==================== Getters & Setters ==================== public int getTotalQuestions() { return totalQuestions; diff --git a/src/main/java/com/model/User.java b/src/main/java/com/model/User.java index 288ce9b..78d530a 100644 --- a/src/main/java/com/model/User.java +++ b/src/main/java/com/model/User.java @@ -2,9 +2,8 @@ package com.model; import java.util.Date; -/** - * 用户模型(纯数据) - */ + +//用户 public class User { private String username; // 用户名 @@ -15,7 +14,7 @@ public class User { private double averageScore; // 平均分 private Date registrationDate; // 注册时间 - // ==================== 构造方法 ==================== + /** * 完整构造方法(用于从文件加载) @@ -44,7 +43,7 @@ public class User { this.registrationDate = new Date(); } - // ==================== Getters & Setters ==================== + public String getUsername() { return username; diff --git a/src/main/java/com/service/UserService.java b/src/main/java/com/service/UserService.java index 72c39c2..06c78bb 100644 --- a/src/main/java/com/service/UserService.java +++ b/src/main/java/com/service/UserService.java @@ -3,12 +3,17 @@ package com.service; import com.model.Grade; import com.model.User; +import com.util.FileUtils; import com.util.PasswordValidator; import java.io.IOException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -35,30 +40,236 @@ public class UserService { this.currentUser = null; } - // ==================== 用户注册 ==================== + // 注册码文件路径 + private static final String REGISTRATION_CODES_FILE = "data/registration_codes.txt"; - public User register(String username, String password, String email) throws IOException { + // 注册码有效期(毫秒) + private static final long CODE_EXPIRY_TIME = 10 * 60 * 1000; // 10分钟 + + // ==================== 注册码管理 ==================== + + /** + * 生成并保存注册码到文件 + * @param email 邮箱 + * @return 生成的注册码 + */ + public String generateRegistrationCode(String email) throws IOException { + if (!validateEmail(email)) { + throw new IllegalArgumentException("邮箱格式错误!"); + } + + // 生成6-10位注册码 + String code = PasswordValidator.generateRegistrationCode(); + long expiryTime = System.currentTimeMillis() + CODE_EXPIRY_TIME; + + // 保存到文件 + saveRegistrationCodeToFile(email, code, expiryTime); + + // 打印注册码(实际项目中可以发邮件) + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + System.out.println("========================================"); + System.out.println("【注册码】"); + System.out.println("邮箱: " + email); + System.out.println("注册码: " + code); + System.out.println("过期时间: " + sdf.format(new Date(expiryTime))); + System.out.println("========================================"); + + return code; + } + + /** + * 保存注册码到文件 + */ + private void saveRegistrationCodeToFile(String email, String code, long expiryTime) throws IOException { + // 读取现有的注册码 + Map codes = loadRegistrationCodesFromFile(); + + // 添加或更新 + codes.put(email, new RegistrationCode(code, expiryTime)); + + // 保存到文件 + StringBuilder content = new StringBuilder(); + content.append("# 注册码记录文件\n"); + content.append("# 格式: 邮箱|注册码|过期时间戳\n"); + content.append("# 过期时间格式: yyyy-MM-dd HH:mm:ss\n\n"); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + for (Map.Entry entry : codes.entrySet()) { + String emailKey = entry.getKey(); + RegistrationCode regCode = entry.getValue(); + + content.append(emailKey).append("|") + .append(regCode.code).append("|") + .append(regCode.expiryTime).append("|") + .append(sdf.format(new Date(regCode.expiryTime))) + .append("\n"); + } + + FileUtils.writeStringToFile(REGISTRATION_CODES_FILE, content.toString()); + } + + /** + * 从文件加载注册码 + */ + private Map loadRegistrationCodesFromFile() throws IOException { + Map codes = new HashMap<>(); + + if (!FileUtils.exists(REGISTRATION_CODES_FILE)) { + return codes; + } + + String content = FileUtils.readFileToString(REGISTRATION_CODES_FILE); + String[] lines = content.split("\n"); + + for (String line : lines) { + line = line.trim(); + + // 跳过注释和空行 + if (line.isEmpty() || line.startsWith("#")) { + continue; + } + + String[] parts = line.split("\\|"); + if (parts.length >= 3) { + String email = parts[0].trim(); + String code = parts[1].trim(); + long expiryTime = Long.parseLong(parts[2].trim()); + + codes.put(email, new RegistrationCode(code, expiryTime)); + } + } + + return codes; + } + + /** + * 验证注册码 + * @param email 邮箱 + * @param code 用户输入的注册码 + * @return true表示验证通过 + */ + public boolean verifyRegistrationCode(String email, String code) throws IOException { + Map codes = loadRegistrationCodesFromFile(); + + RegistrationCode regCode = codes.get(email); + + if (regCode == null) { + throw new IllegalArgumentException("未找到该邮箱的注册码,请先获取注册码!"); + } + + // 检查是否过期 + if (System.currentTimeMillis() > regCode.expiryTime) { + // 删除过期的注册码 + codes.remove(email); + saveAllRegistrationCodes(codes); + throw new IllegalArgumentException("注册码已过期,请重新获取!"); + } + + // 验证注册码 + boolean isValid = regCode.code.equals(code); + + // 验证成功后删除注册码(一次性使用) + if (isValid) { + codes.remove(email); + saveAllRegistrationCodes(codes); + } + + return isValid; + } + + /** + * 保存所有注册码到文件 + */ + private void saveAllRegistrationCodes(Map codes) throws IOException { + StringBuilder content = new StringBuilder(); + content.append("# 注册码记录文件\n"); + content.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n"); + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); + + for (Map.Entry entry : codes.entrySet()) { + content.append(entry.getKey()).append("|") + .append(entry.getValue().code).append("|") + .append(entry.getValue().expiryTime).append("|") + .append(sdf.format(new Date(entry.getValue().expiryTime))) + .append("\n"); + } + + FileUtils.writeStringToFile(REGISTRATION_CODES_FILE, content.toString()); + } + + /** + * 清理过期的注册码 + */ + public void cleanExpiredCodes() throws IOException { + Map codes = loadRegistrationCodesFromFile(); + long now = System.currentTimeMillis(); + + // 移除过期的 + codes.entrySet().removeIf(entry -> now > entry.getValue().expiryTime); + + // 保存回文件 + saveAllRegistrationCodes(codes); + + System.out.println("✓ 已清理过期的注册码"); + } + + // ==================== 注册码内部类 ==================== + + private static class RegistrationCode { + String code; + long expiryTime; + + RegistrationCode(String code, long expiryTime) { + this.code = code; + this.expiryTime = expiryTime; + } + } + + + // ==================== 用户注册==================== + + /** + * 用户注册(需要验证码) + */ + public User register(String username, String password, String email, String verificationCode) throws IOException { + // 1. 验证注册码 + if (!verifyRegistrationCode(email, verificationCode)) { + throw new IllegalArgumentException("注册码错误!"); + } + + // 2. 验证用户名格式 if (!validateUsername(username)) { throw new IllegalArgumentException("用户名格式错误!正确格式:学段-姓名(如:小学-张三)"); } + // 3. 验证用户名是否已存在 if (fileIOService.isUsernameExists(username)) { throw new IllegalArgumentException("用户名已存在!"); } + // 4. 验证密码强度 String passwordError = PasswordValidator.validatePassword(password); if (passwordError != null) { throw new IllegalArgumentException(passwordError); } + // 5. 验证邮箱格式 if (!validateEmail(email)) { throw new IllegalArgumentException("邮箱格式错误!"); } + // 6. 从用户名中提取学段 Grade grade = extractGradeFromUsername(username); + + // 7. 加密密码 String hashedPassword = hashPassword(password); + // 8. 创建用户对象 User user = new User(username, hashedPassword, email, grade); + + // 9. 保存到文件 fileIOService.saveUser(user); System.out.println("✓ 用户注册成功:" + username); @@ -209,7 +420,7 @@ public class UserService { return fileIOService.findUserByUsername(username); } - // ==================== 业务逻辑方法(从 Model 移过来)==================== + // ==================== 业务逻辑方法==================== /** * 从用户名提取真实姓名 diff --git a/src/main/java/com/service/question_generator/QuestionFactoryManager.java b/src/main/java/com/service/question_generator/QuestionFactoryManager.java index 151c3ae..1036792 100644 --- a/src/main/java/com/service/question_generator/QuestionFactoryManager.java +++ b/src/main/java/com/service/question_generator/QuestionFactoryManager.java @@ -23,42 +23,20 @@ public class QuestionFactoryManager { } /** - * 生成单个题目 - * @param grade 学段 - * @return 题目对象 - */ - public static ChoiceQuestion generateQuestion(Grade grade) { - QuestionFactory factory = factories.get(grade); - if (factory == null) { - throw new IllegalArgumentException("不支持的学段: " + grade); - } - return factory.createQuestion(); - } - - /** - * 批量生成题目(不去重) + * 生成题目(带去重功能) * - * @param grade 学段 - * @param count 题目数量 - * @param historyQuestions - * @return 题目列表 - */ - public static List generateQuestions(Grade grade, int count, Set historyQuestions) { - return generateUniqueQuestions(grade, count, new HashSet<>()); - } - - /** - * 批量生成题目(带历史去重) * @param grade 学段 * @param count 题目数量 - * @param historyQuestions 历史题目文本集合 + * @param historyQuestions 历史题目(用于去重,传 null 或空集合则不去重) * @return 题目列表 */ - public static List generateUniqueQuestions( + public static List generateQuestions( Grade grade, int count, Set historyQuestions) { List questions = new ArrayList<>(); - Set currentQuestions = new HashSet<>(historyQuestions); + Set usedQuestions = historyQuestions != null ? + new HashSet<>(historyQuestions) : + new HashSet<>(); int maxAttempts = count * 10; int attempts = 0; @@ -72,26 +50,19 @@ public class QuestionFactoryManager { ChoiceQuestion question = factory.createQuestion(); String questionText = question.getQuestionText(); - if (!currentQuestions.contains(questionText)) { + if (!usedQuestions.contains(questionText)) { questions.add(question); - currentQuestions.add(questionText); + usedQuestions.add(questionText); } attempts++; } if (questions.size() < count) { - System.out.println("警告:只生成了 " + questions.size() + + System.out.println("⚠ 警告:只生成了 " + questions.size() + " 道题,未达到要求的 " + count + " 道"); } return questions; } - - /** - * 注册新的工厂(支持扩展) - */ - public static void registerFactory(Grade grade, QuestionFactory factory) { - factories.put(grade, factory); - } } \ No newline at end of file diff --git a/src/test/java/TestMain.java b/src/test/java/TestMain.java index b002dde..245b50e 100644 --- a/src/test/java/TestMain.java +++ b/src/test/java/TestMain.java @@ -6,7 +6,9 @@ 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; /** * 完整测试类 @@ -147,59 +149,155 @@ public class TestMain { System.out.println(); } - // ==================== 3. 测试用户服务 ==================== + // ==================== 3. 测试用户服务==================== private static void testUserService() throws IOException { - System.out.println("【测试3】用户服务"); + 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"; - // 测试3.1: 用户注册 try { - User user = userService.register(testUsername, "Test123456", "zhangsan@test.com"); - test("用户注册", + 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()); + test("未获取注册码检测", false, "失败:" + e.getMessage()); } - // 测试3.2: 重复注册检测 + // ========== 3.7 测试重复注册检测 ========== + + String testEmail3 = "test003@example.com"; + try { - userService.register(testUsername, "Test123456", "test@test.com"); + 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.3: 用户登录 + // ========== 3.8 测试用户登录 ========== + try { - User user = userService.login(testUsername, "Test123456"); + User user = userService.login(testUsername, testPassword); test("用户登录", user != null && userService.isLoggedIn(), - "用户登录应该成功"); + "用户登录成功"); } catch (Exception e) { test("用户登录", false, "失败:" + e.getMessage()); } - // 测试3.4: 错误密码登录 + // ========== 3.9 测试错误密码登录 ========== + try { userService.logout(); // 先退出 - userService.login(testUsername, "WrongPassword"); + userService.login(testUsername, "WrongPassword123"); test("错误密码登录", false, "应该抛出异常"); } catch (IllegalArgumentException e) { test("错误密码登录", e.getMessage().contains("密码错误"), "应该检测到密码错误"); + } catch (Exception e) { + test("错误密码登录", false, "失败:" + e.getMessage()); } - // 测试3.5: 获取当前用户 + // ========== 3.10 测试不存在的用户登录 ========== + try { - userService.login(testUsername, "Test123456"); + 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), @@ -208,25 +306,155 @@ public class TestMain { test("获取当前用户", false, "失败:" + e.getMessage()); } - // 测试3.6: 提取真实姓名 + // ========== 3.12 测试提取真实姓名 ========== + User user = userService.getCurrentUser(); String realName = userService.getRealName(user); test("提取真实姓名", realName.equals("张三测试"), "应该正确提取真实姓名,实际:" + realName); - // 测试3.7: 获取学段显示名 + // ========== 3.13 测试获取学段显示名 ========== + String gradeName = userService.getGradeDisplayName(user); test("获取学段显示名", gradeName.equals("小学"), "应该返回'小学',实际:" + gradeName); - // 测试3.8: 退出登录 + // ========== 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(); } @@ -236,47 +464,57 @@ public class TestMain { System.out.println("【测试4】题目生成"); System.out.println("----------------------------------------"); - // 测试4.1: 生成小学题目 + // 测试4.1: 生成小学题目(不去重) try { - ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); + List questions = QuestionFactoryManager.generateQuestions( + Grade.ELEMENTARY, 1, null + ); + test("生成小学题目", - q != null && q.getQuestionText() != null && q.getOptions().size() == 4, - "应该生成有效的小学题目"); + questions.size() == 1 && questions.get(0).getQuestionText() != null, + "应该生成1道有效的小学题目"); - // 显示示例题目 - System.out.println(" 示例题目:" + q.getQuestionText()); - System.out.println(" 选项数量:" + q.getOptions().size()); + System.out.println(" 示例题目:" + questions.get(0).getQuestionText()); } catch (Exception e) { test("生成小学题目", false, "失败:" + e.getMessage()); } // 测试4.2: 生成初中题目 try { - ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.MIDDLE); + List questions = QuestionFactoryManager.generateQuestions( + Grade.MIDDLE, 1, null + ); + test("生成初中题目", - q != null && q.getQuestionText() != null, - "应该生成有效的初中题目"); + questions.size() == 1, + "应该生成1道有效的初中题目"); - System.out.println(" 示例题目:" + q.getQuestionText()); + System.out.println(" 示例题目:" + questions.get(0).getQuestionText()); } catch (Exception e) { test("生成初中题目", false, "失败:" + e.getMessage()); } // 测试4.3: 生成高中题目 try { - ChoiceQuestion q = QuestionFactoryManager.generateQuestion(Grade.HIGH); + List questions = QuestionFactoryManager.generateQuestions( + Grade.HIGH, 1, null + ); + test("生成高中题目", - q != null && q.getQuestionText() != null, - "应该生成有效的高中题目"); + questions.size() == 1, + "应该生成1道有效的高中题目"); - System.out.println(" 示例题目:" + q.getQuestionText()); + System.out.println(" 示例题目:" + questions.get(0).getQuestionText()); } catch (Exception e) { test("生成高中题目", false, "失败:" + e.getMessage()); } // 测试4.4: 批量生成题目 try { - List questions = QuestionFactoryManager.generateQuestions(Grade.ELEMENTARY, 10, historyQuestions); + List questions = QuestionFactoryManager.generateQuestions( + Grade.ELEMENTARY, 10, null + ); + test("批量生成题目", questions.size() == 10, "应该生成10道题目,实际:" + questions.size()); @@ -284,23 +522,42 @@ public class TestMain { test("批量生成题目", false, "失败:" + e.getMessage()); } - // 测试4.5: 题目去重 + // 测试4.5: 题目去重功能 try { - ChoiceQuestion q1 = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); - ChoiceQuestion q2 = QuestionFactoryManager.generateQuestion(Grade.ELEMENTARY); + // 第一次生成 + List firstBatch = QuestionFactoryManager.generateQuestions( + Grade.ELEMENTARY, 5, null + ); + + // 收集已生成的题目文本 + Set historyQuestions = new HashSet<>(); + for (ChoiceQuestion q : firstBatch) { + historyQuestions.add(q.getQuestionText()); + } - // 理论上不应该完全相同(概率极低) - boolean different = !q1.getQuestionText().equals(q2.getQuestionText()); - test("题目去重机制", - different, - "连续生成的题目应该不同(高概率)"); + // 第二次生成(带去重) + 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()); + test("题目去重功能", false, "失败:" + e.getMessage()); } System.out.println(); } - // ==================== 5. 测试答题服务 ==================== private static void testQuizService() throws IOException { @@ -406,24 +663,34 @@ public class TestMain { QuizService quizService = new QuizService(fileService, userService); try { - // 步骤1: 注册用户 + // ========== 步骤1: 注册新用户 ========== System.out.println("步骤1: 注册新用户..."); - User user = userService.register("初中-王五", "Test123456", "wangwu@test.com"); + + 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: 登录 + // ========== 步骤2: 用户登录 ========== System.out.println("步骤2: 用户登录..."); - userService.login("初中-王五", "Test123456"); + userService.login(username, password); test("完整流程-登录", userService.isLoggedIn(), "用户登录成功"); - // 步骤3: 开始答题 + // ========== 步骤3: 开始答题 ========== System.out.println("步骤3: 开始答题(10道题)..."); quizService.startNewQuiz(user, 10); test("完整流程-生成题目", quizService.getTotalQuestions() == 10, "题目生成成功"); - // 步骤4: 答题(模拟全部答对) + // ========== 步骤4: 答题(模拟全部答对)========== System.out.println("步骤4: 模拟答题过程..."); for (int i = 0; i < 10; i++) { quizService.goToQuestion(i); @@ -433,7 +700,7 @@ public class TestMain { } test("完整流程-答题", quizService.isAllAnswered(), "所有题目已作答"); - // 步骤5: 计算成绩 + // ========== 步骤5: 计算成绩 ========== System.out.println("步骤5: 计算成绩..."); QuizResult result = quizService.calculateResult(); test("完整流程-计算成绩", @@ -442,12 +709,12 @@ public class TestMain { System.out.println(quizService.formatResult(result)); - // 步骤6: 保存记录 + // ========== 步骤6: 保存记录 ========== System.out.println("步骤6: 保存答题记录..."); quizService.saveQuizHistory(user); // 验证用户统计是否更新 - User updatedUser = fileService.findUserByUsername("初中-王五"); + User updatedUser = fileService.findUserByUsername(username); test("完整流程-保存记录", updatedUser.getTotalQuizzes() == 1, "用户答题次数应该增加,实际:" + updatedUser.getTotalQuizzes()); @@ -456,7 +723,7 @@ public class TestMain { updatedUser.getAverageScore() == 100.0, "平均分应该更新,实际:" + updatedUser.getAverageScore()); - // 步骤7: 退出登录 + // ========== 步骤7: 退出登录 ========== System.out.println("步骤7: 退出登录..."); userService.logout(); test("完整流程-退出", !userService.isLoggedIn(), "退出登录成功"); -- 2.34.1 From 331bba981b33966a79b5c86f3ba5cc340c877370 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Sat, 4 Oct 2025 10:58:28 +0800 Subject: [PATCH 14/28] Remove .idea from repo --- .idea/.gitignore | 8 -------- .idea/PAIR.iml | 9 --------- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 5 files changed, 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/PAIR.iml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/PAIR.iml b/.idea/PAIR.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/PAIR.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6f29fee..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 20bffb7..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file -- 2.34.1 From 24e97401ac8dc2a4355bbf78673177d16a3c959a Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Sat, 4 Oct 2025 11:01:10 +0800 Subject: [PATCH 15/28] Remove .idea from repo --- .idea/.gitignore | 8 -------- .idea/PAIR.iml | 9 --------- .idea/misc.xml | 6 ------ .idea/modules.xml | 8 -------- .idea/vcs.xml | 6 ------ 5 files changed, 37 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/PAIR.iml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 13566b8..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Editor-based HTTP Client requests -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/PAIR.iml b/.idea/PAIR.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/PAIR.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 6f29fee..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 20bffb7..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file -- 2.34.1 From 05b231cafcaeb385469ca5380ee7269e20fe645b Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Sat, 4 Oct 2025 11:02:39 +0800 Subject: [PATCH 16/28] Remove .idea from repo --- .idea/.gitignore | 8 --- .idea/encodings.xml | 7 --- .idea/misc.xml | 14 ----- .idea/uiDesigner.xml | 124 ------------------------------------------- .idea/vcs.xml | 6 --- 5 files changed, 159 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/encodings.xml delete mode 100644 .idea/misc.xml delete mode 100644 .idea/uiDesigner.xml delete mode 100644 .idea/vcs.xml diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 35410ca..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml diff --git a/.idea/encodings.xml b/.idea/encodings.xml deleted file mode 100644 index aa00ffa..0000000 --- a/.idea/encodings.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index fdc35ea..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,14 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/.idea/uiDesigner.xml b/.idea/uiDesigner.xml deleted file mode 100644 index 2b63946..0000000 --- a/.idea/uiDesigner.xml +++ /dev/null @@ -1,124 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file -- 2.34.1 From 35317330dde8a515d4d5b023b344bf725b53ad93 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Sat, 4 Oct 2025 22:35:59 +0800 Subject: [PATCH 17/28] ui design v1 --- .idea/encodings.xml | 7 + .idea/misc.xml | 14 + .idea/uiDesigner.xml | 124 +++++ .idea/vcs.xml | 6 + .idea/workspace.xml | 144 ++++++ data/history/初中-王五_1759552247819.txt | 77 ++++ data/registration_codes.txt | 4 + data/users.json | 67 +++ doc/设计文档/设计文档第四版.md | 436 ++++++++++++++++++ pom.xml | 87 +++- src/main/java/com/Test.java | 24 + src/main/java/com/service/UserService.java | 33 +- src/main/java/com/ui/GradeSelectPanel.java | 52 +-- src/main/java/com/ui/InfGenPage.java | 55 +++ src/main/java/com/ui/LoginPage.java | 49 ++ src/main/java/com/ui/LoginPanel.java | 44 -- src/main/java/com/ui/MainWindow.java | 131 +++--- src/main/java/com/ui/NavigablePanel.java | 29 ++ src/main/java/com/ui/Panel.java | 11 + src/main/java/com/ui/PasswordModifyPage.java | 48 ++ src/main/java/com/ui/PasswordModifyPanel.java | 46 -- src/main/java/com/ui/QuizPage.java | 58 +++ src/main/java/com/ui/QuizPanel.java | 67 --- src/main/java/com/ui/RegisterPage.java | 58 +++ src/main/java/com/ui/RegisterPanel.java | 80 ---- src/main/java/com/ui/ResultPage.java | 49 ++ src/main/java/com/ui/ResultPanel.java | 32 -- src/main/java/com/ui/StartPage.java | 34 ++ src/main/java/com/ui/UIConstants.java | 35 ++ src/main/java/com/util/PasswordValidator.java | 348 +++++++------- src/main/java/com/util/RandomUtils.java | 22 + src/test/java/TestMain.java | 62 +-- 32 files changed, 1712 insertions(+), 621 deletions(-) create mode 100644 .idea/encodings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/uiDesigner.xml create mode 100644 .idea/vcs.xml create mode 100644 .idea/workspace.xml create mode 100644 data/history/初中-王五_1759552247819.txt create mode 100644 data/registration_codes.txt create mode 100644 data/users.json create mode 100644 doc/设计文档/设计文档第四版.md create mode 100644 src/main/java/com/Test.java create mode 100644 src/main/java/com/ui/InfGenPage.java create mode 100644 src/main/java/com/ui/LoginPage.java delete mode 100644 src/main/java/com/ui/LoginPanel.java create mode 100644 src/main/java/com/ui/NavigablePanel.java create mode 100644 src/main/java/com/ui/Panel.java create mode 100644 src/main/java/com/ui/PasswordModifyPage.java delete mode 100644 src/main/java/com/ui/PasswordModifyPanel.java create mode 100644 src/main/java/com/ui/QuizPage.java delete mode 100644 src/main/java/com/ui/QuizPanel.java create mode 100644 src/main/java/com/ui/RegisterPage.java delete mode 100644 src/main/java/com/ui/RegisterPanel.java create mode 100644 src/main/java/com/ui/ResultPage.java delete mode 100644 src/main/java/com/ui/ResultPanel.java create mode 100644 src/main/java/com/ui/StartPage.java create mode 100644 src/main/java/com/ui/UIConstants.java 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..3a08d13 --- /dev/null +++ b/.idea/workspace.xml @@ -0,0 +1,144 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {} + { + "isMigrated": true +} + + + + { + "customColor": "", + "associatedIndex": 1 +} + + + + { + "keyToString": { + "Application.Test.executor": "Run", + "Application.TestMain.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "RunOnceActivity.git.unshallow": "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", + "settings.editor.selected.configurable": "MavenSettings", + "vue.rearranger.settings.migration": "true" + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1759546098925 + + + + + + \ No newline at end of file diff --git a/data/history/初中-王五_1759552247819.txt b/data/history/初中-王五_1759552247819.txt new file mode 100644 index 0000000..0881453 --- /dev/null +++ b/data/history/初中-王五_1759552247819.txt @@ -0,0 +1,77 @@ +========== 答题记录 ========== +用户:初中-王五 +时间:2025-10-04 12:30:47 +总分:100 分 +正确:10 题 错误:0 题 +============================= + +【题目 1】 +√4 + 3² +A. 20.0 B. 14.0 C. 6.0 D. 11.0 +正确答案:D +用户答案:D +结果:✓ 正确 + +【题目 2】 +2² +A. 6.0 B. 8.0 C. 4.0 D. 9.0 +正确答案:C +用户答案:C +结果:✓ 正确 + +【题目 3】 +√100 + 15 +A. 24.0 B. 26.0 C. 25.0 D. 21.0 +正确答案:C +用户答案:C +结果:✓ 正确 + +【题目 4】 +√36 +A. 6.0 B. 8.0 C. 7.0 D. 18.0 +正确答案:A +用户答案:A +结果:✓ 正确 + +【题目 5】 +8² +A. 64.0 B. 71.0 C. 16.0 D. 73.0 +正确答案:A +用户答案:A +结果:✓ 正确 + +【题目 6】 +√16 +A. 4.0 B. 8.0 C. 13.0 D. 6.0 +正确答案:A +用户答案:A +结果:✓ 正确 + +【题目 7】 +12² +A. 144.0 B. 165.0 C. 149.0 D. 24.0 +正确答案:A +用户答案:A +结果:✓ 正确 + +【题目 8】 +√16 + 2 +A. 11.0 B. 1.0 C. 12.0 D. 6.0 +正确答案:D +用户答案:D +结果:✓ 正确 + +【题目 9】 +√81 +A. 14.0 B. 40.5 C. 9.0 D. 2.0 +正确答案:C +用户答案:C +结果:✓ 正确 + +【题目 10】 +3² + 16 +A. 31.0 B. 25.0 C. 30.0 D. 20.0 +正确答案:B +用户答案:B +结果:✓ 正确 + diff --git a/data/registration_codes.txt b/data/registration_codes.txt new file mode 100644 index 0000000..93f4751 --- /dev/null +++ b/data/registration_codes.txt @@ -0,0 +1,4 @@ +# 注册码记录文件 +# 格式: 邮箱|注册码|过期时间戳|过期时间 + +test002@example.com|4R8uZYiQ|1759552847711|2025-10-04 12:40:47 diff --git a/data/users.json b/data/users.json new file mode 100644 index 0000000..f9e127e --- /dev/null +++ b/data/users.json @@ -0,0 +1,67 @@ +{ + "users": [ + { + "username": "小学-测试", + "password": "encrypted123", + "email": "test@test.com", + "grade": "ELEMENTARY", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "小学-张三测试", + "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263", + "email": "test001@example.com", + "grade": "ELEMENTARY", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "初中-李明", + "password": "222711cc1da343bafd214b51a33d189a425e801b5d45774a941bbf68a1116d5c", + "email": "middle@test.com", + "grade": "MIDDLE", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "高中-王华", + "password": "9f3abee248c95d9ed301ee5a5b71318c0838c994ac5f66e70bfc9ee7ecad0150", + "email": "high@test.com", + "grade": "HIGH", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "小学-重载测试", + "password": "bd3910fa48dc018fb9884e1d78649396d96882f6fcd8cbcd87b3e0c8bfc86e15", + "email": "reload@test.com", + "grade": "ELEMENTARY", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "小学-李四", + "password": "encrypted", + "email": "lisi@test.com", + "grade": "ELEMENTARY", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 4, 2025, 12:30:47 PM" + }, + { + "username": "初中-王五", + "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263", + "email": "wangwu@test.com", + "grade": "MIDDLE", + "totalQuizzes": 1, + "averageScore": 100.0, + "registrationDate": "Oct 4, 2025, 12:30:47 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..b23987a 100644 --- a/pom.xml +++ b/pom.xml @@ -2,62 +2,104 @@ + http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 + com.mathquiz MathQuizApp 1.0.0 jar Math Quiz Application - 小初高数学学习软件 - Swing版本 + 小初高数学学习软件 - JavaFX版本 + UTF-8 21 21 + 21.0.2 + + 0.0.8 + - + 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} + + + + + openjfx-repo + https://oss.sonatype.org/content/repositories/openjfx-releases/ + + + + - + 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 + + UTF-8 + + + + + + org.openjfx + javafx-maven-plugin + ${javafx.plugin.version} - - - com.mathquiz.Main - true - - + com.Test - + org.apache.maven.plugins maven-shade-plugin @@ -71,9 +113,20 @@ - com.mathquiz.Main + com.Test + + + + *:* + + META-INF/*.SF + META-INF/*.DSA + META-INF/*.RSA + + + MathQuizApp diff --git a/src/main/java/com/Test.java b/src/main/java/com/Test.java new file mode 100644 index 0000000..a2ad9f3 --- /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, 800, 600); + 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/service/UserService.java b/src/main/java/com/service/UserService.java index 06c78bb..56beb01 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 EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"); // ==================== 构造方法 ==================== @@ -264,7 +267,7 @@ public class UserService { Grade grade = extractGradeFromUsername(username); // 7. 加密密码 - String hashedPassword = hashPassword(password); + String hashedPassword = PasswordValidator.encrypt(password); // 8. 创建用户对象 User user = new User(username, hashedPassword, email, grade); @@ -285,7 +288,7 @@ public class UserService { throw new IllegalArgumentException("用户名不存在!"); } - String hashedPassword = hashPassword(password); + String hashedPassword = PasswordValidator.encrypt(password); if (!user.getPassword().equals(hashedPassword)) { throw new IllegalArgumentException("密码错误!"); } @@ -327,7 +330,7 @@ public class UserService { // ==================== 密码管理 ==================== public boolean changePassword(User user, String oldPassword, String newPassword) throws IOException { - String hashedOldPassword = hashPassword(oldPassword); + String hashedOldPassword = PasswordValidator.encrypt(oldPassword); if (!user.getPassword().equals(hashedOldPassword)) { throw new IllegalArgumentException("旧密码错误!"); } @@ -341,7 +344,7 @@ public class UserService { throw new IllegalArgumentException("新密码不能与旧密码相同!"); } - String hashedNewPassword = hashPassword(newPassword); + String hashedNewPassword = PasswordValidator.encrypt(newPassword); user.setPassword(hashedNewPassword); fileIOService.saveUser(user); @@ -371,7 +374,7 @@ public class UserService { throw new IllegalArgumentException(passwordError); } - String hashedNewPassword = hashPassword(newPassword); + String hashedNewPassword = PasswordValidator.encrypt(newPassword); user.setPassword(hashedNewPassword); fileIOService.saveUser(user); @@ -509,24 +512,4 @@ public class UserService { 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); - } - } } \ 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..94784c7 --- /dev/null +++ b/src/main/java/com/ui/InfGenPage.java @@ -0,0 +1,55 @@ +// 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; + +public class InfGenPage extends NavigablePanel { + + private final Label usernameLabel = 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) { + super(onBack); + } + + @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)); + + gradeChoice.getItems().addAll("小学", "初中", "高中"); + gradeChoice.setValue("小学"); + + questionCountSpinner.setEditable(true); + questionCountSpinner.setPrefWidth(100); + + HBox countBox = new HBox(new Label("题目数量:"), questionCountSpinner); + countBox.setAlignment(Pos.CENTER); + countBox.setSpacing(10); + + generateButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + generateButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + form.getChildren().addAll(titleLabel, usernameLabel, new Label("学段选择:"), gradeChoice, countBox, generateButton, passwordModifyButton); + this.setCenter(form); + } + + public ChoiceBox getGradeChoice() { return gradeChoice; } + public Spinner getQuestionCountSpinner() { return questionCountSpinner; } + public Button getGenerateButton() { return generateButton; } + public Button getPasswordModifyButton() { return passwordModifyButton; } + public void setUsername(String username) { usernameLabel.setText("用户名:" + username); } +} \ 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..7eeb82e --- /dev/null +++ b/src/main/java/com/ui/LoginPage.java @@ -0,0 +1,49 @@ +// com/ui/LoginPage.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; + +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); + } + + @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("邮箱/用户名"); + passwordField.setPromptText("密码(6-10位,含大小写字母和数字)"); + + loginButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + loginButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + 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..668982e 100644 --- a/src/main/java/com/ui/MainWindow.java +++ b/src/main/java/com/ui/MainWindow.java @@ -1,77 +1,94 @@ -package com.mathquiz.ui; +// com/ui/MainWindow.java +package com.ui; -import com.mathquiz.model.User; -import com.mathquiz.service.*; +import javafx.scene.Scene; import javafx.scene.layout.BorderPane; import javafx.stage.Stage; -/** - * 主窗口控制器,持有所有服务引用和当前用户状态 - * 所有 UI 面板通过此窗口切换 - */ public class MainWindow extends BorderPane { - // 服务层引用(便于后期替换实现) - private final UserService userService; - private final FileIOService fileIOService; - private User currentUser; + private final Stage primaryStage; + private Panel currentPanel; public MainWindow(Stage primaryStage) { - // 初始化服务(未来可替换为 DI 容器) - this.fileIOService = new FileIOService(); - this.userService = new UserService(fileIOService); - - // 默认显示登录页 - showLoginPanel(); - } - - // ---------------- 公共方法:面板切换 ---------------- - - public void showPanel(javafx.scene.layout.Pane panel) { - this.setCenter(panel); + this.primaryStage = primaryStage; + showStartPage(); } - public void showLoginPanel() { - showPanel(new LoginPanel(this)); + private void showStartPage() { + StartPage startPage = new StartPage(() -> navigateTo(Panel.LOGIN)); + this.setCenter(startPage); + currentPanel = Panel.START; } - public void showRegisterPanel() { - showPanel(new RegisterPanel(this)); - } - - public void showPasswordModifyPanel() { - if (currentUser != null) { - showPanel(new PasswordModifyPanel(this, currentUser.getEmail())); + private void navigateTo(Panel target) { + switch (target) { + case START -> showStartPage(); + + case LOGIN -> { + LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START)); + loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER)); + loginPage.getLoginButton().setOnAction(e -> navigateTo(Panel.INF_GEN)); + this.setCenter(loginPage); + } + + case REGISTER -> { + RegisterPage registerPage = new RegisterPage(() -> navigateTo(Panel.LOGIN)); + registerPage.getRegisterButton().setOnAction(e -> navigateTo(Panel.LOGIN)); + this.setCenter(registerPage); + } + + case INF_GEN -> { + InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN)); + infGenPage.getGenerateButton().setOnAction(e -> { + // 可在此处校验题数(10-30) + int count = infGenPage.getQuestionCountSpinner().getValue(); + if (count < 10 || count > 30) { + // TODO: 弹出提示“题数必须为10-30” + return; + } + navigateTo(Panel.QUIZ); + }); + infGenPage.getPasswordModifyButton().setOnAction(e -> navigateTo(Panel.PASSWORDMODIFY)); + this.setCenter(infGenPage); + } + + case PASSWORDMODIFY -> { + PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN)); + pwdPage.getModifyButton().setOnAction(e -> { + // TODO: 调用 UserService 修改密码 + navigateTo(Panel.INF_GEN); + }); + this.setCenter(pwdPage); + } + + case QUIZ -> { + QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN)); + quizPage.getSubmitButton().setOnAction(e -> navigateTo(Panel.RESULT)); + // TODO: 初始化题目、绑定选项点击逻辑 + this.setCenter(quizPage); + } + + case RESULT -> { + ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN)); + resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START)); + resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN)); + this.setCenter(resultPage); + } } + currentPanel = target; } - public void showGradeSelectPanel() { - showPanel(new GradeSelectPanel(this)); - } - - public void showQuizPanel(java.util.List questions, QuizService quizService) { - showPanel(new QuizPanel(this, questions, quizService)); + public Panel getCurrentPanel() { + return currentPanel; } - public void showResultPanel(int score, Runnable onContinue) { - showPanel(new ResultPanel(this, score, onContinue)); + 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(); } - // ---------------- Getter ---------------- - - public UserService getUserService() { - return userService; - } - - public FileIOService getFileIOService() { - return fileIOService; - } - - public User getCurrentUser() { - return currentUser; - } - - public void setCurrentUser(User user) { - this.currentUser = user; - } } \ 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..130e06e --- /dev/null +++ b/src/main/java/com/ui/NavigablePanel.java @@ -0,0 +1,29 @@ +// com/ui/NavigablePanel.java +package com.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Button; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.text.Font; + +public abstract class NavigablePanel extends BorderPane { + + public NavigablePanel(Runnable onBack) { + Button backButton = new Button("← 返回"); + backButton.setOnAction(e -> onBack.run()); + backButton.setPrefSize(UIConstants.BACK_BUTTON_WIDTH, UIConstants.BACK_BUTTON_HEIGHT); + backButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + HBox topBar = new HBox(10); + topBar.setPadding(UIConstants.TOP_BAR_PADDING); + topBar.setAlignment(Pos.CENTER_LEFT); + topBar.getChildren().add(backButton); + + this.setTop(topBar); + 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..bacb107 --- /dev/null +++ b/src/main/java/com/ui/PasswordModifyPage.java @@ -0,0 +1,48 @@ +// 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); + } + + @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("旧密码"); + newPasswordField.setPromptText("新密码(6-10位)"); + confirmNewPasswordField.setPromptText("确认新密码"); + + 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..6e4a156 --- /dev/null +++ b/src/main/java/com/ui/QuizPage.java @@ -0,0 +1,58 @@ +// com/ui/QuizPage.java +package com.ui; + +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.*; +import javafx.scene.layout.HBox; +import javafx.scene.layout.VBox; +import javafx.scene.text.Font; + +public class QuizPage extends NavigablePanel { + + 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 nextButton = new Button("下一题"); + private final Button submitButton = new Button("交卷"); + + public QuizPage(Runnable onBack) { + super(onBack); + } + + @Override + protected void buildContent() { + VBox container = new VBox(UIConstants.DEFAULT_SPACING); + container.setAlignment(Pos.CENTER); + container.setPadding(new Insets(20)); + + questionLabel.setWrapText(true); + questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.QUIZ_TITLE_FONT_SIZE)); + + 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); + optionsBox.getChildren().add(options[i]); + } + + HBox buttonBox = new HBox(20); + buttonBox.setAlignment(Pos.CENTER); + buttonBox.getChildren().addAll(nextButton, submitButton); + + container.getChildren().addAll(progressLabel, questionLabel, optionsBox, buttonBox); + this.setCenter(container); + + // 默认隐藏交卷按钮 + submitButton.setVisible(false); + } + + 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 getSubmitButton() { return submitButton; } +} \ 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..31d6e40 --- /dev/null +++ b/src/main/java/com/ui/RegisterPage.java @@ -0,0 +1,58 @@ +// 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); + } + + @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("邮箱"); + codeField.setPromptText("注册码"); + passwordField.setPromptText("密码(6-10位)"); + confirmPasswordField.setPromptText("确认密码"); + + sendCodeButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + sendCodeButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + registerButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + registerButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + 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..a70767b --- /dev/null +++ b/src/main/java/com/ui/ResultPage.java @@ -0,0 +1,49 @@ +// com/ui/ResultPage.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 ResultPage extends NavigablePanel { + + 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) { + super(onBack); + } + + @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)); + gradeLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUBTITLE_FONT_SIZE)); + + continueButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + continueButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + exitButton.setPrefSize(UIConstants.BUTTON_WIDTH, UIConstants.BUTTON_HEIGHT); + exitButton.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BUTTON_FONT_SIZE)); + + form.getChildren().addAll(titleLabel, resultLabel, gradeLabel, continueButton, exitButton); + this.setCenter(form); + } + + 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..98cbb6b --- /dev/null +++ b/src/main/java/com/ui/StartPage.java @@ -0,0 +1,34 @@ +// 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()); + + 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..3879cb5 --- /dev/null +++ b/src/main/java/com/ui/UIConstants.java @@ -0,0 +1,35 @@ +// com/ui/UIConstants.java +package com.ui; + +import javafx.geometry.Insets; + +public final class UIConstants { + private UIConstants() {} + + // 间距与边距 + 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, 10, 10, 10); + + // 字体 + public static final String FONT_FAMILY = "Microsoft YaHei"; + public static final double TITLE_FONT_SIZE = 24.0; + public static final double SUBTITLE_FONT_SIZE = 16.0; + public static final double BUTTON_FONT_SIZE = 16.0; + public static final double LABEL_FONT_SIZE = 14.0; + public static final double QUIZ_TITLE_FONT_SIZE = 18.0; + + // 按钮尺寸 + public static final double BUTTON_WIDTH = 120.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 FORM_STYLE = + "-fx-background-color: white; " + + "-fx-border-radius: 10; " + + "-fx-background-radius: 10; " + + "-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 index 245b50e..8f71efc 100644 --- a/src/test/java/TestMain.java +++ b/src/test/java/TestMain.java @@ -24,32 +24,32 @@ public class TestMain { 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(); - } +// 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. 测试密码验证工具 ==================== @@ -91,11 +91,11 @@ public class TestMain { code.length() >= 6 && code.length() <= 10, "注册码长度应该在6-10位之间,实际:" + code.length()); - // 测试1.7: 密码强度检测 - String strength = PasswordValidator.getPasswordStrength("Abc123!@#"); - test("密码强度检测", - strength != null && !strength.isEmpty(), - "密码强度应该返回有效值,实际:" + strength); +// // 测试1.7: 密码强度检测 +// String strength = PasswordValidator.getPasswordStrength("Abc123!@#"); +// test("密码强度检测", +// strength != null && !strength.isEmpty(), +// "密码强度应该返回有效值,实际:" + strength); System.out.println(); } -- 2.34.1 From d1f7ea140958a6cfdf6ef2a1d702a3fe0568e23d Mon Sep 17 00:00:00 2001 From: bx <2936213174@qq.com> Date: Sun, 5 Oct 2025 11:32:03 +0800 Subject: [PATCH 18/28] =?UTF-8?q?=E7=AC=AC=E5=9B=9B=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/com/service/UserService.java | 2 +- src/main/java/com/util/EmailUtil.java | 49 ---------------------- 2 files changed, 1 insertion(+), 50 deletions(-) diff --git a/src/main/java/com/service/UserService.java b/src/main/java/com/service/UserService.java index 06c78bb..ed9553a 100644 --- a/src/main/java/com/service/UserService.java +++ b/src/main/java/com/service/UserService.java @@ -420,7 +420,7 @@ public class UserService { return fileIOService.findUserByUsername(username); } - // ==================== 业务逻辑方法==================== + // ==================== 业务逻辑方法=================== /** * 从用户名提取真实姓名 diff --git a/src/main/java/com/util/EmailUtil.java b/src/main/java/com/util/EmailUtil.java index ff915ac..64d2f83 100644 --- a/src/main/java/com/util/EmailUtil.java +++ b/src/main/java/com/util/EmailUtil.java @@ -58,52 +58,3 @@ public class EmailUtil { } } -/* - * 如果将来需要真正实现邮件发送,可以参考以下代码: - * - * 1. 在pom.xml添加依赖: - * - * javax.mail - * javax.mail-api - * 1.6.2 - * - * - * com.sun.mail - * javax.mail - * 1.6.2 - * - * - * 2. 实现代码示例: - * - * import javax.mail.*; - * import javax.mail.internet.*; - * import java.util.Properties; - * - * public static boolean sendEmail(String toEmail, String subject, String content) { - * try { - * Properties props = new Properties(); - * props.put("mail.smtp.auth", "true"); - * props.put("mail.smtp.starttls.enable", "true"); - * props.put("mail.smtp.host", "smtp.qq.com"); - * props.put("mail.smtp.port", "587"); - * - * Session session = Session.getInstance(props, new Authenticator() { - * protected PasswordAuthentication getPasswordAuthentication() { - * return new PasswordAuthentication("your-email@qq.com", "your-password"); - * } - * }); - * - * Message message = new MimeMessage(session); - * message.setFrom(new InternetAddress("your-email@qq.com")); - * message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail)); - * message.setSubject(subject); - * message.setText(content); - * - * Transport.send(message); - * return true; - * } catch (Exception e) { - * e.printStackTrace(); - * return false; - * } - * } - */ \ No newline at end of file -- 2.34.1 From 60078081e66692ad5b4159e11f4e69a891222897 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Mon, 6 Oct 2025 01:14:44 +0800 Subject: [PATCH 19/28] partial ui implication --- .idea/workspace.xml | 75 ++++-- data/users.json | 27 ++ src/main/java/com/model/User.java | 12 +- src/main/java/com/service/FileIOService.java | 18 +- src/main/java/com/service/QuizService.java | 13 + src/main/java/com/service/UserService.java | 133 ++++++---- src/main/java/com/ui/InfGenPage.java | 91 ++++++- src/main/java/com/ui/LoginPage.java | 12 + src/main/java/com/ui/MainWindow.java | 254 ++++++++++++++---- src/main/java/com/ui/NavigablePanel.java | 27 +- src/main/java/com/ui/PasswordModifyPage.java | 6 +- src/main/java/com/ui/QuizPage.java | 264 ++++++++++++++++++- src/main/java/com/ui/RegisterPage.java | 9 +- src/main/java/com/ui/ResultPage.java | 6 +- src/main/java/com/ui/StartPage.java | 3 + src/main/java/com/ui/UIConstants.java | 51 +++- 16 files changed, 836 insertions(+), 165 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 3a08d13..64d16d4 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,30 +4,23 @@ \ No newline at end of file diff --git a/data/users.json b/data/users.json index f9e127e..60767b8 100644 --- a/data/users.json +++ b/data/users.json @@ -1,6 +1,7 @@ { "users": [ { + "userId": "a1b2c3d4-5678-90ef-ghij-klmnopqrstuv", "username": "小学-测试", "password": "encrypted123", "email": "test@test.com", @@ -10,6 +11,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "b2c3d4e5-6789-01fg-hijk-lmnopqrstuvw", "username": "小学-张三测试", "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263", "email": "test001@example.com", @@ -19,6 +21,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "c3d4e5f6-7890-12hi-jklm-nopqrstuvwxy", "username": "初中-李明", "password": "222711cc1da343bafd214b51a33d189a425e801b5d45774a941bbf68a1116d5c", "email": "middle@test.com", @@ -28,6 +31,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "d4e5f6g7-8901-23jk-lmno-pqrstuvwxyz", "username": "高中-王华", "password": "9f3abee248c95d9ed301ee5a5b71318c0838c994ac5f66e70bfc9ee7ecad0150", "email": "high@test.com", @@ -37,6 +41,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "e5f6g7h8-9012-34lm-mnop-qrstuvwxyza", "username": "小学-重载测试", "password": "bd3910fa48dc018fb9884e1d78649396d96882f6fcd8cbcd87b3e0c8bfc86e15", "email": "reload@test.com", @@ -46,6 +51,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "f6g7h8i9-0123-45no-opqr-stuvwxyzabc", "username": "小学-李四", "password": "encrypted", "email": "lisi@test.com", @@ -55,6 +61,7 @@ "registrationDate": "Oct 4, 2025, 12:30:47 PM" }, { + "userId": "g7h8i9j0-1234-56op-pqrs-tuvwxyzabcd", "username": "初中-王五", "password": "9a931c55ac02bf216550c464b1992a30c522dfabf6cb31deada5c716bc13a263", "email": "wangwu@test.com", @@ -62,6 +69,26 @@ "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": "ljy", + "password": "950d32ffc1f6079e12d28efdc0e8db995129e30a9dd4f91eae31a81f13389caa", + "email": "ljy.sbp@gmail.com", + "grade": "ELEMENTARY", + "totalQuizzes": 0, + "averageScore": 0.0, + "registrationDate": "Oct 5, 2025, 9:35:09 PM" } ] } \ 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 56beb01..e5e02a3 100644 --- a/src/main/java/com/service/UserService.java +++ b/src/main/java/com/service/UserService.java @@ -26,7 +26,7 @@ public class UserService { 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,}$"); @@ -236,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)) { @@ -264,21 +264,28 @@ public class UserService { } // 6. 从用户名中提取学段 - Grade grade = extractGradeFromUsername(username); +// Grade grade = extractGradeFromUsername(username); + Grade grade = Grade.ELEMENTARY; // 7. 加密密码 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 { @@ -296,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; } @@ -305,7 +312,7 @@ public class UserService { if (user != null) { this.currentUser = user; - System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user)); + // System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user)); } return user; @@ -313,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(); } @@ -398,10 +405,23 @@ 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 updateUserStatistics(User user, int score) throws IOException { int oldTotal = user.getTotalQuizzes(); double oldAverage = user.getAverageScore(); @@ -425,23 +445,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; +// } /** * 获取学段中文名称 @@ -470,7 +490,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"); @@ -483,14 +502,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()) { @@ -501,15 +520,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 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/InfGenPage.java b/src/main/java/com/ui/InfGenPage.java index 94784c7..98ca26e 100644 --- a/src/main/java/com/ui/InfGenPage.java +++ b/src/main/java/com/ui/InfGenPage.java @@ -7,49 +7,120 @@ 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 Label usernameLabel = new Label("用户名:张三"); + 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) { + public InfGenPage(Runnable onBack, String currentUsername, String currentEmail) { super(onBack); + initializeContent(); + usernameField.setText(currentUsername); + emailField.setText(currentEmail); } @Override protected void buildContent() { - VBox form = new VBox(UIConstants.DEFAULT_SPACING); + // 外层容器: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(100); - - HBox countBox = new HBox(new Label("题目数量:"), questionCountSpinner); - countBox.setAlignment(Pos.CENTER); - countBox.setSpacing(10); + 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 + ); - form.getChildren().addAll(titleLabel, usernameLabel, new Label("学段选择:"), gradeChoice, countBox, generateButton, passwordModifyButton); 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; } - public void setUsername(String username) { usernameLabel.setText("用户名:" + username); } + } \ No newline at end of file diff --git a/src/main/java/com/ui/LoginPage.java b/src/main/java/com/ui/LoginPage.java index 7eeb82e..e10a539 100644 --- a/src/main/java/com/ui/LoginPage.java +++ b/src/main/java/com/ui/LoginPage.java @@ -1,6 +1,9 @@ // 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; @@ -17,6 +20,7 @@ public class LoginPage extends NavigablePanel { public LoginPage(Runnable onBack) { super(onBack); + initializeContent(); // 字段初始化 } @Override @@ -30,10 +34,18 @@ public class LoginPage extends NavigablePanel { 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); diff --git a/src/main/java/com/ui/MainWindow.java b/src/main/java/com/ui/MainWindow.java index 668982e..640152e 100644 --- a/src/main/java/com/ui/MainWindow.java +++ b/src/main/java/com/ui/MainWindow.java @@ -1,11 +1,23 @@ // com/ui/MainWindow.java package com.ui; +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; public class MainWindow extends BorderPane { + private final UserService userService = new UserService(); + private final QuizService quizService = new QuizService(); + private User currentUser; private final Stage primaryStage; private Panel currentPanel; @@ -24,61 +36,214 @@ public class MainWindow extends BorderPane { 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; + } - case LOGIN -> { - LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START)); - loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER)); - loginPage.getLoginButton().setOnAction(e -> navigateTo(Panel.INF_GEN)); - this.setCenter(loginPage); - } + // 封装登录页面初始化逻辑 + private void initLoginPage() { + LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START)); + // 注册链接跳转 + loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER)); + // 登录按钮点击事件(绑定封装的登录处理方法) + loginPage.getLoginButton().setOnAction(e -> handleLoginAction(loginPage)); + this.setCenter(loginPage); + } + + // 封装登录核心逻辑(从UI获取数据→调用服务层→处理结果) + private void handleLoginAction(LoginPage loginPage) { + String username = loginPage.getUsernameOrEmailField().getText().trim(); + String password = loginPage.getPasswordField().getText().trim(); + + if (username.isEmpty()) { + NavigablePanel.showErrorAlert("输入错误", "请输入用户名"); + return ; + } + if (password.isEmpty()) { + NavigablePanel.showErrorAlert("输入错误", "请输入密码"); + return ; + } + try { + this.currentUser = userService.login(username, password);; // 保存当前用户 + navigateTo(Panel.INF_GEN); // 登录成功跳转 + } catch (IllegalArgumentException ex) { + NavigablePanel.showErrorAlert("登录失败", ex.getMessage()); + } catch (IOException ex) { + NavigablePanel.showErrorAlert("系统错误", "登录过程中发生错误:" + ex.getMessage()); + } + } + + // 其他页面的初始化方法(保持原有逻辑,统一格式) + private void initRegisterPage() { + RegisterPage registerPage = new RegisterPage(() -> navigateTo(Panel.LOGIN)); + registerPage.getSendCodeButton().setOnAction(e -> handleSendCodeAction(registerPage)); + registerPage.getRegisterButton().setOnAction(e -> handleRegisterAction(registerPage)); + this.setCenter(registerPage); + } + + + private void handleSendCodeAction(RegisterPage registerPage) { + String email = registerPage.getEmailField().getText().trim(); + + 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(); + + 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()); + } + } - case REGISTER -> { - RegisterPage registerPage = new RegisterPage(() -> navigateTo(Panel.LOGIN)); - registerPage.getRegisterButton().setOnAction(e -> navigateTo(Panel.LOGIN)); - this.setCenter(registerPage); + 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(); + 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()); + } + } - case INF_GEN -> { - InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN)); - infGenPage.getGenerateButton().setOnAction(e -> { - // 可在此处校验题数(10-30) - int count = infGenPage.getQuestionCountSpinner().getValue(); - if (count < 10 || count > 30) { - // TODO: 弹出提示“题数必须为10-30” - return; + private void initPasswordModifyPage() { + PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN)); + pwdPage.getModifyButton().setOnAction(e -> { + // TODO: 调用 UserService 修改密码 + navigateTo(Panel.INF_GEN); + }); + this.setCenter(pwdPage); + } + + 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.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; } - navigateTo(Panel.QUIZ); - }); - infGenPage.getPasswordModifyButton().setOnAction(e -> navigateTo(Panel.PASSWORDMODIFY)); - this.setCenter(infGenPage); + } + quizService.submitCurrentAnswer(selectedIndex); } - - case PASSWORDMODIFY -> { - PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN)); - pwdPage.getModifyButton().setOnAction(e -> { - // TODO: 调用 UserService 修改密码 - navigateTo(Panel.INF_GEN); - }); - this.setCenter(pwdPage); + // 无论是否选择,都尝试跳转到下一题 + if (quizService.nextQuestion()) { + quizPage.goToQuestion(quizService.getCurrentQuestionIndex()); + } else { + // 已是最后一题,显示“交卷”按钮 + quizPage.getSubmitButton().setVisible(true); + quizPage.getNextButton().setVisible(false); } - - case QUIZ -> { - QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN)); - quizPage.getSubmitButton().setOnAction(e -> navigateTo(Panel.RESULT)); - // TODO: 初始化题目、绑定选项点击逻辑 - this.setCenter(quizPage); + }); + 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); + }); - case RESULT -> { - ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN)); - resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START)); - resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN)); - this.setCenter(resultPage); - } - } - currentPanel = target; + this.setCenter(quizPage); } + private void initResultPage() { + ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN)); + resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START)); + resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN)); + this.setCenter(resultPage); + } + + // getter和启动方法(保持不变) public Panel getCurrentPanel() { return currentPanel; } @@ -90,5 +255,4 @@ public class MainWindow extends BorderPane { 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 index 130e06e..dc2e4af 100644 --- a/src/main/java/com/ui/NavigablePanel.java +++ b/src/main/java/com/ui/NavigablePanel.java @@ -1,20 +1,30 @@ // 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("← 返回"); + 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); @@ -22,8 +32,21 @@ public abstract class NavigablePanel extends BorderPane { topBar.getChildren().add(backButton); this.setTop(topBar); - buildContent(); } + 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/PasswordModifyPage.java b/src/main/java/com/ui/PasswordModifyPage.java index bacb107..06197fe 100644 --- a/src/main/java/com/ui/PasswordModifyPage.java +++ b/src/main/java/com/ui/PasswordModifyPage.java @@ -16,6 +16,7 @@ public class PasswordModifyPage extends NavigablePanel { public PasswordModifyPage(Runnable onBack) { super(onBack); + initializeContent(); } @Override @@ -29,9 +30,12 @@ public class PasswordModifyPage extends NavigablePanel { 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)); diff --git a/src/main/java/com/ui/QuizPage.java b/src/main/java/com/ui/QuizPage.java index 6e4a156..7798bcc 100644 --- a/src/main/java/com/ui/QuizPage.java +++ b/src/main/java/com/ui/QuizPage.java @@ -1,15 +1,24 @@ // 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.HBox; -import javafx.scene.layout.VBox; +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(); @@ -17,42 +26,271 @@ public class QuizPage extends NavigablePanel { private final Button nextButton = new Button("下一题"); private final Button submitButton = new Button("交卷"); - public QuizPage(Runnable onBack) { + // 题目导航矩阵容器 + 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() { - VBox container = new VBox(UIConstants.DEFAULT_SPACING); - container.setAlignment(Pos.CENTER); - container.setPadding(new Insets(20)); + // 设置整体布局: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(); + + mainContent.getChildren().addAll(leftPanel, rightPanel); + this.setCenter(mainContent); + + // 底部按钮 + HBox bottomBar = new HBox(20); + bottomBar.setAlignment(Pos.CENTER); + bottomBar.getChildren().addAll(nextButton, submitButton); + this.setBottom(bottomBar); + + // 初始化按钮状态 + 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]); } - HBox buttonBox = new HBox(20); - buttonBox.setAlignment(Pos.CENTER); - buttonBox.getChildren().addAll(nextButton, submitButton); + 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; - container.getChildren().addAll(progressLabel, questionLabel, optionsBox, buttonBox); - this.setCenter(container); + 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)); - // 默认隐藏交卷按钮 - submitButton.setVisible(false); + 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 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/RegisterPage.java b/src/main/java/com/ui/RegisterPage.java index 31d6e40..df3585a 100644 --- a/src/main/java/com/ui/RegisterPage.java +++ b/src/main/java/com/ui/RegisterPage.java @@ -18,6 +18,7 @@ public class RegisterPage extends NavigablePanel { public RegisterPage(Runnable onBack) { super(onBack); + initializeContent(); } @Override @@ -31,15 +32,19 @@ public class RegisterPage extends NavigablePanel { 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, diff --git a/src/main/java/com/ui/ResultPage.java b/src/main/java/com/ui/ResultPage.java index a70767b..f207fd5 100644 --- a/src/main/java/com/ui/ResultPage.java +++ b/src/main/java/com/ui/ResultPage.java @@ -17,6 +17,7 @@ public class ResultPage extends NavigablePanel { public ResultPage(Runnable onBack) { super(onBack); + initializeContent(); } @Override @@ -30,13 +31,16 @@ public class ResultPage extends NavigablePanel { 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); diff --git a/src/main/java/com/ui/StartPage.java b/src/main/java/com/ui/StartPage.java index 98cbb6b..d340334 100644 --- a/src/main/java/com/ui/StartPage.java +++ b/src/main/java/com/ui/StartPage.java @@ -28,6 +28,9 @@ public class StartPage extends VBox { 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); } diff --git a/src/main/java/com/ui/UIConstants.java b/src/main/java/com/ui/UIConstants.java index 3879cb5..d41f9f0 100644 --- a/src/main/java/com/ui/UIConstants.java +++ b/src/main/java/com/ui/UIConstants.java @@ -1,35 +1,70 @@ -// com/ui/UIConstants.java +// 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, 10, 10, 10); + 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 = 24.0; + 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 = 16.0; + public static final double BUTTON_FONT_SIZE = 15.0; public static final double LABEL_FONT_SIZE = 14.0; - public static final double QUIZ_TITLE_FONT_SIZE = 18.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 = 120.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-border-radius: 10; " + - "-fx-background-radius: 10; " + + "-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 -- 2.34.1 From 36c01d19508f28aa63037d5fb67f1ea7f302681d Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Mon, 6 Oct 2025 19:19:38 +0800 Subject: [PATCH 20/28] version one ,do not package --- .idea/artifacts/MathQuizApp_jar.xml | 20 + .idea/workspace.xml | 48 +- META-INF/MANIFEST.MF | 3 + data/users.json | 12 +- pom.xml | 71 +- src/main/java/com/Test.java | 2 +- src/main/java/com/model/Grade.java | 30 +- src/main/java/com/model/QuizResult.java | 8 +- src/main/java/com/service/UserService.java | 21 +- src/main/java/com/ui/MainWindow.java | 38 +- src/main/java/com/ui/QuizPage.java | 27 +- src/main/java/com/ui/ResultPage.java | 12 +- src/test/java/TestMain.java | 774 --------------------- 13 files changed, 205 insertions(+), 861 deletions(-) create mode 100644 .idea/artifacts/MathQuizApp_jar.xml create mode 100644 META-INF/MANIFEST.MF delete mode 100644 src/test/java/TestMain.java 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/workspace.xml b/.idea/workspace.xml index 64d16d4..d9fb858 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -1,26 +1,28 @@ + + + + + - + + + - - - + + + + - - - - - - - + + + { "customColor": "", "associatedIndex": 1 @@ -66,6 +71,7 @@ "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", @@ -73,6 +79,9 @@ "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" } @@ -129,7 +138,9 @@ - + + + - @@ -170,6 +189,7 @@ - \ 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/users.json b/data/users.json index 60767b8..9c5d1fb 100644 --- a/data/users.json +++ b/data/users.json @@ -82,13 +82,23 @@ }, { "userId": "3fff6ec1-76f9-4ce6-9880-4a1859b4f5a3", - "username": "ljy", + "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/pom.xml b/pom.xml index b23987a..b719a82 100644 --- a/pom.xml +++ b/pom.xml @@ -5,7 +5,6 @@ http://maven.apache.org/xsd/maven-4.0.0.xsd"> 4.0.0 - com.mathquiz MathQuizApp 1.0.0 @@ -14,26 +13,28 @@ Math Quiz Application 小初高数学学习软件 - JavaFX版本 - + + + + jitpack.io + https://jitpack.io + + + UTF-8 21 21 21.0.2 - - 0.0.8 - - com.google.code.gson gson 2.10.1 - org.openjfx javafx-base @@ -56,18 +57,8 @@ - - - - openjfx-repo - https://oss.sonatype.org/content/repositories/openjfx-releases/ - - - - - org.apache.maven.plugins maven-compiler-plugin @@ -79,7 +70,6 @@ - org.apache.maven.plugins maven-resources-plugin @@ -89,49 +79,32 @@ - org.openjfx javafx-maven-plugin - ${javafx.plugin.version} + 0.0.8 - com.Test - - + com.mathquiz.MathQuizApp/com.Test - - - org.apache.maven.plugins - maven-shade-plugin - 3.5.1 + + false + ${project.name} + com.mathquiz + ${project.version} + true + true + + - package + default-cli - shade + jpackage - - - - com.Test - - - - - - *:* - - META-INF/*.SF - META-INF/*.DSA - META-INF/*.RSA - - - - MathQuizApp - + \ No newline at end of file diff --git a/src/main/java/com/Test.java b/src/main/java/com/Test.java index a2ad9f3..58a542c 100644 --- a/src/main/java/com/Test.java +++ b/src/main/java/com/Test.java @@ -11,7 +11,7 @@ public class Test extends Application { @Override public void start(Stage primaryStage) { MainWindow mainWindow = new MainWindow(primaryStage); - Scene scene = new Scene(mainWindow, 800, 600); + Scene scene = new Scene(mainWindow, 1366, 786); primaryStage.setTitle("中小学数学答题系统"); primaryStage.setScene(scene); primaryStage.setResizable(true); 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/service/UserService.java b/src/main/java/com/service/UserService.java index e5e02a3..ed75153 100644 --- a/src/main/java/com/service/UserService.java +++ b/src/main/java/com/service/UserService.java @@ -336,7 +336,7 @@ public class UserService { // ==================== 密码管理 ==================== - public boolean changePassword(User user, String oldPassword, String newPassword) throws IOException { + 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("旧密码错误!"); @@ -351,6 +351,10 @@ public class UserService { throw new IllegalArgumentException("新密码不能与旧密码相同!"); } + if (!oldPassword.equals(confirmPassword)) { + throw new IllegalArgumentException("两次新密码不同!"); + } + String hashedNewPassword = PasswordValidator.encrypt(newPassword); user.setPassword(hashedNewPassword); @@ -422,6 +426,21 @@ public class UserService { } } + 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(); diff --git a/src/main/java/com/ui/MainWindow.java b/src/main/java/com/ui/MainWindow.java index 640152e..73d4e5d 100644 --- a/src/main/java/com/ui/MainWindow.java +++ b/src/main/java/com/ui/MainWindow.java @@ -137,6 +137,14 @@ public class MainWindow extends BorderPane { 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; @@ -173,13 +181,27 @@ public class MainWindow extends BorderPane { private void initPasswordModifyPage() { PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN)); - pwdPage.getModifyButton().setOnAction(e -> { - // TODO: 调用 UserService 修改密码 - navigateTo(Panel.INF_GEN); - }); + pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage)); this.setCenter(pwdPage); } + private void handlePasswordModify(PasswordModifyPage pwdPage) { + String oldPassword = pwdPage.getOldPasswordField().getText().trim(); + String newPassword = pwdPage.getNewPasswordField().getText().trim(); + String confirmPassword = pwdPage.getConfirmNewPasswordField().getText().trim(); + + try { + userService.changePassword(userService.getCurrentUser(),oldPassword, newPassword, confirmPassword); + navigateTo(Panel.INF_GEN); + } catch (IllegalArgumentException ex) { + NavigablePanel.showErrorAlert("修改失败", ex.getMessage()); + return; + } catch (IOException ex) { + NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage()); + return; + } + } + private void initQuizPage() { try { quizService.startNewQuiz(currentUser, quizService.getAnswerNumber()); @@ -193,6 +215,11 @@ public class MainWindow extends BorderPane { 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(); @@ -237,9 +264,10 @@ public class MainWindow extends BorderPane { } private void initResultPage() { - ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN)); + 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); } diff --git a/src/main/java/com/ui/QuizPage.java b/src/main/java/com/ui/QuizPage.java index 7798bcc..8acfbe8 100644 --- a/src/main/java/com/ui/QuizPage.java +++ b/src/main/java/com/ui/QuizPage.java @@ -23,6 +23,7 @@ public class QuizPage extends NavigablePanel { 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("交卷"); @@ -58,15 +59,32 @@ public class QuizPage extends NavigablePanel { // 右侧:题目导航矩阵 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); // 底部按钮 - HBox bottomBar = new HBox(20); - bottomBar.setAlignment(Pos.CENTER); - bottomBar.getChildren().addAll(nextButton, submitButton); - this.setBottom(bottomBar); + 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(); @@ -278,6 +296,7 @@ public class QuizPage extends NavigablePanel { 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; } // ========== 设置器方法 ========== diff --git a/src/main/java/com/ui/ResultPage.java b/src/main/java/com/ui/ResultPage.java index f207fd5..8417f0a 100644 --- a/src/main/java/com/ui/ResultPage.java +++ b/src/main/java/com/ui/ResultPage.java @@ -1,6 +1,8 @@ // 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; @@ -10,14 +12,16 @@ 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) { + public ResultPage(Runnable onBack, QuizService quizService) { super(onBack); initializeContent(); + this.quizService = quizService; } @Override @@ -46,6 +50,12 @@ public class ResultPage extends NavigablePanel { 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; } diff --git a/src/test/java/TestMain.java b/src/test/java/TestMain.java deleted file mode 100644 index 8f71efc..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 -- 2.34.1 From 1cbcbc1f311e5bea8d82e40b39d8d6f940947863 Mon Sep 17 00:00:00 2001 From: lsbp <2803234009@qq.com> Date: Mon, 6 Oct 2025 21:56:09 +0800 Subject: [PATCH 21/28] =?UTF-8?q?=E5=8F=91=E8=A1=8C=E7=89=881.01?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 34 ++++++----- data/current_user.json | 10 ++++ dependency-reduced-pom.xml | 66 ++++++++++++++++++++++ pom.xml | 47 +++++++++------ src/main/java/com/service/UserService.java | 32 ++++++++++- src/main/java/com/util/FileUtils.java | 26 ++++++++- 6 files changed, 181 insertions(+), 34 deletions(-) create mode 100644 data/current_user.json create mode 100644 dependency-reduced-pom.xml diff --git a/.idea/workspace.xml b/.idea/workspace.xml index d9fb858..30835f7 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -9,20 +9,15 @@