Compare commits

..

6 Commits

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GoogleJavaFormatSettings">
<option name="enabled" value="true" />
</component>
</project>

@ -0,0 +1,265 @@
# 数学答题系统用户手册
## Math Quiz System User Manual
**版本**: 2.0.0
**日期**: 2025年10月10日
**作者**: 梁峻耀
---
## 1. 系统简介 (System Introduction)
### 1.1 产品概述
数学答题系统是一款专为中小学生设计的数学学习软件,覆盖小学、初中、高中三个阶段的数学知识点。系统提供题目自动生成和详细的成绩分析,帮助学生提高数学学习能力。
### 1.2 主要功能
- **多级别支持**: 小学、初中、高中数学题目
- **智能出题**: 根据年级自动生成合适难度的题目
- **成绩分析**: 有成绩统计和平均分计算
- **等级评定**: 优秀、良好、中等、及格、不及格五级评价
- 友好界面**: 直观易用的图形用户界面
- **数据保存**: 自动保存答题记录和学习进度
### 1.3 系统特色
- **个性化学习**: 根据学生水平智能推荐题目
- **即时反馈**: 答题后可到文件查看题目答案
- **离线使用**: 无需网络连接,随时随地学习
- **安全可靠**: 数据本地存储,保护用户隐私
---
## 2. 运行环境 (System Requirements)
### 2.1 硬件要求
- **处理器**: Intel Core i3 或同等性能处理器
- **内存**: 最少512MB推荐1GB及以上
- **硬盘**: 至少100MB可用存储空间
- **显示器**: 支持1366×768或更高分辨率
### 2.2 软件要求
- **操作系统**: Windows 10/11
- **Java环境**: Java 21或更高版本
- **其他**: 需要管理员权限进行安装
### 2.3 安装前准备
1. 确保系统已安装Java 21或更高版本
2. 确认有足够的磁盘空间
4. 以管理员身份运行安装程序
---
## 3. 安装指南 (Installation Guide)
### 3.1 获取安装文件
1. 文件名称: `MathQuizApp-v2.0.0-setup.exe`
2. 文件大小: 约85MB
### 3.2 安装步骤
#### 步骤1: 运行安装程序
双击下载的安装文件,启动安装向导,第一次启动可能优点慢
#### 步骤2: 进入安装向导
点击"下一步"进入安装
#### 步骤3: 选择安装位置
默认安装路径: `C:\Program Files\MathQuizApp`
可点击"更改"选择其他安装路径
#### 步骤4: 确认安装
检查安装设置,点击"安装"开始安装,需要管理员权限,可能有些慢
#### 步骤5: 完成安装
安装完成后,点击"完成"退出安装向导
### 3.3 首次启动
1. 双击桌面快捷方式启动程序
2. 首次启动可能需要几秒钟的初始化时间
3. 如果提示Java版本过低请先升级Java环境
---
## 4. 快速开始 (Quick Start)
### 4.1 基本流程
```
启动程序 → 注册/登录 → 选择年级 → 设置题目数量 → 开始答题 → 查看成绩
```
### 4.2 首次使用
#### 4.2.1 用户注册
1. 在登录界面点击"注册新用户"
2. 填写注册信息:
- 邮箱:输入有效的邮箱地址
- 密码设置登录密码至少6位
- 确认密码:再次输入密码
3. 点击"获取验证码",查收邮箱验证码
4. 输入验证码,点击"注册"
5. **如果遇到错误注册失败得重新获取注册码**
6. 注册成功后自动登录
7. 注册后用户名和邮箱相同,可自行修改
#### 4.2.2 用户登录
1. 输入用户名或邮箱地址
2. 输入登录密码
3. 点击"登录"按钮
4. 成功登录后进入主界面
#### 4.2.3 开始答题
1. 在主界面点击"开始答题"
2. 选择年级:小学/初中/高中
3. 设置题目数量10-30道
4. 点击"生成题目"进入答题界面
5. 选择答案,使用导航按钮切换题目
6. 完成所有题目后点击"交卷"
7. 查看成绩报告
---
## 5. 功能详解 (Detailed Function Description)
### 5.1 用户管理
#### 5.1.1 用户注册
**功能说明**: 创建新用户账户
**操作步骤**:
1. 点击"注册新用户"按钮
2. 填写注册信息:
- **邮箱**: 必须是有效的邮箱格式,用于接收验证码
- **密码**: 长度至少6位必须包含字母和数字不超过10位
- **确认密码**: 必须与密码完全一致
3. 点击"获取验证码",系统会发送验证码到您的邮箱
4. 在验证码输入框中填写收到的验证码
5. 点击"注册"完成注册
**注意事项**:
- 用户名和邮箱地址不能重复
- 验证码有效期为10分钟
- 如未收到邮件,请检查垃圾邮件箱
#### 5.1.2 用户登录
**功能说明**: 使用已有账户登录系统
**操作步骤**:
1. 在用户名/邮箱输入框中输入注册时的邮箱或修改后的用户名
2. 在密码输入框中输入登录密码
3. 点击"登录"按钮
**登录失败处理**:
- 用户名/邮箱错误:提示"用户不存在"
- 密码错误:提示"密码错误"
#### 5.1.3 个人信息管理
**功能说明**: 查看和修改个人信息
**包含功能**:
- **查看个人信息**: 显示用户名、邮箱、答题统计
- **修改密码**: 需要输入原密码和新密码
- **查看答题统计**: 显示总答题次数、平均分数
### 5.2 题目设置
#### 5.2.1 年级选择
**可选年级**:
- **小学**: 适合1-6年级学生
- 加法、减法、乘法、除法
- 括号运算、混合运算
- **初中**: 适合7-9年级学生
- 一元一次方程、一元二次方程
- 平方根、平方运算
- 几何计算(圆面积、三角形面积)
- 一次函数、混合运算
- **高中**: 适合10-12年级学生
- 三角函数sin、cos、tan
- 三角恒等式、三角方程
- 导数计算、函数极值
- 等差数列、对数运算
- 概率计算
### 5.3 答题界面
#### 5.3.1 界面布局
详见"UI设计.pdf"
#### 5.3.2 答题导航
**导航功能**:
- **上一题**: 返回上一道题目
- **下一题**: 前进到下一道题目
- **题号列表**: 显示所有题号,可快速跳转
**状态指示**:
- **已答题**: 绿色背景
- **未答题**: 灰色背景
- **当前题**: 蓝色背景
### 5.4 成绩报告
#### 5.4.1 成绩总览
**显示内容**:
- **总题数**: 本次答题的总题目数量
- **正确数**: 答对的题目数量
- **得分**: 百分比得分
- **等级**: 优秀/良好/中等/及格/不及格
**等级标准**:
- **优秀**: 90-100分
- **良好**: 80-89分
- **中等**: 70-79分
- **及格**: 60-69分
- **不及格**: 0-59分
### 5.5 历史记录
#### 5.5.1 答题历史
**记录内容**:
- 答题时间
- 题目详情
- 得分
- 题目正确答案和用户答案
---
**用户手册结束**
**感谢您选择数学答题系统!**
祝您学习进步,数学成绩更上一层楼!
**最后更新**: 2025年10月10日
**文档版本**: 2.0.0

Binary file not shown.

@ -0,0 +1,442 @@
# 数学答题系统设计文档
## Math Quiz System Design Document
**版本**: 2.0.0
**日期**: 2025年10月10日
**作者**: 梁峻耀
**状态**: 最终版
---
## 1. 系统架构设计 (System Architecture)
### 1.1 总体架构
系统采用**MVC (Model-View-Controller)**架构模式,结合**工厂模式**和**策略模式**实现灵活的题目生成机制。
```
┌─────────────────────────────────────────────────────────────┐
│ 用户界面层 (View) │
├─────────────────────────────────────────────────────────────┤
│ LoginPage RegisterPage InfGenPage QuizPage ResultPage... │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 业务逻辑层 (Controller) │
├─────────────────────────────────────────────────────────────┤
│ MainWindow UserService QuizService FileIOService │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 数据模型层 (Model) │
├─────────────────────────────────────────────────────────────┤
│ User Grade ChoiceQuestion QuizResult │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ 题目生成引擎 (Question Generator) │
├─────────────────────────────────────────────────────────────┤
│ QuestionFactoryManager AbstractQuestionStrategy │
│ Elementary/Middle/High Factories Concrete Strategies │
└─────────────────────────────────────────────────────────────┘
```
### 1.2 架构特点
- **松耦合**: 各层之间通过接口进行解耦
- **高内聚**: 同一层内的组件功能相关性高
- **可扩展**: 支持新的题型和年级级别
- **可维护**: 清晰的职责分离便于维护
---
## 2. 详细设计 (Detailed Design)
### 2.1 类结构设计
#### 2.1.1 核心模型类 (Model Classes)
```java
// 用户模型
public class User {
private String username; // 用户名
private String email; // 邮箱
private String password; // 密码(加密存储)
private Grade grade; // 年级
private int totalQuizzes; // 总答题次数
private double averageScore; // 平均成绩
private LocalDateTime registerTime; // 注册时间
}
// 年级枚举
public enum Grade {
ELEMENTARY("小学"),
MIDDLE("初中"),
HIGH("高中");
}
// 题目模型
public class ChoiceQuestion {
private String question; // 题目内容
private List<String> options; // 选项列表
private int correctAnswer; // 正确答案索引
private Grade grade; // 所属年级
private String type; // 题目类型
}
// 答题结果
public class QuizResult {
private User user; // 用户
private List<ChoiceQuestion> questions; // 题目列表
private List<Integer> userAnswers; // 用户答案
private int correctCount; // 正确数量
private double score; // 得分百分比
private String rating; // 等级评价
private LocalDateTime quizTime; // 答题时间
}
```
#### 2.1.2 服务层设计 (Service Layer)
```java
// 用户服务
public class UserService {
- registerUser(User user): boolean
- loginUser(String credential, String password): User
- updateUser(User user): boolean
- changePassword(String username, String newPassword): boolean
- getUserStatistics(String username): UserStatistics
}
// 答题服务
public class QuizService {
- generateQuestions(Grade grade, int count): List<ChoiceQuestion>
- submitQuiz(QuizResult result): boolean
- calculateScore(List<ChoiceQuestion> questions, List<Integer> answers): QuizResult
- getQuizHistory(String username): List<QuizResult>
}
// 文件IO服务
public class FileIOService {
- saveUserData(User user): boolean
- loadUserData(String username): User
- saveQuizResult(QuizResult result): boolean
- loadQuizHistory(String username): List<QuizResult>
}
```
#### 2.1.3 题目生成引擎 (Question Generator)
```java
// 题目工厂管理器
public class QuestionFactoryManager {
- Map<Grade, QuestionFactory> factories
+ getFactory(Grade grade): QuestionFactory
+ generateQuestions(Grade grade, int count): List<ChoiceQuestion>
}
// 抽象题目工厂
public abstract class QuestionFactory {
+ createQuestion(): ChoiceQuestion
+ createQuestions(int count): List<ChoiceQuestion>
}
// 具体工厂实现
public class ElementaryQuestionFactory extends QuestionFactory {
- List<AbstractQuestionStrategy> strategies
+ createQuestion(): ChoiceQuestion
}
// 抽象策略
public abstract class AbstractQuestionStrategy {
+ generateQuestion(): ChoiceQuestion
+ getQuestionType(): String
}
// 具体策略实现
public class AdditionStrategy extends AbstractQuestionStrategy {
+ generateQuestion(): ChoiceQuestion // 生成加法题目
}
```
### 2.2 用户界面设计 (UI Design)
#### 2.2.1 界面流程图
```
开始 → 启动页面 → 登录/注册 → 主界面 → 题目设置 → 答题界面 → 结果界面 → 返回主界面
```
#### 2.2.2 主要界面设计
**启动页面 (StartPage)**:
- 应用标题和Logo
- 进入系统按钮
- 简洁的背景设计
**登录界面 (LoginPage)**:
- 用户名/邮箱输入框
- 密码输入框
- 登录按钮
- 注册链接
- 错误提示区域
**注册界面 (RegisterPage)**:
- 用户信息输入表单
- 邮箱验证功能
- 注册按钮
- 返回登录链接
**主界面 (MainWindow)**:
- 用户信息显示
- 功能导航菜单
- 统计信息展示
- 退出登录选项
**题目设置界面 (InfGenPage)**:
- 年级选择下拉框
- 题目数量滑块
- 开始答题按钮
**答题界面 (QuizPage)**:
- 题目显示区域
- 选项选择区域
- 导航控制区域
- 进度显示条
- 答题状态标识
**结果界面 (ResultPage)**:
- 成绩总览
- 正确/错误统计
- 等级评价
- 详细查看按钮
- 返回主界面按钮
### 2.3 数据存储设计 (Data Storage)
#### 2.3.1 文件结构
```
data/
├── history/ # 做题历史
│ └── {username}/ # 个人用户目录
│ └── {timestamp}.txt # 答题历史文件
├── users.json # 用户列表
├── registration_codes.json # 注册码文件
└── current_user.json # 当前登录用户信息
```
#### 2.3.2 数据格式设计
**用户数据格式**:
```json
"users": [
{
"userId": "cfe856d6-8087-484a-85ac-55fc1b5e4388",
"username": "ljy_",
"password": "950d32ffc1f6079e12d28efdc0e8db995129e30a9dd4f91eae31a81f13389caa",
"email": "2803234009@qq.com",
"grade": "HIGH",
"totalQuizzes": 2,
"averageScore": 0.0,
"registrationDate": "Oct 10, 2025, 4:40:06PM"
}
]
```
**注册码格式**:
```json
# 注册码记录文件
# 格式: 邮箱|注册码|过期时间戳|过期时间
```
---
## 3. 题目生成算法设计 (Question Generation Algorithm)
### 3.1 算法架构
采用**策略模式**实现不同类型的题目生成算法,每种题型都有独立的策略类。
### 3.2 小学题目算法
#### 3.2.1 加法运算
```java
public class AdditionStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int a = random.nextInt(100); // 0-99
int b = random.nextInt(100); // 0-99
int answer = a + b;
// 生成干扰选项
List<Integer> wrongAnswers = generateWrongAnswers(answer, 3);
return new ChoiceQuestion(
String.format("%d + %d = ?", a, b),
buildOptions(answer, wrongAnswers),
0 // 正确答案索引
);
}
}
```
#### 3.2.2 括号运算
```java
public class ParenthesesAddStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int a = random.nextInt(50) + 1;
int b = random.nextInt(50) + 1;
int c = random.nextInt(50) + 1;
int answer = a + (b + c);
// 生成包含不同运算顺序的干扰选项
int wrong1 = (a + b) + c; // 正确
int wrong2 = a + b - c; // 错误运算
int wrong3 = a * (b + c); // 错误运算符
return new ChoiceQuestion(
String.format("%d + (%d + %d) = ?", a, b, c),
Arrays.asList(String.valueOf(answer),
String.valueOf(wrong1),
String.valueOf(wrong2),
String.valueOf(wrong3)),
findCorrectIndex(answer, options)
);
}
}
```
### 3.3 初中题目算法
#### 3.3.1 一元一次方程
```java
public class LinearEquationStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int a = random.nextInt(9) + 1; // 1-9
int x = random.nextInt(19) - 9; // -9到9
int b = random.nextInt(50) + 1; // 1-50
int c = a * x + b;
String question = String.format("%dx + %d = %d, 求x的值", a, b, c);
int correctAnswer = x;
// 生成干扰选项(常见错误答案)
List<Integer> wrongAnswers = Arrays.asList(
(c - b) / a - 1, // 计算错误
(c - b + a) / a, // 符号错误
c / a - b / a // 运算顺序错误
);
return buildQuestion(question, correctAnswer, wrongAnswers);
}
}
```
#### 3.3.2 几何计算
```java
public class CircleAreaStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int r = random.nextInt(10) + 1; // 1-10
double area = Math.PI * r * r;
double roundedArea = Math.round(area * 100.0) / 100.0; // 保留2位小数
String question = String.format("半径为%d的圆的面积是多少(π取3.14)", r);
// 生成干扰选项
List<Double> wrongAnswers = Arrays.asList(
Math.PI * r * 2, // 周长
Math.PI * r, // 半周长
Math.PI * r * r / 2 // 半面积
);
return buildQuestion(question, roundedArea, wrongAnswers);
}
}
```
### 3.4 高中题目算法
#### 3.4.1 三角函数
```java
public class SinStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int angle = random.nextInt(360); // 0-359度
double sinValue = Math.sin(Math.toRadians(angle));
double roundedValue = Math.round(sinValue * 1000.0) / 1000.0;
String question = String.format("sin(%d°) = ?", angle);
// 生成干扰选项
List<Double> wrongAnswers = Arrays.asList(
Math.cos(Math.toRadians(angle)), // 余弦值
Math.tan(Math.toRadians(angle)), // 正切值
-sinValue // 相反数
);
return buildQuestion(question, roundedValue, wrongAnswers);
}
}
```
#### 3.4.2 导数计算
```java
public class DerivativeStrategy extends AbstractQuestionStrategy {
public ChoiceQuestion generateQuestion() {
int a = random.nextInt(5) + 1; // 系数
int b = random.nextInt(10) - 5; // 系数
int x = random.nextInt(10) - 5; // 求导点
// f(x) = ax² + bx, 求 f'(x)
int derivative = 2 * a * x + b;
String question = String.format("f(x) = %dx² + %dx, 求 f'(%d)", a, b, x);
// 生成干扰选项
List<Integer> wrongAnswers = Arrays.asList(
2 * a * x - b, // 符号错误
a * x + b, // 系数错误
2 * a * x // 漏项
);
return buildQuestion(question, derivative, wrongAnswers);
}
}
```
## 4. 技术选型 (Technology Selection)
### 4.1 开发技术
- **编程语言**: Java 21 (LTS版本性能优异)
- **UI框架**: JavaFX 21 (现代化GUI丰富的控件)
- **构建工具**: Maven 3.8 (依赖管理,自动化构建)
- **版本控制**: Git (分布式版本控制)
### 4.2 第三方库
- **JSON处理**: Gson 2.10.1 (Google JSON库)
- **邮件服务**: JavaMail 1.6.2 (邮件发送)
- **单元测试**: JUnit 5 (测试框架)
- **代码风格**: Google Java Style (编码规范)
### 4.3 运行环境
- **操作系统**: Windows 10/11
- **Java版本**: Java 21或更高
- **内存要求**: 最小512MB推荐1GB
- **存储空间**: 100MB可用空间
---
## 5. 设计决策 (Design Decisions)
### 5.1 架构决策
- **选择MVC架构**: 清晰的职责分离,便于维护和扩展
- **选择文件存储**: 简化部署,降低系统复杂度
- **选择JavaFX**: 现代化的UI框架良好的用户体验
### 5.2 算法决策
- **选择策略模式**: 灵活的题目生成算法管理
- **选择工厂模式**: 统一的题目创建接口
- **选择随机算法**: 保证题目的多样性和随机性
### 5.3 技术决策
- **选择Java 21**: 最新的LTS版本性能和安全性的平衡
- **选择Maven**: 标准化的项目管理和构建工具
- **选择Gson**: 轻量级、高性能的JSON处理库

