Compare commits

...

35 Commits

Author SHA1 Message Date
hnu202304060319 b7be1ec970 FX界面说明文档.md
5 months ago
hnu202304060319 490542888f Merge pull request '1' (#3) from develop into main
5 months ago
hnu202304060319 740ed7c10a 1
5 months ago
hnu202304060319 4c9757be25 Merge pull request '1' (#1) from fanwen_branch into develop
5 months ago
hnu202326010302 6348a65450 ADD file via upload
5 months ago
hnu202326010302 6084431bc5 Delete 'README.md'
5 months ago
hnu202326010302 51a467b08d ADD file via upload
5 months ago
hnu202326010302 576bf8c29e ADD file via upload
5 months ago
hnu202326010302 37047295fa ADD file via upload
5 months ago
hnu202326010302 bd77d69c6a ADD file via upload
5 months ago
hnu202326010302 bda4edbdd7 ADD file via upload
5 months ago
hnu202326010302 8347af64e1 ADD file via upload
5 months ago
hnu202326010302 b10ab09848 ADD file via upload
5 months ago
hnu202326010302 a99d0749b2 ADD file via upload
5 months ago
hnu202326010302 f08d2c2597 ADD file via upload
5 months ago
hnu202326010302 a9d055b92d ADD file via upload
5 months ago
hnu202326010302 be28dc9276 ADD file via upload
5 months ago
hnu202326010302 eaeac8d87d ADD file via upload
5 months ago
hnu202326010302 590c8cd364 ADD file via upload
5 months ago
hnu202326010302 8d8c743d1e ADD file via upload
5 months ago
hnu202326010302 c0e9c07b46 ADD file via upload
5 months ago
hnu202326010302 077f2f0447 Delete 'QuestionSeting.java'
5 months ago
hnu202326010302 803ff633a6 ADD file via upload
5 months ago
hnu202326010302 5cf7ec1dff ADD file via upload
5 months ago
hnu202326010302 39a594adff ADD file via upload
5 months ago
hnu202326010302 54425f1e15 ADD file via upload
5 months ago
hnu202326010302 07b40f16d6 ADD file via upload
5 months ago
hnu202326010302 f10ebc1b3c ADD file via upload
5 months ago
hnu202326010302 e85f3275c2 ADD file via upload
5 months ago
hnu202326010302 4e46dd158d ADD file via upload
5 months ago
hnu202326010302 bc5ccc87a3 ADD file via upload
5 months ago
hnu202326010302 0ab3df6e53 ADD file via upload
5 months ago
hnu202326010302 59f86d258b ADD file via upload
5 months ago
hnu202326010302 8015cbdb12 ADD file via upload
5 months ago
hnu202304060319 5486770922 Delete 'README.md'
5 months ago

@ -0,0 +1,152 @@
# 数学在线考试系统
## 项目简介
一个基于JavaFX开发的数学在线考试系统支持用户注册、登录、密码管理和多学段数学题目生成。系统提供小学、初中、高中三个学段的数学题目支持自动组卷、在线答题和自动评分功能。
## 功能特性
### 用户管理
- 用户注册(邮箱验证)
- 用户登录(支持邮箱/用户名登录)
- 密码重置
- 密码修改
- 用户信息持久化存储
### 考试功能
- **多学段支持**:小学、初中、高中
- **智能题目生成**:根据学段生成相应难度题目
- **题目去重**:同一试卷中不会出现重复题目
- **实时答题**:在线答题界面,支持题目导航
- **自动评分**:答题完成后自动计算得分
- **进度显示**:答题进度可视化
### 题目特点
- **小学**:基础四则运算,确保结果为正数
- **初中**:包含平方、开方等运算
- **高中**:包含三角函数等高级运算
- **选项生成**:智能生成干扰项,避免重复
## 技术架构
### 后端技术栈
- **JavaFX** - 桌面应用框架
- **Jackson** - JSON数据序列化
- **Jakarta Mail** - 邮件发送服务
- **BCrypt** - 密码加密
- **JUnit** - 单元测试
### 项目结构
```
src/main/java/com/example/myapp/
├── controller/ # 控制器层
│ ├── LoginController.java
│ ├── RegisterController.java
│ ├── DashboardController.java
│ ├── ExamController.java
│ └── ...
├── model/ # 数据模型
│ ├── User.java
│ ├── Exam.java
│ ├── Question.java
│ └── Expression.java
├── service/ # 业务服务层
│ ├── UserService.java
│ ├── ExamService.java
│ ├── EmailService.java
│ └── MathQuestionGenerator.java
├── util/ # 工具类
│ └── PasswordUtil.java
└── Main.java # 应用入口
```
## 核心功能模块
### 1. 用户认证系统
java
复制下载
```
// 密码规则6-10位必须包含大小写字母和数字
public static boolean validatePasswordRules(String pwd) {
Pattern validPattern = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$");
return validPattern.matcher(pwd).matches();
}
```
### 2. 题目生成系统
采用工厂模式 + 抽象类实现多学段题目生成:
java
复制下载
```
// 题目生成工厂
public class QueSetingFactory {
public QuestionSeting getQueSeting(String type) {
switch (type) {
case "小学": return new PrimaryQueSeting();
case "初中": return new MiddleQueSeting();
case "高中": return new HighQueSeting();
default: return null;
}
}
}
```
### 3. 数据存储系统
使用JSON文件进行数据持久化
- `data/users.json` - 用户数据
- `data/registration_codes.json` - 注册验证码
- `data/reset_password_codes.json` - 密码重置码
## 安装与运行
### 环境要求
- Java 17 或更高版本
- Maven 3.6+
- 可用的SMTP服务器用于邮件发送
## 使用指南
### 用户注册流程
1. 输入邮箱地址
2. 接收验证码邮件
3. 输入验证码验证
4. 设置用户名和密码
5. 注册完成,进入用户中心
### 考试流程
1. 登录系统
2. 选择学段(小学/初中/高中)
3. 输入题目数量1-100题
4. 进入答题界面
5. 逐题作答,支持前后导航
6. 提交试卷,查看成绩
### 密码管理
- **修改密码**:登录后可在用户中心修改
- **重置密码**:登录页点击"忘记密码",通过邮箱验证重置

@ -0,0 +1,103 @@
package com.example.myapp;
import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;
public class Main extends Application {
private static Stage primaryStage;
@Override
public void start(Stage stage) throws Exception{
primaryStage = stage;
showLogin();
}
public static void showLogin() throws Exception{
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/login.fxml"));
primaryStage.setScene(new Scene(loader.load()));
primaryStage.setTitle("登录");
primaryStage.show();
}
public static void showRegister() throws Exception{
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/register.fxml"));
primaryStage.setScene(new Scene(loader.load()));
primaryStage.setTitle("注册 - 输入邮箱");
}
public static void showSetPassword(String email) throws Exception{
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/set_password.fxml"));
// 传递 email 给 controller
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.SetPasswordController)ctrl).setEmail(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("设置密码");
}
public static void showDashboard(String email) throws Exception{
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/dashboard.fxml"));
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.DashboardController)ctrl).initUser(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("用户界面");
}
// 在 Main.java 中添加以下方法
public static void showGradeSelection(String email) throws Exception {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/grade_selection.fxml"));
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.GradeSelectionController)ctrl).initUser(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("选择学段");
}
public static void showExamPage(String email) throws Exception {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/exam.fxml"));
Parent root = loader.load();
// 获取控制器并初始化
var ctrl = loader.getController();
((com.example.myapp.controller.ExamController)ctrl).initExam(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("答题界面");
}
public static void showScorePage(String email, int score) throws Exception {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/score.fxml"));
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.ScoreController)ctrl).initScore(email, score);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("考试成绩");
}
public static void showChangePassword(String email) throws Exception {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/change_password.fxml"));
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.ChangePasswordController)ctrl).initUser(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("修改密码");
}
// 在 Main.java 中添加
public static void showForgotPassword(String email) throws Exception {
FXMLLoader loader = new FXMLLoader(Main.class.getResource("/fxml/forgot_password.fxml"));
Parent root = loader.load();
var ctrl = loader.getController();
((com.example.myapp.controller.ForgotPasswordController)ctrl).setEmail(email);
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("忘记密码");
}
public static void main(String[] args){
launch(args);
}
}