@ -0,0 +1,167 @@
# 需求文档
Math Quiz System Requirements Document
**版本**: 2.0.0
**日期**: 2025年10月10日
**作者**: 梁峻耀
**状态**: 最终版
---
## 1. 项目概述
### 1.1 项目背景
本系统是一款面向*小学、初中和高中学生*的**桌面端数学学习软件**,通过图形化界面提供交互式答题体验。系统支持用户注册、自由答题、历史记录与学习分析,帮助学生系统性提升数学能力。
### 1.2 项目目标
- 实现基于邮箱验证的用户注册与强密码管理
- 支持小、初、高三个年级的**符合标准的题目生成**
- 提供**自由跳题、回看、修改答案**
- **自动保存每次答题记录**,形成可查询的答题历史
- 可查看**历史答题记录**
- **所有数据通过本地文件持久化,不使用数据库**
### 1.3 项目范围
- 平台Windows 桌面应用GUI
- 技术栈: Java + JavaFX
- 功能:用户注册/登录、年级选择、题目生成、自由答题、自动评分、答题历史、统计分析
- **不包含**:网络题库、教师端、多人协作、云同步
---
## 2. 功能需求
### 2.1 用户注册与登录
#### 2.1.1 邮箱注册
- 用户输入**邮箱地址**,点击“注册”
- 系统校验邮箱格式(必须含 `@` 和有效域名)
- 系统生成6位数字验证码编辑发送邮件
- 用户输入验证码后设置密码完成注册
- 注册后用户名默认为用户邮箱,可自行修改成与他人不同的用户名
#### 2.1.2 密码设置
- 密码长度:**610位**
- 必须包含:**大写字母 + 小写字母 + 数字**
- 需输入两次,一致方可生效
- 设置成功后自动登录
#### 2.1.3 登录与密码修改
- 登录方式:**邮箱 + 密码** / **用户名 + 密码**
- 登录后可随时修改密码:需验证原密码 + 输入两次新密码(同样需满足强规则)
> 同一邮箱**不可重复注册**;用户数据(邮箱、密码哈希)存于 漫游应用数据目录C:\Users\<用户名>\AppData\Roaming\Math-Quiz-App
---
### 2.2 年级与题目设置
#### 2.2.1 年级选择
- 登录后显示:**小学 / 初中 / 高中** 三个按钮
- 点击后进入题目数量输入界面
#### 2.2.2 题目数量
- 输入范围:**1030**
- 超出范围提示错误并要求重输
#### 2.2.3 题目生成规则(严格对齐附表-2 + 适度扩展)
| 年级 | 必须包含 |
| -------- | -------------------------- |
| **小学** | `+ - * /``()` |
| **初中** | 至少一个 `x²``√x` |
| **高中** | 至少一个 `sin`/`cos`/`tan` |
> 所有题目为**单选题**4个选项A/B/C/D
> 同一试卷内**题目内容不得重复**(基于题干字符串去重)
---
每套试卷必有一道上面的题,**增加了如下题型**
| 年级 | 允许扩展题型(仅限选择题) |
| -------- | ---------------------------------------------------- |
| **小学** | 分数加减、简单单位换算(如 cm→m |
| **初中** | 一元一次方程、圆/三角形面积、一次函数求值 |
| **高中** | 对数运算、等差数列、简单概率、基本导数(如 `(x²)'` |
### 2.3 答题管理(支持自由跳转与回看)
#### 2.3.1 题目展示
- 显示当前题号(如 “第 5/20 题”)
- 题干 + 4个选项单选
- 底部显示**题号导航栏**1, 2, 3, ..., N已答题目高亮
#### 2.3.2 自由跳转
- 用户可点击任意题号跳转
- 可随时点击“上一题”/“下一题”
- 已选答案**自动保存至内存**,切换题目不丢失
#### 2.3.3 答案修改
- 在任意题目界面,可重新选择选项并覆盖原答案
- 提交试卷前,所有答案均可修改
#### 2.3.4 提交试卷
- 用户可随时点击“提交试卷”按钮结束答题
- 提交后**自动进入评分界面**
---
### 2.4 成绩与历史管理
#### 2.4.1 自动评分
- 得分 = 答对题数 / 总题数 × 100%
- 显示:总题数、答对数、得分、等级(优秀/良好/中等/及格/不及格)
#### 2.4.2 答题历史保存
- 每次提交后,生成一条记录,保存至:
`C:\Users\<系统用户名>\AppData\Roaming\Math-Quiz-App/data/history/{用户名}/年-月-日-时-分-秒.json`,前面的漫游路径动态获取
- 内容包括:
- 年级、题目数量、得分、时间戳
- 每题的题干、用户答案、正确答案
#### 2.4.3 历史查询
- 主界面提供“答题历史”按钮
- 列出所有历史记录(按时间倒序)
- 点击任一记录可**查看完整试卷与答题详情**
#### 2.4.4 统计分析
- 提供“学习统计”页面,展示:
- 总答题次数
- 平均得分
- 所有数据**实时从历史文件读取并计算**
---
## 3. 非功能需求
### 3.1 数据存储
- **禁止使用数据库**
- 所有数据通过**本地 JSON 文件**存储
- 路径必须为相对路径(如 `./data/`),程序启动时自动创建目录
### 3.2 安全性
- 密码使用 **SHA-256 + Salt** 加密存储
- 邮箱格式校验
- 文件读写异常处理(如权限不足、磁盘满)
### 3.3 界面要求
- 必须为 **GUI非命令行**
- 窗口大小固定或自适应,**不得随题目长度变化**
- 界面切换流畅,无卡顿
### 3.4 架构与代码
- 必须采用 **MVC 架构**
- 必须定义接口(如 `QuestionGenerator`, `HistoryRepository`
- 类数量 ≥ 2
- 单个方法代码行数 ≤ 40前端事件处理可适当放宽
- 遵循语言代码规范(如 Java 使用 Google Style
---
## 4. 系统约束
- **运行平台**Windows 10/11
- **网络依赖**:仅用于“发送”验证码
- **提交要求**
- 项目结构:`src/`, `doc/`
- Git 分支:`main`(稳定)、`develop`(开发)、`姓名_branch`(个人)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

@ -15,7 +15,7 @@
<groupId>com.mathquiz</groupId>
<artifactId>MathQuizApp</artifactId>
<version>1.04</version>
<version>2.0.0</version>
<packaging>jar</packaging>
<name>Math Quiz Application</name>

@ -1,38 +1,36 @@
package com.pair;// src/main/java/com/pair/Test.java
package com.pair; // src/main/java/com/pair/Test.java
import com.pair.ui.MainWindow;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;
import java.io.IOException;
public class Test extends Application {
@Override
public void start(Stage primaryStage) {
try {
System.out.println("✅ 正在初始化 MainWindow...");
MainWindow mainWindow = new MainWindow(primaryStage);
Scene scene = new Scene(mainWindow, 1366, 786);
primaryStage.setTitle("中小学数学答题系统");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
System.out.println("✅ 应用启动成功!");
} catch (Throwable e) { // 捕获所有错误,包括 NoClassDefFoundError
System.err.println("❌ 启动失败,异常信息:");
e.printStackTrace();
// 暂停 15 秒,防止窗口关闭
try {
Thread.sleep(15000);
} catch (InterruptedException ignored) {}
System.exit(1);
}
}
public static void main(String[] args) {
launch(args);
}
public static void main(String[] args) {
launch(args);
@Override
public void start(Stage primaryStage) {
try {
System.out.println("✅ 正在初始化 MainWindow...");
MainWindow mainWindow = new MainWindow(primaryStage);
Scene scene = new Scene(mainWindow, 1366, 786);
primaryStage.setTitle("中小学数学答题系统");
primaryStage.setScene(scene);
primaryStage.setResizable(true);
primaryStage.show();
System.out.println("✅ 应用启动成功!");
} catch (Throwable e) { // 捕获所有错误,包括 NoClassDefFoundError
System.err.println("❌ 启动失败,异常信息:");
e.printStackTrace();
// 暂停 15 秒,防止窗口关闭
try {
Thread.sleep(15000);
} catch (InterruptedException ignored) {
}
System.exit(1);
}
}
}
}

@ -2,69 +2,95 @@ package com.pair.model;
import java.util.List;
/** Choice question model. */
/**
*
*
* <p>
* </p>
*/
public class ChoiceQuestion {
private String questionText; // 题目文本
private Object correctAnswer; // 正确答案
private List<?> options; // 选项列表
private Grade grade; // 所属学段
private String questionText;
private Object correctAnswer;
private List<?> options;
private Grade grade;
public ChoiceQuestion(String questionText, double correctAnswer,
List<Double> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
/**
*
*
* @param questionText
* @param correctAnswer
* @param options
* @param grade
*/
public ChoiceQuestion(
String questionText, double correctAnswer, List<Double> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
public ChoiceQuestion(String questionText, String correctAnswer,
List<String> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
/**
*
*
* @param questionText
* @param correctAnswer
* @param options
* @param grade
*/
public ChoiceQuestion(
String questionText, String correctAnswer, List<String> options, Grade grade) {
this.questionText = questionText;
this.correctAnswer = correctAnswer;
this.options = options;
this.grade = grade;
}
public String getQuestionText() {
return questionText;
}
public String getQuestionText() {
return questionText;
}
public void setQuestionText(String questionText) {
this.questionText = questionText;
}
public void setQuestionText(String questionText) {
this.questionText = questionText;
}
public Object getCorrectAnswer() {
return correctAnswer;
}
public Object getCorrectAnswer() {
return correctAnswer;
}
public void setCorrectAnswer(Object correctAnswer) {
this.correctAnswer = correctAnswer;
}
public void setCorrectAnswer(Object correctAnswer) {
this.correctAnswer = correctAnswer;
}
public List<?> getOptions() {
return options;
}
public List<?> getOptions() {
return options;
}
public void setOptions(List<?> options) {
this.options = options;
}
public void setOptions(List<?> options) {
this.options = options;
}
public Grade getGrade() {
return grade;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
@Override
public String toString() {
return "ChoiceQuestion{" +
"questionText='" + questionText + '\'' +
", correctAnswer=" + correctAnswer +
", options=" + options +
", grade=" + grade +
'}';
}
}
@Override
public String toString() {
return "ChoiceQuestion{"
+ "questionText='"
+ questionText
+ '\''
+ ", correctAnswer="
+ correctAnswer
+ ", options="
+ options
+ ", grade="
+ grade
+ '}';
}
}

@ -1,29 +1,28 @@
package com.pair.model;
public enum Grade {
// 枚举常量,初始化时传入对应的中文描述
ELEMENTARY("小学"),
MIDDLE("初中"),
HIGH("高中");
// 枚举常量,初始化时传入对应的中文描述
ELEMENTARY("小学"),
MIDDLE("初中"),
HIGH("高中");
private final String chineseName;
private final String chineseName;
Grade(String chineseName) {
this.chineseName = 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);
}
public static Grade valueOfChinese(String chineseName) {
// 遍历所有枚举常量,匹配中文描述
for (Grade grade : Grade.values()) {
if (grade.chineseName.equals(chineseName)) {
return grade;
}
}
throw new IllegalArgumentException("不存在对应的年级:" + chineseName);
}
public String getChineseName() {
return chineseName;
}
}

@ -1,79 +1,96 @@
package com.pair.model;
import java.util.Date;
import java.util.List;
//答题历史记录模型
/**
*
*
* <p>
* </p>
*/
public class QuizHistory {
private String username; // 用户名
private Date timestamp; // 答题时间
private List<ChoiceQuestion> questions; // 题目列表
private List<Integer> userAnswers; // 用户答案列表
private int score; // 得分
public QuizHistory(String username, Date timestamp,
List<ChoiceQuestion> questions,
List<Integer> userAnswers,
int score) {
this.username = username;
this.timestamp = timestamp;
this.questions = questions;
this.userAnswers = userAnswers;
this.score = score;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public List<ChoiceQuestion> getQuestions() {
return questions;
}
public void setQuestions(List<ChoiceQuestion> questions) {
this.questions = questions;
}
public List<Integer> getUserAnswers() {
return userAnswers;
}
public void setUserAnswers(List<Integer> 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 +
'}';
}
}
private String username;
private Date timestamp;
private List<ChoiceQuestion> questions;
private List<Integer> userAnswers;
private int score;
/**
*
*
* @param username
* @param timestamp
* @param questions
* @param userAnswers
* @param score
*/
public QuizHistory(
String username,
Date timestamp,
List<ChoiceQuestion> questions,
List<Integer> userAnswers,
int score) {
this.username = username;
this.timestamp = timestamp;
this.questions = questions;
this.userAnswers = userAnswers;
this.score = score;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Date getTimestamp() {
return timestamp;
}
public void setTimestamp(Date timestamp) {
this.timestamp = timestamp;
}
public List<ChoiceQuestion> getQuestions() {
return questions;
}
public void setQuestions(List<ChoiceQuestion> questions) {
this.questions = questions;
}
public List<Integer> getUserAnswers() {
return userAnswers;
}
public void setUserAnswers(List<Integer> 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
+ '}';
}
}

@ -1,58 +1,55 @@
package com.pair.model;
//答题结果
// 答题结果
public class QuizResult {
private int totalQuestions; // 总题数
private int correctCount; // 正确题数
private int wrongCount; // 错误题数
private int score; // 得分
public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) {
this.totalQuestions = totalQuestions;
this.correctCount = correctCount;
this.wrongCount = wrongCount;
this.score = score;
}
public int getTotalQuestions() {
return totalQuestions;
}
public void setTotalQuestions(int totalQuestions) {
this.totalQuestions = totalQuestions;
}
public int getCorrectCount() {
return correctCount;
}
public void setCorrectCount(int correctCount) {
this.correctCount = correctCount;
}
public int getWrongCount() {
return wrongCount;
}
public void setWrongCount(int wrongCount) {
this.wrongCount = wrongCount;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent;
}
}
private int totalQuestions; // 总题数
private int correctCount; // 正确题数
private int wrongCount; // 错误题数
private int score; // 得分
public QuizResult(int totalQuestions, int correctCount, int wrongCount, int score) {
this.totalQuestions = totalQuestions;
this.correctCount = correctCount;
this.wrongCount = wrongCount;
this.score = score;
}
public int getTotalQuestions() {
return totalQuestions;
}
public void setTotalQuestions(int totalQuestions) {
this.totalQuestions = totalQuestions;
}
public int getCorrectCount() {
return correctCount;
}
public void setCorrectCount(int correctCount) {
this.correctCount = correctCount;
}
public int getWrongCount() {
return wrongCount;
}
public void setWrongCount(int wrongCount) {
this.wrongCount = wrongCount;
}
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
@Override
public String toString() {
int correctPercent = (int) ((double) correctCount / totalQuestions * 100);
return "您答对了" + correctCount + "/" + totalQuestions + "题,得分:" + correctPercent;
}
}

@ -3,119 +3,151 @@ package com.pair.model;
import java.util.Date;
import java.util.UUID;
/** User model class. */
/**
*
*
* <p>ID使UUID
*
* </p>
*/
public class User {
private final String userId; // 不可变,账号唯一标识
private String username; // 用户名
private String password; // 密码(加密后)
private String email; // 邮箱
private Grade grade; // 学段
private int totalQuizzes; // 总答题次数
private double averageScore; // 平均分
private Date registrationDate; // 注册时间
/**
*
*/
public User(String userId, String username, String password, String email, Grade grade,
int totalQuizzes, double averageScore, Date registrationDate) {
this.userId = userId;
this.username = username;
this.password = password;
this.email = email;
this.grade = grade;
this.totalQuizzes = totalQuizzes;
this.averageScore = averageScore;
this.registrationDate = registrationDate;
}
/**
*
*/
public User(String username, String password, String email, Grade grade) {
this.userId = UUID.randomUUID().toString();
this.username = username;
this.password = password;
this.email = email;
this.grade = grade;
this.totalQuizzes = 0;
this.averageScore = 0.0;
this.registrationDate = new Date();
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public int getTotalQuizzes() {
return totalQuizzes;
}
public void setTotalQuizzes(int totalQuizzes) {
this.totalQuizzes = totalQuizzes;
}
public double getAverageScore() {
return averageScore;
}
public void setAverageScore(double averageScore) {
this.averageScore = averageScore;
}
public Date getRegistrationDate() {
return registrationDate;
}
public void setRegistrationDate(Date registrationDate) {
this.registrationDate = registrationDate;
}
@Override
public String toString() {
return "User{" +
"username='" + username + '\'' +
", email='" + email + '\'' +
", grade=" + grade +
", totalQuizzes=" + totalQuizzes +
", averageScore=" + String.format("%.1f", averageScore) +
", registrationDate=" + registrationDate +
'}';
}
}
private final String userId;
private String username;
private String password;
private String email;
private Grade grade;
private int totalQuizzes;
private double averageScore;
private Date registrationDate;
/**
*
*
* @param userId
* @param username
* @param password
* @param email
* @param grade //
* @param totalQuizzes
* @param averageScore
* @param registrationDate
*/
public User(
String userId,
String username,
String password,
String email,
Grade grade,
int totalQuizzes,
double averageScore,
Date registrationDate) {
this.userId = userId;
this.username = username;
this.password = password;
this.email = email;
this.grade = grade;
this.totalQuizzes = totalQuizzes;
this.averageScore = averageScore;
this.registrationDate = registrationDate;
}
/**
*
*
* @param username
* @param password
* @param email
* @param grade //
*/
public User(String username, String password, String email, Grade grade) {
this.userId = UUID.randomUUID().toString();
this.username = username;
this.password = password;
this.email = email;
this.grade = grade;
this.totalQuizzes = 0;
this.averageScore = 0.0;
this.registrationDate = new Date();
}
public String getUserId() {
return userId;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public Grade getGrade() {
return grade;
}
public void setGrade(Grade grade) {
this.grade = grade;
}
public int getTotalQuizzes() {
return totalQuizzes;
}
public void setTotalQuizzes(int totalQuizzes) {
this.totalQuizzes = totalQuizzes;
}
public double getAverageScore() {
return averageScore;
}
public void setAverageScore(double averageScore) {
this.averageScore = averageScore;
}
public Date getRegistrationDate() {
return registrationDate;
}
public void setRegistrationDate(Date registrationDate) {
this.registrationDate = registrationDate;
}
@Override
public String toString() {
return "User{"
+ "username='"
+ username
+ '\''
+ ", email='"
+ email
+ '\''
+ ", grade="
+ grade
+ ", totalQuizzes="
+ totalQuizzes
+ ", averageScore="
+ String.format("%.1f", averageScore)
+ ", registrationDate="
+ registrationDate
+ '}';
}
}

@ -1,278 +1,273 @@
package com.pair.service;
import com.pair.model.*;
import com.pair.util.AppDataDirectory;
import com.pair.util.FileUtils;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import com.pair.model.ChoiceQuestion;
import com.pair.model.QuizHistory;
import com.pair.model.User;
import com.pair.util.AppDataDirectory;
import com.pair.util.FileUtils;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Type;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* IO
*
*/
/** 文件IO服务 负责所有数据的读写操作 */
public class FileIOService {
private static final String DATA_DIR = AppDataDirectory.getFullPath("data");
private static final String USERS_DIR = AppDataDirectory.getFullPath("data/users");
private static final String HISTORY_DIR = AppDataDirectory.getFullPath("data/history");
private static final String DATA_DIR = AppDataDirectory.getFullPath("data");
private static final String USERS_DIR = AppDataDirectory.getFullPath("data/users");
private static final String HISTORY_DIR = AppDataDirectory.getFullPath("data/history");
private static final String REGISTRATION_CODES_FILE = AppDataDirectory.getFullPath("data/registration_codes.json");
private static final String USERS_FILE = AppDataDirectory.getFullPath("data/users.json");
private static final String CURRENT_USER_FILE = AppDataDirectory.getFullPath("data/current_user.json");
private static final String REGISTRATION_CODES_FILE =
AppDataDirectory.getFullPath("data/registration_codes.json");
private static final String USERS_FILE = AppDataDirectory.getFullPath("data/users.json");
private static final String CURRENT_USER_FILE =
AppDataDirectory.getFullPath("data/current_user.json");
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting()
.setDateFormat("yyyy-MM-dd HH:mm:ss")
.create();
private static final Gson gson =
new GsonBuilder().setPrettyPrinting().setDateFormat("yyyy-MM-dd HH:mm:ss").create();
// ==================== 初始化 ====================
// ==================== 初始化 ====================
public FileIOService() throws IOException {
initDataDirectory();
}
public void initDataDirectory() throws IOException {
FileUtils.createDirectoryIfNotExists(DATA_DIR);
FileUtils.createDirectoryIfNotExists(HISTORY_DIR);
FileUtils.ensureFileExists(REGISTRATION_CODES_FILE);
public FileIOService() throws IOException {
initDataDirectory();
}
if (!FileUtils.exists(USERS_FILE)) {
Map<String, List<User>> data = new HashMap<>();
data.put("users", new ArrayList<>());
FileUtils.saveAsJson(data, USERS_FILE);
}
public void initDataDirectory() throws IOException {
FileUtils.createDirectoryIfNotExists(DATA_DIR);
FileUtils.createDirectoryIfNotExists(HISTORY_DIR);
FileUtils.ensureFileExists(REGISTRATION_CODES_FILE);
System.out.println("✓ 数据目录初始化完成");
if (!FileUtils.exists(USERS_FILE)) {
Map<String, List<User>> 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<Map<String, List<User>>>(){}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
// ==================== 用户操作 ====================
List<User> users = data.get("users");
public void saveUser(User user) throws IOException {
Type type = new TypeToken<Map<String, List<User>>>() {}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
boolean found = false;
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getUserId().equals(user.getUserId())) {
users.set(i, user);
found = true;
break;
}
}
List<User> users = data.get("users");
if (!found) {
users.add(user);
}
FileUtils.saveAsJson(data, USERS_FILE);
boolean found = false;
for (int i = 0; i < users.size(); i++) {
if (users.get(i).getUserId().equals(user.getUserId())) {
users.set(i, user);
found = true;
break;
}
}
public List<User> loadAllUsers() throws IOException {
if (!FileUtils.exists(USERS_FILE)) {
return new ArrayList<>();
}
Type type = new TypeToken<Map<String, List<User>>>(){}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
return data.get("users");
if (!found) {
users.add(user);
}
public User findUserByUsername(String username) throws IOException {
List<User> users = loadAllUsers();
FileUtils.saveAsJson(data, USERS_FILE);
}
for (User user : users) {
if (user.getUsername().equals(username)) {
return user;
}
}
return null;
}
public boolean existsUsername(String username) throws IOException {
return findUserByUsername(username) != null;
public List<User> loadAllUsers() throws IOException {
if (!FileUtils.exists(USERS_FILE)) {
return new ArrayList<>();
}
public User findUserByEmail(String email) throws IOException {
List<User> users = loadAllUsers();
for (User user : users) {
if (user.getEmail().equals(email)) {
return user;
}
}
Type type = new TypeToken<Map<String, List<User>>>() {}.getType();
Map<String, List<User>> data = FileUtils.readJsonToObject(USERS_FILE, type);
return null;
}
return data.get("users");
}
public boolean existsEmail(String email) throws IOException {
return findUserByEmail(email) != null;
}
public User findUserByUsername(String username) throws IOException {
List<User> users = loadAllUsers();
public boolean isUsernameExists(String username) throws IOException {
return findUserByUsername(username) != null;
for (User user : users) {
if (user.getUsername().equals(username)) {
return user;
}
}
public boolean isEmailExists(String email) throws IOException {
return findUserByEmail(email) != null;
}
return null;
}
public void saveCurrentUser(User user) throws IOException {
FileUtils.saveAsJson(user, CURRENT_USER_FILE);
}
public boolean existsUsername(String username) throws IOException {
return findUserByUsername(username) != null;
}
public User loadCurrentUser() throws IOException {
if (!FileUtils.exists(CURRENT_USER_FILE)) {
return null;
}
return FileUtils.readJsonToObject(CURRENT_USER_FILE, User.class);
}
public User findUserByEmail(String email) throws IOException {
List<User> users = loadAllUsers();
public void clearCurrentUser() {
FileUtils.deleteFile(CURRENT_USER_FILE);
for (User user : users) {
if (user.getEmail().equals(email)) {
return user;
}
}
// ==================== 答题历史操作 ====================
public void saveQuizHistory(QuizHistory history) throws IOException {
String filename = HISTORY_DIR + "/" +
sanitizeFilename(history.getUsername()) + "/" +
System.currentTimeMillis() + ".txt";
return null;
}
StringBuilder content = new StringBuilder();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
public boolean existsEmail(String email) throws IOException {
return findUserByEmail(email) != null;
}
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");
public boolean isUsernameExists(String username) throws IOException {
return findUserByUsername(username) != null;
}
// 调用 QuizService 的业务方法计算正确数和错误数
int correctCount = calculateCorrectCount(history);
int wrongCount = history.getQuestions().size() - correctCount;
public boolean isEmailExists(String email) throws IOException {
return findUserByEmail(email) != null;
}
content.append("正确:").append(correctCount).append(" 题 ");
content.append("错误:").append(wrongCount).append(" 题\n");
content.append("=============================\n\n");
public void saveCurrentUser(User user) throws IOException {
FileUtils.saveAsJson(user, CURRENT_USER_FILE);
}
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> 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");
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<ChoiceQuestion> questions = history.getQuestions();
List<Integer> 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");
}
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");
FileUtils.writeStringToFile(filename, content.toString());
}
int correctIndex = getCorrectAnswerIndex(q);
content.append("正确答案:").append((char)('A' + correctIndex)).append("\n");
public List<String> getHistoryQuestions(String username) throws IOException {
List<String> historyQuestions = new ArrayList<>();
File[] files = FileUtils.listFiles(HISTORY_DIR + "/" + username);
content.append("用户答案:");
if (userAnswer != null) {
content.append((char)('A' + userAnswer));
} else {
content.append("未作答");
}
content.append("\n");
Arrays.sort(files, (f1, f2) -> Long.compare(f2.lastModified(), f1.lastModified()));
boolean isCorrect = (userAnswer != null && userAnswer == correctIndex);
content.append("结果:").append(isCorrect ? "✓ 正确" : "✗ 错误").append("\n\n");
}
int count = 0;
for (File file : files) {
if (count++ >= 20) break;
FileUtils.writeStringToFile(filename, content.toString());
}
try {
String content = FileUtils.readFileToString(file.getAbsolutePath());
String[] lines = content.split("\n");
public List<String> getHistoryQuestions(String username) throws IOException {
List<String> historyQuestions = new ArrayList<>();
File[] files = FileUtils.listFiles(HISTORY_DIR + "/" + username);
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());
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);
}
}
}
}
return historyQuestions;
} catch (IOException e) {
System.err.println("读取历史文件失败: " + file.getName());
}
}
// ==================== 业务逻辑方法(从 Model 移过来)====================
return historyQuestions;
}
/**
*
*/
private int calculateCorrectCount(QuizHistory history) {
int count = 0;
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> userAnswers = history.getUserAnswers();
// ==================== 业务逻辑方法(从 Model 移过来)====================
for (int i = 0; i < questions.size(); i++) {
ChoiceQuestion question = questions.get(i);
Integer userAnswer = userAnswers.get(i);
/** 计算答题历史的正确数 */
private int calculateCorrectCount(QuizHistory history) {
int count = 0;
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> userAnswers = history.getUserAnswers();
if (userAnswer != null && userAnswer == getCorrectAnswerIndex(question)) {
count++;
}
}
return count;
}
for (int i = 0; i < questions.size(); i++) {
ChoiceQuestion question = questions.get(i);
Integer userAnswer = userAnswers.get(i);
/**
*
*/
private int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
if (userAnswer != null && userAnswer == getCorrectAnswerIndex(question)) {
count++;
}
}
return count;
}
// ==================== 工具方法 ====================
/** 获取题目的正确答案索引 */
private int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
}
private String sanitizeFilename(String filename) {
if (filename == null) {
return "unknown";
}
return filename.replaceAll("[\\\\/:*?\"<>|]", "_");
}
// ==================== 工具方法 ====================
public String getRegistrationCodesFilePath() {
return REGISTRATION_CODES_FILE;
private String sanitizeFilename(String filename) {
if (filename == null) {
return "unknown";
}
return filename.replaceAll("[\\\\/:*?\"<>|]", "_");
}
}
public String getRegistrationCodesFilePath() {
return REGISTRATION_CODES_FILE;
}
}

@ -1,445 +1,459 @@
package com.pair.service;
import com.pair.model.*;
import com.pair.service.question_generator.QuestionFactoryManager;
import com.pair.service.questiongenerator.QuestionFactoryManager;
import java.io.IOException;
import java.util.*;
/**
*
*
*
* <p> 使FileIOServiceUserService
* 访
*
* <p>
*
* <ul>
* <li>
* <li>
* <li>
* <li>
* </ul>
*
* @see com.pair.service.FileIOService
* @see com.pair.service.UserService
* @see com.pair.model.ChoiceQuestion
* @see com.pair.model.QuizResult
*/
public class QuizService {
private final FileIOService fileIOService;
private final UserService userService;
private final FileIOService fileIOService;
private final UserService userService;
private final List<Integer> userAnswers;
private List<ChoiceQuestion> currentQuestions;
private int currentQuestionIndex;
private int answerNumber;
public QuizService(FileIOService fileIOService, UserService userService) {
this.fileIOService = fileIOService;
this.userService = userService;
this.currentQuestions = new ArrayList<>();
this.userAnswers = new ArrayList<>();
this.currentQuestionIndex = 0;
}
/**
*
*
* @param user
* @param questionCount 10-30
* @throws IOException IO
*/
public void startNewQuiz(User user, int questionCount) throws IOException {
currentQuestions.clear();
userAnswers.clear();
currentQuestionIndex = 0;
private List<ChoiceQuestion> currentQuestions;
private List<Integer> userAnswers;
private int currentQuestionIndex;
private int answerNumber;
Set<String> historyQuestions = getRecentHistoryQuestions();
// ==================== 构造方法 ====================
Grade grade = user.getGrade();
currentQuestions =
QuestionFactoryManager.generateQuestions(grade, questionCount, historyQuestions);
public QuizService(FileIOService fileIOService, UserService userService) {
this.fileIOService = fileIOService;
this.userService = userService;
this.currentQuestions = new ArrayList<>();
this.userAnswers = new ArrayList<>();
this.currentQuestionIndex = 0;
for (int i = 0; i < currentQuestions.size(); i++) {
userAnswers.add(null);
}
// ==================== 答题会话管理 ====================
System.out.println("✓ 已生成 " + currentQuestions.size() + " 道 " + grade + " 题目");
}
//开始生成
public void startNewQuiz(User user, int questionCount) throws IOException {
currentQuestions.clear();
userAnswers.clear();
currentQuestionIndex = 0;
private Set<String> getRecentHistoryQuestions() throws IOException {
List<String> historyList =
fileIOService.getHistoryQuestions(userService.getCurrentUser().getUsername());
return new HashSet<>(historyList);
}
Set<String> 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<String> getRecentHistoryQuestions() throws IOException {
List<String> historyList = fileIOService.getHistoryQuestions(userService.getCurrentUser().getUsername());
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<ChoiceQuestion> getAllQuestions() {
return new ArrayList<>(currentQuestions);
}
public boolean nextQuestion() {
if (currentQuestionIndex < currentQuestions.size() - 1) {
currentQuestionIndex++;
return true;
}
return false;
/**
*
*
* @return null
*/
public ChoiceQuestion getCurrentQuestion() {
if (currentQuestionIndex >= 0 && currentQuestionIndex < currentQuestions.size()) {
return currentQuestions.get(currentQuestionIndex);
}
return null;
}
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 ChoiceQuestion getQuestion(int index) {
if (index >= 0 && index < currentQuestions.size()) {
return currentQuestions.get(index);
}
return null;
}
public int getCurrentQuestionIndex() {
return currentQuestionIndex;
}
public List<ChoiceQuestion> getAllQuestions() {
return new ArrayList<>(currentQuestions);
}
public int getTotalQuestions() {
return currentQuestions.size();
public boolean nextQuestion() {
if (currentQuestionIndex < currentQuestions.size() - 1) {
currentQuestionIndex++;
return true;
}
return false;
}
public boolean isFirstQuestion() {
return currentQuestionIndex == 0;
public boolean previousQuestion() {
if (currentQuestionIndex > 0) {
currentQuestionIndex--;
return true;
}
return false;
}
public boolean isLastQuestion() {
return currentQuestionIndex == currentQuestions.size() - 1;
public boolean goToQuestion(int index) {
if (index >= 0 && index < currentQuestions.size()) {
currentQuestionIndex = index;
return true;
}
return false;
}
// ==================== 答题操作 ====================
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 int getCurrentQuestionIndex() {
return currentQuestionIndex;
}
public List<Integer> getAllUserAnswers() {
return new ArrayList<>(userAnswers);
}
public int getTotalQuestions() {
return currentQuestions.size();
}
public boolean isAllAnswered() {
for (Integer answer : userAnswers) {
if (answer == null) {
return false;
}
}
return true;
}
public boolean isFirstQuestion() {
return currentQuestionIndex == 0;
}
public int getAnsweredCount() {
int count = 0;
for (Integer answer : userAnswers) {
if (answer != null) {
count++;
}
}
return count;
}
public boolean isLastQuestion() {
return currentQuestionIndex == currentQuestions.size() - 1;
}
public boolean isAnswered(int questionIndex) {
return userAnswers.get(questionIndex) != null ;
public boolean submitAnswer(int questionIndex, int optionIndex) {
if (questionIndex < 0 || questionIndex >= currentQuestions.size()) {
throw new IllegalArgumentException("题目索引无效: " + questionIndex);
}
// ==================== 成绩计算 ====================
public QuizResult calculateResult() {
int correctCount = 0;
int totalQuestions = currentQuestions.size();
ChoiceQuestion question = currentQuestions.get(questionIndex);
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);
if (optionIndex < 0 || optionIndex >= question.getOptions().size()) {
throw new IllegalArgumentException("选项索引无效: " + optionIndex);
}
public List<Integer> getCorrectQuestionIndices() {
List<Integer> correctIndices = new ArrayList<>();
userAnswers.set(questionIndex, optionIndex);
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
return checkAnswer(question, optionIndex);
}
if (userAnswer != null && checkAnswer(question, userAnswer)) {
correctIndices.add(i);
}
}
public boolean submitCurrentAnswer(int optionIndex) {
return submitAnswer(currentQuestionIndex, optionIndex);
}
return correctIndices;
public Integer getUserAnswer(int questionIndex) {
if (questionIndex >= 0 && questionIndex < userAnswers.size()) {
return userAnswers.get(questionIndex);
}
return null;
}
public List<Integer> getWrongQuestionIndices() {
List<Integer> wrongIndices = new ArrayList<>();
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
public List<Integer> getAllUserAnswers() {
return new ArrayList<>(userAnswers);
}
if (userAnswer != null && !checkAnswer(question, userAnswer)) {
wrongIndices.add(i);
}
}
return wrongIndices;
public boolean isAllAnswered() {
for (Integer answer : userAnswers) {
if (answer == null) {
return false;
}
}
return true;
}
public List<Integer> getUnansweredQuestionIndices() {
List<Integer> unansweredIndices = new ArrayList<>();
for (int i = 0; i < userAnswers.size(); i++) {
if (userAnswers.get(i) == null) {
unansweredIndices.add(i);
}
}
return unansweredIndices;
public int getAnsweredCount() {
int count = 0;
for (Integer answer : userAnswers) {
if (answer != null) {
count++;
}
}
return count;
}
// ==================== 业务逻辑方法(从 Model 移过来)====================
public boolean isAnswered(int questionIndex) {
return userAnswers.get(questionIndex) != null;
}
/**
*
*/
public boolean checkAnswer(ChoiceQuestion question, int userAnswerIndex) {
if (userAnswerIndex < 0 || userAnswerIndex >= question.getOptions().size()) {
return false;
}
public QuizResult calculateResult() {
int correctCount = 0;
int totalQuestions = currentQuestions.size();
Object userAnswer = question.getOptions().get(userAnswerIndex);
return question.getCorrectAnswer().equals(userAnswer);
}
/**
*
*/
public int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
}
for (int i = 0; i < totalQuestions; i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
/**
*
*/
public String getCorrectAnswerLetter(ChoiceQuestion question) {
int index = getCorrectAnswerIndex(question);
if (index >= 0 && index < 4) {
return String.valueOf((char)('A' + index));
}
return "未知";
if (userAnswer != null && checkAnswer(question, userAnswer)) {
correctCount++;
}
}
/**
*
*/
public double getAccuracy(QuizResult result) {
if (result.getTotalQuestions() == 0) {
return 0.0;
}
return (result.getCorrectCount() * 100.0) / result.getTotalQuestions();
}
int wrongCount = totalQuestions - correctCount;
int score = totalQuestions > 0 ? (correctCount * 100) / totalQuestions : 0;
/**
*
*/
public boolean isPassed(QuizResult result) {
return result.getScore() >= 60;
}
return new QuizResult(totalQuestions, correctCount, wrongCount, score);
}
/**
*
*/
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 List<Integer> getCorrectQuestionIndices() {
List<Integer> correctIndices = new ArrayList<>();
/**
*
*/
public int getCorrectCount(QuizHistory history) {
int count = 0;
List<ChoiceQuestion> questions = history.getQuestions();
List<Integer> 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;
}
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
/**
*
*/
public int getWrongCount(QuizHistory history) {
return history.getQuestions().size() - getCorrectCount(history);
if (userAnswer != null && checkAnswer(question, userAnswer)) {
correctIndices.add(i);
}
}
// ==================== 格式化输出 ====================
return correctIndices;
}
public String formatQuestion(ChoiceQuestion question) {
StringBuilder sb = new StringBuilder();
sb.append(question.getQuestionText()).append("\n");
public List<Integer> getWrongQuestionIndices() {
List<Integer> wrongIndices = new ArrayList<>();
List<?> options = question.getOptions();
for (int i = 0; i < options.size(); i++) {
sb.append((char)('A' + i)).append(". ").append(options.get(i));
for (int i = 0; i < currentQuestions.size(); i++) {
ChoiceQuestion question = currentQuestions.get(i);
Integer userAnswer = userAnswers.get(i);
if (i % 2 == 1) {
sb.append("\n");
} else {
sb.append(" ");
}
}
return sb.toString();
if (userAnswer != null && !checkAnswer(question, userAnswer)) {
wrongIndices.add(i);
}
}
public String formatCurrentQuestion() {
ChoiceQuestion question = getCurrentQuestion();
if (question == null) {
return "没有可用的题目";
}
return wrongIndices;
}
StringBuilder sb = new StringBuilder();
sb.append("第 ").append(currentQuestionIndex + 1)
.append(" / ").append(currentQuestions.size()).append(" 题\n");
sb.append(formatQuestion(question));
public List<Integer> getUnansweredQuestionIndices() {
List<Integer> unansweredIndices = new ArrayList<>();
return sb.toString();
for (int i = 0; i < userAnswers.size(); i++) {
if (userAnswers.get(i) == null) {
unansweredIndices.add(i);
}
}
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(" ✓");
}
return unansweredIndices;
}
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();
/**
*
*
* @param question
* @param userAnswerIndex
* @return truefalse
*/
public boolean checkAnswer(ChoiceQuestion question, int userAnswerIndex) {
if (userAnswerIndex < 0 || userAnswerIndex >= question.getOptions().size()) {
return false;
}
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();
}
Object userAnswer = question.getOptions().get(userAnswerIndex);
return question.getCorrectAnswer().equals(userAnswer);
}
// ==================== 数据持久化 ====================
public void saveQuizHistory() {
QuizResult result = calculateResult();
QuizHistory history = new QuizHistory(
userService.getCurrentUser().getUsername(),
new Date(),
currentQuestions,
userAnswers,
result.getScore()
);
try {
fileIOService.saveQuizHistory(history);
} catch (IOException e) {
System.err.println(e);
}
try {
userService.updateUserStatistics(userService.getCurrentUser(), result.getScore());
} catch (IOException e) {
System.err.println(e);
}
System.out.println("✓ 答题记录已保存");
}
/** 获取题目的正确答案索引 */
public int getCorrectAnswerIndex(ChoiceQuestion question) {
return question.getOptions().indexOf(question.getCorrectAnswer());
}
// ==================== Getters ====================
public int getAnswerNumber() {
return answerNumber;
/** 获取正确答案的字母形式 */
public String getCorrectAnswerLetter(ChoiceQuestion question) {
int index = getCorrectAnswerIndex(question);
if (index >= 0 && index < 4) {
return String.valueOf((char) ('A' + index));
}
return "未知";
}
public void setAnswerNumber(int answerNumber) {
this.answerNumber = answerNumber;
/** 获取答题结果的正确率 */
public double getAccuracy(QuizResult result) {
if (result.getTotalQuestions() == 0) {
return 0.0;
}
return (result.getCorrectCount() * 100.0) / result.getTotalQuestions();
}
public List<ChoiceQuestion> getCurrentQuestions() {
return new ArrayList<>(currentQuestions);
}
/** 判断是否及格 */
public boolean isPassed(QuizResult result) {
return result.getScore() >= 60;
}
public List<Integer> getUserAnswers() {
return new ArrayList<>(userAnswers);
}
}
/** 获取评级 */
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<ChoiceQuestion> questions = history.getQuestions();
List<Integer> 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 "没有可用的题目";
}
String sb =
"第 "
+ (currentQuestionIndex + 1)
+ " / "
+ currentQuestions.size()
+ " 题\n"
+ formatQuestion(question);
return sb;
}
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) {
String sb =
"\n========== 答题结束 ==========\n"
+ "总题数:"
+ result.getTotalQuestions()
+ " 题\n"
+ "正确:"
+ result.getCorrectCount()
+ " 题\n"
+ "错误:"
+ result.getWrongCount()
+ " 题\n"
+ "得分:"
+ result.getScore()
+ " 分\n"
+ "正确率:"
+ String.format("%.1f%%", getAccuracy(result))
+ "\n"
+ "评级:"
+ getGrade(result)
+ "\n"
+ "===============================\n";
return sb;
}
public void saveQuizHistory() {
QuizResult result = calculateResult();
QuizHistory history =
new QuizHistory(
userService.getCurrentUser().getUsername(),
new Date(),
currentQuestions,
userAnswers,
result.getScore());
try {
fileIOService.saveQuizHistory(history);
} catch (IOException e) {
System.err.println(e);
}
try {
userService.updateUserStatistics(userService.getCurrentUser(), result.getScore());
} catch (IOException e) {
System.err.println(e);
}
System.out.println("✓ 答题记录已保存");
}
public int getAnswerNumber() {
return answerNumber;
}
public void setAnswerNumber(int answerNumber) {
this.answerNumber = answerNumber;
}
public List<ChoiceQuestion> getCurrentQuestions() {
return new ArrayList<>(currentQuestions);
}
public List<Integer> getUserAnswers() {
return new ArrayList<>(userAnswers);
}
}

@ -1,12 +1,12 @@
package com.pair.service;
import static com.pair.util.EmailUtil.validateEmail;
import com.pair.model.Grade;
import com.pair.model.User;
import com.pair.util.EmailUtil;
import com.pair.util.FileUtils;
import com.pair.util.PasswordValidator;
import java.io.File;
import java.io.IOException;
import java.text.SimpleDateFormat;
@ -15,519 +15,517 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import static com.pair.util.EmailUtil.validateEmail;
/**
*
*/
/** 用户服务(包含所有用户相关业务逻辑) */
public class UserService {
private final FileIOService fileIOService;
private User currentUser;
// ==================== 构造方法 ====================
public UserService() throws IOException {
this.fileIOService = new FileIOService();
this.currentUser = null;
// 注册码有效期(毫秒)
private static final long CODE_EXPIRY_TIME = 10 * 60 * 1000; // 10分钟
private final FileIOService fileIOService;
// ==================== 构造方法 ====================
private User currentUser;
public UserService() throws IOException {
this.fileIOService = new FileIOService();
this.currentUser = null;
}
public UserService(FileIOService fileIOService) {
this.fileIOService = fileIOService;
this.currentUser = null;
}
// ==================== 注册码管理 ====================
/**
*
*
* @param email
* @return
*/
public String generateRegistrationCode(String email) throws IOException {
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
public UserService(FileIOService fileIOService) {
this.fileIOService = fileIOService;
this.currentUser = null;
}
// 生成6位注册码
String code = PasswordValidator.generateRegistrationCode();
long expiryTime = System.currentTimeMillis() + CODE_EXPIRY_TIME;
// 保存到文件
System.out.println(expiryTime);
saveRegistrationCodeToFile(email, code, expiryTime);
// 注册码有效期(毫秒)
private static final long CODE_EXPIRY_TIME = 10 * 60 * 1000; // 10分钟
// 发送注册码邮件
boolean isEmailSent = EmailUtil.sendRegistrationCode(email, code);
if (!isEmailSent) {
throw new IllegalArgumentException("邮箱发送失败,请重试");
}
// ==================== 注册码管理 ====================
// 打印注册码(实际项目中可以发邮件)
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("========================================");
System.out.println("【注册码】");
System.out.println("邮箱: " + email);
System.out.println("注册码: " + code);
System.out.println("过期时间: " + sdf.format(new Date(expiryTime)));
System.out.println("========================================");
return code;
}
/** 保存注册码到文件 */
private void saveRegistrationCodeToFile(String email, String code, long expiryTime)
throws IOException {
// 读取现有的注册码
Map<String, RegistrationCode> 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<String, RegistrationCode> 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");
}
/**
*
* @param email
* @return
*/
public String generateRegistrationCode(String email) throws IOException {
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
}
/** 初始化数据目录和文件 */
private void initializeDataDirectory() {
try {
File dataDir = new File("data");
if (!dataDir.exists()) {
dataDir.mkdirs();
System.out.println("✓ 已创建 data 目录");
}
// 确保注册码文件存在(即使是空的也创建)
File codesFile = new File(fileIOService.getRegistrationCodesFilePath());
if (!codesFile.exists()) {
String initialContent = "# 注册码记录文件\n" + "# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n";
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), initialContent);
System.out.println("✓ 已创建注册码文件");
}
} catch (IOException e) {
System.err.println(" 初始化数据目录失败: " + e.getMessage());
}
}
/** 从文件加载注册码 */
private Map<String, RegistrationCode> loadRegistrationCodesFromFile() throws IOException {
Map<String, RegistrationCode> codes = new HashMap<>();
System.out.println(fileIOService.getRegistrationCodesFilePath());
if (!FileUtils.exists(fileIOService.getRegistrationCodesFilePath())) {
throw new IOException("目录不存在");
}
// 生成6位注册码
String code = PasswordValidator.generateRegistrationCode();
long expiryTime = System.currentTimeMillis() + CODE_EXPIRY_TIME;
String content = FileUtils.readFileToString(fileIOService.getRegistrationCodesFilePath());
String[] lines = content.split("\n");
// 保存到文件
System.out.println(expiryTime);
saveRegistrationCodeToFile(email, code, expiryTime);
for (String line : lines) {
line = line.trim();
//发送注册码邮件
boolean isEmailSent = EmailUtil.sendRegistrationCode(email, code);
if (!isEmailSent) {
throw new IllegalArgumentException("邮箱发送失败,请重试");
}
// 跳过注释和空行
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
// 打印注册码(实际项目中可以发邮件)
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("========================================");
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());
return code;
codes.put(email, new RegistrationCode(code, expiryTime));
}
}
/**
*
*/
private void saveRegistrationCodeToFile(String email, String code, long expiryTime) throws IOException {
// 读取现有的注册码
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
return codes;
}
// 添加或更新
codes.put(email, new RegistrationCode(code, expiryTime));
/**
*
*
* @param email
* @param code
* @return true
*/
public boolean verifyRegistrationCode(String email, String code) throws IOException {
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
// 保存到文件
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<String, RegistrationCode> entry : codes.entrySet()) {
String emailKey = entry.getKey();
RegistrationCode regCode = entry.getValue();
content.append(emailKey).append("|")
.append(regCode.code).append("|")
.append(regCode.expiryTime).append("|")
.append(sdf.format(new Date(regCode.expiryTime)))
.append("\n");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
}
/**
*
*/
private void initializeDataDirectory() {
try {
File dataDir = new File("data");
if (!dataDir.exists()) {
dataDir.mkdirs();
System.out.println("✓ 已创建 data 目录");
}
// 确保注册码文件存在(即使是空的也创建)
File codesFile = new File(fileIOService.getRegistrationCodesFilePath());
if (!codesFile.exists()) {
StringBuilder initialContent = new StringBuilder();
initialContent.append("# 注册码记录文件\n");
initialContent.append("# 格式: 邮箱|注册码|过期时间戳|过期时间\n\n");
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), initialContent.toString());
System.out.println("✓ 已创建注册码文件");
}
} catch (IOException e) {
System.err.println(" 初始化数据目录失败: " + e.getMessage());
}
}
/**
*
*/
private Map<String, RegistrationCode> loadRegistrationCodesFromFile() throws IOException {
Map<String, RegistrationCode> codes = new HashMap<>();
System.out.println(fileIOService.getRegistrationCodesFilePath());
if (!FileUtils.exists(fileIOService.getRegistrationCodesFilePath())) {
throw new IOException("目录不存在");
}
String content = FileUtils.readFileToString(fileIOService.getRegistrationCodesFilePath());
String[] lines = content.split("\n");
for (String line : lines) {
line = line.trim();
// 跳过注释和空行
if (line.isEmpty() || line.startsWith("#")) {
continue;
}
RegistrationCode regCode = codes.get(email);
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<String, RegistrationCode> 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;
if (regCode == null) {
throw new IllegalArgumentException("未找到该邮箱的注册码,请先获取注册码!");
}
/**
*
*/
private void saveAllRegistrationCodes(Map<String, RegistrationCode> 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<String, RegistrationCode> entry : codes.entrySet()) {
content.append(entry.getKey()).append("|")
.append(entry.getValue().code).append("|")
.append(entry.getValue().expiryTime).append("|")
.append(sdf.format(new Date(entry.getValue().expiryTime)))
.append("\n");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
// 检查是否过期
if (System.currentTimeMillis() > regCode.expiryTime) {
// 删除过期的注册码
codes.remove(email);
saveAllRegistrationCodes(codes);
throw new IllegalArgumentException("注册码已过期,请重新获取!");
}
/**
*
*/
public void cleanExpiredCodes() throws IOException {
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
long now = System.currentTimeMillis();
// 移除过期的
codes.entrySet().removeIf(entry -> now > entry.getValue().expiryTime);
// 验证注册码
boolean isValid = regCode.code.equals(code);
// 保存回文件
saveAllRegistrationCodes(codes);
System.out.println("✓ 已清理过期的注册码");
// 验证成功后删除注册码(一次性使用)
if (isValid) {
codes.remove(email);
saveAllRegistrationCodes(codes);
}
// ==================== 注册码内部类 ====================
private static class RegistrationCode {
String code;
long expiryTime;
RegistrationCode(String code, long expiryTime) {
this.code = code;
this.expiryTime = expiryTime;
}
return isValid;
}
/** 保存所有注册码到文件 */
private void saveAllRegistrationCodes(Map<String, RegistrationCode> 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<String, RegistrationCode> entry : codes.entrySet()) {
content
.append(entry.getKey())
.append("|")
.append(entry.getValue().code)
.append("|")
.append(entry.getValue().expiryTime)
.append("|")
.append(sdf.format(new Date(entry.getValue().expiryTime)))
.append("\n");
}
FileUtils.writeStringToFile(fileIOService.getRegistrationCodesFilePath(), content.toString());
}
// ==================== 用户注册====================
/** 清理过期的注册码 */
public void cleanExpiredCodes() throws IOException {
Map<String, RegistrationCode> codes = loadRegistrationCodesFromFile();
long now = System.currentTimeMillis();
/**
*
*/
public User register(String password, String email, String verificationCode) throws IOException {
// 1. 验证注册码
if (!verifyRegistrationCode(email, verificationCode)) {
throw new IllegalArgumentException("注册码错误!");
}
//2.验证邮箱格式
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
// 移除过期的
codes.entrySet().removeIf(entry -> now > entry.getValue().expiryTime);
// 3. 验证用户名是否已存在
if (fileIOService.isEmailExists(email)) {
throw new IllegalArgumentException("邮箱已经注册!");
}
// 保存回文件
saveAllRegistrationCodes(codes);
// 5. 验证密码格式
try {
PasswordValidator.validatePassword(password);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("密码格式错误!");
}
System.out.println("✓ 已清理过期的注册码");
}
// 6. 初始化为小学学段
Grade grade = Grade.ELEMENTARY;
// ==================== 注册码内部类 ====================
// 7. 加密密码
String hashedPassword = PasswordValidator.encrypt(password);
// 8. 创建用户对象
User user = new User(email, hashedPassword, email, grade);
this.setCurrentUser(user);
// 9. 保存到文件
fileIOService.saveUser(user);
return user;
/** 用户注册(需要验证码) */
public User register(String password, String email, String verificationCode) throws IOException {
// 1. 验证注册码
if (!verifyRegistrationCode(email, verificationCode)) {
throw new IllegalArgumentException("注册码错误!");
}
public void setCurrentUser(User user) throws IOException {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
// 2.验证邮箱格式
if (!validateEmail(email)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
// ==================== 用户登录 ====================
public User login(String username, String password) throws IOException {
User user;
if (EmailUtil.validateEmail(username)) {
user = fileIOService.findUserByEmail(username);
} else {
user = fileIOService.findUserByUsername(username);
}
// 3. 验证用户名是否已存在
if (fileIOService.isEmailExists(email)) {
throw new IllegalArgumentException("邮箱已经注册!");
}
if (user == null) {
throw new IllegalArgumentException("用户名或邮箱不存在!");
}
// 5. 验证密码格式
try {
PasswordValidator.validatePassword(password);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("密码格式错误!");
}
String hashedPassword = PasswordValidator.encrypt(password);
if (!user.getPassword().equals(hashedPassword)) {
throw new IllegalArgumentException("密码错误!");
}
// 6. 初始化为小学学段
Grade grade = Grade.ELEMENTARY;
this.currentUser = user;
fileIOService.saveCurrentUser(user);
// 7. 加密密码
String hashedPassword = PasswordValidator.encrypt(password);
// System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "" + getGradeDisplayName(user) + "");
return user;
}
// 8. 创建用户对象
User user = new User(email, hashedPassword, email, grade);
public User autoLogin() throws IOException {
User user = fileIOService.loadCurrentUser();
this.setCurrentUser(user);
// 9. 保存到文件
fileIOService.saveUser(user);
if (user != null) {
this.currentUser = user;
// System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
}
return user;
}
return user;
}
// ==================== 用户注册====================
public void logout() {
if (currentUser != null) {
// System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
this.currentUser = null;
fileIOService.clearCurrentUser();
}
public User login(String username, String password) throws IOException {
User user;
if (EmailUtil.validateEmail(username)) {
user = fileIOService.findUserByEmail(username);
} else {
user = fileIOService.findUserByUsername(username);
}
public User getCurrentUser() {
return currentUser;
if (user == null) {
throw new IllegalArgumentException("用户名或邮箱不存在!");
}
public boolean isLoggedIn() {
return currentUser != null;
String hashedPassword = PasswordValidator.encrypt(password);
if (!user.getPassword().equals(hashedPassword)) {
throw new IllegalArgumentException("密码错误!");
}
// ==================== 密码管理 ====================
public boolean changePassword(User user, String oldPassword, String newPassword, String confirmPassword) throws IOException {
String hashedOldPassword = PasswordValidator.encrypt(oldPassword);
if (!user.getPassword().equals(hashedOldPassword)) {
throw new IllegalArgumentException("旧密码错误!");
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
this.currentUser = user;
fileIOService.saveCurrentUser(user);
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
// System.out.println("✓ 登录成功,欢迎 " + getRealName(user) + "" + getGradeDisplayName(user) + "");
return user;
}
fileIOService.saveUser(user);
public User autoLogin() throws IOException {
User user = fileIOService.loadCurrentUser();
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
System.out.println("✓ 密码修改成功");
return true;
if (user != null) {
this.currentUser = user;
// System.out.println("✓ 自动登录成功,欢迎回来 " + getRealName(user));
}
public boolean resetPassword(String username, String email, String newPassword) throws IOException {
User user = fileIOService.findUserByUsername(username);
return user;
}
if (user == null) {
throw new IllegalArgumentException("用户名不存在!");
}
// ==================== 用户登录 ====================
if (!user.getEmail().equals(email)) {
throw new IllegalArgumentException("邮箱验证失败!");
}
public void logout() {
if (currentUser != null) {
// System.out.println("✓ " + getRealName(currentUser) + " 已退出登录");
this.currentUser = null;
fileIOService.clearCurrentUser();
}
}
public User getCurrentUser() {
return currentUser;
}
public void setCurrentUser(User user) throws IOException {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
public boolean isLoggedIn() {
return currentUser != null;
}
public boolean changePassword(
User user, String oldPassword, String newPassword, String confirmPassword)
throws IOException {
String hashedOldPassword = PasswordValidator.encrypt(oldPassword);
if (!user.getPassword().equals(hashedOldPassword)) {
throw new IllegalArgumentException("旧密码错误!");
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
fileIOService.saveUser(user);
fileIOService.saveUser(user);
System.out.println("✓ 密码重置成功");
return true;
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
// ==================== 用户信息管理 ====================
System.out.println("✓ 密码修改成功");
return true;
}
public void updateEmail(User user, String newEmail) throws IOException {
if (!validateEmail(newEmail)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
if (user.getEmail().equals(newEmail)) {
return ;
}
boolean exist = fileIOService.isEmailExists(newEmail);
if (exist) {
throw new IllegalArgumentException("邮箱已存在!");
}
user.setEmail(newEmail);
fileIOService.saveUser(user);
// ==================== 密码管理 ====================
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
public boolean resetPassword(String username, String email, String newPassword)
throws IOException {
User user = fileIOService.findUserByUsername(username);
if (user == null) {
throw new IllegalArgumentException("用户名不存在!");
}
public void updateUsername(User user, String newUsername) throws IOException {
if (newUsername.isEmpty()) {
throw new IllegalArgumentException("用户名不为空!");
}
if (user.getUsername().equals(newUsername)) {
return;
}
boolean exist = fileIOService.existsUsername(newUsername);
if (exist) {
throw new IllegalArgumentException("用户名已存在!");
}
user.setUsername(newUsername);
fileIOService.saveUser(user);
if (!user.getEmail().equals(email)) {
throw new IllegalArgumentException("邮箱验证失败!");
}
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
try {
PasswordValidator.validatePassword(newPassword);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(e.getMessage());
}
public void updateGrade(User user, String chinesename) throws IOException {
if (chinesename.isEmpty()) {
throw new IllegalArgumentException("学段中文名为空");
}
Grade newGrade = Grade.valueOfChinese(chinesename);
String hashedNewPassword = PasswordValidator.encrypt(newPassword);
user.setPassword(hashedNewPassword);
user.setGrade(newGrade);
fileIOService.saveUser(user);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
System.out.println("✓ 密码重置成功");
return true;
}
public void updateEmail(User user, String newEmail) throws IOException {
if (!validateEmail(newEmail)) {
throw new IllegalArgumentException("邮箱格式错误!");
}
if (user.getEmail().equals(newEmail)) {
return;
}
boolean exist = fileIOService.isEmailExists(newEmail);
if (exist) {
throw new IllegalArgumentException("邮箱已存在!");
}
user.setEmail(newEmail);
fileIOService.saveUser(user);
public void updateUserStatistics(User user, int score) throws IOException {
int oldTotal = user.getTotalQuizzes();
double oldAverage = user.getAverageScore();
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
}
int newTotal = oldTotal + 1;
double newAverage = (oldAverage * oldTotal + score) / newTotal;
// ==================== 用户信息管理 ====================
user.setTotalQuizzes(newTotal);
user.setAverageScore(newAverage);
public void updateUsername(User user, String newUsername) throws IOException {
if (newUsername.isEmpty()) {
throw new IllegalArgumentException("用户名不为空!");
}
if (user.getUsername().equals(newUsername)) {
return;
}
boolean exist = fileIOService.existsUsername(newUsername);
if (exist) {
throw new IllegalArgumentException("用户名已存在!");
}
user.setUsername(newUsername);
fileIOService.saveUser(user);
fileIOService.saveUser(user);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
}
public List<User> getAllUsers() throws IOException {
return fileIOService.loadAllUsers();
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);
public User findUser(String username) throws IOException {
return fileIOService.findUserByUsername(username);
if (currentUser != null && currentUser.getUsername().equals(user.getUsername())) {
this.currentUser = user;
fileIOService.saveCurrentUser(user);
}
}
public void updateUserStatistics(User user, int score) throws IOException {
int oldTotal = user.getTotalQuizzes();
double oldAverage = user.getAverageScore();
// ==================== 业务逻辑方法===================
int newTotal = oldTotal + 1;
double newAverage = (oldAverage * oldTotal + score) / newTotal;
user.setTotalQuizzes(newTotal);
user.setAverageScore(newAverage);
/**
*
*/
public String getGradeDisplayName(User user) {
if (user == null || user.getGrade() == null) {
return "未知";
}
fileIOService.saveUser(user);
}
switch (user.getGrade()) {
case ELEMENTARY:
return "小学";
case MIDDLE:
return "初中";
case HIGH:
return "高中";
default:
return "未知";
}
}
public List<User> getAllUsers() throws IOException {
return fileIOService.loadAllUsers();
}
/**
*
*/
public String getUserStatistics(User user) {
StringBuilder sb = new StringBuilder();
sb.append("========== 用户统计 ==========\n");
sb.append("用户名:").append(user.getUsername()).append("\n");
sb.append("学段:").append(getGradeDisplayName(user)).append("\n");
sb.append("邮箱:").append(user.getEmail()).append("\n");
sb.append("总答题次数:").append(user.getTotalQuizzes()).append(" 次\n");
sb.append("平均分:").append(String.format("%.1f", user.getAverageScore())).append(" 分\n");
sb.append("注册时间:").append(user.getRegistrationDate()).append("\n");
sb.append("=============================\n");
public User findUser(String username) throws IOException {
return fileIOService.findUserByUsername(username);
}
return sb.toString();
/** 获取学段中文名称 */
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) {
String sb =
"========== 用户统计 ==========\n"
+ "用户名:"
+ user.getUsername()
+ "\n"
+ "学段:"
+ getGradeDisplayName(user)
+ "\n"
+ "邮箱:"
+ user.getEmail()
+ "\n"
+ "总答题次数:"
+ user.getTotalQuizzes()
+ " 次\n"
+ "平均分:"
+ String.format("%.1f", user.getAverageScore())
+ " 分\n"
+ "注册时间:"
+ user.getRegistrationDate()
+ "\n"
+ "=============================\n";
return sb;
}
private static class RegistrationCode {
String code;
long expiryTime;
RegistrationCode(String code, long expiryTime) {
this.code = code;
this.expiryTime = expiryTime;
}
}
}

@ -1,67 +0,0 @@
package com.pair.service.question_generator;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.factory.ElementaryQuestionFactory;
import com.pair.service.question_generator.factory.HighQuestionFactory;
import com.pair.service.question_generator.factory.MiddleQuestionFactory;
import com.pair.service.question_generator.factory.QuestionFactory;
import java.util.*;
/**
*
*/
public class QuestionFactoryManager {
private static final Map<Grade, QuestionFactory> factories = new HashMap<>();
static {
factories.put(Grade.ELEMENTARY, new ElementaryQuestionFactory());
factories.put(Grade.MIDDLE, new MiddleQuestionFactory());
factories.put(Grade.HIGH, new HighQuestionFactory());
}
/**
*
*
* @param grade
* @param count
* @param historyQuestions null
* @return
*/
public static List<ChoiceQuestion> generateQuestions(
Grade grade, int count, Set<String> historyQuestions) {
List<ChoiceQuestion> questions = new ArrayList<>();
Set<String> usedQuestions =
historyQuestions != null ? new HashSet<>(historyQuestions) : new HashSet<>();
int maxAttempts = count * 10;
int attempts = 0;
QuestionFactory factory = factories.get(grade);
if (factory == null) {
throw new IllegalArgumentException("不支持的学段: " + grade);
}
while (questions.size() < count && attempts < maxAttempts) {
ChoiceQuestion question = factory.createQuestion();
String questionText = question.getQuestionText();
if (!usedQuestions.contains(questionText)) {
questions.add(question);
usedQuestions.add(questionText);
}
attempts++;
}
if (questions.size() < count) {
System.out.println(
"⚠ 警告:只生成了 " + questions.size() + " 道题,未达到要求的 " + count + " 道");
}
return questions;
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.elementary.*;
import com.pair.util.RandomUtils;
import com.pair.service.question_generator.strategy.*;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class ElementaryQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> 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;
}
}

@ -1,43 +0,0 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.QuestionStrategy;
import com.pair.service.question_generator.strategy.high.*;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class HighQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public HighQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有高中题目生成策略
strategies.add(new SinStrategy());
strategies.add(new CosStrategy());
strategies.add(new TanStrategy());
strategies.add(new TrigIdentityStrategy());
strategies.add(new DerivativeStrategy());
strategies.add(new ArithmeticSequenceSumStrategy());
strategies.add(new LogarithmStrategy());
strategies.add(new ProbabilityStrategy());
strategies.add(new FunctionExtremeStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.HIGH;
}
}

@ -1,50 +0,0 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.elementary.ParenthesesAddStrategy;
import com.pair.service.question_generator.strategy.elementary.ParenthesesMultiplyStrategy;
import com.pair.service.question_generator.strategy.middle.*;
import com.pair.util.RandomUtils;
import com.pair.service.question_generator.strategy.QuestionStrategy;
import java.util.ArrayList;
import java.util.List;
/**
*
*/
public class MiddleQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public MiddleQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有初中题目生成策略
strategies.add(new SquareStrategy());
strategies.add(new SquareAddStrategy());
strategies.add(new SqrtStrategy());
strategies.add(new SqrtAddStrategy());
strategies.add(new MixedSquareSqrtStrategy());
strategies.add(new ParenthesesAddStrategy());
strategies.add(new ParenthesesMultiplyStrategy());
strategies.add(new LinearEquationStrategy());
strategies.add(new QuadraticEquationStrategy());
strategies.add(new TriangleAreaStrategy());
strategies.add(new CircleAreaStrategy());
strategies.add(new LinearFunctionStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.MIDDLE;
}
}

@ -1,17 +0,0 @@
package com.pair.service.question_generator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
/**
*
*/
public interface QuestionFactory {
//创建题目
ChoiceQuestion createQuestion();
//获取工厂支持的学段
Grade getSupportedGrade();
}

@ -1,138 +0,0 @@
package com.pair.service.question_generator.strategy;
import com.pair.model.Grade;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
*
*
*/
public abstract class AbstractQuestionStrategy implements QuestionStrategy {
protected final Grade grade;
/**
*
* @param grade
*/
protected AbstractQuestionStrategy(Grade grade) {
this.grade = grade;
}
/**
* 3
* @param correctAnswer
* @return
*/
protected List<Double> generateNumericOptions(double correctAnswer) {
List<Double> 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<Double> generateNumericOptionsWithCommonError(
double correctAnswer, double commonError) {
List<Double> 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<String> generateStringOptions(
String correctAnswer, List<String> allPossibleValues) {
List<String> options = new ArrayList<>();
options.add(correctAnswer);
List<String> 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;
}
}

@ -1,12 +0,0 @@
package com.pair.service.question_generator.strategy;
import com.pair.model.ChoiceQuestion;
//题目生成题型接口
public interface QuestionStrategy {
//生成题目
ChoiceQuestion generate();
//题型
String getStrategyName();
}

@ -1,35 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Addition strategy for generating addition questions. */
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<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Addition";
}
}

@ -1,39 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
*/
public class DivisionStrategy extends AbstractQuestionStrategy {
public DivisionStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int divisor = RandomUtils.nextInt(2, 10);
int quotient = RandomUtils.nextInt(1, 10);
int dividend = divisor * quotient;
String questionText = dividend + " ÷ " + divisor;
double answer = quotient;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "除法";
}
}

@ -1,34 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Multiplication strategy for generating multiplication questions. */
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<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Multiplication";
}
}

@ -1,40 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* (a + b) × c
*/
public class ParenthesesAddStrategy extends AbstractQuestionStrategy {
public ParenthesesAddStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
String questionText = "(" + num1 + " + " + num2 + ") × " + num3;
double answer = (num1 + num2) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号加法乘法";
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* (a - b) × c
*/
public class ParenthesesMultiplyStrategy extends AbstractQuestionStrategy {
public ParenthesesMultiplyStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = "(" + larger + " - " + smaller + ") × " + num3;
double answer = (larger - smaller) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号减法乘法";
}
}

@ -1,38 +0,0 @@
package com.pair.service.question_generator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Subtraction strategy for generating subtraction questions. */
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);
// Ensure positive result
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = larger + " - " + smaller;
double answer = larger - smaller;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Subtraction";
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 3 2 10
*/
public class ArithmeticSequenceSumStrategy extends AbstractQuestionStrategy {
public ArithmeticSequenceSumStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a1 = RandomUtils.nextInt(1, 10); // 首项
int d = RandomUtils.nextInt(1, 5); // 公差
int n = RandomUtils.nextInt(5, 15); // 项数
String questionText = "等差数列首项为 " + a1 + ",公差为 " + d +
",求前 " + n + " 项和";
// Sn = n * a1 + n(n-1)/2 * d
double answer = n * a1 + n * (n - 1) / 2.0 * d;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "等差数列求和";
}
}

@ -1,57 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* cos(45°)
*/
public class CosStrategy extends AbstractQuestionStrategy {
// 特殊角的余弦值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(COS_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "余弦函数";
}
}

@ -1,40 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* f(x) = 3x² + 2x f'(2)
*/
public class DerivativeStrategy extends AbstractQuestionStrategy {
public DerivativeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(1, 6);
int x = RandomUtils.nextInt(1, 5);
String questionText = "f(x) = " + a + "x² + " + b + "x求 f'(" + x + ")";
// f'(x) = 2ax + b
double answer = 2 * a * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "导数计算";
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* f(x) = -x² + 4x + 1
*/
public class FunctionExtremeStrategy extends AbstractQuestionStrategy {
public FunctionExtremeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = -1; // 开口向下
int b = RandomUtils.nextInt(2, 8) * 2; // 偶数,方便计算
int c = RandomUtils.nextInt(1, 10);
String questionText = "f(x) = -x² + " + b + "x + " + c + ",求最大值";
// 顶点坐标 x = -b/(2a), y = (4ac - b²)/(4a)
double xVertex = -b / (2.0 * a);
double answer = (4.0 * a * c - b * b) / (4.0 * a);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "函数极值";
}
}

@ -1,55 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* log8 + log9
*/
public class LogarithmStrategy extends AbstractQuestionStrategy {
private static final int[][] LOG_PAIRS = {
{2, 4, 2}, // log₂4 = 2
{2, 8, 3}, // log₂8 = 3
{2, 16, 4}, // log₂16 = 4
{3, 9, 2}, // log₃9 = 2
{3, 27, 3}, // log₃27 = 3
{5, 25, 2}, // log₅25 = 2
{10, 100, 2} // log₁₀100 = 2
};
public LogarithmStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int index1 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int index2 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int base1 = LOG_PAIRS[index1][0];
int num1 = LOG_PAIRS[index1][1];
int result1 = LOG_PAIRS[index1][2];
int base2 = LOG_PAIRS[index2][0];
int num2 = LOG_PAIRS[index2][1];
int result2 = LOG_PAIRS[index2][2];
String questionText = "log₍" + base1 + "₎" + num1 + " + log₍" + base2 + "₎" + num2;
double answer = result1 + result2;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "对数运算";
}
}

@ -1,40 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5 3
*/
public class ProbabilityStrategy extends AbstractQuestionStrategy {
public ProbabilityStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int red = RandomUtils.nextInt(3, 8);
int white = RandomUtils.nextInt(2, 6);
int total = red + white;
String questionText = "袋中有 " + red + " 个红球," + white +
" 个白球,随机抽一个,抽到红球的概率是多少?(保留两位小数)";
double answer = Math.round((double) red / total * 100.0) / 100.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "概率计算";
}
}

@ -1,56 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.*;
import java.util.List;
/**
*
* sin(30°)
*/
public class SinStrategy extends AbstractQuestionStrategy {
// 特殊角的正弦值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(SIN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正弦函数";
}
}

@ -1,56 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* tan(45°)
*/
public class TanStrategy extends AbstractQuestionStrategy {
// 特殊角的正切值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(TAN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正切函数";
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
import java.util.*;
/**
*
* sin²(θ) + cos²(θ) = ?
*/
public class TrigIdentityStrategy extends AbstractQuestionStrategy {
public TrigIdentityStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int[] angles = {30, 45, 60, 90};
int angle = angles[RandomUtils.nextInt(0, angles.length - 1)];
String questionText = "sin²(" + angle + "°) + cos²(" + angle + "°) = ?";
String correctAnswer = "1"; // 三角恒等式永远等于1
List<String> allValues = Arrays.asList("1", "0", "2", "√2", "1/2");
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "三角恒等式";
}
}

@ -1,36 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5cm (π3.14)
*/
public class CircleAreaStrategy extends AbstractQuestionStrategy {
public CircleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int radius = RandomUtils.nextInt(3, 10);
String questionText = "半径为 " + radius + "cm 的圆,面积是多少?(π取3.14)";
double answer = 3.14 * radius * radius;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "圆的面积";
}
}

@ -1,39 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 2x + 5 = 13 x
*/
public class LinearEquationStrategy extends AbstractQuestionStrategy {
public LinearEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 10); // 系数
int x = RandomUtils.nextInt(1, 10); // 真实的 x 值
int b = RandomUtils.nextInt(1, 15); // 常数项
int result = a * x + b;
String questionText = a + "x + " + b + " = " + result + ",求 x";
double answer = x;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元一次方程";
}
}

@ -1,41 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* y = 2x + 3 x = 5 y
*/
public class LinearFunctionStrategy extends AbstractQuestionStrategy {
public LinearFunctionStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int k = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(-5, 10);
int x = RandomUtils.nextInt(1, 10);
String questionText = "函数 y = " + k + "x " +
(b >= 0 ? "+ " : "") + b +
",当 x = " + x + " 时y 的值是多少?";
double answer = k * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一次函数求值";
}
}

@ -1,40 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49 + 3²
*/
public class MixedSquareSqrtStrategy extends AbstractQuestionStrategy {
public MixedSquareSqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int sqrtRoot = RandomUtils.nextInt(2, 8);
int sqrtNum = sqrtRoot * sqrtRoot;
int squareBase = RandomUtils.nextInt(2, 6);
String questionText = "√" + sqrtNum + " + " + squareBase + "²";
double answer = sqrtRoot + (squareBase * squareBase);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方开方混合";
}
}

@ -1,44 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* x² - 5x + 6 = 0
*/
public class QuadraticEquationStrategy extends AbstractQuestionStrategy {
public QuadraticEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
// 生成两个根
int root1 = RandomUtils.nextInt(1, 6);
int root2 = RandomUtils.nextInt(1, 6);
// 根据韦达定理x² - (root1+root2)x + (root1*root2) = 0
int b = -(root1 + root2);
int c = root1 * root2;
String questionText = "x² " + (b >= 0 ? "+ " : "") + b + "x " +
(c >= 0 ? "+ " : "") + c + " = 0求较大的根";
double answer = Math.max(root1, root2);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元二次方程";
}
}

@ -1,39 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49 + 5
*/
public class SqrtAddStrategy extends AbstractQuestionStrategy {
public SqrtAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 10);
int num = root * root;
int add = RandomUtils.nextInt(1, 20);
String questionText = "√" + num + " + " + add;
double answer = root + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方加法";
}
}

@ -1,39 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 49
*/
public class SqrtStrategy extends AbstractQuestionStrategy {
public SqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 12);
int num = root * root; // 完全平方数
String questionText = "√" + num;
double answer = root;
// 常见错误把开方当成除以2
double commonError = num / 2.0;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方";
}
}

@ -1,37 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5² + 10
*/
public class SquareAddStrategy extends AbstractQuestionStrategy {
public SquareAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(2, 10);
int add = RandomUtils.nextInt(1, 20);
String questionText = base + "² + " + add;
double answer = base * base + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方加法";
}
}

@ -1,39 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 5²
*/
public class SquareStrategy extends AbstractQuestionStrategy {
public SquareStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int num = RandomUtils.nextInt(1, 15);
String questionText = num + "²";
double answer = num * num;
// 常见错误把平方当成乘以2
double commonError = num * 2;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方";
}
}

@ -1,37 +0,0 @@
package com.pair.service.question_generator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.question_generator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/**
*
* 6cm 8cm
*/
public class TriangleAreaStrategy extends AbstractQuestionStrategy {
public TriangleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(4, 15);
int height = RandomUtils.nextInt(4, 15);
String questionText = "底为 " + base + "cm高为 " + height + "cm 的三角形面积是多少?";
double answer = (base * height) / 2.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "三角形面积";
}
}

@ -0,0 +1,63 @@
package com.pair.service.questiongenerator;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.factory.ElementaryQuestionFactory;
import com.pair.service.questiongenerator.factory.HighQuestionFactory;
import com.pair.service.questiongenerator.factory.MiddleQuestionFactory;
import com.pair.service.questiongenerator.factory.QuestionFactory;
import java.util.*;
/** 题目工厂管理器 */
public class QuestionFactoryManager {
private static final Map<Grade, QuestionFactory> factories = new HashMap<>();
static {
factories.put(Grade.ELEMENTARY, new ElementaryQuestionFactory());
factories.put(Grade.MIDDLE, new MiddleQuestionFactory());
factories.put(Grade.HIGH, new HighQuestionFactory());
}
/**
*
*
* @param grade
* @param count
* @param historyQuestions null
* @return
*/
public static List<ChoiceQuestion> generateQuestions(
Grade grade, int count, Set<String> historyQuestions) {
List<ChoiceQuestion> questions = new ArrayList<>();
Set<String> usedQuestions =
historyQuestions != null ? new HashSet<>(historyQuestions) : new HashSet<>();
int maxAttempts = count * 10;
int attempts = 0;
QuestionFactory factory = factories.get(grade);
if (factory == null) {
throw new IllegalArgumentException("不支持的学段: " + grade);
}
while (questions.size() < count && attempts < maxAttempts) {
ChoiceQuestion question = factory.createQuestion();
String questionText = question.getQuestionText();
if (!usedQuestions.contains(questionText)) {
questions.add(question);
usedQuestions.add(questionText);
}
attempts++;
}
if (questions.size() < count) {
System.out.println("⚠ 警告:只生成了 " + questions.size() + " 道题,未达到要求的 " + count + " 道");
}
return questions;
}
}