@ -0,0 +1,82 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.UserService;
import com.example.myapp.util.PasswordUtil;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class ChangePasswordController {
@FXML private Label userLabel;
@FXML private PasswordField oldPwd;
@FXML private PasswordField newPwd;
@FXML private PasswordField newPwdConfirm;
@FXML private Button changeBtn;
@FXML private Button backBtn;
@FXML private Button clearBtn;
@FXML private Label statusLabel;
private String email;
private final UserService userService = UserService.getInstance();
public void initUser(String email) {
this.email = email;
userLabel.setText("修改密码 - " + email);
clearFields();
}
@FXML
public void onChangePassword() {
String oldp = oldPwd.getText();
String np = newPwd.getText();
String np2 = newPwdConfirm.getText();
if (oldp.isEmpty() || np.isEmpty() || np2.isEmpty()) {
statusLabel.setText("请填写所有密码字段");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!userService.checkPassword(email, oldp)) {
statusLabel.setText("原密码不正确");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!np.equals(np2)) {
statusLabel.setText("两次新密码不一致");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!PasswordUtil.validatePasswordRules(np)) {
statusLabel.setText("新密码不符合规则6-10位含大小写和数字");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
userService.setPassword(email, PasswordUtil.hash(np));
statusLabel.setText("密码修改成功!");
statusLabel.setStyle("-fx-text-fill: green;");
clearFields();
}
@FXML
public void onBack() {
try {
Main.showDashboard(email);
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onClear() {
clearFields();
statusLabel.setText("");
}
private void clearFields() {
oldPwd.clear();
newPwd.clear();
newPwdConfirm.clear();
}
}

@ -0,0 +1,49 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.UserService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class DashboardController {
@FXML private Label welcomeLabel;
@FXML private Button changePasswordBtn;
@FXML private Button studyBtn;
@FXML private Button logoutBtn;
private String email;
private final UserService userService = UserService.getInstance();
public void initUser(String email){
this.email = email;
String displayName = userService.getDisplayName(email);
welcomeLabel.setText("欢迎: " + displayName);
}
@FXML
public void onChangePassword() {
try {
Main.showChangePassword(email);
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onStudy() {
try {
Main.showGradeSelection(email);
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onLogout() {
try {
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,161 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.model.Exam;
import com.example.myapp.model.Question;
import com.example.myapp.service.ExamService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class ExamController {
@FXML private Label questionNumberLabel;
@FXML private Label questionTextLabel;
@FXML private RadioButton optionARadio;
@FXML private RadioButton optionBRadio;
@FXML private RadioButton optionCRadio;
@FXML private RadioButton optionDRadio;
@FXML private Label optionALabel;
@FXML private Label optionBLabel;
@FXML private Label optionCLabel;
@FXML private Label optionDLabel;
@FXML private Button previousBtn;
@FXML private Button nextBtn;
@FXML private Button submitBtn;
@FXML private ProgressIndicator progressIndicator;
private String email;
private ExamService examService = ExamService.getInstance();
private int currentQuestionIndex = 0;
private ToggleGroup optionsToggleGroup;
// 添加初始化方法,使用 @FXML 注解确保在FXML加载后立即执行
@FXML
public void initialize() {
// 在这里初始化 ToggleGroup确保在FXML加载完成后立即执行
this.optionsToggleGroup = new ToggleGroup();
optionARadio.setToggleGroup(optionsToggleGroup);
optionBRadio.setToggleGroup(optionsToggleGroup);
optionCRadio.setToggleGroup(optionsToggleGroup);
optionDRadio.setToggleGroup(optionsToggleGroup);
// 设置默认禁用状态
previousBtn.setDisable(true);
submitBtn.setDisable(true);
}
public void initExam(String email) {
this.email = email;
// 确保有考试数据
Exam exam = examService.getCurrentExam(email);
if (exam == null || exam.getQuestions().isEmpty()) {
showAlert("错误", "没有找到考试数据,请重新选择学段");
try {
Main.showGradeSelection(email);
} catch (Exception e) {
e.printStackTrace();
}
return;
}
loadQuestion(0);
}
@FXML
public void onPreviousQuestion() {
saveCurrentAnswer();
if (currentQuestionIndex > 0) {
loadQuestion(currentQuestionIndex - 1);
}
}
@FXML
public void onNextQuestion() {
saveCurrentAnswer();
Exam exam = examService.getCurrentExam(email);
if (currentQuestionIndex < exam.getQuestions().size() - 1) {
loadQuestion(currentQuestionIndex + 1);
}
}
@FXML
public void onSubmitExam() {
saveCurrentAnswer();
// 计算分数
int score = examService.calculateScore(email);
// 显示分数界面
try {
Main.showScorePage(email, score);
} catch (Exception e) {
e.printStackTrace();
}
}
private void loadQuestion(int index) {
Exam exam = examService.getCurrentExam(email);
if (exam == null || index < 0 || index >= exam.getQuestions().size()) {
return;
}
currentQuestionIndex = index;
Question question = exam.getQuestions().get(index);
// 更新界面
questionNumberLabel.setText("第 " + (index + 1) + " 题 / 共 " + exam.getQuestions().size() + " 题");
questionTextLabel.setText(question.getQuestionText());
optionALabel.setText("A. " + question.getOptionA());
optionBLabel.setText("B. " + question.getOptionB());
optionCLabel.setText("C. " + question.getOptionC());
optionDLabel.setText("D. " + question.getOptionD());
// 清除选择
optionsToggleGroup.selectToggle(null);
// 恢复用户之前的选择
if (question.getUserAnswer() != null) {
switch (question.getUserAnswer()) {
case "A": optionARadio.setSelected(true); break;
case "B": optionBRadio.setSelected(true); break;
case "C": optionCRadio.setSelected(true); break;
case "D": optionDRadio.setSelected(true); break;
}
}
// 更新按钮状态
previousBtn.setDisable(index == 0);
nextBtn.setDisable(index == exam.getQuestions().size() - 1);
submitBtn.setDisable(index != exam.getQuestions().size() - 1);
// 更新进度
progressIndicator.setProgress((double) (index + 1) / exam.getQuestions().size());
}
private void saveCurrentAnswer() {
// 添加空值检查
if (optionsToggleGroup == null) {
return;
}
RadioButton selectedRadio = (RadioButton) optionsToggleGroup.getSelectedToggle();
if (selectedRadio != null) {
String answer = "";
if (selectedRadio == optionARadio) answer = "A";
else if (selectedRadio == optionBRadio) answer = "B";
else if (selectedRadio == optionCRadio) answer = "C";
else if (selectedRadio == optionDRadio) answer = "D";
examService.submitAnswer(email, currentQuestionIndex, answer);
}
}
private void showAlert(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -0,0 +1,105 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.EmailService;
import com.example.myapp.service.UserService;
import com.example.myapp.util.PasswordUtil;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import java.util.Random;
public class ForgotPasswordController {
@FXML private Label emailLabel;
@FXML private Button sendCodeBtn;
@FXML private TextField codeField;
@FXML private PasswordField newPasswordField;
@FXML private PasswordField confirmPasswordField;
@FXML private Button resetPasswordBtn;
@FXML private Button backToLoginBtn;
@FXML private Label statusLabel;
private String email;
private final UserService userService = UserService.getInstance();
private EmailService emailService;
public ForgotPasswordController() {
try {
emailService = new EmailService();
} catch (Exception e) {
e.printStackTrace();
}
}
public void setEmail(String email) {
this.email = email;
emailLabel.setText("重置密码 - " + email);
}
@FXML
public void onSendCode() {
String code = generateCode();
userService.createResetPasswordCode(email, code);
try {
emailService.sendRegistrationCode(email, code);
statusLabel.setText("验证码已发送到您的邮箱,请查收");
statusLabel.setStyle("-fx-text-fill: green;");
} catch (Exception e) {
e.printStackTrace();
statusLabel.setText("发送失败: " + e.getMessage());
statusLabel.setStyle("-fx-text-fill: red;");
}
}
@FXML
public void onResetPassword() {
String code = codeField.getText().trim();
String newPassword = newPasswordField.getText();
String confirmPassword = confirmPasswordField.getText();
if (!userService.verifyResetPasswordCode(email, code)) {
statusLabel.setText("验证码错误或已过期");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!newPassword.equals(confirmPassword)) {
statusLabel.setText("两次输入的密码不一致");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!PasswordUtil.validatePasswordRules(newPassword)) {
statusLabel.setText("密码不符合规则6-10位须含大写、小写和数字");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
String hash = PasswordUtil.hash(newPassword);
userService.setPassword(email, hash);
statusLabel.setText("密码重置成功,请重新登录");
statusLabel.setStyle("-fx-text-fill: green;");
try {
Thread.sleep(2000);
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onBackToLogin() {
try {
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
private String generateCode() {
Random r = new Random();
int v = 100000 + r.nextInt(900000);
return String.valueOf(v);
}
}

@ -0,0 +1,92 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.ExamService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class GradeSelectionController {
@FXML private Label welcomeLabel;
private String email;
private final ExamService examService = ExamService.getInstance();
public void initUser(String email) {
this.email = email;
welcomeLabel.setText("欢迎 " + email + ",请选择学段");
}
@FXML
public void onPrimarySchoolSelected() {
showQuestionCountDialog("小学");
}
@FXML
public void onMiddleSchoolSelected() {
showQuestionCountDialog("初中");
}
@FXML
public void onHighSchoolSelected() {
showQuestionCountDialog("高中");
}
@FXML
public void onBackToLogin() {
try {
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onBackToDashboard() {
try {
Main.showDashboard(email);
} catch (Exception e) {
e.printStackTrace();
}
}
private void showQuestionCountDialog(String gradeLevel) {
TextInputDialog dialog = new TextInputDialog("10");
dialog.setTitle("题目数量");
dialog.setHeaderText("选择" + gradeLevel + "题目");
dialog.setContentText("请输入题目数量(1-100):");
dialog.showAndWait().ifPresent(countStr -> {
try {
int questionCount = Integer.parseInt(countStr);
if (questionCount < 1 || questionCount > 100) {
showAlert("题目数量必须在1-100之间");
return;
}
boolean success = examService.generateExam(gradeLevel, questionCount, email);
if (success) {
// 修复:添加异常处理
try {
Main.showExamPage(email);
} catch (Exception e) {
e.printStackTrace();
showAlert("无法进入考试页面");
}
} else {
showAlert("生成试卷失败,请重试");
}
} catch (NumberFormatException e) {
showAlert("请输入有效的数字");
}
});
}
private void showAlert(String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle("错误");
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

@ -0,0 +1,121 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.UserService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class LoginController {
@FXML private TextField identifierField; // 修改改为identifier
@FXML private PasswordField passwordField;
@FXML private Button loginBtn;
@FXML private Button registerBtn;
@FXML private Button forgotPasswordBtn;
@FXML private Label statusLabel;
private final UserService userService = UserService.getInstance();
@FXML
public void onLogin(){
String identifier = identifierField.getText().trim();
String pwd = passwordField.getText();
if(identifier.isEmpty() || pwd.isEmpty()){
statusLabel.setText("请输入用户名/邮箱和密码");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if(!userService.userExists(identifier)){
statusLabel.setText("账号不存在,请先注册");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 获取用户邮箱(用于后续操作)
String email = identifier;
if (!identifier.contains("@")) {
// 如果是用户名登录,需要获取对应的邮箱
var user = userService.getUserByIdentifier(identifier);
if (user != null) {
email = user.getEmail();
}
}
if(!userService.isUserRegistered(email)){
statusLabel.setText("该账号未完成注册,请完成注册流程");
statusLabel.setStyle("-fx-text-fill: orange;");
return;
}
boolean ok = userService.checkPassword(identifier, pwd);
if(ok){
statusLabel.setText("登录成功");
statusLabel.setStyle("-fx-text-fill: green;");
try {
Main.showDashboard(email);
} catch(Exception e){
e.printStackTrace();
}
} else {
statusLabel.setText("密码错误");
statusLabel.setStyle("-fx-text-fill: red;");
}
}
@FXML
public void onRegister(){
try {
Main.showRegister();
} catch(Exception e){
e.printStackTrace();
}
}
@FXML
public void onForgotPassword() {
String identifier = identifierField.getText().trim();
if (identifier.isEmpty()) {
statusLabel.setText("请输入用户名/邮箱");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (!userService.userExists(identifier)) {
statusLabel.setText("该账号未注册,请先注册");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 获取用户邮箱
String email = identifier;
if (!identifier.contains("@")) {
var user = userService.getUserByIdentifier(identifier);
if (user != null) {
email = user.getEmail();
}
}
if (!userService.isUserRegistered(email)) {
statusLabel.setText("该账号未完成注册,请先完成注册");
statusLabel.setStyle("-fx-text-fill: orange;");
return;
}
try {
Main.showForgotPassword(email);
} catch (Exception e) {
e.printStackTrace();
statusLabel.setText("系统错误: " + e.getMessage());
statusLabel.setStyle("-fx-text-fill: red;");
}
}
@FXML
public void onClear() {
identifierField.clear();
passwordField.clear();
statusLabel.setText("");
}
}

@ -0,0 +1,142 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.EmailService;
import com.example.myapp.service.UserService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import java.util.Random;
public class RegisterController {
@FXML private TextField emailField;
@FXML private Button sendCodeBtn;
@FXML private TextField codeField;
@FXML private Button verifyBtn;
@FXML private Label statusLabel;
private final UserService userService = UserService.getInstance();
private EmailService emailService;
public RegisterController(){
try {
emailService = new EmailService();
} catch(Exception e){
e.printStackTrace();
}
}
@FXML
public void onSendCode(){
String email = emailField.getText().trim();
// 验证邮箱格式
if(email.isEmpty()){
statusLabel.setText("请输入邮箱地址");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if(!isValidEmail(email)){
statusLabel.setText("请输入有效的邮箱地址");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 检查邮箱是否已被注册
if(userService.emailExists(email)){
// 检查用户是否已完成注册(设置了密码)
if(userService.isUserRegistered(email)){
statusLabel.setText("该邮箱已被注册,请直接登录");
statusLabel.setStyle("-fx-text-fill: red;");
return;
} else {
// 用户已存在但未完成注册,可以重新发送验证码
statusLabel.setText("该邮箱正在注册流程中,重新发送验证码");
statusLabel.setStyle("-fx-text-fill: orange;");
}
}
String code = generateCode();
userService.createPendingUser(email, code);
try{
emailService.sendRegistrationCode(email, code);
statusLabel.setText("注册码已发送到您的邮箱,请查收");
statusLabel.setStyle("-fx-text-fill: green;");
// 禁用发送按钮一段时间,防止重复发送
disableSendButtonTemporarily();
}catch(Exception e){
e.printStackTrace();
statusLabel.setText("发送失败: " + e.getMessage());
statusLabel.setStyle("-fx-text-fill: red;");
}
}
private String generateCode(){
Random r = new Random();
int v = 100000 + r.nextInt(900000);
return String.valueOf(v);
}
@FXML
public void onVerify(){
String email = emailField.getText().trim();
String code = codeField.getText().trim();
if(email.isEmpty() || code.isEmpty()){
statusLabel.setText("请输入邮箱和验证码");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if(userService.verifyCode(email, code)){
statusLabel.setText("验证通过,请设置密码");
statusLabel.setStyle("-fx-text-fill: green;");
try {
Main.showSetPassword(email);
} catch(Exception e){
e.printStackTrace();
}
} else {
statusLabel.setText("注册码错误或已过期");
statusLabel.setStyle("-fx-text-fill: red;");
}
}
@FXML
public void onClear() {
emailField.clear();
codeField.clear();
statusLabel.setText("");
sendCodeBtn.setDisable(false);
}
@FXML
public void onBackToLogin() {
try {
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
private boolean isValidEmail(String email) {
String emailRegex = "^[a-zA-Z0-9_+&*-]+(?:\\.[a-zA-Z0-9_+&*-]+)*@(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,7}$";
return email.matches(emailRegex);
}
private void disableSendButtonTemporarily() {
sendCodeBtn.setDisable(true);
new java.util.Timer().schedule(
new java.util.TimerTask() {
@Override
public void run() {
javafx.application.Platform.runLater(() -> {
sendCodeBtn.setDisable(false);
});
}
},
60000 // 60秒后才能重新发送
);
}
}

@ -0,0 +1,65 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.ExamService;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class ScoreController {
@FXML private Label scoreLabel;
@FXML private Label commentLabel;
@FXML private Button continueBtn;
@FXML private Button exitBtn;
private String email;
private int score;
private ExamService examService = ExamService.getInstance();
public void initScore(String email, int score) {
this.email = email;
this.score = score;
scoreLabel.setText("得分: " + score + " 分");
// 根据分数给出评价
if (score >= 90) {
commentLabel.setText("优秀!表现非常出色!");
commentLabel.setStyle("-fx-text-fill: #27ae60;");
} else if (score >= 80) {
commentLabel.setText("良好!继续努力!");
commentLabel.setStyle("-fx-text-fill: #2980b9;");
} else if (score >= 60) {
commentLabel.setText("及格!还有提升空间!");
commentLabel.setStyle("-fx-text-fill: #f39c12;");
} else {
commentLabel.setText("不及格!需要加强学习!");
commentLabel.setStyle("-fx-text-fill: #e74c3c;");
}
}
@FXML
public void onContinue() {
// 清除当前考试数据
examService.clearExam(email);
// 返回选择界面
try {
Main.showGradeSelection(email);
} catch (Exception e) {
e.printStackTrace();
}
}
@FXML
public void onExit() {
// 清除考试数据
examService.clearExam(email);
// 返回登录界面
try {
Main.showLogin();
} catch (Exception e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,83 @@
package com.example.myapp.controller;
import com.example.myapp.Main;
import com.example.myapp.service.UserService;
import com.example.myapp.util.PasswordUtil;
import javafx.fxml.FXML;
import javafx.scene.control.*;
public class SetPasswordController {
@FXML private Label emailLabel;
@FXML private TextField usernameField;
@FXML private PasswordField pwdField;
@FXML private PasswordField pwdConfirmField;
@FXML private Button setBtn;
@FXML private Label statusLabel;
private String email;
private final UserService userService = UserService.getInstance();
public void setEmail(String email){
this.email = email;
emailLabel.setText("设置账号信息 - " + email);
}
@FXML
public void onSetPassword(){
String username = usernameField.getText().trim();
String p1 = pwdField.getText();
String p2 = pwdConfirmField.getText();
// 验证用户名
if (username.isEmpty()) {
statusLabel.setText("请输入用户名");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if (username.length() < 2 || username.length() > 20) {
statusLabel.setText("用户名长度应为2-20个字符");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 检查用户名是否已存在
if (userService.usernameExists(username)) {
statusLabel.setText("用户名已存在,请选择其他用户名");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 验证密码
if(!p1.equals(p2)){
statusLabel.setText("两次密码不一致");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
if(!PasswordUtil.validatePasswordRules(p1)){
statusLabel.setText("密码不符合规则6-10位须含大写、小写和数字");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
// 设置用户名和密码
boolean usernameSet = userService.setUsername(email, username);
if (!usernameSet) {
statusLabel.setText("设置用户名失败");
statusLabel.setStyle("-fx-text-fill: red;");
return;
}
String hash = PasswordUtil.hash(p1);
userService.setPassword(email, hash);
statusLabel.setText("注册成功!即将进入用户中心");
statusLabel.setStyle("-fx-text-fill: green;");
try {
Main.showDashboard(email);
} catch(Exception e){
e.printStackTrace();
}
}
}

@ -0,0 +1,40 @@
package com.example.myapp.model;
import java.util.List;
import java.time.LocalDateTime;
public class Exam {
private String gradeLevel;
private int questionCount;
private List<Question> questions;
private int score;
private LocalDateTime startTime;
private LocalDateTime endTime;
public Exam(String gradeLevel, int questionCount, List<Question> questions) {
this.gradeLevel = gradeLevel;
this.questionCount = questionCount;
this.questions = questions;
this.startTime = LocalDateTime.now();
this.score = -1; // -1表示未评分
}
// Getters and Setters
public String getGradeLevel() { return gradeLevel; }
public void setGradeLevel(String gradeLevel) { this.gradeLevel = gradeLevel; }
public int getQuestionCount() { return questionCount; }
public void setQuestionCount(int questionCount) { this.questionCount = questionCount; }
public List<Question> getQuestions() { return questions; }
public void setQuestions(List<Question> questions) { this.questions = questions; }
public int getScore() { return score; }
public void setScore(int score) { this.score = score; }
public LocalDateTime getStartTime() { return startTime; }
public void setStartTime(LocalDateTime startTime) { this.startTime = startTime; }
public LocalDateTime getEndTime() { return endTime; }
public void setEndTime(LocalDateTime endTime) { this.endTime = endTime; }
}

@ -0,0 +1,14 @@
// Expression.java
package com.example.myapp.model;
public class Expression {
public String expression;
public double value; // 改为 double 类型支持小数答案
public String mainOperator;
public Expression(String expression, double value, String mainOperator) {
this.expression = expression;
this.value = value;
this.mainOperator = mainOperator;
}
}

@ -0,0 +1,52 @@
package com.example.myapp.model;
public class Question {
private String questionText;
private String optionA;
private String optionB;
private String optionC;
private String optionD;
private String correctAnswer;
private String userAnswer;
// public Question(String questionText, String optionA, String optionB,
// String optionC, String optionD, String correctAnswer) {
// this.questionText = questionText;
// this.optionA = optionA;
// this.optionB = optionB;
// this.optionC = optionC;
// this.optionD = optionD;
// this.correctAnswer = correctAnswer;
// }
// Getters and Setters
public Question(String questionText, String optionA, String optionB,
String optionC, String optionD, String correctAnswer) {
this.questionText = questionText;
this.optionA = optionA;
this.optionB = optionB;
this.optionC = optionC;
this.optionD = optionD;
this.correctAnswer = correctAnswer;
}
public String getQuestionText() { return questionText; }
public void setQuestionText(String questionText) { this.questionText = questionText; }
public String getOptionA() { return optionA; }
public void setOptionA(String optionA) { this.optionA = optionA; }
public String getOptionB() { return optionB; }
public void setOptionB(String optionB) { this.optionB = optionB; }
public String getOptionC() { return optionC; }
public void setOptionC(String optionC) { this.optionC = optionC; }
public String getOptionD() { return optionD; }
public void setOptionD(String optionD) { this.optionD = optionD; }
public String getCorrectAnswer() { return correctAnswer; }
public void setCorrectAnswer(String correctAnswer) { this.correctAnswer = correctAnswer; }
public String getUserAnswer() { return userAnswer; }
public void setUserAnswer(String userAnswer) { this.userAnswer = userAnswer; }
}

@ -0,0 +1,62 @@
package com.example.myapp.model;
import java.io.Serializable;
import java.time.LocalDateTime;
public class User implements Serializable {
private String email;
private String username; // 新增用户名字段
private String passwordHash;
private boolean verified;
private LocalDateTime createdAt;
private LocalDateTime lastModified;
public User() {
this.createdAt = LocalDateTime.now();
this.lastModified = LocalDateTime.now();
}
public User(String email, String username) {
this.email = email;
this.username = username;
this.verified = false;
this.createdAt = LocalDateTime.now();
this.lastModified = LocalDateTime.now();
}
// Getters and Setters
public String getEmail() { return email; }
public void setEmail(String email) {
this.email = email;
this.lastModified = LocalDateTime.now();
}
public String getUsername() { return username; }
public void setUsername(String username) {
this.username = username;
this.lastModified = LocalDateTime.now();
}
public String getPasswordHash() { return passwordHash; }
public void setPasswordHash(String passwordHash) {
this.passwordHash = passwordHash;
this.lastModified = LocalDateTime.now();
}
public boolean isVerified() { return verified; }
public void setVerified(boolean verified) {
this.verified = verified;
this.lastModified = LocalDateTime.now();
}
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getLastModified() { return lastModified; }
public void setLastModified(LocalDateTime lastModified) { this.lastModified = lastModified; }
@Override
public String toString() {
return "User{email='" + email + "', username='" + username + "', verified=" + verified + "}";
}
}

@ -0,0 +1,80 @@
// AbstractQuestionSeting.java
package com.example.myapp.service;
import com.example.myapp.model.Expression;
import java.util.Random;
public abstract class AbstractQuestionSeting implements QuestionSeting {
protected Random rand = new Random();
// 只生成整数1-50
protected String getRandomNumber() {
int num = 1 + rand.nextInt(50);
return String.valueOf(num);
}
// 生成完全平方数(用于开方运算)
protected String getPerfectSquare() {
int base = 1 + rand.nextInt(12); // 1-12的平方
int square = base * base;
return String.valueOf(square);
}
// 生成适合开方的数(完全平方数)
protected String getNumberForSqrt() {
// 80%概率生成完全平方数20%概率生成普通数
//if (rand.nextDouble() < 1) {
return getPerfectSquare();
// } else {
// return getRandomNumber();
// }
}
protected String getRandomOperator() {
String[] ops = {"+", "-", "*", "/"};
return ops[rand.nextInt(ops.length)];
}
protected int getPriority(String op) {
if (op == null) return 3;
switch (op) {
case "+":
case "-":
return 1;
case "*":
case "/":
return 2;
case "²":
case "√":
case "sin":
case "cos":
case "tan":
return 3;
default:
return 0;
}
}
// 解析数字字符串为double
protected double parseNumber(String numStr) {
return Double.parseDouble(numStr);
}
// 格式化结果为字符串,如果是整数显示整数,小数显示小数
protected String formatResult(double value) {
if (value == (int) value) {
return String.valueOf((int) value);
} else {
// 保留2位小数但去除末尾的0
String formatted = String.format("%.2f", value);
if (formatted.endsWith(".00")) {
return formatted.substring(0, formatted.length() - 3);
} else if (formatted.endsWith("0")) {
return formatted.substring(0, formatted.length() - 1);
}
return formatted;
}
}
public abstract String addParenthesesIfNeeded(Expression child, String parentOp, boolean isRightChild);
}

@ -0,0 +1,46 @@
package com.example.myapp.service;
import jakarta.mail.*;
import jakarta.mail.internet.*;
import java.io.InputStream;
import java.util.Properties;
public class EmailService {
private final Properties props = new Properties();
private final String username;
private final String password;
private final String from;
public EmailService() throws Exception{
try(InputStream is = getClass().getResourceAsStream("/app.properties")){
Properties p = new Properties();
p.load(is);
String host = p.getProperty("smtp.host");
String port = p.getProperty("smtp.port");
username = p.getProperty("smtp.username");
password = p.getProperty("smtp.password");
from = p.getProperty("smtp.from");
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true");
props.put("mail.smtp.host", host);
props.put("mail.smtp.port", port);
}
}
public void sendRegistrationCode(String toEmail, String code) throws MessagingException {
Session session = Session.getInstance(props, new Authenticator(){
protected PasswordAuthentication getPasswordAuthentication(){
return new PasswordAuthentication(username, password);
}
});
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(from));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject("你的注册码");
message.setText("你的注册码是: " + code + "\n\n请输入此注册码完成注册。");
Transport.send(message);
}
}

@ -0,0 +1,90 @@
// ExamService.java
package com.example.myapp.service;
import com.example.myapp.model.Exam;
import com.example.myapp.model.Question;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.Map;
public class ExamService {
private Map<String, Exam> userExams = new ConcurrentHashMap<>();
private static final ExamService instance = new ExamService();
public static ExamService getInstance() {
return instance;
}
public boolean generateExam(String gradeLevel, int questionCount, String email) {
try {
List<Question> questions = MathQuestionGenerator.generateQuestions(gradeLevel, questionCount);
Exam exam = new Exam(gradeLevel, questionCount, questions);
userExams.put(email, exam);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
public Exam getCurrentExam(String email) {
return userExams.get(email);
}
public boolean submitAnswer(String email, int questionIndex, String selectedAnswer) {
Exam exam = userExams.get(email);
if (exam != null && questionIndex >= 0 && questionIndex < exam.getQuestions().size()) {
exam.getQuestions().get(questionIndex).setUserAnswer(selectedAnswer);
return true;
}
return false;
}
// 在 ExamService.java 的 calculateScore 方法中修改答案比较逻辑
public int calculateScore(String email) {
Exam exam = userExams.get(email);
if (exam == null) return 0;
int correctCount = 0;
for (Question question : exam.getQuestions()) {
String userAnswer = question.getUserAnswer();
String correctAnswer = question.getCorrectAnswer();
// 使用容差比较答案(针对小数)
if (isAnswerCorrect(userAnswer, correctAnswer)) {
correctCount++;
}
}
int score = (int) ((correctCount * 100.0) / exam.getQuestions().size());
exam.setScore(score);
return score;
}
// 新增答案比较方法,支持小数容差
private boolean isAnswerCorrect(String userAnswer, String correctAnswer) {
if (userAnswer == null || correctAnswer == null) {
return false;
}
try {
// 去除选项标签(如"A. "
String cleanUserAnswer = userAnswer.replaceAll("^[A-D]\\.\\s*", "");
String cleanCorrectAnswer = correctAnswer.replaceAll("^[A-D]\\.\\s*", "");
// 尝试解析为数字进行比较
double userValue = Double.parseDouble(cleanUserAnswer);
double correctValue = Double.parseDouble(cleanCorrectAnswer);
// 使用容差比较
return Math.abs(userValue - correctValue) < 0.01;
} catch (NumberFormatException e) {
// 如果无法解析为数字,进行字符串比较
return userAnswer.trim().equals(correctAnswer.trim());
}
}
public void clearExam(String email) {
userExams.remove(email);
}
}

@ -0,0 +1,104 @@
package com.example.myapp.service;
import com.example.myapp.model.User;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class FileStorageService {
private static final String DATA_DIR = "data";
private static final String USERS_FILE = DATA_DIR + "/users.json";
private static final String REGISTRATION_CODES_FILE = DATA_DIR + "/registration_codes.json";
private static final String RESET_PASSWORD_CODES_FILE = DATA_DIR + "/reset_password_codes.json";
private final ObjectMapper objectMapper;
public FileStorageService() {
this.objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.enable(SerializationFeature.INDENT_OUTPUT);
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
try {
Files.createDirectories(Paths.get(DATA_DIR));
} catch (IOException e) {
e.printStackTrace();
}
}
// 读取用户数据
public Map<String, User> readUsersData() {
try {
File file = new File(USERS_FILE);
if (!file.exists()) {
return new ConcurrentHashMap<>();
}
return objectMapper.readValue(file, new TypeReference<ConcurrentHashMap<String, User>>() {});
} catch (Exception e) {
e.printStackTrace();
return new ConcurrentHashMap<>();
}
}
// 读取注册码数据
public Map<String, String> readRegistrationCodesData() {
try {
File file = new File(REGISTRATION_CODES_FILE);
if (!file.exists()) {
return new ConcurrentHashMap<>();
}
return objectMapper.readValue(file, new TypeReference<ConcurrentHashMap<String, String>>() {});
} catch (Exception e) {
e.printStackTrace();
return new ConcurrentHashMap<>();
}
}
// 新增:读取重置密码码数据
public Map<String, String> readResetPasswordCodesData() {
try {
File file = new File(RESET_PASSWORD_CODES_FILE);
if (!file.exists()) {
return new ConcurrentHashMap<>();
}
return objectMapper.readValue(file, new TypeReference<ConcurrentHashMap<String, String>>() {});
} catch (Exception e) {
e.printStackTrace();
return new ConcurrentHashMap<>();
}
}
// 写入用户数据
public void writeUsersData(Map<String, User> data) {
try {
objectMapper.writeValue(new File(USERS_FILE), data);
} catch (IOException e) {
e.printStackTrace();
}
}
// 写入注册码数据
public void writeRegistrationCodesData(Map<String, String> data) {
try {
objectMapper.writeValue(new File(REGISTRATION_CODES_FILE), data);
} catch (IOException e) {
e.printStackTrace();
}
}
// 新增:写入重置密码码数据
public void writeResetPasswordCodesData(Map<String, String> data) {
try {
objectMapper.writeValue(new File(RESET_PASSWORD_CODES_FILE), data);
} catch (IOException e) {
e.printStackTrace();
}
}
}

@ -0,0 +1,139 @@
// HighQueSeting.java
package com.example.myapp.service;
import com.example.myapp.model.Expression;
public class HighQueSeting extends AbstractQuestionSeting {
@Override
public String addParenthesesIfNeeded(Expression child, String parentOp, boolean isRightChild) {
if (child.mainOperator == null
|| child.mainOperator.equals("²") || child.mainOperator.equals("√")
|| child.mainOperator.equals("sin") || child.mainOperator.equals("cos")
|| child.mainOperator.equals("tan")) {
return child.expression;
}
int parentPriority = getPriority(parentOp);
int childPriority = getPriority(child.mainOperator);
if (childPriority < parentPriority) {
return "(" + child.expression + ")";
}
if (isRightChild && (parentOp.equals("-") || parentOp.equals("/"))) {
if (parentPriority == childPriority) {
return "(" + child.expression + ")";
}
}
return child.expression;
}
public Expression applyUnary(Expression child, String op) {
switch (op) {
case "²":
if (child.mainOperator == null) {
return new Expression(child.expression + "²", child.value * child.value, "²");
}
return new Expression("(" + child.expression + ")²", child.value * child.value, "²");
case "√":
String numStr = getNumberForSqrt();
child = new Expression(numStr, parseNumber(numStr), null);
double sqrtValue = Math.sqrt(child.value);
if (child.mainOperator == null) {
return new Expression("√" + child.expression, sqrtValue, "√");
}
return new Expression("√(" + child.expression + ")", sqrtValue, "√");
case "sin":
int[] sinAngles = {0, 30, 45, 60, 90, 120, 135, 150, 180};
int sinAngle = sinAngles[rand.nextInt(sinAngles.length)];
child = new Expression(String.valueOf(sinAngle), sinAngle, null);
return new Expression("sin(" + child.expression + "°)",
Math.sin(Math.toRadians(child.value)), "sin");
case "cos":
int[] cosAngles = {0, 30, 45, 60, 90, 120, 135, 150, 180};
int cosAngle = cosAngles[rand.nextInt(cosAngles.length)];
child = new Expression(String.valueOf(cosAngle), cosAngle, null);
return new Expression("cos(" + child.expression + "°)",
Math.cos(Math.toRadians(child.value)), "cos");
case "tan":
int[] tanAngles = {0, 30, 45, 60, 120, 135, 150, 180};
int tanAngle = tanAngles[rand.nextInt(tanAngles.length)];
child = new Expression(String.valueOf(tanAngle), tanAngle, null);
double tanValue = Math.tan(Math.toRadians(child.value));
if (Math.abs(tanValue) > 1000) {
tanValue = tanValue > 0 ? 1000 : -1000;
}
return new Expression("tan(" + child.expression + "°)", tanValue, "tan");
default:
return child;
}
}
@Override
public Expression setQuestion(int count) {
Expression result = firstSetQuestion(count);
// 确保高中题目包含至少一个三角函数
int attempts = 0;
while (!result.expression.contains("sin") && !result.expression.contains("cos")
&& !result.expression.contains("tan") && attempts < 10) {
result = firstSetQuestion(count);
attempts++;
}
return result;
}
public Expression probability(Expression result) {
// 高中阶段40%概率添加一元运算
if (rand.nextDouble() < 0.4) {
String[] unaryOps = {"²", "√", "sin", "cos", "tan"};
String unaryOp = unaryOps[rand.nextInt(unaryOps.length)];
result = applyUnary(result, unaryOp);
result.mainOperator = unaryOp;
}
return result;
}
public Expression firstSetQuestion(int count) {
if (count == 1) {
String numStr = getRandomNumber();
Expression expr = new Expression(numStr, parseNumber(numStr), null);
expr = probability(expr);
return expr;
}
int leftCount = 1 + rand.nextInt(count - 1);
int rightCount = count - leftCount;
Expression left = firstSetQuestion(leftCount);
Expression right = firstSetQuestion(rightCount);
String op = getRandomOperator();
double value = 0;
switch (op) {
case "+":
value = left.value + right.value;
break;
case "-":
value = left.value - right.value;
break;
case "*":
value = left.value * right.value;
break;
case "/":
if (Math.abs(right.value) < 1e-10) {
return firstSetQuestion(count);
}
value = left.value / right.value;
break;
}
String leftExpr = addParenthesesIfNeeded(left, op, false);
String rightExpr = addParenthesesIfNeeded(right, op, true);
Expression result = new Expression(leftExpr + " " + op + " " + rightExpr, value, op);
result = probability(result);
return result;
}
}

@ -0,0 +1,139 @@
// MathQuestionGenerator.java
package com.example.myapp.service;
import com.example.myapp.model.Question;
import com.example.myapp.model.Expression;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
public class MathQuestionGenerator {
private static final Random random = new Random();
public static List<Question> generateQuestions(String gradeLevel, int count) {
List<Question> questions = new ArrayList<>();
QueSetingFactory factory = new QueSetingFactory();
QuestionSeting questionSeting = factory.getQueSeting(gradeLevel);
Set<String> generatedExpressions = new HashSet<>();
int maxAttempts = count * 3; // 最大尝试次数,防止无限循环
for (int i = 0; i < count; i++) {
boolean questionGenerated = false;
int attempts = 0;
while (!questionGenerated && attempts < maxAttempts) {
try {
int nodeCount = 2 + random.nextInt(3);
Expression expression = questionSeting.setQuestion(nodeCount);
if (generatedExpressions.contains(expression.expression)) {
attempts++;
continue; // 如果重复,重新生成
}
String correctAnswer = formatValue(expression.value);
String questionText = "计算: " + expression.expression + " = ?";
List<String> options = generateOptions(expression.value);
Question question = new Question(
questionText, options.get(0), // A
options.get(1), // B
options.get(2), // C
options.get(3), // D
getCorrectAnswerLabel(options, correctAnswer) // 正确答案标签
);
questions.add(question);
generatedExpressions.add(expression.expression); // 记录已生成的题目
questionGenerated = true;
} catch (Exception e) {
System.err.println("生成题目失败: " + e.getMessage());
attempts++;
}
}
if (!questionGenerated) {
questions.add(createFallbackQuestion(i, generatedExpressions));
}
}
return questions;
}
// 格式化数值整数显示整数小数显示小数去除末尾的0
private static String formatValue(double value) {
if (value == (int) value) {
return String.valueOf((int) value);
} else {
// 保留2位小数但去除末尾的0
String formatted = String.format("%.2f", value);
if (formatted.endsWith(".00")) {
return formatted.substring(0, formatted.length() - 3);
} else if (formatted.endsWith("0")) {
return formatted.substring(0, formatted.length() - 1);
}
return formatted;
}
}
private static List<String> generateOptions(double correctValue) {
List<String> options = new ArrayList<>();
String correctAnswerStr = formatValue(correctValue);
options.add(correctAnswerStr); // 正确答案
// 生成3个错误答案
while (options.size() < 4) {
double deviation;
// 根据正确答案的大小调整偏差
if (Math.abs(correctValue) < 10) {
// 小数值使用较小的偏差
deviation = 0.5 + random.nextDouble() * 3.0;
} else {
// 大数值使用较大的偏差
deviation = 1 + random.nextInt(5);
}
boolean positive = random.nextBoolean();
double wrongValue = positive ? correctValue + deviation : correctValue - deviation;
// 确保错误答案合理且与正确答案不同
String wrongValueStr = formatValue(wrongValue);
if (!options.contains(wrongValueStr)) {
options.add(wrongValueStr);
}
}
Collections.shuffle(options);
return options;
}
private static String getCorrectAnswerLabel(List<String> options, String correctAnswer) {
for (int i = 0; i < options.size(); i++) {
if (options.get(i).equals(correctAnswer)) {
return String.valueOf((char)('A' + i));
}
}
return "D";
}
// 修改备用题目生成方法,也避免重复
private static Question createFallbackQuestion(int index, Set<String> generatedExpressions) {
int baseNum = index + 1;
String expression;
// 尝试生成不重复的简单表达式
if (!generatedExpressions.contains(baseNum + " + " + (baseNum + 1))) {
expression = baseNum + " + " + (baseNum + 1);
} else if (!generatedExpressions.contains(baseNum + " × " + (baseNum + 2))) {
expression = baseNum + " × " + (baseNum + 2);
} else {
expression = (baseNum + 5) + " - " + baseNum;
}
generatedExpressions.add(expression);
return new Question(
"计算: " + expression + " = ?",
"A. " + (2 * baseNum + 1),
"B. " + (2 * baseNum + 2),
"C. " + (2 * baseNum + 3),
"D. " + (2 * baseNum + 4),
"C"
);
}
}

@ -0,0 +1,113 @@
// MiddleQueSeting.java
package com.example.myapp.service;
import com.example.myapp.model.Expression;
public class MiddleQueSeting extends AbstractQuestionSeting {
public Expression applyUnary(Expression child, String op) {
switch (op) {
case "²":
if (child.mainOperator == null) {
return new Expression(child.expression + "²", child.value * child.value, "²");
}
return new Expression("(" + child.expression + ")²", child.value * child.value, "²");
case "√":
// 使用适合开方的数(主要是完全平方数)
String numStr = getNumberForSqrt();
child = new Expression(numStr, parseNumber(numStr), null);
double sqrtValue = Math.sqrt(child.value);
if (child.mainOperator == null) {
return new Expression("√" + child.expression, sqrtValue, "√");
}
return new Expression("√(" + child.expression + ")", sqrtValue, "√");
default:
return child;
}
}
@Override
public String addParenthesesIfNeeded(Expression child, String parentOp, boolean isRightChild) {
if (child.mainOperator == null || child.mainOperator.equals("²") || child.mainOperator.equals("√")) {
return child.expression;
}
int parentPriority = getPriority(parentOp);
int childPriority = getPriority(child.mainOperator);
if (childPriority < parentPriority) {
return "(" + child.expression + ")";
}
if (isRightChild && (parentOp.equals("-") || parentOp.equals("/"))) {
if (parentPriority == childPriority) {
return "(" + child.expression + ")";
}
}
return child.expression;
}
@Override
public Expression setQuestion(int count) {
Expression result = firstSetQuestion(count);
// 确保初中题目包含平方或开方
while (!result.expression.contains("²") && !result.expression.contains("√")) {
result = firstSetQuestion(count);
}
return result;
}
public Expression firstSetQuestion(int count) {
if (count == 1) {
String numStr = getRandomNumber();
Expression expr = new Expression(numStr, parseNumber(numStr), null);
return probability(expr);
}
int leftCount = 1 + rand.nextInt(count - 1);
int rightCount = count - leftCount;
Expression left = firstSetQuestion(leftCount);
Expression right = firstSetQuestion(rightCount);
String op = getRandomOperator();
double value = 0;
switch (op) {
case "+":
value = left.value + right.value;
break;
case "-":
value = left.value - right.value;
if (value < 0) {
Expression temp = left;
left = right;
right = temp;
value = left.value - right.value;
}
break;
case "*":
value = left.value * right.value;
break;
case "/":
if (Math.abs(right.value) < 1e-10) {
right = firstSetQuestion(rightCount);
}
value = left.value / right.value;
break;
}
String leftExpr = addParenthesesIfNeeded(left, op, false);
String rightExpr = addParenthesesIfNeeded(right, op, true);
Expression result = new Expression(leftExpr + " " + op + " " + rightExpr, value, op);
result = probability(result);
return result;
}
public Expression probability(Expression result) {
if (rand.nextDouble() < 0.3) {
String[] unaryOps = {"²", "√"};
String unaryOp = unaryOps[rand.nextInt(unaryOps.length)];
result = applyUnary(result, unaryOp);
result.mainOperator = unaryOp;
}
return result;
}
}

@ -0,0 +1,69 @@
// PrimaryQueSeting.java
package com.example.myapp.service;
import com.example.myapp.model.Expression;
public class PrimaryQueSeting extends AbstractQuestionSeting {
@Override
public Expression setQuestion(int count) {
if (count == 1) {
String expr = getRandomNumber();
return new Expression(expr, parseNumber(expr), null);
}
int leftCount = 1 + rand.nextInt(count - 1);
int rightCount = count - leftCount;
Expression left = setQuestion(leftCount);
Expression right = setQuestion(rightCount);
String op = getRandomOperator();
// 处理除数为0的情况
while (op.equals("/") && Math.abs(right.value) < 1e-10) {
right = setQuestion(rightCount);
}
// 确保减法结果不为负数
if (op.equals("-") && left.value < right.value) {
Expression temp = left;
left = right;
right = temp;
}
String leftExpr = addParenthesesIfNeeded(left, op, false);
String rightExpr = addParenthesesIfNeeded(right, op, true);
double value = switch (op) {
case "+" -> left.value + right.value;
case "-" -> left.value - right.value;
case "*" -> left.value * right.value;
case "/" -> left.value / right.value;
default -> 0;
};
return new Expression(leftExpr + " " + op + " " + rightExpr, value, op);
}
@Override
public String addParenthesesIfNeeded(Expression child, String parentOp, boolean isRightChild) {
if (child.mainOperator == null) {
return child.expression;
}
int parentPriority = getPriority(parentOp);
int childPriority = getPriority(child.mainOperator);
if (childPriority < parentPriority) {
return "(" + child.expression + ")";
}
if (isRightChild && (parentOp.equals("-") || parentOp.equals("/"))) {
if (parentPriority == childPriority) {
return "(" + child.expression + ")";
}
}
return child.expression;
}
}

@ -0,0 +1,18 @@
// QueSetingFactory.java
package com.example.myapp.service;
public class QueSetingFactory {
public QuestionSeting getQueSeting(String type) {
switch (type) {
case "小学":
return new PrimaryQueSeting();
case "初中":
return new MiddleQueSeting();
case "高中":
return new HighQueSeting();
default:
System.out.println("类型错误");
return null;
}
}
}

@ -0,0 +1,8 @@
// QuestionSeting.java
package com.example.myapp.service;
import com.example.myapp.model.Expression;
public interface QuestionSeting {
Expression setQuestion(int count);
}

@ -0,0 +1,181 @@
package com.example.myapp.service;
import com.example.myapp.model.User;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class UserService {
private Map<String, User> users;
private Map<String, String> usernameToEmail;
private Map<String, String> registrationCodes;
private Map<String, String> resetPasswordCodes;
private final FileStorageService fileStorageService;
private static final UserService instance = new UserService();
public static UserService getInstance() {
return instance;
}
private UserService() {
this.fileStorageService = new FileStorageService();
loadData();
}
private void loadData() {
users = fileStorageService.readUsersData();
registrationCodes = fileStorageService.readRegistrationCodesData();
resetPasswordCodes = fileStorageService.readResetPasswordCodesData();
usernameToEmail = new ConcurrentHashMap<>();
if (users != null) {
for (User user : users.values()) {
if (user.getUsername() != null && !user.getUsername().trim().isEmpty()) {
usernameToEmail.put(user.getUsername().toLowerCase(), user.getEmail());
}
}
}
if (users == null) users = new ConcurrentHashMap<>();
if (registrationCodes == null) registrationCodes = new ConcurrentHashMap<>();
if (resetPasswordCodes == null) resetPasswordCodes = new ConcurrentHashMap<>();
if (usernameToEmail == null) usernameToEmail = new ConcurrentHashMap<>();
}
private void saveUsers() {
fileStorageService.writeUsersData(users);
}
private void saveRegistrationCodes() {
fileStorageService.writeRegistrationCodesData(registrationCodes);
}
private void saveResetPasswordCodes() {
fileStorageService.writeResetPasswordCodesData(resetPasswordCodes);
}
public boolean emailExists(String email) {
return users.containsKey(email);
}
public boolean usernameExists(String username) {
return usernameToEmail.containsKey(username.toLowerCase());
}
public User getUserByIdentifier(String identifier) {
if (identifier.contains("@")) {
return users.get(identifier);
} else {
String email = usernameToEmail.get(identifier.toLowerCase());
return email != null ? users.get(email) : null;
}
}
public boolean userExists(String identifier) {
if (identifier.contains("@")) {
return emailExists(identifier);
} else {
return usernameExists(identifier);
}
}
public boolean isUserRegistered(String email) {
User user = users.get(email);
return user != null && user.getPasswordHash() != null && !user.getPasswordHash().isEmpty();
}
public void createPendingUser(String email, String code) {
if (users.containsKey(email)) {
User existingUser = users.get(email);
if (existingUser.getPasswordHash() == null || existingUser.getPasswordHash().isEmpty()) {
registrationCodes.put(email, code);
saveRegistrationCodes();
return;
}
}
users.putIfAbsent(email, new User(email, null));
registrationCodes.put(email, code);
saveUsers();
saveRegistrationCodes();
}
public boolean setUsername(String email, String username) {
if (username == null || username.trim().isEmpty()) {
return false;
}
String usernameLower = username.toLowerCase();
if (usernameToEmail.containsKey(usernameLower)) {
return false;
}
User user = users.get(email);
if (user != null) {
if (user.getUsername() != null) {
usernameToEmail.remove(user.getUsername().toLowerCase());
}
user.setUsername(username);
usernameToEmail.put(usernameLower, email);
saveUsers();
return true;
}
return false;
}
public boolean verifyCode(String email, String code) {
String storedCode = registrationCodes.get(email);
if (storedCode != null && storedCode.equals(code)) {
User u = users.get(email);
if (u != null) {
u.setVerified(true);
registrationCodes.remove(email);
saveUsers();
saveRegistrationCodes();
return true;
}
}
return false;
}
// 忘记密码相关方法
public void createResetPasswordCode(String email, String code) {
resetPasswordCodes.put(email, code);
saveResetPasswordCodes();
}
public boolean verifyResetPasswordCode(String email, String code) {
String storedCode = resetPasswordCodes.get(email);
if (storedCode != null && storedCode.equals(code)) {
resetPasswordCodes.remove(email);
saveResetPasswordCodes();
return true;
}
return false;
}
public void setPassword(String email, String passwordHash) {
User u = users.get(email);
if (u != null) {
u.setPasswordHash(passwordHash);
u.setVerified(true);
saveUsers();
}
}
public boolean checkPassword(String identifier, String plain) {
User user = getUserByIdentifier(identifier);
if (user == null || user.getPasswordHash() == null) {
return false;
}
return com.example.myapp.util.PasswordUtil.check(plain, user.getPasswordHash());
}
public String getDisplayName(String identifier) {
User user = getUserByIdentifier(identifier);
if (user == null) return identifier;
return user.getUsername() != null ? user.getUsername() : user.getEmail();
}
}

@ -0,0 +1,21 @@
package com.example.myapp.util;
import org.mindrot.jbcrypt.BCrypt;
import java.util.regex.Pattern;
public class PasswordUtil {
private static final Pattern validPattern = Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d).{6,10}$");
public static boolean validatePasswordRules(String pwd){
if(pwd==null) return false;
return validPattern.matcher(pwd).matches();
}
public static String hash(String plain){
return BCrypt.hashpw(plain, BCrypt.gensalt(12));
}
public static boolean check(String plain, String hash){
return BCrypt.checkpw(plain, hash);
}
}
Loading…
Cancel
Save