@ -0,0 +1,39 @@
package com.pair.service.questiongenerator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.QuestionStrategy;
import com.pair.service.questiongenerator.strategy.elementary.*;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/** 小学题目工厂 */
public class ElementaryQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> 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;
}
}

@ -0,0 +1,40 @@
package com.pair.service.questiongenerator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.QuestionStrategy;
import com.pair.service.questiongenerator.strategy.high.*;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/** 高中题目工厂 */
public class HighQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public HighQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有高中题目生成策略
strategies.add(new SinStrategy());
strategies.add(new CosStrategy());
strategies.add(new TanStrategy());
strategies.add(new TrigIdentityStrategy());
strategies.add(new DerivativeStrategy());
strategies.add(new ArithmeticSequenceSumStrategy());
strategies.add(new LogarithmStrategy());
strategies.add(new ProbabilityStrategy());
strategies.add(new FunctionExtremeStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.HIGH;
}
}

@ -0,0 +1,45 @@
package com.pair.service.questiongenerator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.QuestionStrategy;
import com.pair.service.questiongenerator.strategy.elementary.ParenthesesAddStrategy;
import com.pair.service.questiongenerator.strategy.elementary.ParenthesesMultiplyStrategy;
import com.pair.service.questiongenerator.strategy.middle.*;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.List;
/** 初中题目工厂 */
public class MiddleQuestionFactory implements QuestionFactory {
private final List<QuestionStrategy> strategies;
public MiddleQuestionFactory() {
strategies = new ArrayList<>();
// 注册所有初中题目生成策略
strategies.add(new SquareStrategy());
strategies.add(new SquareAddStrategy());
strategies.add(new SqrtStrategy());
strategies.add(new SqrtAddStrategy());
strategies.add(new MixedSquareSqrtStrategy());
strategies.add(new ParenthesesAddStrategy());
strategies.add(new ParenthesesMultiplyStrategy());
strategies.add(new LinearEquationStrategy());
strategies.add(new QuadraticEquationStrategy());
strategies.add(new TriangleAreaStrategy());
strategies.add(new CircleAreaStrategy());
strategies.add(new LinearFunctionStrategy());
}
@Override
public ChoiceQuestion createQuestion() {
QuestionStrategy strategy = RandomUtils.randomChoice(strategies);
return strategy.generate();
}
@Override
public Grade getSupportedGrade() {
return Grade.MIDDLE;
}
}

@ -0,0 +1,27 @@
package com.pair.service.questiongenerator.factory;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
/**
*
*
* <p>
* </p>
*/
public interface QuestionFactory {
/**
*
*
* @return
*/
ChoiceQuestion createQuestion();
/**
*
*
* @return
*/
Grade getSupportedGrade();
}

@ -0,0 +1,138 @@
package com.pair.service.questiongenerator.strategy;
import com.pair.model.Grade;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/** 抽象题目生成策略基类 提供通用的选项生成方法,减少重复代码 */
public abstract class AbstractQuestionStrategy implements QuestionStrategy {
protected final Grade grade;
/**
*
*
* @param grade
*/
protected AbstractQuestionStrategy(Grade grade) {
this.grade = grade;
}
/**
* 3
*
* @param correctAnswer
* @return
*/
protected List<Double> generateNumericOptions(double correctAnswer) {
List<Double> 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<Double> generateNumericOptionsWithCommonError(
double correctAnswer, double commonError) {
List<Double> 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<String> generateStringOptions(
String correctAnswer, List<String> allPossibleValues) {
List<String> options = new ArrayList<>();
options.add(correctAnswer);
List<String> 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;
}
}

@ -0,0 +1,26 @@
package com.pair.service.questiongenerator.strategy;
import com.pair.model.ChoiceQuestion;
/**
*
*
* <p>
* </p>
*/
public interface QuestionStrategy {
/**
*
*
* @return
*/
ChoiceQuestion generate();
/**
*
*
* @return
*/
String getStrategyName();
}

@ -0,0 +1,33 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Addition strategy for generating addition questions. */
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<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Addition";
}
}

@ -0,0 +1,34 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 除法策略(确保整除) */
public class DivisionStrategy extends AbstractQuestionStrategy {
public DivisionStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int divisor = RandomUtils.nextInt(2, 10);
int quotient = RandomUtils.nextInt(1, 10);
int dividend = divisor * quotient;
String questionText = dividend + " ÷ " + divisor;
double answer = quotient;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "除法";
}
}

@ -0,0 +1,33 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Multiplication strategy for generating multiplication questions. */
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<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Multiplication";
}
}

@ -0,0 +1,34 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 带括号的加法乘法混合运算策略 例如:(a + b) × c */
public class ParenthesesAddStrategy extends AbstractQuestionStrategy {
public ParenthesesAddStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
String questionText = "(" + num1 + " + " + num2 + ") × " + num3;
double answer = (num1 + num2) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号加法乘法";
}
}

@ -0,0 +1,37 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 带括号的减法乘法混合运算策略 例如:(a - b) × c */
public class ParenthesesMultiplyStrategy extends AbstractQuestionStrategy {
public ParenthesesMultiplyStrategy() {
super(Grade.ELEMENTARY);
}
@Override
public ChoiceQuestion generate() {
int num1 = RandomUtils.nextInt(1, 20);
int num2 = RandomUtils.nextInt(1, 20);
int num3 = RandomUtils.nextInt(2, 10);
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = "(" + larger + " - " + smaller + ") × " + num3;
double answer = (larger - smaller) * num3;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "括号减法乘法";
}
}

@ -0,0 +1,37 @@
package com.pair.service.questiongenerator.strategy.elementary;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** Subtraction strategy for generating subtraction questions. */
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);
// Ensure positive result
int larger = Math.max(num1, num2);
int smaller = Math.min(num1, num2);
String questionText = larger + " - " + smaller;
double answer = larger - smaller;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "Subtraction";
}
}

@ -0,0 +1,36 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 等差数列求和策略 例如:首项 3公差 2求前 10 项和 */
public class ArithmeticSequenceSumStrategy extends AbstractQuestionStrategy {
public ArithmeticSequenceSumStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a1 = RandomUtils.nextInt(1, 10); // 首项
int d = RandomUtils.nextInt(1, 5); // 公差
int n = RandomUtils.nextInt(5, 15); // 项数
String questionText = "等差数列首项为 " + a1 + ",公差为 " + d + ",求前 " + n + " 项和";
// Sn = n * a1 + n(n-1)/2 * d
double answer = n * a1 + n * (n - 1) / 2.0 * d;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "等差数列求和";
}
}

@ -0,0 +1,53 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** 余弦函数策略 例如cos(45°) */
public class CosStrategy extends AbstractQuestionStrategy {
// 特殊角的余弦值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(COS_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "余弦函数";
}
}

@ -0,0 +1,36 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 导数计算策略 例如f(x) = 3x² + 2x求 f'(2) */
public class DerivativeStrategy extends AbstractQuestionStrategy {
public DerivativeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(1, 6);
int x = RandomUtils.nextInt(1, 5);
String questionText = "f(x) = " + a + "x² + " + b + "x求 f'(" + x + ")";
// f'(x) = 2ax + b
double answer = 2 * a * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "导数计算";
}
}

@ -0,0 +1,37 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 函数极值策略 例如f(x) = -x² + 4x + 1求最大值 */
public class FunctionExtremeStrategy extends AbstractQuestionStrategy {
public FunctionExtremeStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int a = -1; // 开口向下
int b = RandomUtils.nextInt(2, 8) * 2; // 偶数,方便计算
int c = RandomUtils.nextInt(1, 10);
String questionText = "f(x) = -x² + " + b + "x + " + c + ",求最大值";
// 顶点坐标 x = -b/(2a), y = (4ac - b²)/(4a)
double xVertex = -b / (2.0 * a);
double answer = (4.0 * a * c - b * b) / (4.0 * a);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "函数极值";
}
}

@ -0,0 +1,51 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 对数运算策略 例如log₂8 + log₃9 */
public class LogarithmStrategy extends AbstractQuestionStrategy {
private static final int[][] LOG_PAIRS = {
{2, 4, 2}, // log₂4 = 2
{2, 8, 3}, // log₂8 = 3
{2, 16, 4}, // log₂16 = 4
{3, 9, 2}, // log₃9 = 2
{3, 27, 3}, // log₃27 = 3
{5, 25, 2}, // log₅25 = 2
{10, 100, 2} // log₁₀100 = 2
};
public LogarithmStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int index1 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int index2 = RandomUtils.nextInt(0, LOG_PAIRS.length - 1);
int base1 = LOG_PAIRS[index1][0];
int num1 = LOG_PAIRS[index1][1];
int result1 = LOG_PAIRS[index1][2];
int base2 = LOG_PAIRS[index2][0];
int num2 = LOG_PAIRS[index2][1];
int result2 = LOG_PAIRS[index2][2];
String questionText = "log₍" + base1 + "₎" + num1 + " + log₍" + base2 + "₎" + num2;
double answer = result1 + result2;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "对数运算";
}
}

@ -0,0 +1,35 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 概率计算策略 例如:袋中有 5 个红球3 个白球,随机抽一个,抽到红球的概率是多少? */
public class ProbabilityStrategy extends AbstractQuestionStrategy {
public ProbabilityStrategy() {
super(Grade.HIGH);
}
@Override
public ChoiceQuestion generate() {
int red = RandomUtils.nextInt(3, 8);
int white = RandomUtils.nextInt(2, 6);
int total = red + white;
String questionText = "袋中有 " + red + " 个红球," + white + " 个白球,随机抽一个,抽到红球的概率是多少?(保留两位小数)";
double answer = Math.round((double) red / total * 100.0) / 100.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "概率计算";
}
}

@ -0,0 +1,53 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** 正弦函数策略 例如sin(30°) */
public class SinStrategy extends AbstractQuestionStrategy {
// 特殊角的正弦值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(SIN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正弦函数";
}
}

@ -0,0 +1,52 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/** 正切函数策略 例如tan(45°) */
public class TanStrategy extends AbstractQuestionStrategy {
// 特殊角的正切值表
private static final Map<Integer, String> 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<Integer> 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<String> allValues = new ArrayList<>(TAN_VALUES.values());
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "正切函数";
}
}

@ -0,0 +1,35 @@
package com.pair.service.questiongenerator.strategy.high;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.Arrays;
import java.util.List;
/** 三角恒等式策略 例如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<String> allValues = Arrays.asList("1", "0", "2", "√2", "1/2");
List<String> options = generateStringOptions(correctAnswer, allValues);
return new ChoiceQuestion(questionText, correctAnswer, options, grade);
}
@Override
public String getStrategyName() {
return "三角恒等式";
}
}

@ -0,0 +1,32 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 圆的面积策略 例如:半径为 5cm 的圆,面积是多少?(π取3.14) */
public class CircleAreaStrategy extends AbstractQuestionStrategy {
public CircleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int radius = RandomUtils.nextInt(3, 10);
String questionText = "半径为 " + radius + "cm 的圆,面积是多少?(π取3.14)";
double answer = 3.14 * radius * radius;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "圆的面积";
}
}

@ -0,0 +1,35 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 一元一次方程策略 例如2x + 5 = 13求 x */
public class LinearEquationStrategy extends AbstractQuestionStrategy {
public LinearEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int a = RandomUtils.nextInt(2, 10); // 系数
int x = RandomUtils.nextInt(1, 10); // 真实的 x 值
int b = RandomUtils.nextInt(1, 15); // 常数项
int result = a * x + b;
String questionText = a + "x + " + b + " = " + result + ",求 x";
double answer = x;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元一次方程";
}
}

@ -0,0 +1,36 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 一次函数求值策略 例如:函数 y = 2x + 3当 x = 5 时y 的值是多少? */
public class LinearFunctionStrategy extends AbstractQuestionStrategy {
public LinearFunctionStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int k = RandomUtils.nextInt(2, 8);
int b = RandomUtils.nextInt(-5, 10);
int x = RandomUtils.nextInt(1, 10);
String questionText =
"函数 y = " + k + "x " + (b >= 0 ? "+ " : "") + b + ",当 x = " + x + " 时y 的值是多少?";
double answer = k * x + b;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一次函数求值";
}
}

@ -0,0 +1,35 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 平方开方混合运算策略 例如√49 + 3² */
public class MixedSquareSqrtStrategy extends AbstractQuestionStrategy {
public MixedSquareSqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int sqrtRoot = RandomUtils.nextInt(2, 8);
int sqrtNum = sqrtRoot * sqrtRoot;
int squareBase = RandomUtils.nextInt(2, 6);
String questionText = "√" + sqrtNum + " + " + squareBase + "²";
double answer = sqrtRoot + (squareBase * squareBase);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方开方混合";
}
}

@ -0,0 +1,40 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 一元二次方程策略(求较大根) 例如x² - 5x + 6 = 0求较大的根 */
public class QuadraticEquationStrategy extends AbstractQuestionStrategy {
public QuadraticEquationStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
// 生成两个根
int root1 = RandomUtils.nextInt(1, 6);
int root2 = RandomUtils.nextInt(1, 6);
// 根据韦达定理x² - (root1+root2)x + (root1*root2) = 0
int b = -(root1 + root2);
int c = root1 * root2;
String questionText =
"x² " + (b >= 0 ? "+ " : "") + b + "x " + (c >= 0 ? "+ " : "") + c + " = 0求较大的根";
double answer = Math.max(root1, root2);
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "一元二次方程";
}
}

@ -0,0 +1,34 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 开方加法混合策略 例如√49 + 5 */
public class SqrtAddStrategy extends AbstractQuestionStrategy {
public SqrtAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 10);
int num = root * root;
int add = RandomUtils.nextInt(1, 20);
String questionText = "√" + num + " + " + add;
double answer = root + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方加法";
}
}

@ -0,0 +1,35 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 开方运算策略(完全平方数) 例如√49 */
public class SqrtStrategy extends AbstractQuestionStrategy {
public SqrtStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int root = RandomUtils.nextInt(2, 12);
int num = root * root; // 完全平方数
String questionText = "√" + num;
double answer = root;
// 常见错误把开方当成除以2
double commonError = num / 2.0;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "开方";
}
}

@ -0,0 +1,33 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 平方加法混合策略 例如5² + 10 */
public class SquareAddStrategy extends AbstractQuestionStrategy {
public SquareAddStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(2, 10);
int add = RandomUtils.nextInt(1, 20);
String questionText = base + "² + " + add;
double answer = base * base + add;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方加法";
}
}

@ -0,0 +1,34 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 平方运算策略 例如5² */
public class SquareStrategy extends AbstractQuestionStrategy {
public SquareStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int num = RandomUtils.nextInt(1, 15);
String questionText = num + "²";
double answer = num * num;
// 常见错误把平方当成乘以2
double commonError = num * 2;
List<Double> options = generateNumericOptionsWithCommonError(answer, commonError);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "平方";
}
}

@ -0,0 +1,33 @@
package com.pair.service.questiongenerator.strategy.middle;
import com.pair.model.ChoiceQuestion;
import com.pair.model.Grade;
import com.pair.service.questiongenerator.strategy.AbstractQuestionStrategy;
import com.pair.util.RandomUtils;
import java.util.List;
/** 三角形面积计算策略 例如:底 6cm高 8cm 的三角形面积 */
public class TriangleAreaStrategy extends AbstractQuestionStrategy {
public TriangleAreaStrategy() {
super(Grade.MIDDLE);
}
@Override
public ChoiceQuestion generate() {
int base = RandomUtils.nextInt(4, 15);
int height = RandomUtils.nextInt(4, 15);
String questionText = "底为 " + base + "cm高为 " + height + "cm 的三角形面积是多少?";
double answer = (base * height) / 2.0;
List<Double> options = generateNumericOptions(answer);
return new ChoiceQuestion(questionText, answer, options, grade);
}
@Override
public String getStrategyName() {
return "三角形面积";
}
}

@ -1,358 +1,383 @@
package com.pair.ui;
import com.pair.model.Grade;
import com.pair.service.UserService;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.scene.layout.Priority;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
public class InfGenPage extends NavigablePanel {
private final TextField usernameField = new TextField();
private final TextField emailField = new TextField();
private final Label passwordLabel = new Label("******");
private final ChoiceBox<String> gradeChoice = new ChoiceBox<>();
private final Spinner<Integer> questionCountSpinner = new Spinner<>(10, 30, 10);
private final Button passwordModifyButton = new Button("修改密码");
private final Button generateButton = new Button("生成题目");
private final Button modifyUsernameButton = new Button("修改用户名");
private final Button modifyEmailButton = new Button("修改邮箱");
// 用户统计信息标签
private final Label totalQuizzesLabel = new Label("0");
private final Label averageScoreLabel = new Label("0.0");
public InfGenPage(Runnable onBack, String currentUsername, String currentEmail, Grade currentGrade, int totalQuizzes, double averageScore) {
super(onBack);
initializeContent();
usernameField.setText(currentUsername);
emailField.setText(currentEmail);
gradeChoice.getSelectionModel().select(currentGrade.ordinal());
// 更新统计信息
updateStatistics(totalQuizzes, averageScore);
}
@Override
protected void buildContent() {
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);"
);
// 初始化控件样式
gradeChoice.getItems().addAll("小学", "初中", "高中");
gradeChoice.setValue("小学");
StyleHelper.styleTextField(usernameField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleTextField(emailField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleChoiceBox(gradeChoice, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleSpinner(questionCountSpinner);
passwordLabel.setStyle("-fx-font-size: " + UIConstants.BODY_FONT_SIZE + "px; -fx-pref-width: " + UIConstants.INPUT_MEDIUM_WIDTH + "px;");
// 按钮样式(注意:你类中已有 styleInfoButton / stylePrimaryButton直接调用
styleInfoButton(passwordModifyButton);
stylePrimaryButton(generateButton);
styleInfoButton(modifyUsernameButton);
styleInfoButton(modifyEmailButton);
VBox.setMargin(title, new Insets(0, 0, UIConstants.XLARGE_SPACING, 0));
// ===== 新布局:左侧表单 + 右侧统计 =====
HBox mainContent = new HBox(60); // 增加间距避免拥挤
mainContent.setAlignment(Pos.TOP_CENTER);
mainContent.setMinWidth(Region.USE_PREF_SIZE);
// ========== 左侧:用户信息 + 答题设置 ==========
VBox leftPanel = new VBox(UIConstants.LARGE_SPACING);
leftPanel.setAlignment(Pos.TOP_LEFT);
leftPanel.setMinWidth(450); // 增加最小宽度
leftPanel.setMaxWidth(500); // 增加最大宽度
leftPanel.setStyle("-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 8; -fx-padding: 25;");
// --- 用户信息 ---
Label userInfoTitle = new Label("用户信息");
userInfoTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
userInfoTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox userForm = new VBox(UIConstants.MEDIUM_SPACING);
userForm.getChildren().addAll(
createRow("用户名:", usernameField, modifyUsernameButton),
createRow("邮箱:", emailField, modifyEmailButton),
createRow("密码:", passwordLabel, passwordModifyButton)
);
// --- 答题设置 ---
Label quizSettingsTitle = new Label("答题设置");
quizSettingsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
quizSettingsTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox quizSettings = new VBox(UIConstants.MEDIUM_SPACING);
quizSettings.getChildren().addAll(
createRow("学段选择:", gradeChoice, null),
createRow("题目数量:", questionCountSpinner, generateButton)
);
// 组装左侧
leftPanel.getChildren().addAll(userInfoTitle, userForm, new Separator(), quizSettingsTitle, quizSettings);
// ========== 右侧:仅学习统计 ==========
VBox rightPanel = new VBox();
rightPanel.setAlignment(Pos.CENTER); // 统计区域垂直+水平居中
rightPanel.setMinWidth(340); // 增加最小宽度
rightPanel.setMaxWidth(400); // 增加最大宽度
VBox statsArea = createStatsArea();
rightPanel.getChildren().add(statsArea);
// 组装主内容
mainContent.getChildren().addAll(leftPanel, rightPanel);
// 组装完整卡片
card.getChildren().addAll(title, mainContent);
setCenter(card);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
}
private HBox createRow(String text, Control field, Button btn) {
HBox row = new HBox(20); // 间距适中
row.setAlignment(Pos.CENTER_LEFT);
row.setPadding(new Insets(8, 0, 8, 0));
// 左侧标签容器 - 确保宽度足够
VBox labelContainer = new VBox();
labelContainer.setPrefWidth(130); // 足够显示“用户名:”等标签
labelContainer.setAlignment(Pos.CENTER_RIGHT);
Label label = new Label(text);
label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
label.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
labelContainer.getChildren().add(label);
// 中间字段容器 - 不再强制拉伸,避免挤压按钮
VBox fieldContainer = new VBox();
fieldContainer.setAlignment(Pos.CENTER_LEFT);
if (field != null) {
fieldContainer.getChildren().add(field);
if (field instanceof TextField || field instanceof PasswordField) {
field.setStyle(field.getStyle() + "-fx-padding: 8 12 8 12;");
} else if (field instanceof ChoiceBox) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
} else if (field instanceof Spinner) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
}
}
// 右侧按钮容器 - 关键:移除左边距,让按钮自然对齐
VBox buttonContainer = new VBox();
buttonContainer.setAlignment(Pos.CENTER_LEFT);
if (btn != null) {
buttonContainer.getChildren().add(btn);
// 👇 移除这个!它会导致按钮错位
// VBox.setMargin(btn, new Insets(0, 0, 0, 20)); ← 删除这行!
}
// ⚠️ 重要:不要对任何容器设置 Hgrow避免布局挤压
HBox.setHgrow(labelContainer, Priority.NEVER);
HBox.setHgrow(fieldContainer, Priority.NEVER);
HBox.setHgrow(buttonContainer, Priority.NEVER);
row.getChildren().addAll(labelContainer, fieldContainer, buttonContainer);
return row;
}
public TextField getUsernameField() {
return usernameField;
}
public TextField getEmailField() {
return emailField;
}
public ChoiceBox<String> getGradeChoice() {
return gradeChoice;
}
public Spinner<Integer> getQuestionCountSpinner() {
return questionCountSpinner;
private final TextField usernameField = new TextField();
private final TextField emailField = new TextField();
private final Label passwordLabel = new Label("******");
private final ChoiceBox<String> gradeChoice = new ChoiceBox<>();
private final Spinner<Integer> questionCountSpinner = new Spinner<>(10, 30, 10);
private final Button passwordModifyButton = new Button("修改密码");
private final Button generateButton = new Button("生成题目");
private final Button modifyUsernameButton = new Button("修改用户名");
private final Button modifyEmailButton = new Button("修改邮箱");
// 用户统计信息标签
private final Label totalQuizzesLabel = new Label("0");
private final Label averageScoreLabel = new Label("0.0");
public InfGenPage(
Runnable onBack,
String currentUsername,
String currentEmail,
Grade currentGrade,
int totalQuizzes,
double averageScore) {
super(onBack);
initializeContent();
usernameField.setText(currentUsername);
emailField.setText(currentEmail);
gradeChoice.getSelectionModel().select(currentGrade.ordinal());
// 更新统计信息
updateStatistics(totalQuizzes, averageScore);
}
@Override
protected void buildContent() {
VBox card = StyleHelper.createMediumCard();
Label title = new Label("中小学数学答题系统");
title.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
title.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);");
// 初始化控件样式
gradeChoice.getItems().addAll("小学", "初中", "高中");
gradeChoice.setValue("小学");
StyleHelper.styleTextField(usernameField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleTextField(emailField, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleChoiceBox(gradeChoice, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
StyleHelper.styleSpinner(questionCountSpinner);
passwordLabel.setStyle(
"-fx-font-size: "
+ UIConstants.BODY_FONT_SIZE
+ "px; -fx-pref-width: "
+ UIConstants.INPUT_MEDIUM_WIDTH
+ "px;");
// 按钮样式
styleInfoButton(passwordModifyButton);
stylePrimaryButton(generateButton);
styleInfoButton(modifyUsernameButton);
styleInfoButton(modifyEmailButton);
VBox.setMargin(title, new Insets(0, 0, UIConstants.XLARGE_SPACING, 0));
// ===== 新布局:左侧表单 + 右侧统计 =====
HBox mainContent = new HBox(60); // 增加间距避免拥挤
mainContent.setAlignment(Pos.TOP_CENTER);
mainContent.setMinWidth(Region.USE_PREF_SIZE);
// ========== 左侧:用户信息 + 答题设置 ==========
VBox leftPanel = new VBox(UIConstants.LARGE_SPACING);
leftPanel.setAlignment(Pos.TOP_LEFT);
leftPanel.setMinWidth(450);
leftPanel.setMaxWidth(500);
leftPanel.setStyle(
"-fx-background-color: white; -fx-border-color: #e0e0e0; -fx-border-radius: 8; -fx-padding: 25;");
// --- 用户信息 ---
Label userInfoTitle = new Label("用户信息");
userInfoTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
userInfoTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox userForm = new VBox(UIConstants.MEDIUM_SPACING);
userForm
.getChildren()
.addAll(
createRow("用户名:", usernameField, modifyUsernameButton),
createRow("邮箱:", emailField, modifyEmailButton),
createRow("密码:", passwordLabel, passwordModifyButton));
// --- 答题设置 ---
Label quizSettingsTitle = new Label("答题设置");
quizSettingsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
quizSettingsTitle.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
VBox quizSettings = new VBox(UIConstants.MEDIUM_SPACING);
quizSettings
.getChildren()
.addAll(
createRow("学段选择:", gradeChoice, null),
createRow("题目数量:", questionCountSpinner, generateButton));
// 组装左侧
leftPanel
.getChildren()
.addAll(userInfoTitle, userForm, new Separator(), quizSettingsTitle, quizSettings);
// ========== 右侧:仅学习统计 ==========
VBox rightPanel = new VBox();
rightPanel.setAlignment(Pos.CENTER);
rightPanel.setMinWidth(340);
rightPanel.setMaxWidth(400);
VBox statsArea = createStatsArea();
rightPanel.getChildren().add(statsArea);
// 组装主内容
mainContent.getChildren().addAll(leftPanel, rightPanel);
// 组装完整卡片
card.getChildren().addAll(title, mainContent);
setCenter(card);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
}
private HBox createRow(String text, Control field, Button btn) {
HBox row = new HBox(20); // 间距适中
row.setAlignment(Pos.CENTER_LEFT);
row.setPadding(new Insets(8, 0, 8, 0));
// 左侧标签容器
VBox labelContainer = new VBox();
labelContainer.setPrefWidth(130);
labelContainer.setAlignment(Pos.CENTER_RIGHT);
Label label = new Label(text);
label.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
label.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
labelContainer.getChildren().add(label);
// 中间字段容器
VBox fieldContainer = new VBox();
fieldContainer.setAlignment(Pos.CENTER_LEFT);
if (field != null) {
fieldContainer.getChildren().add(field);
if (field instanceof TextField || field instanceof PasswordField) {
field.setStyle(field.getStyle() + "-fx-padding: 8 12 8 12;");
} else if (field instanceof ChoiceBox) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
} else if (field instanceof Spinner) {
field.setStyle(field.getStyle() + "-fx-pref-width: 180;");
}
}
public Button getGenerateButton() {
return generateButton;
// 右侧按钮容器
VBox buttonContainer = new VBox();
buttonContainer.setAlignment(Pos.CENTER_LEFT);
if (btn != null) {
buttonContainer.getChildren().add(btn);
}
public Button getPasswordModifyButton() {
return passwordModifyButton;
}
public Button getModifyUsernameButton() {
return modifyUsernameButton;
}
public Button getModifyEmailButton() {
return modifyEmailButton;
}
/**
* - 使
*/
private void styleInfoButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #4CAF50, #45a049); " +
"-fx-text-fill: white; " +
"-fx-font-weight: bold; " +
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: transparent; " +
"-fx-border-width: 2; " +
"-fx-font-family: '" + UIConstants.FONT_FAMILY + "'; " +
"-fx-font-size: " + UIConstants.BTN_FONT_SIZE + "px; " +
"-fx-cursor: hand; " +
"-fx-padding: 8 16 8 16; "; // 固定内边距,让文字有呼吸空间
String normalStyle = baseStyle +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
String hoverStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #45a049, #3d8b40); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String pressedStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #3d8b40, #357a38); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
// ✅ 关键修复:不要硬编码宽度!让按钮自动适应内容
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
/**
*
*/
private VBox createStatsArea() {
VBox statsBox = new VBox(20); // 增加间距
statsBox.setAlignment(Pos.CENTER);
statsBox.setPadding(new Insets(25)); // 增加内边距
statsBox.setStyle(
"-fx-background-color: linear-gradient(to bottom, " + UIConstants.toWeb(UIConstants.COLOR_LIGHT) + ", " + UIConstants.toWeb(UIConstants.COLOR_BG) + "); " +
"-fx-background-radius: 15; " +
"-fx-border-radius: 15; " +
"-fx-border-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-border-width: 2; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.15), 12, 0, 0, 6);"
);
statsBox.setMinWidth(250);
statsBox.setMaxWidth(300);
// 标题
Label statsTitle = new Label("📊 学习统计");
statsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
statsTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 总答题次数
VBox quizzesBox = new VBox(8);
quizzesBox.setAlignment(Pos.CENTER);
quizzesBox.setStyle(
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + "20; " +
"-fx-background-radius: 10; " +
"-fx-padding: 15;"
);
Label quizzesTitle = new Label("总答题次数");
quizzesTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
quizzesTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
totalQuizzesLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
totalQuizzesLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + ";");
quizzesBox.getChildren().addAll(quizzesTitle, totalQuizzesLabel);
// 平均分
VBox scoreBox = new VBox(8);
scoreBox.setAlignment(Pos.CENTER);
scoreBox.setStyle(
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + "20; " +
"-fx-background-radius: 10; " +
"-fx-padding: 15;"
);
Label scoreTitle = new Label("平均分");
scoreTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
scoreTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
averageScoreLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
averageScoreLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + ";");
scoreBox.getChildren().addAll(scoreTitle, averageScoreLabel);
// 添加学习建议标签
Label suggestionLabel = new Label("继续加油,提升数学能力!");
suggestionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
suggestionLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + "; " +
"-fx-font-style: italic;");
statsBox.getChildren().addAll(statsTitle, quizzesBox, scoreBox, suggestionLabel);
return statsBox;
}
/**
*
*/
public void updateStatistics(int totalQuizzes, double averageScore) {
totalQuizzesLabel.setText(String.valueOf(totalQuizzes));
averageScoreLabel.setText(String.format("%.1f", averageScore));
}
/**
* - 使
*/
private void stylePrimaryButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #FF9800, #F57C00); " +
"-fx-text-fill: white; " +
"-fx-font-weight: bold; " +
"-fx-background-radius: 8; " +
"-fx-border-radius: 8; " +
"-fx-border-color: transparent; " +
"-fx-border-width: 2; " +
"-fx-font-family: '" + UIConstants.FONT_FAMILY + "'; " +
"-fx-font-size: " + (UIConstants.BTN_FONT_SIZE + 1) + "px; " +
"-fx-cursor: hand; " +
"-fx-padding: 10 20 10 20; ";
String normalStyle = baseStyle +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String hoverStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #F57C00, #EF6C00); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 10, 0, 0, 4);";
String pressedStyle = baseStyle +
"-fx-background-color: linear-gradient(to bottom, #EF6C00, #E65100); " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
// ✅ 关键修复:让“生成题目”按钮也自适应宽度
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
}
HBox.setHgrow(labelContainer, Priority.NEVER);
HBox.setHgrow(fieldContainer, Priority.NEVER);
HBox.setHgrow(buttonContainer, Priority.NEVER);
row.getChildren().addAll(labelContainer, fieldContainer, buttonContainer);
return row;
}
public TextField getUsernameField() {
return usernameField;
}
public TextField getEmailField() {
return emailField;
}
public ChoiceBox<String> getGradeChoice() {
return gradeChoice;
}
public Spinner<Integer> getQuestionCountSpinner() {
return questionCountSpinner;
}
public Button getGenerateButton() {
return generateButton;
}
public Button getPasswordModifyButton() {
return passwordModifyButton;
}
public Button getModifyUsernameButton() {
return modifyUsernameButton;
}
public Button getModifyEmailButton() {
return modifyEmailButton;
}
/** 新的按钮样式 - 使用更现代的颜色渐变,修复悬停变形问题 */
private void styleInfoButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #4CAF50, #45a049); "
+ "-fx-text-fill: white; "
+ "-fx-font-weight: bold; "
+ "-fx-background-radius: 8; "
+ "-fx-border-radius: 8; "
+ "-fx-border-color: transparent; "
+ "-fx-border-width: 2; "
+ "-fx-font-family: '"
+ UIConstants.FONT_FAMILY
+ "'; "
+ "-fx-font-size: "
+ UIConstants.BTN_FONT_SIZE
+ "px; "
+ "-fx-cursor: hand; "
+ "-fx-padding: 8 16 8 16; "; // 固定内边距,让文字有呼吸空间
String normalStyle =
baseStyle + "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
String hoverStyle =
baseStyle
+ "-fx-background-color: linear-gradient(to bottom, #45a049, #3d8b40); "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String pressedStyle =
baseStyle
+ "-fx-background-color: linear-gradient(to bottom, #3d8b40, #357a38); "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
/** 创建统计信息区域 */
private VBox createStatsArea() {
VBox statsBox = new VBox(20); // 增加间距
statsBox.setAlignment(Pos.CENTER);
statsBox.setPadding(new Insets(25)); // 增加内边距
statsBox.setStyle(
"-fx-background-color: linear-gradient(to bottom, "
+ UIConstants.toWeb(UIConstants.COLOR_LIGHT)
+ ", "
+ UIConstants.toWeb(UIConstants.COLOR_BG)
+ "); "
+ "-fx-background-radius: 15; "
+ "-fx-border-radius: 15; "
+ "-fx-border-color: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-border-width: 2; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.15), 12, 0, 0, 6);");
statsBox.setMinWidth(250);
statsBox.setMaxWidth(300);
// 标题
Label statsTitle = new Label("📊 学习统计");
statsTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
statsTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 总答题次数
VBox quizzesBox = new VBox(8);
quizzesBox.setAlignment(Pos.CENTER);
quizzesBox.setStyle(
"-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_SUCCESS)
+ "20; "
+ "-fx-background-radius: 10; "
+ "-fx-padding: 15;");
Label quizzesTitle = new Label("总答题次数");
quizzesTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
quizzesTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
totalQuizzesLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
totalQuizzesLabel.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + ";");
quizzesBox.getChildren().addAll(quizzesTitle, totalQuizzesLabel);
// 平均分
VBox scoreBox = new VBox(8);
scoreBox.setAlignment(Pos.CENTER);
scoreBox.setStyle(
"-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_WARNING)
+ "20; "
+ "-fx-background-radius: 10; "
+ "-fx-padding: 15;");
Label scoreTitle = new Label("平均分");
scoreTitle.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
scoreTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
averageScoreLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 28));
averageScoreLabel.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + ";");
scoreBox.getChildren().addAll(scoreTitle, averageScoreLabel);
// 添加学习建议标签
Label suggestionLabel = new Label("继续加油,提升数学能力!");
suggestionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
suggestionLabel.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB)
+ "; "
+ "-fx-font-style: italic;");
statsBox.getChildren().addAll(statsTitle, quizzesBox, scoreBox, suggestionLabel);
return statsBox;
}
/** 更新统计信息 */
public void updateStatistics(int totalQuizzes, double averageScore) {
totalQuizzesLabel.setText(String.valueOf(totalQuizzes));
averageScoreLabel.setText(String.format("%.1f", averageScore));
}
/** 主要按钮样式 - 使用橙色渐变突出显示,修复悬停变形问题 */
private void stylePrimaryButton(Button btn) {
String baseStyle =
"-fx-background-color: linear-gradient(to bottom, #FF9800, #F57C00); "
+ "-fx-text-fill: white; "
+ "-fx-font-weight: bold; "
+ "-fx-background-radius: 8; "
+ "-fx-border-radius: 8; "
+ "-fx-border-color: transparent; "
+ "-fx-border-width: 2; "
+ "-fx-font-family: '"
+ UIConstants.FONT_FAMILY
+ "'; "
+ "-fx-font-size: "
+ (UIConstants.BTN_FONT_SIZE + 1)
+ "px; "
+ "-fx-cursor: hand; "
+ "-fx-padding: 10 20 10 20; ";
String normalStyle =
baseStyle + "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
String hoverStyle =
baseStyle
+ "-fx-background-color: linear-gradient(to bottom, #F57C00, #EF6C00); "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.4), 10, 0, 0, 4);";
String pressedStyle =
baseStyle
+ "-fx-background-color: linear-gradient(to bottom, #EF6C00, #E65100); "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
btn.setStyle(normalStyle);
btn.setOnMouseEntered(e -> btn.setStyle(hoverStyle));
btn.setOnMouseExited(e -> btn.setStyle(normalStyle));
btn.setOnMousePressed(e -> btn.setStyle(pressedStyle));
btn.setOnMouseReleased(e -> btn.setStyle(normalStyle));
btn.setMinWidth(Region.USE_PREF_SIZE);
btn.setMaxWidth(Double.MAX_VALUE);
btn.setPrefWidth(Region.USE_COMPUTED_SIZE);
}
}

@ -1,6 +1,5 @@
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
@ -8,46 +7,64 @@ import javafx.scene.text.FontWeight;
public class LoginPage extends NavigablePanel {
private final TextField usernameOrEmailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final Button loginButton = new Button("登录");
private final Hyperlink registerLink = new Hyperlink("注册");
public LoginPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
private final TextField usernameOrEmailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final Button loginButton = new Button("登录");
private final Hyperlink registerLink = new Hyperlink("注册");
public LoginPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(
Font.font(
UIConstants.FONT_FAMILY,
FontWeight.BOLD,
UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
usernameOrEmailField.setPromptText("邮箱/用户名");
StyleHelper.styleInputField(usernameOrEmailField);
// 使用统一的输入框样式
usernameOrEmailField.setPromptText("邮箱/用户名");
StyleHelper.styleInputField(usernameOrEmailField);
passwordField.setPromptText("密码6-10位含大小写字母和数字");
StyleHelper.stylePasswordField(passwordField);
// 使用统一的按钮样式
StyleHelper.stylePrimaryButton(loginButton);
registerLink.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
card.getChildren()
.addAll(title, usernameOrEmailField, passwordField, loginButton, registerLink);
setCenter(card);
}
passwordField.setPromptText("密码6-10位含大小写字母和数字");
StyleHelper.stylePasswordField(passwordField);
public TextField getUsernameOrEmailField() {
return usernameOrEmailField;
}
// 使用统一的按钮样式
StyleHelper.stylePrimaryButton(loginButton);
registerLink.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
public PasswordField getPasswordField() {
return passwordField;
}
card.getChildren().addAll(title, usernameOrEmailField, passwordField, loginButton, registerLink);
setCenter(card);
}
public Button getLoginButton() {
return loginButton;
}
public TextField getUsernameOrEmailField() { return usernameOrEmailField; }
public PasswordField getPasswordField() { return passwordField; }
public Button getLoginButton() { return loginButton; }
public Hyperlink getRegisterLink() { return registerLink; }
}
public Hyperlink getRegisterLink() {
return registerLink;
}
}

@ -1,4 +1,3 @@
// com/ui/MainWindow.java
package com.pair.ui;
import com.pair.model.User;
@ -6,298 +5,328 @@ import com.pair.service.FileIOService;
import com.pair.service.QuizService;
import com.pair.service.UserService;
import com.pair.util.AsyncRegistrationHelper;
import javafx.application.Platform;
import javafx.concurrent.Task;
import java.io.IOException;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Toggle;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
/**
*
*
* <p> Panel
*/
public class MainWindow extends BorderPane {
private final UserService userService;
private final QuizService quizService;
private User currentUser;
private final UserService userService;
private final QuizService quizService;
private User currentUser;
private Panel currentPanel;
private final Stage primaryStage;
private Panel currentPanel;
/**
*
*
* @param primaryStage JavaFX
* @throws IOException IO
*/
public MainWindow(Stage primaryStage) throws IOException {
userService = new UserService();
quizService = new QuizService(new FileIOService(), userService);
showStartPage();
}
public MainWindow(Stage primaryStage) throws IOException {
userService = new UserService();
quizService = new QuizService(new FileIOService(), userService);
this.primaryStage = primaryStage;
showStartPage();
}
/**
*
*
* @param stage JavaFX
* @throws IOException IO
*/
public static void start(Stage stage) throws IOException {
MainWindow mainWindow = new MainWindow(stage);
Scene scene = new Scene(mainWindow, 800, 600);
stage.setScene(scene);
stage.setTitle("中小学数学答题系统");
stage.show();
}
private void showStartPage() {
StartPage startPage = new StartPage(() -> navigateTo(Panel.LOGIN));
this.setCenter(startPage);
currentPanel = Panel.START;
}
private void showStartPage() {
StartPage startPage = new StartPage(() -> navigateTo(Panel.LOGIN));
this.setCenter(startPage);
currentPanel = Panel.START;
}
private void navigateTo(Panel target) {
switch (target) {
case START -> showStartPage();
case LOGIN -> initLoginPage();
case REGISTER -> initRegisterPage();
case INF_GEN -> initInfGenPage();
case PASSWORDMODIFY -> initPasswordModifyPage();
case QUIZ -> initQuizPage();
case RESULT -> initResultPage();
}
currentPanel = target;
private void navigateTo(Panel target) {
switch (target) {
case START -> showStartPage();
case LOGIN -> initLoginPage();
case REGISTER -> initRegisterPage();
case INF_GEN -> initInfGenPage();
case PASSWORDMODIFY -> initPasswordModifyPage();
case QUIZ -> initQuizPage();
case RESULT -> initResultPage();
}
currentPanel = target;
}
// 封装登录页面初始化逻辑
private void initLoginPage() {
LoginPage loginPage = new LoginPage(() -> navigateTo(Panel.START));
// 注册链接跳转
loginPage.getRegisterLink().setOnAction(e -> navigateTo(Panel.REGISTER));
// 登录按钮点击事件(绑定封装的登录处理方法)
loginPage.getLoginButton().setOnAction(e -> handleLoginAction(loginPage));
this.setCenter(loginPage);
}
// 封装登录页面初始化逻辑
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();
// 封装登录核心逻辑从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());
}
if (username.isEmpty()) {
NavigablePanel.showErrorAlert("输入错误", "请输入用户名");
return;
}
// 其他页面的初始化方法(保持原有逻辑,统一格式)
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);
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();
private void handleSendCodeAction(RegisterPage registerPage) {
String email = registerPage.getEmailField().getText().trim();
Button btn = registerPage.getSendCodeButton();
Button btn = registerPage.getSendCodeButton();
AsyncRegistrationHelper.sendRegistrationCode(
email,
userService,
() -> {
btn.setDisable(true);
btn.setText("发送中...");
},
text -> btn.setText(text), // 倒计时更新
() -> {
btn.setText("获取注册码");
btn.setDisable(false);
},
msg -> NavigablePanel.showInfoAlert("成功", msg),
msg -> NavigablePanel.showErrorAlert("获取注册码失败", msg)
);
}
AsyncRegistrationHelper.sendRegistrationCode(
email,
userService,
() -> {
btn.setDisable(true);
btn.setText("发送中...");
},
btn::setText, // 倒计时更新
() -> {
btn.setText("获取注册码");
btn.setDisable(false);
},
msg -> NavigablePanel.showInfoAlert("成功", msg),
msg -> NavigablePanel.showErrorAlert("获取注册码失败", msg));
}
private void handleRegisterAction(RegisterPage registerPage) {
String email = registerPage.getEmailField().getText().trim();
String code = registerPage.getCodeField().getText().trim();
String password = registerPage.getPasswordField().getText().trim();
String confirmPassword = registerPage.getConfirmPasswordField().getText().trim();
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());
}
if (password.isEmpty()) {
NavigablePanel.showErrorAlert("密码错误", "密码不能为空");
return;
}
if (confirmPassword.isEmpty()) {
NavigablePanel.showErrorAlert("密码错误", "请重复密码");
return;
}
if (!password.equals(confirmPassword)) {
NavigablePanel.showErrorAlert("密码错误", "两次密码不同");
return;
}
try {
this.currentUser = userService.register(password, email, code);
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("注册失败", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void initInfGenPage() {
User currentUser = userService.getCurrentUser();
InfGenPage infGenPage = new InfGenPage(() -> navigateTo(Panel.LOGIN),
currentUser.getUsername(),
currentUser.getEmail(),
currentUser.getGrade(),
currentUser.getTotalQuizzes(),
currentUser.getAverageScore());
infGenPage.getGenerateButton().setOnAction(e -> {
int count = infGenPage.getQuestionCountSpinner().getValue();
try {
userService.updateGrade(userService.getCurrentUser(), infGenPage.getGradeChoice().getValue());
} catch (IllegalArgumentException ex) {
private void initInfGenPage() {
User currentUser = userService.getCurrentUser();
InfGenPage infGenPage =
new InfGenPage(
() -> navigateTo(Panel.LOGIN),
currentUser.getUsername(),
currentUser.getEmail(),
currentUser.getGrade(),
currentUser.getTotalQuizzes(),
currentUser.getAverageScore());
infGenPage
.getGenerateButton()
.setOnAction(
e -> {
int count = infGenPage.getQuestionCountSpinner().getValue();
try {
userService.updateGrade(
userService.getCurrentUser(), infGenPage.getGradeChoice().getValue());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("学段错误", ex.getMessage());
return;
} catch (IOException ex) {
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
if (count < 10 || count > 30) {
}
if (count < 10 || count > 30) {
NavigablePanel.showErrorAlert("输入错误", "题数必须为10-30");
return;
}
quizService.setAnswerNumber(count);
navigateTo(Panel.QUIZ);
});
infGenPage.getPasswordModifyButton().setOnAction(e -> {
navigateTo(Panel.PASSWORDMODIFY);
});
infGenPage.getModifyUsernameButton().setOnAction(e -> handleUsernameModifyAction(infGenPage));
infGenPage.getModifyEmailButton().setOnAction(e -> handleEmailModifyAction(infGenPage));
}
quizService.setAnswerNumber(count);
navigateTo(Panel.QUIZ);
});
infGenPage.getPasswordModifyButton().setOnAction(e -> navigateTo(Panel.PASSWORDMODIFY));
infGenPage.getModifyUsernameButton().setOnAction(e -> handleUsernameModifyAction(infGenPage));
infGenPage.getModifyEmailButton().setOnAction(e -> handleEmailModifyAction(infGenPage));
this.setCenter(infGenPage);
}
private void handleUsernameModifyAction(InfGenPage infGenPage) {
try {
userService.updateUsername(userService.getCurrentUser(), infGenPage.getUsernameField().getText().trim());
NavigablePanel.showInfoAlert("成功", "用户名修改成功");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("用户名错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void handleEmailModifyAction(InfGenPage infGenPage) {
try {
userService.updateEmail(userService.getCurrentUser(), infGenPage.getEmailField().getText().trim());
NavigablePanel.showInfoAlert("成功","邮箱修改成功");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
this.setCenter(infGenPage);
}
private void handleUsernameModifyAction(InfGenPage infGenPage) {
try {
userService.updateUsername(
userService.getCurrentUser(), infGenPage.getUsernameField().getText().trim());
NavigablePanel.showInfoAlert("成功", "用户名修改成功");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("用户名错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void initPasswordModifyPage() {
PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
this.setCenter(pwdPage);
private void handleEmailModifyAction(InfGenPage infGenPage) {
try {
userService.updateEmail(
userService.getCurrentUser(), infGenPage.getEmailField().getText().trim());
NavigablePanel.showInfoAlert("成功", "邮箱修改成功");
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("邮箱错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
}
}
private void handlePasswordModify(PasswordModifyPage pwdPage) {
String oldPassword = pwdPage.getOldPasswordField().getText().trim();
String newPassword = pwdPage.getNewPasswordField().getText().trim();
String confirmPassword = pwdPage.getConfirmNewPasswordField().getText().trim();
private void initPasswordModifyPage() {
PasswordModifyPage pwdPage = new PasswordModifyPage(() -> navigateTo(Panel.INF_GEN));
pwdPage.getModifyButton().setOnAction(e -> handlePasswordModify(pwdPage));
this.setCenter(pwdPage);
}
try {
userService.changePassword(userService.getCurrentUser(),oldPassword, newPassword, confirmPassword);
NavigablePanel.showInfoAlert("成功", "密码修改成功");
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
return;
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
return;
}
private void handlePasswordModify(PasswordModifyPage pwdPage) {
String oldPassword = pwdPage.getOldPasswordField().getText().trim();
String newPassword = pwdPage.getNewPasswordField().getText().trim();
String confirmPassword = pwdPage.getConfirmNewPasswordField().getText().trim();
try {
userService.changePassword(
userService.getCurrentUser(), oldPassword, newPassword, confirmPassword);
NavigablePanel.showInfoAlert("成功", "密码修改成功");
navigateTo(Panel.INF_GEN);
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("修改失败", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误 ", ex.getMessage());
}
}
private void initQuizPage() {
try {
quizService.startNewQuiz(currentUser, quizService.getAnswerNumber());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("生成题目错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
navigateTo(Panel.INF_GEN);
return;
}
QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN), quizService);
quizPage.setTotalQuestions(quizService.getTotalQuestions());
quizPage.goToQuestion(0);
quizPage.getPrevButton().setOnAction(e -> {
if (quizService.previousQuestion()) {
private void initQuizPage() {
try {
quizService.startNewQuiz(currentUser, quizService.getAnswerNumber());
} catch (IllegalArgumentException ex) {
NavigablePanel.showErrorAlert("生成题目错误", ex.getMessage());
} catch (IOException ex) {
NavigablePanel.showErrorAlert("系统错误", ex.getMessage());
navigateTo(Panel.INF_GEN);
return;
}
QuizPage quizPage = new QuizPage(() -> navigateTo(Panel.INF_GEN), quizService);
quizPage.setTotalQuestions(quizService.getTotalQuestions());
quizPage.goToQuestion(0);
quizPage
.getPrevButton()
.setOnAction(
e -> {
if (quizService.previousQuestion()) {
quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
}
});
quizPage.getNextButton().setOnAction(e -> {
// 获取用户当前选择(可能为 null
Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
}
});
quizPage
.getNextButton()
.setOnAction(
e -> {
// 获取用户当前选择(可能为 null
Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
// 如果有选择,则提交答案
if (selected != null) {
// 如果有选择,则提交答案
if (selected != null) {
int selectedIndex = -1;
for (int i = 0; i < 4; i++) {
if (quizPage.getOptions()[i] == selected) {
selectedIndex = i;
break;
}
if (quizPage.getOptions()[i] == selected) {
selectedIndex = i;
break;
}
}
quizService.submitCurrentAnswer(selectedIndex);
}
// 无论是否选择,都尝试跳转到下一题
if (quizService.nextQuestion()) {
}
// 无论是否选择,都尝试跳转到下一题
if (quizService.nextQuestion()) {
quizPage.goToQuestion(quizService.getCurrentQuestionIndex());
} else {
} else {
// 已是最后一题,显示“交卷”按钮
quizPage.getSubmitButton().setVisible(true);
quizPage.getNextButton().setVisible(false);
}
});
quizPage.getSubmitButton().setOnAction(e -> {
Toggle selected = quizPage.getOptionGroup().getSelectedToggle();
if (selected != null) {
}
});
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;
}
if (quizPage.getOptions()[i] == selected) {
selectedIndex = i;
break;
}
}
quizService.submitCurrentAnswer(selectedIndex);
}
// 直接交卷,不强制所有题都作答
navigateTo(Panel.RESULT);
});
}
// 直接交卷,不强制所有题都作答
navigateTo(Panel.RESULT);
});
this.setCenter(quizPage);
}
this.setCenter(quizPage);
}
private void initResultPage() {
ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN), quizService);
resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START));
resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN));
resultPage.updateResult();
this.setCenter(resultPage);
}
private void initResultPage() {
ResultPage resultPage = new ResultPage(() -> navigateTo(Panel.INF_GEN), quizService);
resultPage.getExitButton().setOnAction(e -> navigateTo(Panel.START));
resultPage.getContinueButton().setOnAction(e -> navigateTo(Panel.INF_GEN));
resultPage.updateResult();
this.setCenter(resultPage);
}
// getter和启动方法保持不变
public Panel getCurrentPanel() {
return currentPanel;
}
public static void start(Stage stage) throws IOException {
MainWindow mainWindow = new MainWindow(stage);
Scene scene = new Scene(mainWindow, 800, 600);
stage.setScene(scene);
stage.setTitle("中小学数学答题系统");
stage.show();
}
}
/**
*
*
* @return
*/
public Panel getCurrentPanel() {
return currentPanel;
}
}

@ -1,4 +1,3 @@
// com/ui/NavigablePanel.java
package com.pair.ui;
import javafx.application.Platform;
@ -12,61 +11,59 @@ 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(50, 50);
backButton.setFont(Font.font(UIConstants.FONT_FAMILY, 18));
backButton.setStyle(
"-fx-background-radius: 50; "
+ "-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "E6; "
+ "-fx-text-fill: white; "
+ "-fx-font-weight: bold; "
+ "-fx-cursor: hand; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 2);");
public NavigablePanel(Runnable onBack) {
Button backButton = new Button("←");
backButton.setOnAction(e -> onBack.run());
backButton.setPrefSize(50, 50); // 调整为正方形按钮
backButton.setFont(Font.font(UIConstants.FONT_FAMILY, 18)); // 增大字体
/* 创建悬浮按钮样式 - 半透明圆形按钮 */
backButton.setStyle(
"-fx-background-radius: 50; " +
"-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "E6; " + // 添加透明度
"-fx-text-fill: white; " +
"-fx-font-weight: bold; " +
"-fx-cursor: hand; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 2);"); // 添加阴影效果
backButton.setOnMouseEntered(
e -> backButton.setStyle(backButton.getStyle().replace("E6;", "FF;")));
backButton.setOnMouseExited(
e -> backButton.setStyle(backButton.getStyle().replace("FF;", "E6;")));
/* 添加悬停效果 */
backButton.setOnMouseEntered(e ->
backButton.setStyle(backButton.getStyle().replace("E6;", "FF;")) // 鼠标悬停时不透明
);
backButton.setOnMouseExited(e ->
backButton.setStyle(backButton.getStyle().replace("FF;", "E6;")) // 鼠标离开时半透明
);
HBox leftBar = new HBox(10);
leftBar.setPadding(new Insets(20, 0, 0, 20));
leftBar.setAlignment(Pos.TOP_LEFT);
leftBar.getChildren().add(backButton);
/* 将返回按钮悬浮在左上角,带有边距 */
HBox leftBar = new HBox(10);
leftBar.setPadding(new Insets(20, 0, 0, 20)); // 顶部20px左侧20px边距
leftBar.setAlignment(Pos.TOP_LEFT);
leftBar.getChildren().add(backButton);
this.setLeft(leftBar);
}
this.setLeft(leftBar);
}
protected static void showErrorAlert(String title, String message) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
protected static void showErrorAlert(String title, String message) {
Platform.runLater(
() -> {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
});
}
}
protected static void showInfoAlert(String title, String message) {
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
protected static void showInfoAlert(String title, String message) {
Platform.runLater(
() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
});
}
}
protected final void initializeContent() {
buildContent();
}
// 子类构造函数中调用
protected final void initializeContent() {
buildContent();
}
protected abstract void buildContent();
}
protected abstract void buildContent();
}

@ -1,11 +1,11 @@
package com.pair.ui;
public enum Panel {
START, // 开始页面
LOGIN, // 登录页面
REGISTER, // 注册页面
INF_GEN, // 个人信息+生成题目页面
PASSWORDMODIFY, // 修改密码页面
QUIZ, // 答题页面
RESULT // 得分页面
START, // 开始页面
LOGIN, // 登录页面
REGISTER, // 注册页面
INF_GEN, // 个人信息+生成题目页面
PASSWORDMODIFY, // 修改密码页面
QUIZ, // 答题页面
RESULT // 得分页面
}

@ -1,55 +1,68 @@
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class PasswordModifyPage extends NavigablePanel {
private final PasswordField oldPasswordField = new PasswordField();
private final PasswordField newPasswordField = new PasswordField();
private final PasswordField confirmNewPasswordField = new PasswordField();
private final Button modifyButton = new Button("修改");
public PasswordModifyPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("修改密码");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_WARNING) + "; " + // 使用警告色(橙色)突出显示
private final PasswordField oldPasswordField = new PasswordField();
private final PasswordField newPasswordField = new PasswordField();
private final PasswordField confirmNewPasswordField = new PasswordField();
private final Button modifyButton = new Button("修改");
public PasswordModifyPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
VBox card = StyleHelper.createMediumCard();
Label title = new Label("修改密码");
title.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
title.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_WARNING)
+ "; "
+ // 使用警告色(橙色)突出显示
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
oldPasswordField.setPromptText("旧密码");
StyleHelper.stylePasswordField(oldPasswordField);
oldPasswordField.setPromptText("旧密码");
StyleHelper.stylePasswordField(oldPasswordField);
newPasswordField.setPromptText("新密码6-10位");
StyleHelper.stylePasswordField(newPasswordField);
confirmNewPasswordField.setPromptText("确认新密码");
StyleHelper.stylePasswordField(confirmNewPasswordField);
StyleHelper.stylePrimaryButton(modifyButton);
newPasswordField.setPromptText("新密码6-10位");
StyleHelper.stylePasswordField(newPasswordField);
card.getChildren()
.addAll(title, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton);
setCenter(card);
}
confirmNewPasswordField.setPromptText("确认新密码");
StyleHelper.stylePasswordField(confirmNewPasswordField);
public PasswordField getOldPasswordField() {
return oldPasswordField;
}
// 使用统一的按钮样式
StyleHelper.stylePrimaryButton(modifyButton);
public PasswordField getNewPasswordField() {
return newPasswordField;
}
card.getChildren().addAll(title, oldPasswordField, newPasswordField, confirmNewPasswordField, modifyButton);
setCenter(card);
}
public PasswordField getConfirmNewPasswordField() {
return confirmNewPasswordField;
}
public PasswordField getOldPasswordField() { return oldPasswordField; }
public PasswordField getNewPasswordField() { return newPasswordField; }
public PasswordField getConfirmNewPasswordField() { return confirmNewPasswordField; }
public Button getModifyButton() { return modifyButton; }
}
public Button getModifyButton() {
return modifyButton;
}
}

@ -2,6 +2,7 @@ package com.pair.ui;
import com.pair.model.ChoiceQuestion;
import com.pair.service.QuizService;
import java.util.List;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
@ -10,311 +11,343 @@ import javafx.scene.layout.*;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import java.util.List;
public class QuizPage extends NavigablePanel {
private final QuizService quizService;
private final Label titleLabel = new Label("中小学数学答题系统");
private final Label progressLabel = new Label("完成 0/10");
private final Label questionLabel = new Label("题目加载中...");
private final ToggleGroup optionGroup = new ToggleGroup();
private final RadioButton[] options = new RadioButton[4];
private final Button prevButton = new Button("上一题");
private final Button nextButton = new Button("下一题");
private final Button submitButton = new Button("交卷");
private final GridPane questionNavGrid = new GridPane();
private int totalQuestions = 10;
private int currentQuestionIndex = 0;
public QuizPage(Runnable onBack, QuizService quizService) {
super(onBack);
this.quizService = quizService;
initializeContent();
}
@Override
protected void buildContent() {
setPadding(new Insets(20));
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
/* 顶部标题栏卡片 整体下移 20px */
VBox topWrap = new VBox(createTopBar());
topWrap.setPadding(new Insets(0, 0, 20, 0));
setTop(topWrap);
/* 中部:左右两卡片 调整布局创造参差美感 */
// 创建主内容区域使用BorderPane实现更灵活的布局
BorderPane mainContent = new BorderPane();
/* 左侧题目卡片 - 占据主要区域 */
Pane leftCard = createLeftCard();
mainContent.setCenter(leftCard);
/* 右侧导航区域 - 放置题目导航 */
VBox rightArea = new VBox(20);
rightArea.setAlignment(Pos.TOP_CENTER);
rightArea.setPadding(new Insets(0, 0, 100, 0)); // 增加底部内边距,让导航下移
// 创建题目导航标题
Label navTitle = new Label("题目导航");
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
// 创建题目导航网格
initQuestionNavGrid();
// 添加标题和导航到右侧区域
rightArea.getChildren().addAll(navTitle, questionNavGrid);
mainContent.setRight(rightArea);
/* 将主内容区域添加到中心,减少上移间距 */
VBox centerWrap = new VBox(mainContent);
centerWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少上移间距
setCenter(centerWrap);
/* 底部按钮栏卡片 位置上移,更接近导航 */
VBox bottomWrap = new VBox(createBottomCard());
bottomWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少间距,让导航更接近底部
setBottom(bottomWrap);
}
/* ---------------- 私有构造区域 ---------------- */
private HBox createTopBar() {
HBox bar = new HBox(20);
bar.setAlignment(Pos.CENTER_LEFT);
bar.setPadding(UIConstants.CARD_PADDING);
bar.setStyle(UIConstants.CARD_STYLE);
bar.setEffect(UIConstants.CARD_SHADOW);
// 增强标题视觉效果
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
titleLabel.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
private final QuizService quizService;
private final Label titleLabel = new Label("中小学数学答题系统");
private final Label progressLabel = new Label("完成 0/10");
private final Label questionLabel = new Label("题目加载中...");
private final ToggleGroup optionGroup = new ToggleGroup();
private final RadioButton[] options = new RadioButton[4];
private final Button prevButton = new Button("上一题");
private final Button nextButton = new Button("下一题");
private final Button submitButton = new Button("交卷");
private final GridPane questionNavGrid = new GridPane();
private int totalQuestions = 10;
private int currentQuestionIndex = 0;
public QuizPage(Runnable onBack, QuizService quizService) {
super(onBack);
this.quizService = quizService;
initializeContent();
}
@Override
protected void buildContent() {
setPadding(new Insets(20));
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
/* 顶部标题栏卡片 整体下移 20px */
VBox topWrap = new VBox(createTopBar());
topWrap.setPadding(new Insets(0, 0, 20, 0));
setTop(topWrap);
/* 中部:左右两卡片 调整布局创造参差美感 */
// 创建主内容区域使用BorderPane实现更灵活的布局
BorderPane mainContent = new BorderPane();
/* 左侧题目卡片 - 占据主要区域 */
Pane leftCard = createLeftCard();
mainContent.setCenter(leftCard);
/* 右侧导航区域 - 放置题目导航 */
VBox rightArea = new VBox(20);
rightArea.setAlignment(Pos.TOP_CENTER);
rightArea.setPadding(new Insets(0, 0, 100, 0)); // 增加底部内边距,让导航下移
// 创建题目导航标题
Label navTitle = new Label("题目导航");
navTitle.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
// 创建题目导航网格
initQuestionNavGrid();
// 添加标题和导航到右侧区域
rightArea.getChildren().addAll(navTitle, questionNavGrid);
mainContent.setRight(rightArea);
/* 将主内容区域添加到中心,减少上移间距 */
VBox centerWrap = new VBox(mainContent);
centerWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少上移间距
setCenter(centerWrap);
/* 底部按钮栏卡片 位置上移,更接近导航 */
VBox bottomWrap = new VBox(createBottomCard());
bottomWrap.setPadding(new Insets(10, 0, 0, 0)); // 减少间距,让导航更接近底部
setBottom(bottomWrap);
}
/* ---------------- 私有构造区域 ---------------- */
private HBox createTopBar() {
HBox bar = new HBox(20);
bar.setAlignment(Pos.CENTER_LEFT);
bar.setPadding(UIConstants.CARD_PADDING);
bar.setStyle(UIConstants.CARD_STYLE);
bar.setEffect(UIConstants.CARD_SHADOW);
// 增强标题视觉效果
titleLabel.setFont(
Font.font(
UIConstants.FONT_FAMILY,
FontWeight.BOLD,
UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
titleLabel.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
/* 放大、加粗、主色显示进度 */
progressLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
progressLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
bar.getChildren().addAll(titleLabel, spacer, progressLabel);
return bar;
}
private Pane createLeftCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_LEFT);
card.setMaxWidth(680); // 稍宽一点
card.setMinHeight(450); // 增加最小高度,让题目区域更大
card.setStyle(card.getStyle() + "-fx-padding: 30;"); // 增加内边距,让内容更舒展
questionLabel.setWrapText(true);
questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUB_TITLE_FONT_SIZE));
questionLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
VBox optionsBox = new VBox(15); // 增加选项间距
optionsBox.setAlignment(Pos.CENTER_LEFT);
optionsBox.setPadding(new Insets(20, 0, 0, 0)); // 增加选项区域的上边距
for (int i = 0; i < 4; i++) {
options[i] = new RadioButton("选项 " + (char) ('A' + i));
options[i].setToggleGroup(optionGroup);
options[i].setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
// 添加选项变化监听器,实现实时保存
final int optionIndex = i;
options[i].selectedProperty().addListener((obs, wasSelected, isSelected) -> {
/* 放大、加粗、主色显示进度 */
progressLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, 18));
progressLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
Region spacer = new Region();
HBox.setHgrow(spacer, Priority.ALWAYS);
bar.getChildren().addAll(titleLabel, spacer, progressLabel);
return bar;
}
private Pane createLeftCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_LEFT);
card.setMaxWidth(680); // 稍宽一点
card.setMinHeight(450); // 增加最小高度,让题目区域更大
card.setStyle(card.getStyle() + "-fx-padding: 30;"); // 增加内边距,让内容更舒展
questionLabel.setWrapText(true);
questionLabel.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SUB_TITLE_FONT_SIZE));
questionLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT) + ";");
VBox optionsBox = new VBox(15); // 增加选项间距
optionsBox.setAlignment(Pos.CENTER_LEFT);
optionsBox.setPadding(new Insets(20, 0, 0, 0)); // 增加选项区域的上边距
for (int i = 0; i < 4; i++) {
options[i] = new RadioButton("选项 " + (char) ('A' + i));
options[i].setToggleGroup(optionGroup);
options[i].setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.BODY_FONT_SIZE));
// 添加选项变化监听器,实现实时保存
final int optionIndex = i;
options[i]
.selectedProperty()
.addListener(
(obs, wasSelected, isSelected) -> {
if (isSelected) {
saveCurrentAnswer(optionIndex);
saveCurrentAnswer(optionIndex);
}
});
optionsBox.getChildren().add(options[i]);
}
card.getChildren().addAll(questionLabel, optionsBox);
return card;
});
optionsBox.getChildren().add(options[i]);
}
private Pane createRightCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_CENTER);
card.setMaxWidth(300);
card.setMinHeight(350); // 设置比左侧稍小的高度,创造参差美感
card.setStyle(card.getStyle() + "-fx-padding: 20;"); // 增加内边距
Label navTitle = new Label("题目导航");
navTitle.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
navTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
initQuestionNavGrid();
card.getChildren().addAll(navTitle, questionNavGrid);
return card;
}
private HBox createBottomCard() {
HBox card = new HBox(20);
card.setAlignment(Pos.CENTER);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
// 使用统一的按钮样式
StyleHelper.styleButton(prevButton); // 使用默认样式
StyleHelper.styleButton(nextButton); // 使用默认样式
StyleHelper.styleErrorButton(submitButton); // 提交按钮使用错误样式(红色)突出显示
// 为上一题按钮添加保存逻辑
prevButton.setOnAction(e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex > 0) {
goToQuestion(currentQuestionIndex - 1);
}
card.getChildren().addAll(questionLabel, optionsBox);
return card;
}
private Pane createRightCard() {
VBox card = StyleHelper.createCard();
card.setAlignment(Pos.TOP_CENTER);
card.setMaxWidth(300);
card.setMinHeight(350); // 设置比左侧稍小的高度,创造参差美感
card.setStyle(card.getStyle() + "-fx-padding: 20;"); // 增加内边距
Label navTitle = new Label("题目导航");
navTitle.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.BODY_FONT_SIZE));
navTitle.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
initQuestionNavGrid();
card.getChildren().addAll(navTitle, questionNavGrid);
return card;
}
private HBox createBottomCard() {
HBox card = new HBox(20);
card.setAlignment(Pos.CENTER);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
StyleHelper.styleButton(prevButton);
StyleHelper.styleButton(nextButton);
StyleHelper.styleErrorButton(submitButton);
// 为上一题按钮添加保存逻辑
prevButton.setOnAction(
e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex > 0) {
goToQuestion(currentQuestionIndex - 1);
}
});
// 为下一题按钮添加保存逻辑
nextButton.setOnAction(e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex < totalQuestions - 1) {
goToQuestion(currentQuestionIndex + 1);
}
// 为下一题按钮添加保存逻辑
nextButton.setOnAction(
e -> {
saveCurrentAnswer(); // 保存当前答案
if (currentQuestionIndex < totalQuestions - 1) {
goToQuestion(currentQuestionIndex + 1);
}
});
// 提交按钮不需要保存,直接交卷
// submitButton的点击事件由外部处理
card.getChildren().addAll(prevButton, nextButton, submitButton);
return card;
// 提交按钮不需要保存,直接交卷
// submitButton的点击事件由外部处理
card.getChildren().addAll(prevButton, nextButton, submitButton);
return card;
}
/* ---------------- 导航网格 ---------------- */
private void initQuestionNavGrid() {
questionNavGrid.getChildren().clear();
questionNavGrid.setHgap(8);
questionNavGrid.setVgap(8);
questionNavGrid.setAlignment(Pos.CENTER);
int cols = 5;
for (int i = 0; i < totalQuestions; i++) {
int row = i / cols;
int col = i % cols;
Button btn = new Button(String.valueOf(i + 1));
btn.setPrefSize(52, 44); // 稍大一点的按钮
btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
btn.setStyle(getNavBtnStyle(i));
final int index = i;
btn.setOnAction(e -> goToQuestion(index));
questionNavGrid.add(btn, col, row);
}
/* ---------------- 导航网格 ---------------- */
private void initQuestionNavGrid() {
questionNavGrid.getChildren().clear();
questionNavGrid.setHgap(8); // 增加水平间距
questionNavGrid.setVgap(8); // 增加垂直间距
questionNavGrid.setAlignment(Pos.CENTER);
int cols = 5;
for (int i = 0; i < totalQuestions; i++) {
int row = i / cols;
int col = i % cols;
Button btn = new Button(String.valueOf(i + 1));
btn.setPrefSize(52, 44); // 稍大一点的按钮
btn.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
btn.setStyle(getNavBtnStyle(i));
final int index = i;
btn.setOnAction(e -> goToQuestion(index));
questionNavGrid.add(btn, col, row);
}
private String getNavBtnStyle(int index) {
if (index == currentQuestionIndex)
return "-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; -fx-text-fill: white; -fx-font-weight: bold;";
if (quizService.isAnswered(index))
return "-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_SECONDARY)
+ "; -fx-text-fill: white;";
return "-fx-background-color: "
+ UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB)
+ "; -fx-text-fill: white;";
}
/* ---------------- 答案保存功能 ---------------- */
private void saveCurrentAnswer(int selectedOptionIndex) {
// 保存当前选择的答案
quizService.submitAnswer(currentQuestionIndex, selectedOptionIndex);
// 更新进度显示
updateProgress();
// 刷新导航按钮状态
refreshNavButtons();
}
private void saveCurrentAnswer() {
// 获取当前选中的选项
Toggle selectedToggle = optionGroup.getSelectedToggle();
if (selectedToggle != null) {
for (int i = 0; i < 4; i++) {
if (options[i] == selectedToggle) {
saveCurrentAnswer(i);
break;
}
}
} else {
// 如果没有选中任何选项,清除当前答案
// QuizService没有直接的清除方法我们保存一个无效索引来表示清除
// 或者使用submitCurrentAnswer(-1)来表示清除(如果支持)
// 暂时不处理清除逻辑,保持原有状态
updateProgress();
refreshNavButtons();
}
private String getNavBtnStyle(int index) {
if (index == currentQuestionIndex)
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) +
"; -fx-text-fill: white; -fx-font-weight: bold;";
if (quizService.isAnswered(index))
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_SECONDARY) +
"; -fx-text-fill: white;";
return "-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) +
"; -fx-text-fill: white;";
}
/* ---------------- public 供外部调用 ---------------- */
public void goToQuestion(int index) {
// 在切换题目之前,先保存当前题目的答案
saveCurrentAnswer();
currentQuestionIndex = index;
quizService.goToQuestion(index);
updateProgress();
refreshNavButtons();
loadQuestion();
updateBottomBtnVisibility();
}
private void updateProgress() {
int answered = quizService.getAnsweredCount();
progressLabel.setText("完成 " + answered + "/" + totalQuestions + " 题");
}
private void refreshNavButtons() {
for (Node node : questionNavGrid.getChildren()) {
if (node instanceof Button btn) {
int idx = Integer.parseInt(btn.getText()) - 1;
btn.setStyle(getNavBtnStyle(idx));
}
}
/* ---------------- 答案保存功能 ---------------- */
private void saveCurrentAnswer(int selectedOptionIndex) {
// 保存当前选择的答案
quizService.submitAnswer(currentQuestionIndex, selectedOptionIndex);
// 更新进度显示
updateProgress();
// 刷新导航按钮状态
refreshNavButtons();
}
private void updateBottomBtnVisibility() {
boolean isLast = currentQuestionIndex == totalQuestions - 1;
nextButton.setVisible(!isLast);
submitButton.setVisible(isLast);
}
private void loadQuestion() {
ChoiceQuestion q = quizService.getQuestion(currentQuestionIndex);
if (q == null) return;
questionLabel.setText("第 " + (currentQuestionIndex + 1) + " 题:\n" + q.getQuestionText());
List<?> opts = q.getOptions();
for (int i = 0; i < 4; i++) {
String txt = opts.get(i) == null ? "未知" : opts.get(i).toString();
options[i].setText((char) ('A' + i) + ". " + txt);
}
private void saveCurrentAnswer() {
// 获取当前选中的选项
Toggle selectedToggle = optionGroup.getSelectedToggle();
if (selectedToggle != null) {
for (int i = 0; i < 4; i++) {
if (options[i] == selectedToggle) {
saveCurrentAnswer(i);
break;
}
}
} else {
// 如果没有选中任何选项,清除当前答案
// QuizService没有直接的清除方法我们保存一个无效索引来表示清除
// 或者使用submitCurrentAnswer(-1)来表示清除(如果支持)
// 暂时不处理清除逻辑,保持原有状态
updateProgress();
refreshNavButtons();
}
}
/* ---------------- public 供外部调用 ---------------- */
public void goToQuestion(int index) {
// 在切换题目之前,先保存当前题目的答案
saveCurrentAnswer();
currentQuestionIndex = index;
quizService.goToQuestion(index);
updateProgress();
refreshNavButtons();
loadQuestion();
updateBottomBtnVisibility();
}
private void updateProgress() {
int answered = quizService.getAnsweredCount();
progressLabel.setText("完成 " + answered + "/" + totalQuestions + " 题");
}
private void refreshNavButtons() {
for (Node node : questionNavGrid.getChildren()) {
if (node instanceof Button) {
Button btn = (Button) node;
int idx = Integer.parseInt(btn.getText()) - 1;
btn.setStyle(getNavBtnStyle(idx));
}
}
}
private void updateBottomBtnVisibility() {
boolean isLast = currentQuestionIndex == totalQuestions - 1;
nextButton.setVisible(!isLast);
submitButton.setVisible(isLast);
}
private void loadQuestion() {
ChoiceQuestion q = quizService.getQuestion(currentQuestionIndex);
if (q == null) return;
questionLabel.setText("第 " + (currentQuestionIndex + 1) + " 题:\n" + q.getQuestionText());
List<?> opts = q.getOptions();
for (int i = 0; i < 4; i++) {
String txt = opts.get(i) == null ? "未知" : opts.get(i).toString();
options[i].setText((char) ('A' + i) + ". " + txt);
}
Integer ans = quizService.getUserAnswer(currentQuestionIndex);
optionGroup.selectToggle(ans == null ? null : options[ans]);
}
/* ---------------- Getters ---------------- */
public Label getProgressLabel() { return progressLabel; }
public Label getQuestionLabel() { return questionLabel; }
public RadioButton[] getOptions() { return options; }
public ToggleGroup getOptionGroup() { return optionGroup; }
public Button getNextButton() { return nextButton; }
public Button getPrevButton() { return prevButton; }
public Button getSubmitButton() { return submitButton; }
public void setTotalQuestions(int total) {
this.totalQuestions = total;
initQuestionNavGrid();
updateProgress();
}
}
Integer ans = quizService.getUserAnswer(currentQuestionIndex);
optionGroup.selectToggle(ans == null ? null : options[ans]);
}
/* ---------------- Getters ---------------- */
public Label getProgressLabel() {
return progressLabel;
}
public Label getQuestionLabel() {
return questionLabel;
}
public RadioButton[] getOptions() {
return options;
}
public ToggleGroup getOptionGroup() {
return optionGroup;
}
public Button getNextButton() {
return nextButton;
}
public Button getPrevButton() {
return prevButton;
}
public Button getSubmitButton() {
return submitButton;
}
public void setTotalQuestions(int total) {
this.totalQuestions = total;
initQuestionNavGrid();
updateProgress();
}
}

@ -1,64 +1,89 @@
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.*;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.PasswordField;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
public class RegisterPage extends NavigablePanel {
private final TextField emailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final PasswordField confirmPasswordField = new PasswordField();
private final TextField codeField = new TextField();
private final Button sendCodeButton = new Button("获取注册码");
private final Button registerButton = new Button("注册");
public RegisterPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
// 使用新的统一卡片样式
VBox card = StyleHelper.createMediumCard();
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE)); // 使用更大的字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
private final TextField emailField = new TextField();
private final PasswordField passwordField = new PasswordField();
private final PasswordField confirmPasswordField = new PasswordField();
private final TextField codeField = new TextField();
private final Button sendCodeButton = new Button("获取注册码");
private final Button registerButton = new Button("注册");
public RegisterPage(Runnable onBack) {
super(onBack);
initializeContent();
}
@Override
protected void buildContent() {
VBox card = StyleHelper.createMediumCard();
Label title = new Label("中小学数学答题系统");
title.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
title.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);" // 添加阴影效果
);
// 使用统一的输入框样式
emailField.setPromptText("邮箱");
StyleHelper.styleInputField(emailField);
emailField.setPromptText("邮箱");
StyleHelper.styleInputField(emailField);
codeField.setPromptText("注册码");
StyleHelper.styleInputField(codeField);
passwordField.setPromptText("密码6-10位");
StyleHelper.stylePasswordField(passwordField);
confirmPasswordField.setPromptText("确认密码");
StyleHelper.stylePasswordField(confirmPasswordField);
StyleHelper.styleSecondaryButton(sendCodeButton);
StyleHelper.stylePrimaryButton(registerButton);
card.getChildren()
.addAll(
title,
emailField,
sendCodeButton,
codeField,
passwordField,
confirmPasswordField,
registerButton);
setCenter(card);
}
codeField.setPromptText("注册码");
StyleHelper.styleInputField(codeField);
public TextField getEmailField() {
return emailField;
}
passwordField.setPromptText("密码6-10位");
StyleHelper.stylePasswordField(passwordField);
public TextField getCodeField() {
return codeField;
}
confirmPasswordField.setPromptText("确认密码");
StyleHelper.stylePasswordField(confirmPasswordField);
public PasswordField getPasswordField() {
return passwordField;
}
// 使用统一的按钮样式
StyleHelper.styleSecondaryButton(sendCodeButton); // 次要按钮样式
StyleHelper.stylePrimaryButton(registerButton); // 主要按钮样式
public PasswordField getConfirmPasswordField() {
return confirmPasswordField;
}
card.getChildren().addAll(title, emailField, sendCodeButton, codeField,
passwordField, confirmPasswordField, registerButton);
setCenter(card);
}
public Button getSendCodeButton() {
return sendCodeButton;
}
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; }
}
public Button getRegisterButton() {
return registerButton;
}
}

@ -1,4 +1,3 @@
// com/pair/ui/ResultPage.java
package com.pair.ui;
import com.pair.model.QuizResult;
@ -12,57 +11,71 @@ import javafx.scene.text.FontWeight;
public class ResultPage extends NavigablePanel {
private final QuizService quizService;
private final Label resultLabel = new Label("您答对了 8/10 题得分80%");
private final Label gradeLabel = new Label("评级:优秀");
private final Button continueButton = new Button("继续答题");
private final Button exitButton = new Button("退出");
public ResultPage(Runnable onBack, QuizService quizService) {
super(onBack);
initializeContent();
this.quizService = quizService;
}
@Override
protected void buildContent() {
// 使用 StyleHelper 创建中型卡片(与 InfGenPage 一致)
VBox card = StyleHelper.createMediumCard();
card.setAlignment(Pos.CENTER); // 结果页内容居中
// 标题
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
titleLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 答题结果 - 使用大号字体 + 成功色
resultLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
resultLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + ";");
// 评级
gradeLabel.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUB_TITLE_FONT_SIZE));
gradeLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 按钮:使用 StyleHelper 统一样式
StyleHelper.stylePrimaryButton(continueButton); // 主按钮:继续答题
StyleHelper.styleSecondaryButton(exitButton); // 次要按钮:退出
// 组装
card.getChildren().addAll(titleLabel, resultLabel, gradeLabel, continueButton, exitButton);
this.setCenter(card);
this.setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
}
public void updateResult() {
QuizResult result = quizService.calculateResult();
quizService.saveQuizHistory();
resultLabel.setText(result.toString());
gradeLabel.setText(quizService.getGrade(result));
}
public Label getResultLabel() { return resultLabel; }
public Label getGradeLabel() { return gradeLabel; }
public Button getContinueButton() { return continueButton; }
public Button getExitButton() { return exitButton; }
}
private final QuizService quizService;
private final Label resultLabel = new Label("您答对了 8/10 题得分80%");
private final Label gradeLabel = new Label("评级:优秀");
private final Button continueButton = new Button("继续答题");
private final Button exitButton = new Button("退出");
public ResultPage(Runnable onBack, QuizService quizService) {
super(onBack);
initializeContent();
this.quizService = quizService;
}
@Override
protected void buildContent() {
// 使用 StyleHelper 创建中型卡片
VBox card = StyleHelper.createMediumCard();
card.setAlignment(Pos.CENTER); // 结果页内容居中
// 标题
Label titleLabel = new Label("中小学数学答题系统");
titleLabel.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.LARGE_TITLE_FONT_SIZE));
titleLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 答题结果
resultLabel.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.TITLE_FONT_SIZE));
resultLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_SUCCESS) + ";");
// 评级
gradeLabel.setFont(
Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.SUB_TITLE_FONT_SIZE));
gradeLabel.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + ";");
// 按钮:使用 StyleHelper 统一样式
StyleHelper.stylePrimaryButton(continueButton); // 主按钮:继续答题
StyleHelper.styleSecondaryButton(exitButton); // 次要按钮:退出
// 组装
card.getChildren().addAll(titleLabel, resultLabel, gradeLabel, continueButton, exitButton);
this.setCenter(card);
this.setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
}
public void updateResult() {
QuizResult result = quizService.calculateResult();
quizService.saveQuizHistory();
resultLabel.setText(result.toString());
gradeLabel.setText(quizService.getGrade(result));
}
public Label getResultLabel() {
return resultLabel;
}
public Label getGradeLabel() {
return gradeLabel;
}
public Button getContinueButton() {
return continueButton;
}
public Button getExitButton() {
return exitButton;
}
}

@ -9,30 +9,36 @@ import javafx.scene.text.FontWeight;
public class StartPage extends VBox {
private final Button startButton;
public StartPage(Runnable onStart) {
setAlignment(Pos.CENTER);
setSpacing(UIConstants.DEFAULT_SPACING);
setPadding(UIConstants.PAGE_PADDING);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
// 增强标题视觉效果 - StartPage使用超大字体作为主页面
Label title = new Label("中小学数学答题系统");
title.setFont(Font.font(UIConstants.FONT_FAMILY, FontWeight.BOLD, UIConstants.XLARGE_TITLE_FONT_SIZE)); // 使用超大字体
title.setStyle(
"-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_PRIMARY) + "; " +
"-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4);" // 添加更强的阴影效果
private final Button startButton;
public StartPage(Runnable onStart) {
setAlignment(Pos.CENTER);
setSpacing(UIConstants.DEFAULT_SPACING);
setPadding(UIConstants.PAGE_PADDING);
setStyle("-fx-background-color: " + UIConstants.toWeb(UIConstants.COLOR_BG) + ";");
// 增强标题视觉效果
Label title = new Label("中小学数学答题系统");
title.setFont(
Font.font(
UIConstants.FONT_FAMILY,
FontWeight.BOLD,
UIConstants.XLARGE_TITLE_FONT_SIZE)); // 使用超大字体
title.setStyle(
"-fx-text-fill: "
+ UIConstants.toWeb(UIConstants.COLOR_PRIMARY)
+ "; "
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 10, 0, 0, 4);" // 添加更强的阴影效果
);
Label sub = new Label("HNU@梁峻耀 吴佰轩");
sub.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
sub.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
Label sub = new Label("HNU@梁峻耀 吴佰轩");
sub.setFont(Font.font(UIConstants.FONT_FAMILY, UIConstants.SMALL_FONT_SIZE));
sub.setStyle("-fx-text-fill: " + UIConstants.toWeb(UIConstants.COLOR_TEXT_SUB) + ";");
startButton = new Button("开始");
StyleHelper.styleButton(startButton);
startButton.setOnAction(e -> onStart.run());
startButton = new Button("开始");
StyleHelper.styleButton(startButton);
startButton.setOnAction(e -> onStart.run());
getChildren().addAll(title, sub, startButton);
}
}
getChildren().addAll(title, sub, startButton);
}
}

@ -1,153 +1,152 @@
/* com/ui/StyleHelper.java */
package com.pair.ui;
import javafx.geometry.Pos;
import javafx.scene.control.Button;
import javafx.scene.layout.*;
import javafx.scene.control.TextField;
import javafx.scene.control.PasswordField;
import javafx.scene.control.ChoiceBox;
import javafx.scene.control.Spinner;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
public class StyleHelper {
/** 给按钮一次性加上通用样式、hover、pressed 动画 - 兼容方法 */
public static void styleButton(Button btn) {
btn.setStyle(UIConstants.BTN_NORMAL);
// 仅背景深浅变化,字体、圆角始终一致
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.BTN_HOVER));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.BTN_PRESSED));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setPrefSize(UIConstants.BTN_WIDTH, UIConstants.BTN_HEIGHT);
}
/** 新的统一按钮样式方法 - 支持不同类型按钮 */
public static void styleButton(Button btn, String type) {
btn.setStyle(UIConstants.getButtonNormalStyle(type));
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.getButtonHoverStyle(type)));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.getButtonPressedStyle(type)));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
// 根据按钮类型设置尺寸
switch (type) {
case UIConstants.BTN_TYPE_PRIMARY:
case UIConstants.BTN_TYPE_WARNING:
btn.setPrefSize(UIConstants.BTN_LARGE_WIDTH, UIConstants.BTN_LARGE_HEIGHT);
break;
case UIConstants.BTN_TYPE_SUCCESS:
case UIConstants.BTN_TYPE_ERROR:
case UIConstants.BTN_TYPE_INFO:
btn.setPrefSize(UIConstants.BTN_MEDIUM_WIDTH, UIConstants.BTN_MEDIUM_HEIGHT);
break;
default:
btn.setPrefSize(UIConstants.BTN_SMALL_WIDTH, UIConstants.BTN_SMALL_HEIGHT);
break;
}
}
/** 主要按钮 - 使用主色调 */
public static void stylePrimaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_PRIMARY);
}
/** 次要按钮 - 使用辅助色调 */
public static void styleSecondaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SECONDARY);
}
/** 成功按钮 - 使用绿色调 */
public static void styleSuccessButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SUCCESS);
}
/** 警告按钮 - 使用橙色调 */
public static void styleWarningButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_WARNING);
}
/** 错误按钮 - 使用红色调 */
public static void styleErrorButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_ERROR);
}
/** 信息按钮 - 使用蓝色调 */
public static void styleInfoButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_INFO);
}
/** 快速生成"白色卡片"VBox - 兼容方法 */
public static VBox createCard() {
VBox card = new VBox(UIConstants.DEFAULT_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
return card;
}
/** 新的卡片生成方法 - 支持不同尺寸 */
public static VBox createCard(String size) {
VBox card = new VBox(UIConstants.MEDIUM_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setAlignment(Pos.CENTER);
switch (size) {
case "SMALL":
card.setStyle(UIConstants.CARD_STYLE_SMALL);
break;
case "LARGE":
card.setStyle(UIConstants.CARD_STYLE_LARGE);
break;
default: // MEDIUM
card.setStyle(UIConstants.CARD_STYLE_MEDIUM);
break;
}
return card;
}
/** 创建小型卡片 */
public static VBox createSmallCard() {
return createCard("SMALL");
}
/** 创建中型卡片 */
public static VBox createMediumCard() {
return createCard("MEDIUM");
}
/** 创建大型卡片 */
public static VBox createLargeCard() {
return createCard("LARGE");
}
/** 统一输入框样式 - TextField */
public static void styleTextField(TextField textField, String width) {
textField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - PasswordField */
public static void stylePasswordField(PasswordField passwordField, String width) {
passwordField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - ChoiceBox */
public static void styleChoiceBox(ChoiceBox<?> choiceBox, String width) {
choiceBox.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - Spinner */
public static void styleSpinner(Spinner<?> spinner) {
spinner.getEditor().setStyle(UIConstants.getInputStyle(String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH)));
spinner.setStyle("-fx-background-radius: 8; -fx-border-radius: 8;");
}
/** 快速设置输入框为中等宽度 */
public static void styleInputField(TextField field) {
styleTextField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
/** 快速设置密码框为中等宽度 */
public static void stylePasswordField(PasswordField field) {
stylePasswordField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
}
/** 给按钮一次性加上通用样式、hover、pressed 动画 - 兼容方法 */
public static void styleButton(Button btn) {
btn.setStyle(UIConstants.BTN_NORMAL);
// 仅背景深浅变化,字体、圆角始终一致
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.BTN_HOVER));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.BTN_PRESSED));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.BTN_NORMAL));
btn.setPrefSize(UIConstants.BTN_WIDTH, UIConstants.BTN_HEIGHT);
}
/** 新的统一按钮样式方法 - 支持不同类型按钮 */
public static void styleButton(Button btn, String type) {
btn.setStyle(UIConstants.getButtonNormalStyle(type));
btn.setOnMouseEntered(e -> btn.setStyle(UIConstants.getButtonHoverStyle(type)));
btn.setOnMouseExited(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
btn.setOnMousePressed(e -> btn.setStyle(UIConstants.getButtonPressedStyle(type)));
btn.setOnMouseReleased(e -> btn.setStyle(UIConstants.getButtonNormalStyle(type)));
// 根据按钮类型设置尺寸
switch (type) {
case UIConstants.BTN_TYPE_PRIMARY:
case UIConstants.BTN_TYPE_WARNING:
btn.setPrefSize(UIConstants.BTN_LARGE_WIDTH, UIConstants.BTN_LARGE_HEIGHT);
break;
case UIConstants.BTN_TYPE_SUCCESS:
case UIConstants.BTN_TYPE_ERROR:
case UIConstants.BTN_TYPE_INFO:
btn.setPrefSize(UIConstants.BTN_MEDIUM_WIDTH, UIConstants.BTN_MEDIUM_HEIGHT);
break;
default:
btn.setPrefSize(UIConstants.BTN_SMALL_WIDTH, UIConstants.BTN_SMALL_HEIGHT);
break;
}
}
/** 主要按钮 - 使用主色调 */
public static void stylePrimaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_PRIMARY);
}
/** 次要按钮 - 使用辅助色调 */
public static void styleSecondaryButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SECONDARY);
}
/** 成功按钮 - 使用绿色调 */
public static void styleSuccessButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_SUCCESS);
}
/** 警告按钮 - 使用橙色调 */
public static void styleWarningButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_WARNING);
}
/** 错误按钮 - 使用红色调 */
public static void styleErrorButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_ERROR);
}
/** 信息按钮 - 使用蓝色调 */
public static void styleInfoButton(Button btn) {
styleButton(btn, UIConstants.BTN_TYPE_INFO);
}
/** 快速生成"白色卡片"VBox - 兼容方法 */
public static VBox createCard() {
VBox card = new VBox(UIConstants.DEFAULT_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setStyle(UIConstants.CARD_STYLE);
card.setEffect(UIConstants.CARD_SHADOW);
return card;
}
/** 新的卡片生成方法 - 支持不同尺寸 */
public static VBox createCard(String size) {
VBox card = new VBox(UIConstants.MEDIUM_SPACING);
card.setPadding(UIConstants.CARD_PADDING);
card.setAlignment(Pos.CENTER);
switch (size) {
case "SMALL":
card.setStyle(UIConstants.CARD_STYLE_SMALL);
break;
case "LARGE":
card.setStyle(UIConstants.CARD_STYLE_LARGE);
break;
default: // MEDIUM
card.setStyle(UIConstants.CARD_STYLE_MEDIUM);
break;
}
return card;
}
/** 创建小型卡片 */
public static VBox createSmallCard() {
return createCard("SMALL");
}
/** 创建中型卡片 */
public static VBox createMediumCard() {
return createCard("MEDIUM");
}
/** 创建大型卡片 */
public static VBox createLargeCard() {
return createCard("LARGE");
}
/** 统一输入框样式 - TextField */
public static void styleTextField(TextField textField, String width) {
textField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - PasswordField */
public static void stylePasswordField(PasswordField passwordField, String width) {
passwordField.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - ChoiceBox */
public static void styleChoiceBox(ChoiceBox<?> choiceBox, String width) {
choiceBox.setStyle(UIConstants.getInputStyle(width));
}
/** 统一输入框样式 - Spinner */
public static void styleSpinner(Spinner<?> spinner) {
spinner
.getEditor()
.setStyle(UIConstants.getInputStyle(String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH)));
spinner.setStyle("-fx-background-radius: 8; -fx-border-radius: 8;");
}
/** 快速设置输入框为中等宽度 */
public static void styleInputField(TextField field) {
styleTextField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
/** 快速设置密码框为中等宽度 */
public static void stylePasswordField(PasswordField field) {
stylePasswordField(field, String.valueOf(UIConstants.INPUT_MEDIUM_WIDTH));
}
}

@ -5,213 +5,285 @@ import javafx.scene.effect.DropShadow;
import javafx.scene.paint.Color;
public final class UIConstants {
private UIConstants() {}
/* ====== 间距 & 边距 ====== */
public static final double DEFAULT_SPACING = 16;
public static final Insets PAGE_PADDING = new Insets(40);
public static final Insets CARD_PADDING = new Insets(24);
public static final Insets TOP_BAR_PADDING = new Insets(12);
// 新增:统一间距规范
public static final double SMALL_SPACING = 8;
public static final double MEDIUM_SPACING = 16;
public static final double LARGE_SPACING = 24;
public static final double XLARGE_SPACING = 32;
// 新增:卡片尺寸规范
public static final double CARD_SMALL_WIDTH = 400;
public static final double CARD_MEDIUM_WIDTH = 600;
public static final double CARD_LARGE_WIDTH = 800;
public static final double CARD_MIN_HEIGHT = 500;
// 新增:输入框宽度规范
public static final double INPUT_SMALL_WIDTH = 200;
public static final double INPUT_MEDIUM_WIDTH = 300;
public static final double INPUT_LARGE_WIDTH = 400;
// 新增:按钮尺寸规范
public static final double BTN_SMALL_WIDTH = 100;
public static final double BTN_MEDIUM_WIDTH = 120;
public static final double BTN_LARGE_WIDTH = 140;
public static final double BTN_SMALL_HEIGHT = 32;
public static final double BTN_MEDIUM_HEIGHT = 36;
public static final double BTN_LARGE_HEIGHT = 40;
/* ====== 字体 ====== */
public static final String FONT_FAMILY = "Microsoft YaHei";
public static final double TITLE_FONT_SIZE = 26;
public static final double SUB_TITLE_FONT_SIZE = 18;
public static final double BODY_FONT_SIZE = 14;
public static final double BTN_FONT_SIZE = 14;
public static final double SMALL_FONT_SIZE = 12;
// 新增:更大的标题字体
public static final double LARGE_TITLE_FONT_SIZE = 32;
public static final double XLARGE_TITLE_FONT_SIZE = 40;
/* ====== 圆角 & 阴影 ====== */
public static final double CARD_RADIUS = 12;
public static final DropShadow CARD_SHADOW = new DropShadow(15, 0, 4, Color.web("#00000020"));
/* ====== 主题色(一键换色只改这里) ====== */
public static final Color COLOR_PRIMARY = Color.web("#2563eb"); // 主色 - 蓝色
public static final Color COLOR_SECONDARY = Color.web("#10b981"); // 辅助 - 绿色
public static final Color COLOR_ERROR = Color.web("#ef4444"); // 错误 - 红色
public static final Color COLOR_BG = Color.web("#f3f4f6"); // 背景 - 浅灰
public static final Color COLOR_TEXT = Color.web("#1f2937"); // 文字 - 深灰
public static final Color COLOR_TEXT_SUB = Color.web("#6b7280"); // 副文字 - 中灰
// 新增:扩展配色方案
public static final Color COLOR_SUCCESS = Color.web("#22c55e"); // 成功 - 亮绿
public static final Color COLOR_WARNING = Color.web("#f59e0b"); // 警告 - 橙色
public static final Color COLOR_INFO = Color.web("#3b82f6"); // 信息 - 亮蓝
public static final Color COLOR_DARK = Color.web("#374151"); // 深色 - 深灰
public static final Color COLOR_LIGHT = Color.web("#ffffff"); // 浅色 - 白色
/* ====== 通用按钮 ====== */
public static final double BTN_WIDTH = 140;
public static final double BTN_HEIGHT = 40;
// 新增:按钮类型常量
public static final String BTN_TYPE_PRIMARY = "PRIMARY";
public static final String BTN_TYPE_SECONDARY = "SECONDARY";
public static final String BTN_TYPE_SUCCESS = "SUCCESS";
public static final String BTN_TYPE_WARNING = "WARNING";
public static final String BTN_TYPE_ERROR = "ERROR";
public static final String BTN_TYPE_INFO = "INFO";
// 保留原有样式用于兼容
public static final String BTN_NORMAL =
"-fx-background-color: "
+ toWeb(COLOR_PRIMARY)
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;" // 圆角
+ "-fx-font-family: '"
+ FONT_FAMILY
+ "';"
+ "-fx-font-size: "
+ BTN_FONT_SIZE
+ "px;"
+ "-fx-cursor: hand;";
public static final String BTN_HOVER =
"-fx-background-color: "
+ toWeb(COLOR_PRIMARY.darker())
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"; // 保持圆角
public static final String BTN_PRESSED =
"-fx-background-color: "
+ toWeb(COLOR_PRIMARY.darker().darker())
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;";
// 新增:输入框宽度预设
public static final String INPUT_STYLE_SMALL = getInputStyle(String.valueOf(INPUT_SMALL_WIDTH));
public static final String INPUT_STYLE_MEDIUM = getInputStyle(String.valueOf(INPUT_MEDIUM_WIDTH));
public static final String INPUT_STYLE_LARGE = getInputStyle(String.valueOf(INPUT_LARGE_WIDTH));
// 保留原有样式用于兼容
public static final String INPUT_STYLE =
"-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: "
+ toWeb(COLOR_TEXT_SUB)
+ ";"
+ "-fx-padding: 10;"
+ "-fx-font-family: '"
+ FONT_FAMILY
+ "';"
+ "-fx-font-size: "
+ BODY_FONT_SIZE
+ "px;"
+ "-fx-pref-width: 240;";
// 新增:卡片尺寸预设
public static final String CARD_STYLE_SMALL = getCardStyle(CARD_SMALL_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_MEDIUM = getCardStyle(CARD_MEDIUM_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_LARGE = getCardStyle(CARD_LARGE_WIDTH, CARD_MIN_HEIGHT);
// 保留原有样式用于兼容
public static final String CARD_STYLE =
"-fx-background-color: white;"
+ "-fx-background-radius: "
+ CARD_RADIUS
+ ";"
+ "-fx-border-radius: "
+ CARD_RADIUS
+ ";";
/* ====== 间距 & 边距 ====== */
public static final double DEFAULT_SPACING = 16;
public static final Insets PAGE_PADDING = new Insets(40);
public static final Insets CARD_PADDING = new Insets(24);
public static final Insets TOP_BAR_PADDING = new Insets(12);
private UIConstants() {}
// 新增:统一间距规范
public static final double SMALL_SPACING = 8;
public static final double MEDIUM_SPACING = 16;
public static final double LARGE_SPACING = 24;
public static final double XLARGE_SPACING = 32;
// 新增:卡片尺寸规范
public static final double CARD_SMALL_WIDTH = 400;
public static final double CARD_MEDIUM_WIDTH = 600;
public static final double CARD_LARGE_WIDTH = 800;
public static final double CARD_MIN_HEIGHT = 500;
// 新增:输入框宽度规范
public static final double INPUT_SMALL_WIDTH = 200;
public static final double INPUT_MEDIUM_WIDTH = 300;
public static final double INPUT_LARGE_WIDTH = 400;
// 新增:按钮尺寸规范
public static final double BTN_SMALL_WIDTH = 100;
public static final double BTN_MEDIUM_WIDTH = 120;
public static final double BTN_LARGE_WIDTH = 140;
public static final double BTN_SMALL_HEIGHT = 32;
public static final double BTN_MEDIUM_HEIGHT = 36;
public static final double BTN_LARGE_HEIGHT = 40;
/* ====== 字体 ====== */
public static final String FONT_FAMILY = "Microsoft YaHei";
public static final double TITLE_FONT_SIZE = 26;
public static final double SUB_TITLE_FONT_SIZE = 18;
public static final double BODY_FONT_SIZE = 14;
public static final double BTN_FONT_SIZE = 14;
public static final double SMALL_FONT_SIZE = 12;
// 新增:更大的标题字体
public static final double LARGE_TITLE_FONT_SIZE = 32;
public static final double XLARGE_TITLE_FONT_SIZE = 40;
/* ====== 圆角 & 阴影 ====== */
public static final double CARD_RADIUS = 12;
public static final DropShadow CARD_SHADOW = new DropShadow(15, 0, 4, Color.web("#00000020"));
/* ====== 主题色(一键换色只改这里) ====== */
public static final Color COLOR_PRIMARY = Color.web("#2563eb"); // 主色 - 蓝色
public static final Color COLOR_SECONDARY = Color.web("#10b981"); // 辅助 - 绿色
public static final Color COLOR_ERROR = Color.web("#ef4444"); // 错误 - 红色
public static final Color COLOR_BG = Color.web("#f3f4f6"); // 背景 - 浅灰
public static final Color COLOR_TEXT = Color.web("#1f2937"); // 文字 - 深灰
public static final Color COLOR_TEXT_SUB = Color.web("#6b7280"); // 副文字 - 中灰
// 新增:扩展配色方案
public static final Color COLOR_SUCCESS = Color.web("#22c55e"); // 成功 - 亮绿
public static final Color COLOR_WARNING = Color.web("#f59e0b"); // 警告 - 橙色
public static final Color COLOR_INFO = Color.web("#3b82f6"); // 信息 - 亮蓝
public static final Color COLOR_DARK = Color.web("#374151"); // 深色 - 深灰
public static final Color COLOR_LIGHT = Color.web("#ffffff"); // 浅色 - 白色
/* ====== 通用按钮 ====== */
public static final double BTN_WIDTH = 140;
public static final double BTN_HEIGHT = 40;
// 新增:按钮类型常量
public static final String BTN_TYPE_PRIMARY = "PRIMARY";
public static final String BTN_TYPE_SECONDARY = "SECONDARY";
public static final String BTN_TYPE_SUCCESS = "SUCCESS";
public static final String BTN_TYPE_WARNING = "WARNING";
public static final String BTN_TYPE_ERROR = "ERROR";
public static final String BTN_TYPE_INFO = "INFO";
// 新增:统一按钮样式生成方法
public static String getButtonNormalStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY; break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY; break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS; break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING; break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR; break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO; break;
default: bgColor = COLOR_PRIMARY; break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BTN_FONT_SIZE + "px;"
+ "-fx-cursor: hand;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
}
public static String getButtonHoverStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY.darker(); break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY.darker(); break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS.darker(); break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING.darker(); break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR.darker(); break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO.darker(); break;
default: bgColor = COLOR_PRIMARY.darker(); break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
}
public static String getButtonPressedStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY: bgColor = COLOR_PRIMARY.darker().darker(); break;
case BTN_TYPE_SECONDARY: bgColor = COLOR_SECONDARY.darker().darker(); break;
case BTN_TYPE_SUCCESS: bgColor = COLOR_SUCCESS.darker().darker(); break;
case BTN_TYPE_WARNING: bgColor = COLOR_WARNING.darker().darker(); break;
case BTN_TYPE_ERROR: bgColor = COLOR_ERROR.darker().darker(); break;
case BTN_TYPE_INFO: bgColor = COLOR_INFO.darker().darker(); break;
default: bgColor = COLOR_PRIMARY.darker().darker(); break;
}
return "-fx-background-color: " + toWeb(bgColor) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
// 新增:统一按钮样式生成方法
public static String getButtonNormalStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY:
bgColor = COLOR_PRIMARY;
break;
case BTN_TYPE_SECONDARY:
bgColor = COLOR_SECONDARY;
break;
case BTN_TYPE_SUCCESS:
bgColor = COLOR_SUCCESS;
break;
case BTN_TYPE_WARNING:
bgColor = COLOR_WARNING;
break;
case BTN_TYPE_ERROR:
bgColor = COLOR_ERROR;
break;
case BTN_TYPE_INFO:
bgColor = COLOR_INFO;
break;
default:
bgColor = COLOR_PRIMARY;
break;
}
return "-fx-background-color: "
+ toWeb(bgColor)
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-font-family: '"
+ FONT_FAMILY
+ "';"
+ "-fx-font-size: "
+ BTN_FONT_SIZE
+ "px;"
+ "-fx-cursor: hand;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.2), 6, 0, 0, 2);";
}
// 保留原有样式用于兼容
public static final String BTN_NORMAL =
"-fx-background-color: " + toWeb(COLOR_PRIMARY) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;" // 圆角
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BTN_FONT_SIZE + "px;"
+ "-fx-cursor: hand;";
public static final String BTN_HOVER =
"-fx-background-color: " + toWeb(COLOR_PRIMARY.darker()) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"; // 保持圆角
public static final String BTN_PRESSED =
"-fx-background-color: " + toWeb(COLOR_PRIMARY.darker().darker()) + ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;";
/* ====== 输入框 ====== */
// 新增:输入框样式生成方法
public static String getInputStyle(String width) {
return "-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: " + toWeb(COLOR_TEXT_SUB) + ";"
+ "-fx-padding: 12;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BODY_FONT_SIZE + "px;"
+ "-fx-pref-width: " + width + ";"
+ "-fx-background-color: " + toWeb(COLOR_LIGHT) + ";"
+ "-fx-effect: innershadow(gaussian, rgba(0,0,0,0.05), 2, 0, 0, 1);";
public static String getButtonHoverStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY:
bgColor = COLOR_PRIMARY.darker();
break;
case BTN_TYPE_SECONDARY:
bgColor = COLOR_SECONDARY.darker();
break;
case BTN_TYPE_SUCCESS:
bgColor = COLOR_SUCCESS.darker();
break;
case BTN_TYPE_WARNING:
bgColor = COLOR_WARNING.darker();
break;
case BTN_TYPE_ERROR:
bgColor = COLOR_ERROR.darker();
break;
case BTN_TYPE_INFO:
bgColor = COLOR_INFO.darker();
break;
default:
bgColor = COLOR_PRIMARY.darker();
break;
}
return "-fx-background-color: "
+ toWeb(bgColor)
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.3), 8, 0, 0, 3);";
}
// 新增:输入框宽度预设
public static final String INPUT_STYLE_SMALL = getInputStyle(String.valueOf(INPUT_SMALL_WIDTH));
public static final String INPUT_STYLE_MEDIUM = getInputStyle(String.valueOf(INPUT_MEDIUM_WIDTH));
public static final String INPUT_STYLE_LARGE = getInputStyle(String.valueOf(INPUT_LARGE_WIDTH));
// 保留原有样式用于兼容
public static final String INPUT_STYLE =
"-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: " + toWeb(COLOR_TEXT_SUB) + ";"
+ "-fx-padding: 10;"
+ "-fx-font-family: '" + FONT_FAMILY + "';"
+ "-fx-font-size: " + BODY_FONT_SIZE + "px;"
+ "-fx-pref-width: 240;";
/* ====== 卡片容器 ====== */
// 新增:卡片容器样式生成方法
public static String getCardStyle(double width, double minHeight) {
return "-fx-background-color: " + toWeb(COLOR_LIGHT) + ";"
+ "-fx-background-radius: " + CARD_RADIUS + ";"
+ "-fx-border-radius: " + CARD_RADIUS + ";"
+ "-fx-pref-width: " + width + ";"
+ "-fx-min-height: " + minHeight + ";"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 15, 0, 4, 4);";
public static String getButtonPressedStyle(String type) {
Color bgColor;
switch (type) {
case BTN_TYPE_PRIMARY:
bgColor = COLOR_PRIMARY.darker().darker();
break;
case BTN_TYPE_SECONDARY:
bgColor = COLOR_SECONDARY.darker().darker();
break;
case BTN_TYPE_SUCCESS:
bgColor = COLOR_SUCCESS.darker().darker();
break;
case BTN_TYPE_WARNING:
bgColor = COLOR_WARNING.darker().darker();
break;
case BTN_TYPE_ERROR:
bgColor = COLOR_ERROR.darker().darker();
break;
case BTN_TYPE_INFO:
bgColor = COLOR_INFO.darker().darker();
break;
default:
bgColor = COLOR_PRIMARY.darker().darker();
break;
}
return "-fx-background-color: "
+ toWeb(bgColor)
+ ";"
+ "-fx-text-fill: white;"
+ "-fx-background-radius: 8;"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 4, 0, 0, 1);";
}
// 新增:卡片尺寸预设
public static final String CARD_STYLE_SMALL = getCardStyle(CARD_SMALL_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_MEDIUM = getCardStyle(CARD_MEDIUM_WIDTH, CARD_MIN_HEIGHT);
public static final String CARD_STYLE_LARGE = getCardStyle(CARD_LARGE_WIDTH, CARD_MIN_HEIGHT);
/* ====== 输入框 ====== */
// 新增:输入框样式生成方法
public static String getInputStyle(String width) {
return "-fx-background-radius: 8;"
+ "-fx-border-radius: 8;"
+ "-fx-border-color: "
+ toWeb(COLOR_TEXT_SUB)
+ ";"
+ "-fx-padding: 12;"
+ "-fx-font-family: '"
+ FONT_FAMILY
+ "';"
+ "-fx-font-size: "
+ BODY_FONT_SIZE
+ "px;"
+ "-fx-pref-width: "
+ width
+ ";"
+ "-fx-background-color: "
+ toWeb(COLOR_LIGHT)
+ ";"
+ "-fx-effect: innershadow(gaussian, rgba(0,0,0,0.05), 2, 0, 0, 1);";
}
// 保留原有样式用于兼容
public static final String CARD_STYLE =
"-fx-background-color: white;"
+ "-fx-background-radius: " + CARD_RADIUS + ";"
+ "-fx-border-radius: " + CARD_RADIUS + ";";
/* ====== 卡片容器 ====== */
// 新增:卡片容器样式生成方法
public static String getCardStyle(double width, double minHeight) {
return "-fx-background-color: "
+ toWeb(COLOR_LIGHT)
+ ";"
+ "-fx-background-radius: "
+ CARD_RADIUS
+ ";"
+ "-fx-border-radius: "
+ CARD_RADIUS
+ ";"
+ "-fx-pref-width: "
+ width
+ ";"
+ "-fx-min-height: "
+ minHeight
+ ";"
+ "-fx-effect: dropshadow(three-pass-box, rgba(0,0,0,0.1), 15, 0, 4, 4);";
}
/* ====== 工具 ====== */
public static String toWeb(Color c) {
return String.format("#%02X%02X%02X", (int) (c.getRed() * 255),
(int) (c.getGreen() * 255), (int) (c.getBlue() * 255));
}
}
/* ====== 工具 ====== */
public static String toWeb(Color c) {
return String.format(
"#%02X%02X%02X",
(int) (c.getRed() * 255), (int) (c.getGreen() * 255), (int) (c.getBlue() * 255));
}
}

@ -1,43 +1,36 @@
package com.pair.util;
/**
*
*
*/
/** 应用数据目录管理器 根据不同操作系统返回合适的应用数据存储路径 */
public class AppDataDirectory {
private static final String APP_NAME = "Math-Quiz-App"; // 替换为你的应用名
private static final String APP_NAME = "Math-Quiz-App"; // 替换为你的应用名
/**
*
*/
public static String getApplicationDataDirectory() {
String os = System.getProperty("os.name").toLowerCase();
String basePath;
/** 获取应用数据根目录 */
public static String getApplicationDataDirectory() {
String os = System.getProperty("os.name").toLowerCase();
String basePath;
if (os.contains("win")) {
// Windows
String appData = System.getenv("APPDATA");
basePath = (appData != null) ? appData : System.getProperty("user.home") + "/AppData/Roaming";
} else if (os.contains("mac")) {
// macOS
basePath = System.getProperty("user.home") + "/Library/Application Support";
} else {
// Linux/Unix
String xdgDataHome = System.getenv("XDG_DATA_HOME");
if (xdgDataHome == null) {
xdgDataHome = System.getProperty("user.home") + "/.local/share";
}
basePath = xdgDataHome;
}
return basePath + "/" + APP_NAME;
if (os.contains("win")) {
// Windows
String appData = System.getenv("APPDATA");
basePath = (appData != null) ? appData : System.getProperty("user.home") + "/AppData/Roaming";
} else if (os.contains("mac")) {
// macOS
basePath = System.getProperty("user.home") + "/Library/Application Support";
} else {
// Linux/Unix
String xdgDataHome = System.getenv("XDG_DATA_HOME");
if (xdgDataHome == null) {
xdgDataHome = System.getProperty("user.home") + "/.local/share";
}
basePath = xdgDataHome;
}
/**
*
*/
public static String getFullPath(String relativePath) {
String appDataDir = getApplicationDataDirectory();
return appDataDir + "/" + relativePath;
}
}
return basePath + "/" + APP_NAME;
}
/** 获取完整的应用数据路径 */
public static String getFullPath(String relativePath) {
String appDataDir = getApplicationDataDirectory();
return appDataDir + "/" + relativePath;
}
}

@ -2,70 +2,75 @@
package com.pair.util;
import com.pair.service.UserService;
import java.io.IOException;
import java.util.function.Consumer;
import javafx.application.Platform;
import javafx.concurrent.Task;
import javafx.concurrent.WorkerStateEvent;
import java.io.IOException;
import java.util.function.Consumer;
public class AsyncRegistrationHelper {
private static final int COUNTDOWN_SECONDS = 60;
private static final int COUNTDOWN_SECONDS = 60;
public static void sendRegistrationCode(
String email,
UserService userService,
Runnable onPrepare,
Consumer<String> onCountdown,
Runnable onComplete,
Consumer<String> onSuccess,
Consumer<String> onFailure) {
public static void sendRegistrationCode(
String email,
UserService userService,
Runnable onPrepare,
Consumer<String> onCountdown,
Runnable onComplete,
Consumer<String> onSuccess,
Consumer<String> onFailure) {
Platform.runLater(onPrepare);
Platform.runLater(onPrepare);
Task<Void> task = new Task<>() {
@Override
protected Void call() throws Exception {
userService.generateRegistrationCode(email);
return null;
}
Task<Void> task =
new Task<>() {
@Override
protected Void call() throws Exception {
userService.generateRegistrationCode(email);
return null;
}
};
task.setOnSucceeded(e -> {
onSuccess.accept("注册码已发送到邮箱10分钟内有效");
task.setOnSucceeded(
e -> {
onSuccess.accept("注册码已发送到邮箱10分钟内有效");
new Thread(() -> {
try {
for (int i = COUNTDOWN_SECONDS; i >= 0; i--) {
new Thread(
() -> {
try {
for (int i = COUNTDOWN_SECONDS; i >= 0; i--) {
int sec = i;
Platform.runLater(() -> {
if (sec == 0) onComplete.run();
else onCountdown.accept(sec + "秒后重试");
});
Platform.runLater(
() -> {
if (sec == 0) onComplete.run();
else onCountdown.accept(sec + "秒后重试");
});
Thread.sleep(1000);
}
} catch (InterruptedException ignored) {
Platform.runLater(onComplete);
Thread.currentThread().interrupt();
}
} catch (InterruptedException ignored) {
Platform.runLater(onComplete);
Thread.currentThread().interrupt();
}
}).start();
})
.start();
});
task.setOnFailed((WorkerStateEvent e) -> {
Throwable ex = task.getException();
String msg;
if (ex instanceof IllegalArgumentException iae) {
msg = iae.getMessage();
} else if (ex instanceof IOException ioe) {
msg = "系统错误:" + ioe.getMessage();
} else {
msg = "发送失败,请检查网络或稍后重试";
}
onFailure.accept(msg);
Platform.runLater(onComplete);
task.setOnFailed(
(WorkerStateEvent e) -> {
Throwable ex = task.getException();
String msg;
if (ex instanceof IllegalArgumentException iae) {
msg = iae.getMessage();
} else if (ex instanceof IOException ioe) {
msg = "系统错误:" + ioe.getMessage();
} else {
msg = "发送失败,请检查网络或稍后重试";
}
onFailure.accept(msg);
Platform.runLater(onComplete);
});
new Thread(task).start();
}
}
new Thread(task).start();
}
}

@ -1,84 +1,81 @@
package com.pair.util;
import javax.mail.*;
import javax.mail.internet.AddressException;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
import java.util.Date;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.mail.*;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
public class EmailUtil {
// [用户名]@[域名主体].[顶级域名]
public static final Pattern EMAIL_PATTERN = Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
//验证邮箱格式
public static boolean validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
Matcher matcher = EMAIL_PATTERN.matcher(email);
return matcher.matches();
// [用户名]@[域名主体].[顶级域名]
public static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$");
// ==================== 邮箱配置 ====================
private static final String SENDER_EMAIL = "2936213174@qq.com"; // 我的QQ邮箱
private static final String SENDER_PASSWORD = "gxfjdzqviasuddci"; // QQ邮箱授权码
private static final String SMTP_HOST = "smtp.qq.com"; // QQ邮箱SMTP服务器
private static final String SMTP_PORT = "587"; // 端口
// 验证邮箱格式
public static boolean validateEmail(String email) {
if (email == null || email.trim().isEmpty()) {
return false;
}
// ==================== 邮箱配置 ====================
private static final String SENDER_EMAIL = "2936213174@qq.com"; // 我的QQ邮箱
private static final String SENDER_PASSWORD = "gxfjdzqviasuddci"; // QQ邮箱授权码
private static final String SMTP_HOST = "smtp.qq.com"; // QQ邮箱SMTP服务器
private static final String SMTP_PORT = "587"; // 端口
/**
*
* @param toEmail
* @param registrationCode
* @return truefalse
*/
public static boolean sendRegistrationCode(String toEmail, String registrationCode) {
try {
// 1. 配置邮件服务器
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.port", SMTP_PORT);
// 2. 创建会话
Session session = Session.getInstance(props, new Authenticator() {
Matcher matcher = EMAIL_PATTERN.matcher(email);
return matcher.matches();
}
/**
*
*
* @param toEmail
* @param registrationCode
* @return truefalse
*/
public static boolean sendRegistrationCode(String toEmail, String registrationCode) {
try {
// 1. 配置邮件服务器
Properties props = new Properties();
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.port", SMTP_PORT);
// 2. 创建会话
Session session =
Session.getInstance(
props,
new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD);
return new PasswordAuthentication(SENDER_EMAIL, SENDER_PASSWORD);
}
});
});
// 3. 创建邮件
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER_EMAIL));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject("注册验证码");
// 3. 创建邮件
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(SENDER_EMAIL));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject("注册验证码");
// 邮件内容
String content = "您的注册验证码是:" + registrationCode + "\n" +
"验证码有效期为10分钟请尽快使用。\n" +
"如非本人操作,请忽略此邮件。";
message.setText(content);
// 邮件内容
String content =
"您的注册验证码是:" + registrationCode + "\n" + "验证码有效期为10分钟请尽快使用。\n" + "如非本人操作,请忽略此邮件。";
message.setText(content);
// 4. 发送邮件
Transport.send(message);
// 4. 发送邮件
Transport.send(message);
System.out.println("✓ 验证码已发送到:" + toEmail);
return true;
System.out.println("✓ 验证码已发送到:" + toEmail);
return true;
} catch (Exception e) {
System.err.println("✗ 邮件发送失败:" + e.getMessage());
e.printStackTrace();
return false;
}
} catch (Exception e) {
System.err.println("✗ 邮件发送失败:" + e.getMessage());
e.printStackTrace();
return false;
}
}
}

@ -3,208 +3,225 @@ package com.pair.util;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import java.io.*;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.lang.reflect.Type;
/**
*
* JSON
*/
/** 文件操作工具类 提供文件读写、目录创建、JSON序列化等常用操作 */
public class FileUtils {
private static final Gson gson = new GsonBuilder()
.setPrettyPrinting() // 格式化输出JSON
.create();
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static String readFileToString(String filePath) throws IOException {
return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void writeStringToFile(String filePath, String content) throws IOException {
File file = new File(filePath);
// 确保父目录存在
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
// 写入文件
try (BufferedWriter writer = new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
writer.write(content);
}
}
/**
*
* @param dirPath
* @throws IOException
*/
public static void createDirectoryIfNotExists(String dirPath) throws IOException {
Path path = Paths.get(dirPath);
if (!Files.exists(path)) {
System.out.println(1);
Files.createDirectories(path);
System.out.println(2);
}
}
public static void ensureFileExists(String filePath) throws IOException {
if (!FileUtils.exists(filePath)) {
// 自动创建父目录
String parentDir = Paths.get(filePath).getParent().toString();
FileUtils.createDirectoryIfNotExists(parentDir);
// 创建空文件
FileUtils.writeStringToFile(filePath, "");
}
}
/**
*
* @param filePath
* @return true
*/
public static boolean exists(String filePath) {
return Files.exists(Paths.get(filePath));
}
/**
*
* @param filePath
* @return true
*/
public static boolean deleteFile(String filePath) {
try {
return Files.deleteIfExists(Paths.get(filePath));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
*
* @param dirPath
* @return
*/
public static File[] listFiles(String dirPath) {
File dir = new File(dirPath);
if (dir.exists() && dir.isDirectory()) {
return dir.listFiles();
}
return new File[0];
}
/**
*
* @param filePath
* @param content
* @throws IOException
*/
public static void appendToFile(String filePath, String content) throws IOException {
Files.writeString(Paths.get(filePath), content, StandardCharsets.UTF_8,
StandardOpenOption.CREATE, StandardOpenOption.APPEND);
}
/**
*
* @param sourcePath
* @param targetPath
* @throws IOException
*/
public static void copyFile(String sourcePath, String targetPath) throws IOException {
Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING);
}
/**
*
* @param filePath
* @return
* @throws IOException
*/
public static long getFileSize(String filePath) throws IOException {
return Files.size(Paths.get(filePath));
}
// ==================== JSON 操作方法 ====================
/**
* JSON
* @param data
* @param filePath
* @throws IOException
*/
public static void saveAsJson(Object data, String filePath) throws IOException {
String json = gson.toJson(data);
writeStringToFile(filePath, json);
}
/**
* JSON
* @param filePath
* @param classOfT Class
* @return
* @throws IOException
*/
public static <T> T readJsonToObject(String filePath, Class<T> classOfT) throws IOException {
String json = readFileToString(filePath);
return gson.fromJson(json, classOfT);
}
/**
* JSON
* @param filePath
* @param typeOfT Type
* @return
* @throws IOException
*/
public static <T> 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> T fromJson(String json, Class<T> classOfT) {
return gson.fromJson(json, classOfT);
}
/**
* JSON
* @param json JSON
* @param typeOfT Type
* @return
*/
public static <T> T fromJson(String json, Type typeOfT) {
return gson.fromJson(json, typeOfT);
}
}
private static final Gson gson =
new GsonBuilder()
.setPrettyPrinting() // 格式化输出JSON
.create();
/**
*
*
* @param filePath
* @return
* @throws IOException
*/
public static String readFileToString(String filePath) throws IOException {
return Files.readString(Paths.get(filePath), StandardCharsets.UTF_8);
}
/**
*
*
* @param filePath
* @param content
* @throws IOException
*/
public static void writeStringToFile(String filePath, String content) throws IOException {
File file = new File(filePath);
// 确保父目录存在
File parentDir = file.getParentFile();
if (parentDir != null && !parentDir.exists()) {
parentDir.mkdirs();
}
// 写入文件
try (BufferedWriter writer =
new BufferedWriter(
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
writer.write(content);
}
}
/**
*
*
* @param dirPath
* @throws IOException
*/
public static void createDirectoryIfNotExists(String dirPath) throws IOException {
Path path = Paths.get(dirPath);
if (!Files.exists(path)) {
System.out.println(1);
Files.createDirectories(path);
System.out.println(2);
}
}
public static void ensureFileExists(String filePath) throws IOException {
if (!FileUtils.exists(filePath)) {
// 自动创建父目录
String parentDir = Paths.get(filePath).getParent().toString();
FileUtils.createDirectoryIfNotExists(parentDir);
// 创建空文件
FileUtils.writeStringToFile(filePath, "");
}
}
/**
*
*
* @param filePath
* @return true
*/
public static boolean exists(String filePath) {
return Files.exists(Paths.get(filePath));
}
/**
*
*
* @param filePath
* @return true
*/
public static boolean deleteFile(String filePath) {
try {
return Files.deleteIfExists(Paths.get(filePath));
} catch (IOException e) {
e.printStackTrace();
return false;
}
}
/**
*
*
* @param dirPath
* @return
*/
public static File[] listFiles(String dirPath) {
File dir = new File(dirPath);
if (dir.exists() && dir.isDirectory()) {
return dir.listFiles();
}
return new File[0];
}
/**
*
*
* @param filePath
* @param content
* @throws IOException
*/
public static void appendToFile(String filePath, String content) throws IOException {
Files.writeString(
Paths.get(filePath),
content,
StandardCharsets.UTF_8,
StandardOpenOption.CREATE,
StandardOpenOption.APPEND);
}
/**
*
*
* @param sourcePath
* @param targetPath
* @throws IOException
*/
public static void copyFile(String sourcePath, String targetPath) throws IOException {
Files.copy(Paths.get(sourcePath), Paths.get(targetPath), StandardCopyOption.REPLACE_EXISTING);
}
/**
*
*
* @param filePath
* @return
* @throws IOException
*/
public static long getFileSize(String filePath) throws IOException {
return Files.size(Paths.get(filePath));
}
// ==================== JSON 操作方法 ====================
/**
* JSON
*
* @param data
* @param filePath
* @throws IOException
*/
public static void saveAsJson(Object data, String filePath) throws IOException {
String json = gson.toJson(data);
writeStringToFile(filePath, json);
}
/**
* JSON
*
* @param filePath
* @param classOfT Class
* @return
* @throws IOException
*/
public static <T> T readJsonToObject(String filePath, Class<T> classOfT) throws IOException {
String json = readFileToString(filePath);
return gson.fromJson(json, classOfT);
}
/**
* JSON
*
* @param filePath
* @param typeOfT Type
* @return
* @throws IOException
*/
public static <T> 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> T fromJson(String json, Class<T> classOfT) {
return gson.fromJson(json, classOfT);
}
/**
* JSON
*
* @param json JSON
* @param typeOfT Type
* @return
*/
public static <T> T fromJson(String json, Type typeOfT) {
return gson.fromJson(json, typeOfT);
}
}

@ -3,145 +3,139 @@ package com.pair.util;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
/**
*
*
*/
/** 密码验证和加密工具类 提供密码格式验证、加密、匹配等功能 */
public class PasswordValidator {
// 密码长度限制
private static final int CODE_LENGTH = 6;
private static final int MIN_LENGTH = 6;
private static final int MAX_LENGTH = 10;
// 用于生成随机注册码的字符集
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
private static final String DIGITS = "0123456789";
private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
// ==================== 密码验证 ====================
/**
*
*
* @param password
* @return null
*/
public static void validatePassword(String password) {
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("密码不能为空!");
}
if (password.length() < MIN_LENGTH) {
throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH + " 位!");
}
if (password.length() > MAX_LENGTH) {
throw new IllegalArgumentException("密码长度不能超过 " + MAX_LENGTH + " 位!");
}
if (password.contains(" ")) {
throw new IllegalArgumentException("密码不能包含空格!");
}
// 检查是否包含小写字母
boolean hasLowerLetter = password.matches(".*[a-z].*");
if (!hasLowerLetter) {
throw new IllegalArgumentException("必须包含小写字母!");
}
// 检查是否包含大写字母
boolean hasUpperLetter = password.matches(".*[A-Z].*");
if (!hasUpperLetter) {
throw new IllegalArgumentException("必须包含大写字母!");
}
// 检查是否包含数字
boolean hasDigit = password.matches(".*\\d.*");
if (!hasDigit) {
throw new IllegalArgumentException("密码必须包含数字!");
}
// 密码长度限制
private static final int CODE_LENGTH = 6;
private static final int MIN_LENGTH = 6;
private static final int MAX_LENGTH = 10;
// 用于生成随机注册码的字符集
private static final String UPPERCASE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
private static final String LOWERCASE = "abcdefghijklmnopqrstuvwxyz";
private static final String DIGITS = "0123456789";
private static final String ALL_CHARS = UPPERCASE + LOWERCASE + DIGITS;
// ==================== 密码验证 ====================
/**
*
*
* @param password
* @return null
*/
public static void validatePassword(String password) {
if (password == null || password.isEmpty()) {
throw new IllegalArgumentException("密码不能为空!");
}
// ==================== 密码加密 ====================
/**
* 使SHA-256
*
* @param password
* @return 16
*/
public static String encrypt(String password) {
if (password == null) {
throw new IllegalArgumentException("密码不能为null");
}
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes());
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256算法不可用", e);
}
if (password.length() < MIN_LENGTH) {
throw new IllegalArgumentException("密码长度不能少于 " + MIN_LENGTH + " 位!");
}
/**
*
*
* @param plainPassword
* @param encryptedPassword
* @return true
*/
public static boolean matches(String plainPassword, String encryptedPassword) {
if (plainPassword == null || encryptedPassword == null) {
return false;
}
if (password.length() > MAX_LENGTH) {
throw new IllegalArgumentException("密码长度不能超过 " + MAX_LENGTH + " 位!");
}
return encrypt(plainPassword).equals(encryptedPassword);
if (password.contains(" ")) {
throw new IllegalArgumentException("密码不能包含空格!");
}
// ==================== 注册码生成 ====================
// 检查是否包含小写字母
boolean hasLowerLetter = password.matches(".*[a-z].*");
if (!hasLowerLetter) {
throw new IllegalArgumentException("必须包含小写字母!");
}
/**
* 6-10
*
* @return
*/
public static String generateRegistrationCode() {
return generateRegistrationCode(CODE_LENGTH);
// 检查是否包含大写字母
boolean hasUpperLetter = password.matches(".*[A-Z].*");
if (!hasUpperLetter) {
throw new IllegalArgumentException("必须包含大写字母!");
}
/**
*
*
* @param @
* @return
*/
public static String generateRegistrationCode(int codeLength) {
// 检查是否包含数字
boolean hasDigit = password.matches(".*\\d.*");
if (!hasDigit) {
throw new IllegalArgumentException("密码必须包含数字!");
}
}
// ==================== 密码加密 ====================
/**
* 使SHA-256
*
* @param password
* @return 16
*/
public static String encrypt(String password) {
if (password == null) {
throw new IllegalArgumentException("密码不能为null");
}
StringBuilder code = new StringBuilder(codeLength);
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(password.getBytes());
// 填充剩余字符
for (int i = 0; i < codeLength; i++) {
code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("SHA-256算法不可用", e);
}
}
/**
*
*
* @param plainPassword
* @param encryptedPassword
* @return true
*/
public static boolean matches(String plainPassword, String encryptedPassword) {
if (plainPassword == null || encryptedPassword == null) {
return false;
}
// 打乱字符顺序
return RandomUtils.shuffleString(code.toString());
return encrypt(plainPassword).equals(encryptedPassword);
}
// ==================== 注册码生成 ====================
/**
* 6-10
*
* @return
*/
public static String generateRegistrationCode() {
return generateRegistrationCode(CODE_LENGTH);
}
/**
*
*
* @param codeLength
* @return
*/
public static String generateRegistrationCode(int codeLength) {
StringBuilder code = new StringBuilder(codeLength);
// 填充剩余字符
for (int i = 0; i < codeLength; i++) {
code.append(DIGITS.charAt(RandomUtils.nextInt(0, DIGITS.length() - 1)));
}
}
// 打乱字符顺序
return RandomUtils.shuffleString(code.toString());
}
}

@ -6,84 +6,83 @@ import java.util.Random;
/** Random number generation utilities. */
public class RandomUtils {
private static final Random random = new Random();
private static final Random random = new Random();
// Generate random integer in range [min, max] (inclusive)
public static int nextInt(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("Min cannot be greater than max");
}
return min + random.nextInt(max - min + 1);
// Generate random integer in range [min, max] (inclusive)
public static int nextInt(int min, int max) {
if (min > max) {
throw new IllegalArgumentException("Min cannot be greater than max");
}
return min + random.nextInt(max - min + 1);
}
// Randomly select an element from array (template method)
public static <T> T randomChoice(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array cannot be empty");
}
return array[random.nextInt(array.length)];
// Randomly select an element from array (template method)
public static <T> T randomChoice(T[] array) {
if (array == null || array.length == 0) {
throw new IllegalArgumentException("Array cannot be empty");
}
return array[random.nextInt(array.length)];
}
//从列表中随机选择一个元素
public static <T> T randomChoice(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("列表不能为空");
}
return list.get(random.nextInt(list.size()));
}
/**
*
* @param list
*/
public static <T> void shuffle(List<T> list) {
Collections.shuffle(list, random);
// 从列表中随机选择一个元素
public static <T> T randomChoice(List<T> list) {
if (list == null || list.isEmpty()) {
throw new IllegalArgumentException("列表不能为空");
}
return list.get(random.nextInt(list.size()));
}
/**
* 使Fisher-Yates
* @param str
* @return
*/
public static String shuffleString(String str) {
if (str == null || str.length() <= 1) {
return str;
}
/**
*
*
* @param list
*/
public static <T> void shuffle(List<T> list) {
Collections.shuffle(list, random);
}
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);
/**
* 使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);
//生成指定范围内的随机双精度浮点数
public static double nextDouble(double min, double max) {
if (min > max) {
throw new IllegalArgumentException("min不能大于max");
}
return min + (max - min) * random.nextDouble();
// 交换字符
char temp = chars[i];
chars[i] = chars[j];
chars[j] = temp;
}
return new String(chars);
}
//生成随机布尔值
public static boolean nextBoolean() {
return random.nextBoolean();
// 生成指定范围内的随机双精度浮点数
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;
// 按概率返回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;
}
}

Loading…
Cancel
Save