Compare commits

...

5 Commits

Binary file not shown.

@ -1,2 +1,144 @@
# Partner_Project
# 小初高数学学习系统
## 项目简介
这是一个基于Java Swing开发的数学学习软件专门为小学、初中和高中学生设计。系统提供个性化的数学题目生成、答题练习和成绩记录功能支持用户注册、登录和密码管理。
## 功能特性
### 用户管理
- **用户注册**:支持邮箱、用户名、密码和年级选择注册
- **用户登录**:支持邮箱或用户名登录
- **密码修改**:安全的密码修改功能
- **数据持久化**用户数据使用JSON格式存储
### 题目生成
- **分级题目**:根据用户年级(小学、初中、高中)生成相应难度的数学题目
- **智能去重**:避免重复题目,确保练习效果
- **题目保存**:自动保存生成的题目到用户专属文件夹
### 答题系统
- **选择题形式**每题提供4个选项
- **实时评分**:答题后立即显示结果
- **成绩记录**:记录每次练习的成绩
### 年级适配
- **小学**:基础四则运算
- **初中**:包含平方、平方根运算
- **高中**:包含三角函数运算
## 技术架构
### 开发环境
- **语言**Java 21
- **构建工具**Maven
- **GUI框架**Swing
- **数据格式**JSON
### 项目结构
```
src/
├── model/ # 数据模型层
│ ├── UserManager.java # 用户管理
│ ├── QuestionMaker.java # 题目生成器
│ ├── Question.java # 题目模型
│ ├── Grade.java # 年级枚举
│ ├── PrimaryMaker.java # 小学题目生成
│ ├── MiddleMaker.java # 初中题目生成
│ └── HighMaker.java # 高中题目生成
├── ui/ # 用户界面层
│ ├── MathLearningApp.java # 主程序入口
│ ├── LoginFrame.java # 登录界面
│ ├── RegisterDialog.java # 注册对话框
│ ├── GradeSelectionFrame.java # 年级选择界面
│ ├── QuizFrame.java # 答题界面
│ ├── ResultFrame.java # 结果界面
│ ├── UserSettingsDialog.java # 用户设置
│ └── ChangePasswordDialog.java # 修改密码
├── others/ # 资源文件
└── Math.exe # 可执行文件
```
### 依赖库
- **Gson 2.10.1**JSON数据处理
- **JavaMail 1.6.2**:邮件功能支持
- **JAF 1.1.1**JavaBeans激活框架
## 安装与运行
### 平台支持
![Windows](https://img.shields.io/badge/Windows-Supported-0078D6?logo=windows)
| 操作系统 | 版本要求 | 支持状态 | 应用 |
|---------|-------------|--------|---------|
| Windows | 11 | ✅ 完全支持 | exe应用程序 |
运行前,请确保系统已安装以下环境:
**Windows:**
- **openjdk** 21.0.5(2024-10-15 LTS)
- **OpenJDK Runtime Environment** build 21.0.5+11-LTS
- **OpenJDK 64-Bit Server VM** build 21.0.5+11-LTS, mixed mode, sharing
- **Maven** 3.6或更高版本
### 直接运行
- 双击Math.exe程序
### 编译运行
```bash
# 使用Maven编译
mvn compile
# 运行程序
mvn exec:java
# 打包为可执行JAR
mvn package
```
## 使用说明
### 首次使用
1. 启动程序后进入登录界面
2. 点击"注册"按钮创建新账户
3. 填写邮箱、验证码
4. 验证完成后填写用户名、密码并选择年级
5. 注册成功后返回登录界面登录
### 练习流程
1. 登录成功后选择年级系统会根据注册信息自动推荐以及题目数量10-30
2. 进入答题界面开始练习
3. 选择答案后点击“下一题”继续
4. 完成所有题目后自动生成并显示成绩
### 用户设置
- 在登录后界面可以修改密码
- 系统会自动保存练习记录
## 数据存储
### 用户数据
- 位置:`users.json`
- 格式JSON格式存储用户信息
### 题目记录
- 位置:`questions/{邮箱}/` 目录
- 格式:按时间戳命名的文本文件
## 开发说明
### 扩展题目类型
要添加新的题目类型,需要:
1. 在相应的`*Maker.java`类中添加题目生成逻辑
2. 在`QuestionMaker.java`中更新表达式解析逻辑
3. 如果需要新的年级,更新`Grade.java`枚举
### 自定义界面
所有UI组件都使用Swing开发可以通过修改对应的`*.java`文件来自定义界面样式和布局。
### 分工
后端:张一帆
前端:赵俊杰
邮箱注册实现:张一帆,赵俊杰
调试:赵俊杰,张一帆

@ -0,0 +1,143 @@
# 小初高数学学习系统
## 项目简介
这是一个基于Java Swing开发的数学学习软件专门为小学、初中和高中学生设计。系统提供个性化的数学题目生成、答题练习和成绩记录功能支持用户注册、登录和密码管理。
## 功能特性
### 用户管理
- **用户注册**:支持邮箱、用户名、密码和年级选择注册
- **用户登录**:支持邮箱或用户名登录
- **密码修改**:安全的密码修改功能
- **数据持久化**用户数据使用JSON格式存储
### 题目生成
- **分级题目**:根据用户年级(小学、初中、高中)生成相应难度的数学题目
- **智能去重**:避免重复题目,确保练习效果
- **题目保存**:自动保存生成的题目到用户专属文件夹
### 答题系统
- **选择题形式**每题提供4个选项
- **实时评分**:答题后立即显示结果
- **成绩记录**:记录每次练习的成绩
### 年级适配
- **小学**:基础四则运算
- **初中**:包含平方、平方根运算
- **高中**:包含三角函数运算
## 技术架构
### 开发环境
- **语言**Java 21
- **构建工具**Maven
- **GUI框架**Swing
- **数据格式**JSON
### 项目结构
```
src/
├── model/ # 数据模型层
│ ├── UserManager.java # 用户管理
│ ├── QuestionMaker.java # 题目生成器
│ ├── Question.java # 题目模型
│ ├── Grade.java # 年级枚举
│ ├── PrimaryMaker.java # 小学题目生成
│ ├── MiddleMaker.java # 初中题目生成
│ └── HighMaker.java # 高中题目生成
├── ui/ # 用户界面层
│ ├── MathLearningApp.java # 主程序入口
│ ├── LoginFrame.java # 登录界面
│ ├── RegisterDialog.java # 注册对话框
│ ├── GradeSelectionFrame.java # 年级选择界面
│ ├── QuizFrame.java # 答题界面
│ ├── ResultFrame.java # 结果界面
│ ├── UserSettingsDialog.java # 用户设置
│ └── ChangePasswordDialog.java # 修改密码
├── others/ # 资源文件
└── Math.exe # 可执行文件
```
### 依赖库
- **Gson 2.10.1**JSON数据处理
- **JavaMail 1.6.2**:邮件功能支持
- **JAF 1.1.1**JavaBeans激活框架
## 安装与运行
### 平台支持
![Windows](https://img.shields.io/badge/Windows-Supported-0078D6?logo=windows)
| 操作系统 | 版本要求 | 支持状态 | 应用 |
|---------|-------------|--------|---------|
| Windows | 11 | ✅ 完全支持 | exe应用程序 |
运行前,请确保系统已安装以下环境:
**Windows:**
- **openjdk** 21.0.5(2024-10-15 LTS)
- **OpenJDK Runtime Environment** build 21.0.5+11-LTS
- **OpenJDK 64-Bit Server VM** build 21.0.5+11-LTS, mixed mode, sharing
- **Maven** 3.6或更高版本
### 直接运行
- 双击Math.exe程序
### 编译运行
```bash
# 使用Maven编译
mvn compile
# 运行程序
mvn exec:java
# 打包为可执行JAR
mvn package
```
## 使用说明
### 首次使用
1. 启动程序后进入登录界面
2. 点击"注册"按钮创建新账户
3. 填写邮箱、验证码
4. 验证完成后填写用户名、密码并选择年级
5. 注册成功后返回登录界面登录
### 练习流程
1. 登录成功后选择年级系统会根据注册信息自动推荐以及题目数量10-30
2. 进入答题界面开始练习
3. 选择答案后点击“下一题”继续
4. 完成所有题目后自动生成并显示成绩
### 用户设置
- 在登录后界面可以修改密码
- 系统会自动保存练习记录
## 数据存储
### 用户数据
- 位置:`users.json`
- 格式JSON格式存储用户信息
### 题目记录
- 位置:`questions/{邮箱}/` 目录
- 格式:按时间戳命名的文本文件
## 开发说明
### 扩展题目类型
要添加新的题目类型,需要:
1. 在相应的`*Maker.java`类中添加题目生成逻辑
2. 在`QuestionMaker.java`中更新表达式解析逻辑
3. 如果需要新的年级,更新`Grade.java`枚举
### 自定义界面
所有UI组件都使用Swing开发可以通过修改对应的`*.java`文件来自定义界面样式和布局。
### 分工
后端:张一帆
前端:赵俊杰
邮箱注册实现:张一帆,赵俊杰
调试:赵俊杰,张一帆

Binary file not shown.

Binary file not shown.

Binary file not shown.

@ -0,0 +1,19 @@
1. 78 / 96 - 22
2. 15 - 17 * (49 / 15)
3. 66 + 28 / 8 + (1 / 25)
4. 16 * 2
5. 26 * (98 * 90)
6. 4 + 89 * (70 / 3)
7. 41 + 36 + 55 + 9
8. 79 * 72 - 19 * 32
9. 2 - 2
10. (32 - 82) + 16

@ -0,0 +1,19 @@
1. 70^2 / 64
2. 11^2 + 31 + 85^2 - 61
3. √95 - 73^2 + 54^2
4. 53 - (√44 / 53 / 5^2) * 10^2
5. 5^2 * 75^2 / √24 - √20 + 74^2
6. √74 / 82
7. 31 * 5 * √42 / 53^2 + √19
8. 48 + 24^2 / 12^2 - 87
9. (37 * 38^2 + √82 / 27) + 32
10. 95 - √72

@ -0,0 +1,19 @@
1. 47 * sin(70) * 75 - cos(16)
2. sin(65) * 38 - 16 / 68
3. 78 * sin(50) / cos(70) * tan(52)
4. (cos(70) + sin(12) + 85) - cos(46)
5. cos(60) / tan(4)
6. 70 * sin(28) * sin(7)
7. tan(74) - cos(55) - cos(42) + tan(38)
8. tan(62) + 45
9. 52 + sin(33)
10. sin(40) * 29

@ -0,0 +1,19 @@
1. 5 - 41
2. 12 / (22 / 21 + 5) + 60
3. 88 * (22 * 75)
4. 5 - 23
5. 72 * 61 / 59 + 40
6. 2 * 50
7. (37 * 99) - 92 * 43
8. 85 * 88
9. 38 / 98 - 75 - 71 / 74
10. 63 / (44 - 25)

@ -0,0 +1,59 @@
1. 6 + 63 - 75 - 63 / 89
2. 38 - (63 * 76) + 41
3. 31 * 90 - 8 + 28 + 19
4. 21 - 94
5. 66 + (49 / 9)
6. 36 * 30 * 74
7. 63 / 1 / 57 - (12 - 60)
8. (91 - 68 + 66) * 7 * 83
9. 68 - 47 / 15 / 84
10. 38 - 47 * 19
11. 4 * 19
12. 48 / 38 - 62
13. 27 - 13
14. 32 * 61 / 25 + 11
15. 10 - 71
16. 92 - (27 + 99 - 57)
17. 9 / (79 - 59)
18. 76 - (68 + 73 + 20)
19. 97 - 1
20. 66 - 43 * 19 / 2 / 2
21. 78 / 35 / 94 + 12 - 16
22. 54 + 96
23. 34 - 5
24. 4 - 39
25. 58 / 17 / 45 * 51
26. 3 + (71 - 43)
27. 21 - (93 - 83)
28. 46 / 10 / 20 + 42
29. 35 + (37 - 90)
30. 83 * 82 - 11

@ -0,0 +1,59 @@
1. (58 + 75) / 19
2. 99 * 13 / 10
3. 97 + 77
4. (93 + 10) - 26
5. 21 + 77 - 78 / (56 - 28)
6. 97 + 19
7. 97 * (31 + 25 * 33 * 68)
8. 90 / 18 - 80 - 51
9. 53 / 28 - 39 + 21
10. 58 - 94
11. 10 - 85
12. 100 - 42 * 9 - 66 - 49
13. 22 / (19 + 65 / 17)
14. 48 + (5 + 2) - 25
15. 91 * 9 * (26 + 70) / 93
16. 20 + 3 / 42
17. 24 / 39 * 69
18. (88 + 55) + 14 * 42
19. 90 / 16 / 9 * 75
20. 84 * 65 / (84 - 70)
21. 30 * (85 / 83)
22. 89 / 15 - 55 - 60 + 37
23. (12 * 66) / 82
24. (27 + 47) / 85
25. 12 * 80 + 27 * 86 * 80
26. 47 + 88 + (45 * 29)
27. 93 + 7 + 28 + 85 / 92
28. 9 * 4 + (62 / 31)
29. 6 - 26
30. (58 * 70) + 55

@ -0,0 +1,19 @@
1. 14 * 25 * (59 * 79)
2. 21 / 91 / 4 - (87 * 16)
3. 46 / 20 + 87 * 59
4. 86 / (7 * 59)
5. 85 / 35 * 30
6. 9 - 88
7. 27 + 10
8. 94 - 76 / 59
9. 37 / (5 / 89)
10. 25 * 46 + 28

@ -0,0 +1,19 @@
1. (39 + 54^2 * 23) / 1 * 50
2. 69^2 - 82
3. √36 + 49
4. √44 + 45 * 95
5. 10 - 76^2 - 70^2
6. √72 * √98 * 16
7. (27^2 / 48) / 10
8. 9 + √40 - 25 / √69 / 13^2
9. 73 * (8^2 / 61 - 96)
10. √34 / (√64 * 79)

@ -0,0 +1,19 @@
1. 57 / cos(76)
2. sin(5) - sin(22)
3. sin(90) + sin(66) * 79
4. (sin(42) + tan(30)) - cos(68)
5. tan(96) - tan(54) / tan(30)
6. tan(24) / 73
7. 76 + sin(86) - sin(98) * 25
8. 16 / cos(84) * 22
9. cos(77) + sin(21) * 14
10. sin(19) * cos(53) / 28

@ -0,0 +1,19 @@
1. 40 * 27 + 23 - 30
2. 27 / 53
3. 55 + 18 / 51 + 70 * 20
4. 66 - 81
5. 68 + (87 / 23)
6. 48 / 62
7. (79 + 74 * 6) + 99
8. 66 * 67
9. 20 + 5 - 43 * 28 * 16
10. (46 + 78) + 1

@ -0,0 +1,19 @@
1. 81 / 75 - 19
2. 95 + 44
3. 6 + 8 / 22 + (57 / 34)
4. 22 + 85 / 59
5. 60 + (30 * 8)
6. 63 - (38 / 14 * 93)
7. 64 / 88 * 54
8. 79 * (34 + 5) - 47
9. 73 - 84 * 29 + 26 - 44
10. 9 * 75 - 38 / 25 * 95

@ -0,0 +1,59 @@
1. 52 + 30 + 60 - 7 / 65
2. 23 / 47 - 30
3. 36 / 9
4. 95 + 60 - 100 - 96
5. 19 * 7
6. 99 / 90 - 100 * 63 + 21
7. 63 / (85 - 71)
8. 74 * (38 * 43 * 87) / 91
9. 66 * 9
10. (93 / 61) / 62 + 45 + 72
11. 92 * 27 - 23 * 75 / 90
12. 14 / 53 + 93 * 62
13. 54 * 43 / (39 - 87 + 57)
14. 15 * 35 * 2 * 87
15. 1 / 70 + (26 * 63)
16. 74 / (1 / 42 / 83) - 21
17. 79 - 26 - 36 + (85 + 4)
18. 61 + (57 / 14 - 47) * 47
19. 3 + 96
20. 13 / 71 + 82 / (78 + 34)
21. 82 - 18
22. 77 / 93 * 8 * 8
23. 30 + 16 - 32 + 22 + 50
24. 34 * (7 - 14 - 44)
25. 9 - 23 / (37 * 28)
26. 8 / 78
27. 81 + 18 / (34 + 28)
28. 60 + 76 * 10 - 27
29. 51 - (62 / 92 * 12)
30. 53 * 88

@ -0,0 +1,19 @@
1. √27 / 1^2
2. 68 / √67
3. √52 / 59^2 / √95 + 37
4. 49 * (√15 + 5^2)
5. √61 + 84^2
6. √32 - 39^2 * 63^2 * 92^2 / √38
7. √92 + 24^2 + 91
8. √57 * 82^2 - 18 - 4^2 * √88
9. 20 - 76 + 29 * √88 * 85
10. √95 + 3^2

@ -0,0 +1,19 @@
1. tan(19) - sin(59) + cos(77)
2. cos(36) / tan(52) + tan(21)
3. (1 - 17 * 45 - cos(30)) + 68
4. 30 + cos(93) - 94
5. 26 * (sin(13) / cos(33)) - sin(28)
6. tan(82) - 7 - 33 * cos(86)
7. sin(99) * 97
8. 41 + tan(8) + tan(96) + 23 / sin(37)
9. 21 + cos(59)
10. tan(56) * 36 * sin(37) + 77

@ -0,0 +1,19 @@
1. 31 * (35 + 41)
2. 20 + 42 / 41 / (83 + 75)
3. 50 - 89
4. 42 * 55
5. 93 / 14 + 6 / 28
6. (14 / 64) + 94
7. 99 * 38 - 33 / 57 * 84
8. 46 - 23 / 94 - (28 - 45)
9. 49 + 23
10. 78 + (94 / 21 / 65)

@ -0,0 +1,19 @@
1. ( 18 / 41 ) + 99
2. 90 + 68 - ( 36 / 50 + 47 )
3. 98 - ( 87 + 3 / 44 )
4. 76 * 44 - 23 * 81 / 33
5. 69 * 4 * 73 + 47 - 47
6. 94 + 59
7. 62 - 62 + 22
8. 42 * 38 * 77 + 10
9. 3 - ( 3 / 49 ) / 10
10. 80 + 9 / 13 + 90

@ -0,0 +1,19 @@
1. 37 - 37
2. 48 - 8 + 15 + 99 + 48
3. 55 * 64 * 67 + 11 + 11
4. 88 * 44
5. 44 - 44
6. ( 71 - 16 ) / 64
7. 27 - 27
8. ( 33 / 51 ) * 83
9. 93 / 58
10. 99 * 52

@ -0,0 +1,19 @@
1. 77 * ( 87 / 13 + 74 )
2. 95 - 4 * 16
3. 28 / ( 51 + 88 )
4. 56 * 30
5. 67 + ( 53 - 53 )
6. ( 67 / 78 + 92 ) / 24 * 5
7. 10 + 60 / 96
8. 87 + 48 / 85
9. 55 / 79 + 72
10. ( 88 + 48 ) - 48

@ -0,0 +1,19 @@
1. √42 + √99
2. √85 - 9^2 / 4 * 89 * 49^2
3. 7^2 - 7^2 / (17^2 / √96)
4. √32 / 92 - √37 + 20
5. 75 * √93
6. 29 / (24^2 + 46 / 89)
7. √56 * 17^2 + √80
8. 95^2 - 43 - √77 * 13 * √60
9. 1^2 + 9^2
10. (56^2 - 24) * 71^2

@ -0,0 +1,3 @@
Manifest-Version: 1.0
Main-Class: ui.MathLearningApp

@ -0,0 +1,7 @@
package model;
public enum Grade {
primary,
middle,
high
}

@ -0,0 +1,64 @@
package model;
/**
*
*/
public class HighMaker extends Student {
private static final String[] BINARY_OPS = {"+", "-", "*", "/"};
private static final String[] TRIG_OPS = {"sin", "cos", "tan"};
private int operandCount;
private String[] questionParts;
private boolean[] specialOpFlags;
private int parenStart;
private int parenEnd;
public HighMaker(String name, String password, String path) {
super(name, password, path);
}
/**
*
*/
public void getRandom() {
this.operandCount = random.nextInt(4) + 2;
this.questionParts = new String[this.operandCount];
this.specialOpFlags = randomMaker(this.operandCount);
for (int i = 0; i < operandCount; i++) {
int operand = random.nextInt(100) + 1;
if (specialOpFlags[i]) {
String op = TRIG_OPS[random.nextInt(TRIG_OPS.length)];
questionParts[i] = op + "(" + operand + ")";
} else {
questionParts[i] = String.valueOf(operand);
}
}
int[] parenPos = bracketMaker(this.operandCount);
this.parenStart = parenPos[0];
this.parenEnd = parenPos[1];
}
@Override
protected String makeOneQuestion() {
getRandom();
StringBuilder question = new StringBuilder();
for (int i = 0; i < operandCount; i++) {
if (i == parenStart) {
question.append("(");
}
question.append(questionParts[i]);
if (i == parenEnd) {
question.append(")");
}
if (i < operandCount - 1) {
String op = BINARY_OPS[random.nextInt(BINARY_OPS.length)];
question.append(" ").append(op).append(" ");
}
}
return question.toString();
}
}

@ -0,0 +1,65 @@
package model;
/**
*
*/
public class MiddleMaker extends Student {
private static final String[] BINARY_OPS = {"+", "-", "*", "/"};
private static final String[] UNARY_OPS = {"^2", "√"};
private int operandCount;
private String[] questionParts;
private int parenStart;
private int parenEnd;
public MiddleMaker(String name, String password, String path) {
super(name, password, path);
}
/**
*
*/
public void getRandom() {
this.operandCount = random.nextInt(4) + 2;
this.questionParts = new String[this.operandCount];
boolean[] specialOpFlags = randomMaker(this.operandCount);
for (int i = 0; i < this.operandCount; i++) {
int operand = random.nextInt(100) + 1;
if (specialOpFlags[i]) {
String op = UNARY_OPS[random.nextInt(UNARY_OPS.length)];
if (op.equals("√")) {
this.questionParts[i] = op + operand;
} else {
this.questionParts[i] = operand + op;
}
} else {
this.questionParts[i] = String.valueOf(operand);
}
}
int[] parenPos = bracketMaker(this.operandCount);
this.parenStart = parenPos[0];
this.parenEnd = parenPos[1];
}
@Override
protected String makeOneQuestion() {
getRandom();
StringBuilder question = new StringBuilder();
for (int i = 0; i < operandCount; i++) {
if (i == parenStart) {
question.append("(");
}
question.append(questionParts[i]);
if (i == parenEnd) {
question.append(")");
}
if (i < operandCount - 1) {
question.append(" ").append(BINARY_OPS[random.nextInt(BINARY_OPS.length)]).append(" ");
}
}
return question.toString();
}
}

@ -0,0 +1,217 @@
package model;
import java.util.Stack;
/**
*
*/
public class PrimaryMaker extends Student {
private static final char[] OPERATORS = {'+', '-', '*', '/'};
private static final int MIN_OPERANDS = 2;
private static final int MAX_OPERANDS = 5;
private static final int MAX_OPERAND_VALUE = 100;
private int operandCount;
private int[] operands;
private int parenStart;
private int parenEnd;
public PrimaryMaker(String name, String password, String path) {
super(name, password, path);
}
@Override
protected String makeOneQuestion() {
String question;
while (true) {
question = generateSingleQuestion();
if (isQuestionValid(question)) {
break;
}
}
return question;
}
private String generateSingleQuestion() {
generateRandomParameters();
StringBuilder question = new StringBuilder();
for (int j = 0; j < operandCount; j++) {
appendOperandWithParentheses(question, j);
if (j < operandCount - 1) {
appendOperator(question, j);
}
}
return question.toString();
}
private void generateRandomParameters() {
operandCount = random.nextInt(MAX_OPERANDS - MIN_OPERANDS + 1) + MIN_OPERANDS;
operands = new int[operandCount];
for (int j = 0; j < operandCount; j++) {
operands[j] = random.nextInt(MAX_OPERAND_VALUE) + 1;
}
int[] parenPos = bracketMaker(operandCount);
parenStart = parenPos[0];
parenEnd = parenPos[1];
}
private void appendOperandWithParentheses(StringBuilder question, int index) {
if (index == parenStart) {
question.append("( ");
}
question.append(operands[index]);
if (index == parenEnd) {
question.append(" )");
}
}
private void appendOperator(StringBuilder question, int currentIndex) {
char op = OPERATORS[random.nextInt(OPERATORS.length)];
if (op == '-') {
ensureSubtractionIsValid(currentIndex);
}
if (op == '/') {
ensureDivisionIsValid(currentIndex);
}
question.append(" ").append(op).append(" ");
}
private void ensureSubtractionIsValid(int currentIndex) {
if (operands[currentIndex] < operands[currentIndex + 1]) {
int temp = operands[currentIndex];
operands[currentIndex] = operands[currentIndex + 1];
operands[currentIndex + 1] = temp;
}
}
private void ensureDivisionIsValid(int currentIndex) {
if (operands[currentIndex + 1] == 0) {
operands[currentIndex + 1] = 1;
}
}
private boolean isQuestionValid(String expression) {
try {
evaluateExpression(expression, true);
return true;
} catch (ArithmeticException e) {
return false;
}
}
private double evaluateExpression(String expression, boolean checkNegative)
throws ArithmeticException {
Stack<Double> numbers = new Stack<>();
Stack<Character> ops = new Stack<>();
for (int i = 0; i < expression.length(); i++) {
char c = expression.charAt(i);
if (c == ' ') {
continue;
}
if (Character.isDigit(c)) {
i = parseAndPushNumber(expression, i, numbers);
} else if (c == '(') {
ops.push(c);
} else if (c == ')') {
processClosingParenthesis(numbers, ops, checkNegative);
} else if (isOperator(c)) {
processOperator(c, numbers, ops, checkNegative);
}
}
processRemainingOperators(numbers, ops, checkNegative);
double finalResult = numbers.pop();
validateResult(finalResult, checkNegative);
return finalResult;
}
private int parseAndPushNumber(String expression, int startIndex,
Stack<Double> numbers) {
StringBuilder sb = new StringBuilder();
int i = startIndex;
while (i < expression.length()
&& (Character.isDigit(expression.charAt(i)) || expression.charAt(i) == '.')) {
sb.append(expression.charAt(i++));
}
numbers.push(Double.parseDouble(sb.toString()));
return i - 1;
}
private void processClosingParenthesis(Stack<Double> numbers, Stack<Character> ops,
boolean checkNegative) {
while (ops.peek() != '(') {
double result = applyOp(ops.pop(), numbers.pop(), numbers.pop());
validateResult(result, checkNegative);
numbers.push(result);
}
ops.pop();
}
private void processOperator(char currentOp, Stack<Double> numbers,
Stack<Character> ops, boolean checkNegative) {
while (!ops.isEmpty() && hasPrecedence(currentOp, ops.peek())) {
double result = applyOp(ops.pop(), numbers.pop(), numbers.pop());
validateResult(result, checkNegative);
numbers.push(result);
}
ops.push(currentOp);
}
private void processRemainingOperators(Stack<Double> numbers, Stack<Character> ops,
boolean checkNegative) {
while (!ops.isEmpty()) {
double result = applyOp(ops.pop(), numbers.pop(), numbers.pop());
validateResult(result, checkNegative);
numbers.push(result);
}
}
private void validateResult(double result, boolean checkNegative) {
if (checkNegative && result < 0) {
throw new ArithmeticException("Negative result");
}
}
private boolean isOperator(char c) {
return c == '+' || c == '-' || c == '*' || c == '/';
}
private boolean hasPrecedence(char op1, char op2) {
if (op2 == '(' || op2 == ')') {
return false;
}
return (op1 != '*' && op1 != '/') || (op2 != '+' && op2 != '-');
}
private double applyOp(char op, double b, double a) {
switch (op) {
case '+':
return a + b;
case '-':
if (a < b) {
throw new ArithmeticException("Negative result in subtraction");
}
return a - b;
case '*':
return a * b;
case '/':
if (b == 0) {
throw new ArithmeticException("Cannot divide by zero");
}
return a / b;
default:
return 0;
}
}
}

@ -0,0 +1,57 @@
package model;
/**
*
*/
public record Question(String expression, String questionText, String[] options, int correctAnswer) {
/**
*
*
* @param expression
* @param questionText
* @param options (4)
* @param correctAnswer (0-3)
*/
public Question {
}
/**
*
*
* @return
*/
@Override
public String questionText() {
return questionText;
}
/**
*
*
* @return
*/
@Override
public String[] options() {
return options;
}
/**
*
*
* @return
*/
@Override
public int correctAnswer() {
return correctAnswer;
}
/**
*
*
* @return
*/
@Override
public String expression() {
return expression;
}
}

@ -0,0 +1,415 @@
package model;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Random;
import java.util.Set;
/**
*
*/
public class QuestionMaker {
private static final int MAX_ATTEMPTS = 100;
private static final String QUESTIONS_DIR = "questions";
private static final String FILE_SEPARATOR = File.separator;
private static final int OPTION_COUNT = 4;
private Student student;
private final Random random;
public QuestionMaker() {
this.random = new Random();
}
/**
*
*
* @param grade
* @param sum
* @param email
* @return
*/
public List<Question> makeQuestions(Grade grade, int sum, String email) {
initializeStudent(grade, email);
List<Question> questions = new ArrayList<>();
Set<String> existingQuestions = loadQuestions(email);
Set<String> currentQuestions = new HashSet<>();
int attempt = 0;
while (questions.size() < sum && attempt < MAX_ATTEMPTS) {
String expression = student.makeOneQuestion();
if (isUniqueExpression(expression, existingQuestions, currentQuestions)) {
tryAddQuestion(expression, grade, questions, existingQuestions,
currentQuestions);
}
attempt++;
}
saveQuestions(email, new ArrayList<>(currentQuestions));
return questions;
}
private void initializeStudent(Grade grade, String email) {
String path = QUESTIONS_DIR + FILE_SEPARATOR + email;
switch (grade) {
case primary:
student = new PrimaryMaker("primary", "", path);
break;
case middle:
student = new MiddleMaker("middle", "", path);
break;
case high:
student = new HighMaker("high", "", path);
break;
default:
throw new IllegalArgumentException("Unknown grade: " + grade);
}
}
private boolean isUniqueExpression(String expression,
Set<String> existingQuestions, Set<String> currentQuestions) {
return !existingQuestions.contains(expression)
&& !currentQuestions.contains(expression);
}
private void tryAddQuestion(String expression, Grade grade,
List<Question> questions, Set<String> existingQuestions,
Set<String> currentQuestions) {
try {
double result = evaluateExpression(expression, grade);
Question question = createQuestion(expression, result);
questions.add(question);
existingQuestions.add(expression);
currentQuestions.add(expression);
} catch (Exception e) {
// Skip invalid expressions
}
}
private double evaluateExpression(String expression, Grade grade) {
String processedExpression = preprocessExpression(expression, grade);
return parseAndEvaluate(processedExpression);
}
private String preprocessExpression(String expression, Grade grade) {
String result = expression.replace(" ", "");
if (grade == Grade.middle) {
result = preprocessMiddleSchoolExpression(result);
} else if (grade == Grade.high) {
result = preprocessHighSchoolExpression(result);
}
return result;
}
private String preprocessMiddleSchoolExpression(String expression) {
// Handle squares: (5)^2 -> (5*5) and 5^2 -> (5*5)
String result = expression.replaceAll("\\((\\d+)\\)\\^2", "($1*$1)");
result = result.replaceAll("(\\d+)\\^2", "($1*$1)");
// Handle square roots: √16 -> Math.sqrt(16)
result = result.replaceAll("√(\\d+)", "Math.sqrt($1)");
return result;
}
private String preprocessHighSchoolExpression(String expression) {
String result = expression.replaceAll("sin\\((\\d+)\\)",
"Math.sin(Math.toRadians($1))");
result = result.replaceAll("cos\\((\\d+)\\)",
"Math.cos(Math.toRadians($1))");
result = result.replaceAll("tan\\((\\d+)\\)",
"Math.tan(Math.toRadians($1))");
return result;
}
private double parseAndEvaluate(String expression) {
return new ExpressionParser(expression).parse();
}
private static class ExpressionParser {
private final String expression;
private int pos = -1;
private int ch;
ExpressionParser(String expression) {
this.expression = expression;
}
double parse() {
nextChar();
double result = parseExpression();
if (pos < expression.length()) {
throw new RuntimeException("Unexpected: " + (char) ch);
}
return result;
}
private void nextChar() {
ch = (++pos < expression.length()) ? expression.charAt(pos) : -1;
}
private boolean eat(int charToEat) {
while (ch == ' ') {
nextChar();
}
if (ch == charToEat) {
nextChar();
return true;
}
return false;
}
private double parseExpression() {
double x = parseTerm();
while (true) {
if (eat('+')) {
x += parseTerm();
} else if (eat('-')) {
x -= parseTerm();
} else {
return x;
}
}
}
private double parseTerm() {
double x = parseFactor();
while (true) {
if (eat('*')) {
x *= parseFactor();
} else if (eat('/')) {
x /= parseFactor();
} else {
return x;
}
}
}
private double parseFactor() {
if (eat('+')) {
return parseFactor();
}
if (eat('-')) {
return -parseFactor();
}
int startPos = pos;
if (eat('(')) {
return parseParenthesizedExpression();
} else if (isDigit()) {
return parseNumber(startPos);
} else if (isUpperCaseLetter()) {
return parseMathFunction(startPos);
} else if (isLowerCaseLetter()) {
return parseCustomFunction(startPos);
} else {
throw new RuntimeException("Unexpected: " + (char) ch);
}
}
private boolean isDigit() {
return (ch >= '0' && ch <= '9') || ch == '.';
}
private boolean isUpperCaseLetter() {
return ch >= 'A' && ch <= 'Z';
}
private boolean isLowerCaseLetter() {
return ch >= 'a' && ch <= 'z';
}
private double parseParenthesizedExpression() {
double x = parseExpression();
eat(')');
return x;
}
private double parseNumber(int startPos) {
while (isDigit()) {
nextChar();
}
return Double.parseDouble(expression.substring(startPos, pos));
}
private double parseMathFunction(int startPos) {
while ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch == '.') {
nextChar();
}
String func = expression.substring(startPos, pos);
eat('(');
double x = parseExpression();
eat(')');
return applyMathFunction(func, x);
}
private double parseCustomFunction(int startPos) {
while (ch >= 'a' && ch <= 'z') {
nextChar();
}
String function = expression.substring(startPos, pos);
double x = parseFactor();
return applyCustomFunction(function, x);
}
private double applyMathFunction(String func, double value) {
switch (func) {
case "Math.sqrt":
return Math.sqrt(value);
case "Math.sin":
return Math.sin(value);
case "Math.cos":
return Math.cos(value);
case "Math.tan":
return Math.tan(value);
case "Math.toRadians":
return Math.toRadians(value);
default:
throw new RuntimeException("Unknown function: " + func);
}
}
private double applyCustomFunction(String function, double value) {
switch (function) {
case "sqrt":
return Math.sqrt(value);
case "sin":
return Math.sin(value);
case "cos":
return Math.cos(value);
case "tan":
return Math.tan(value);
default:
throw new RuntimeException("Unknown function: " + function);
}
}
}
private Question createQuestion(String expression, double result) {
String text = "计算: " + expression + " = ? ";
double[] options = generateOptions(result);
int correctIndex = random.nextInt(OPTION_COUNT);
options[correctIndex] = result;
String[] optionStrings = formatOptions(options);
return new Question(expression, text, optionStrings, correctIndex);
}
private double[] generateOptions(double correctAnswer) {
double[] options = new double[OPTION_COUNT];
for (int i = 0; i < OPTION_COUNT; i++) {
double offset = random.nextDouble() * 20 - 10;
if (Math.abs(offset) < 1) {
offset = random.nextBoolean() ? 5 : -5;
}
options[i] = offset + correctAnswer;
}
return options;
}
private String[] formatOptions(double[] options) {
String[] optionStrings = new String[OPTION_COUNT];
for (int i = 0; i < OPTION_COUNT; i++) {
optionStrings[i] = String.format("%c. %.2f",
(char) ('A' + i), options[i]);
}
return optionStrings;
}
private Set<String> loadQuestions(String email) {
Set<String> questions = new HashSet<>();
File userDir = getUserDirectory(email);
if (!userDir.exists() || !userDir.isDirectory()) {
return questions;
}
File[] files = userDir.listFiles((dir, name) -> name.endsWith(".txt"));
if (files == null) {
return questions;
}
for (File file : files) {
loadQuestionsFromFile(file, questions);
}
return questions;
}
private void loadQuestionsFromFile(File file, Set<String> questions) {
try (BufferedReader reader = new BufferedReader(new FileReader(file))) {
String line;
while ((line = reader.readLine()) != null) {
String question = extractQuestion(line);
if (question != null) {
questions.add(question);
}
}
} catch (IOException e) {
System.err.println("读取文件失败: " + e.getMessage());
}
}
private String extractQuestion(String line) {
line = line.trim();
if (line.matches("\\d+\\.\\s+.*")) {
return line.substring(line.indexOf('.') + 1).trim();
}
return null;
}
private void saveQuestions(String email, List<String> questions) {
File userDir = getUserDirectory(email);
ensureDirectoryExists(userDir);
String fileName = generateFileName();
File file = new File(userDir, fileName);
writeQuestionsToFile(file, questions);
}
private File getUserDirectory(String email) {
return new File(QUESTIONS_DIR + FILE_SEPARATOR + email);
}
private void ensureDirectoryExists(File directory) {
if (!directory.exists()) {
if (!directory.mkdirs()) {
System.err.println("创建目录失败");
}
}
}
private String generateFileName() {
LocalDateTime now = LocalDateTime.now();
return now.format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) + ".txt";
}
private void writeQuestionsToFile(File file, List<String> questions) {
try (PrintWriter writer = new PrintWriter(new FileWriter(file))) {
for (int i = 0; i < questions.size(); i++) {
writer.println((i + 1) + ". " + questions.get(i));
if (i < questions.size() - 1) {
writer.println();
}
}
} catch (IOException e) {
System.err.println("保存文件失败: " + e.getMessage());
}
}
}

@ -0,0 +1,61 @@
package model;
import java.util.Random;
public abstract class Student {
protected String name;
protected String password;
protected String path;
protected Random random;
public Student(String name, String password, String path) {
this.name = name;
this.password = password;
this.path = path;
this.random = new Random();
}
/**
*
*
*
* @param operands
* @return [parenStart, parenEnd]
*/
protected int[] bracketMaker(int operands) {
boolean useParen = operands > 2 && random.nextBoolean();
int parenStart = 0;
int parenEnd = operands - 1;
while (parenStart == 0 && parenEnd == operands - 1) {
parenStart = useParen ? random.nextInt(operands - 1) : -2;
parenEnd = useParen ? random.nextInt(operands - 1 - parenStart) + parenStart + 1 : -2;
}
return new int[]{parenStart, parenEnd};
}
/**
*
*
* @param operands
* @return
*/
protected boolean[] randomMaker(int operands) {
int specialNumber = Math.min(operands, random.nextInt(operands) + 1);
boolean[] specialOpFlags = new boolean[operands];
for (int i = 0; i < specialNumber; i++) {
int pos;
do {
pos = random.nextInt(operands);
} while (specialOpFlags[pos]);
specialOpFlags[pos] = true;
}
return specialOpFlags;
}
protected abstract String makeOneQuestion();
}

@ -0,0 +1,262 @@
package model;
import java.io.*;
import java.util.HashMap;
import java.util.Map;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.gson.reflect.TypeToken;
import java.lang.reflect.Type;
/**
*
*
*
*/
public class UserManager {
private static final String DATA_FILE = "users.json"; // 数据文件路径
private final Map<String, UserInfo> users = new HashMap<>();
private final Map<String, String> usernameToEmail = new HashMap<>();
private final Gson gson;
/**
*
*/
public UserManager() {
gson = new GsonBuilder().setPrettyPrinting().create();
loadUsersFromFile();
}
/**
*
*/
private void loadUsersFromFile() {
File file = new File(DATA_FILE);
if (!file.exists()) {
System.out.println("用户数据文件不存在,将创建新文件");
return;
}
try (Reader reader = new FileReader(file)) {
Type type = new TypeToken<Map<String, UserInfo>>(){}.getType();
Map<String, UserInfo> loadedUsers = gson.fromJson(reader, type);
if (loadedUsers != null) {
users.putAll(loadedUsers);
// 重建用户名到邮箱的映射
for (UserInfo userInfo : users.values()) {
usernameToEmail.put(userInfo.getUsername(), userInfo.getEmail());
}
System.out.println("成功加载 " + users.size() + " 个用户数据");
}
} catch (IOException e) {
System.err.println("加载用户数据失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
*
*/
private void saveUsersToFile() {
try (Writer writer = new FileWriter(DATA_FILE)) {
gson.toJson(users, writer);
System.out.println("用户数据已保存");
} catch (IOException e) {
System.err.println("保存用户数据失败: " + e.getMessage());
e.printStackTrace();
}
}
/**
*
*
* @param email
* @param password
* @param grade
* @param username
* @return
*/
public boolean register(String email, String password, Grade grade, String username) {
if (users.containsKey(email) || usernameToEmail.containsKey(username)) {
return false;
}
users.put(email, new UserInfo(email, password, grade, username));
usernameToEmail.put(username, email);
saveUsersToFile(); // 注册成功后立即保存
return true;
}
/**
* Grade
*
* @param email
* @param password
* @return ,null
*/
public Grade login(String email, String password) {
UserInfo user = users.get(email);
if (user == null || !user.getPassword().equals(password)) {
return null;
}
return user.getGrade();
}
/**
* UserInfo
*
* @param email
* @param password
* @return ,null
*/
public UserInfo loginWithUserInfo(String email, String password) {
UserInfo user = users.get(email);
if (user == null || !user.getPassword().equals(password)) {
return null;
}
return user;
}
/**
* 使
*
* @param emailOrUsername
* @param password
* @return ,null
*/
public UserInfo loginWithEmailOrUsername(String emailOrUsername, String password) {
// 先尝试作为邮箱登录
UserInfo user = users.get(emailOrUsername);
// 如果邮箱不存在,尝试作为用户名查找
if (user == null && usernameToEmail.containsKey(emailOrUsername)) {
String email = usernameToEmail.get(emailOrUsername);
user = users.get(email);
}
// 验证密码
if (user == null || !user.getPassword().equals(password)) {
return null;
}
return user;
}
/**
*
*
* @param email
* @return
*/
public boolean userExists(String email) {
return users.containsKey(email);
}
/**
*
*
* @param username
* @return
*/
public boolean usernameExists(String username) {
return usernameToEmail.containsKey(username);
}
/**
*
*
* @param email
* @param oldPassword
* @param newPassword
* @return
*/
public boolean changePassword(String email, String oldPassword, String newPassword) {
UserInfo user = users.get(email);
if (user == null || !user.getPassword().equals(oldPassword)) {
return false;
}
user.setPassword(newPassword);
saveUsersToFile(); // 修改密码后立即保存
return true;
}
/**
*
*
* @param email
* @return null
*/
public UserInfo getUserInfo(String email) {
return users.get(email);
}
/**
*
*/
public static class UserInfo {
private final String email;
private String password;
private final Grade grade;
private final String username;
/**
*
*
* @param email
* @param password
* @param grade
* @param username
*/
public UserInfo(String email, String password, Grade grade, String username) {
this.email = email;
this.password = password;
this.grade = grade;
this.username = username;
}
/**
*
*
* @return
*/
public String getEmail() {
return email;
}
/**
*
*
* @return
*/
public String getPassword() {
return password;
}
/**
*
*
* @param password
*/
public void setPassword(String password) {
this.password = password;
}
/**
*
*
* @return
*/
public Grade getGrade() {
return grade;
}
/**
*
*
* @return
*/
public String getUsername() {
return username;
}
}
}

@ -0,0 +1,159 @@
package ui;
import model.UserManager;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.regex.Pattern;
/**
*
*
*/
public class ChangePasswordDialog extends JDialog {
private static final long serialVersionUID = 1L;
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z0-9]{6,10}$");
private final JPasswordField oldPasswordField;
private final JPasswordField newPassword1Field;
private final JPasswordField newPassword2Field;
private final JButton confirmButton;
private final UserManager userManager;
private final String email;
/**
*
*
* @param parent
* @param email
*/
public ChangePasswordDialog(Frame parent, String email) {
super(parent, "修改密码", true);
this.email = email;
this.userManager = new UserManager();
setSize(600, 360);
setLocationRelativeTo(parent);
setResizable(true);
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel infoLabel = new JLabel("<html>新密码要求:<br>" +
"1. 长度 6-10位<br>" +
"2. 必须包含大写字母<br>" +
"3. 必须包含小写字母<br>" +
"4. 必须包含数字<br>" +
"5. 不能包含特殊字符</html>");
infoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
panel.add(infoLabel, gbc);
gbc.gridwidth = 1;
gbc.gridy = 1;
panel.add(new JLabel("原密码:"), gbc);
oldPasswordField = new JPasswordField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(oldPasswordField, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 2;
panel.add(new JLabel("新密码:"), gbc);
newPassword1Field = new JPasswordField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(newPassword1Field, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 3;
panel.add(new JLabel("确认新密码:"), gbc);
newPassword2Field = new JPasswordField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(newPassword2Field, gbc);
gbc.weightx = 0.0;
confirmButton = new JButton("确认修改");
confirmButton.setPreferredSize(new Dimension(120, 30));
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
gbc.anchor = GridBagConstraints.CENTER;
panel.add(confirmButton, gbc);
add(panel);
confirmButton.addActionListener(e -> handleConfirm());
getRootPane().setDefaultButton(confirmButton);
}
/**
*
*/
private void handleConfirm() {
String oldPassword = new String(oldPasswordField.getPassword());
String newPassword1 = new String(newPassword1Field.getPassword());
String newPassword2 = new String(newPassword2Field.getPassword());
if (oldPassword.isEmpty() || newPassword1.isEmpty() ||
newPassword2.isEmpty()) {
showError("所有字段都不能为空");
return;
}
if (!newPassword1.equals(newPassword2)) {
showError("两次输入的新密码不一致");
return;
}
if (!PASSWORD_PATTERN.matcher(newPassword1).matches()) {
showError("新密码不符合要求:\n" +
"长度6-10位, 必须包含大小写字母和数字, 且不能包含特殊字符");
return;
}
if (userManager.changePassword(email, oldPassword, newPassword1)) {
JOptionPane.showMessageDialog(this,
"密码修改成功!",
"成功",
JOptionPane.INFORMATION_MESSAGE);
dispose();
} else {
showError("原密码错误");
}
}
/**
*
*
* @param message
*/
private void showError(String message) {
JOptionPane.showMessageDialog(this,
message,
"错误",
JOptionPane.ERROR_MESSAGE);
}
}

@ -0,0 +1,234 @@
package ui;
import model.Grade;
import model.UserManager;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
/**
*
* 退
*/
public class GradeSelectionFrame extends JFrame {
private static final long serialVersionUID = 1L;
private final String email;
private final String username;
private Grade currentGrade;
private final JLabel gradeLabel;
private final JComboBox<String> gradeComboBox;
private final JSpinner questionSpinner;
private final JButton startButton;
private final JButton userSettingsButton;
private final JButton logoutButton;
/**
* 使
*
* @param email
* @param initialGrade
*/
public GradeSelectionFrame(String email, Grade initialGrade) {
this(email, initialGrade, email); // 默认显示邮箱作为用户名
}
/**
* 使 UserInfo
*
* @param userInfo
*/
public GradeSelectionFrame(UserManager.UserInfo userInfo) {
this(userInfo.getEmail(), userInfo.getGrade(), userInfo.getUsername());
}
/**
*
*
* @param email
* @param initialGrade
* @param username
*/
private GradeSelectionFrame(String email, Grade initialGrade, String username) {
this.email = email;
this.username = username;
this.currentGrade = initialGrade;
setTitle("数学学习软件 - 选择年级");
setSize(700, 450);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(true);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
// --- Top Panel ---
JPanel topPanel = new JPanel(new BorderLayout());
// 欢迎信息(显示用户名)
JLabel welcomeLabel = new JLabel("欢迎, " + username, JLabel.LEFT);
welcomeLabel.setFont(new Font("微软雅黑", Font.BOLD, 16));
topPanel.add(welcomeLabel, BorderLayout.WEST);
// 用户设置按钮(右上角)
userSettingsButton = new JButton("用户设置");
JPanel topRightPanel = new JPanel(new FlowLayout(FlowLayout.RIGHT, 0, 0));
topRightPanel.add(userSettingsButton);
topPanel.add(topRightPanel, BorderLayout.EAST);
// 当前年级显示
gradeLabel = new JLabel("当前年级: " + getGradeName(currentGrade),
JLabel.CENTER);
gradeLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
gradeLabel.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0));
topPanel.add(gradeLabel, BorderLayout.SOUTH);
mainPanel.add(topPanel, BorderLayout.NORTH);
// --- Center Panel ---
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 5, 10, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = 0;
centerPanel.add(new JLabel("选择年级:"), gbc);
String[] grades = {"小学", "初中", "高中"};
gradeComboBox = new JComboBox<>(grades);
gradeComboBox.setSelectedItem(getGradeName(currentGrade));
gbc.gridx = 1;
gbc.weightx = 1.0;
centerPanel.add(gradeComboBox, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 1;
centerPanel.add(new JLabel("题目数量:"), gbc);
SpinnerNumberModel spinnerModel = new SpinnerNumberModel(10, 10, 30, 1);
questionSpinner = new JSpinner(spinnerModel);
// 禁用 JSpinner 的文本输入功能,只允许通过按钮调节
JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor) questionSpinner.getEditor();
editor.getTextField().setEditable(false);
gbc.gridx = 1;
gbc.weightx = 1.0;
centerPanel.add(questionSpinner, gbc);
gbc.weightx = 0.0;
startButton = new JButton("开始答题");
startButton.setPreferredSize(new Dimension(200, 40));
startButton.setFont(new Font("微软雅黑", Font.BOLD, 14));
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 2;
gbc.anchor = GridBagConstraints.CENTER;
centerPanel.add(startButton, gbc);
mainPanel.add(centerPanel, BorderLayout.CENTER);
// --- Bottom Panel ---
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
logoutButton = new JButton("退出登录");
bottomPanel.add(logoutButton);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
add(mainPanel);
// --- Action Listeners ---
startButton.addActionListener(e -> startQuiz());
userSettingsButton.addActionListener(e -> openUserSettings());
logoutButton.addActionListener(e -> logout());
gradeComboBox.addActionListener(e -> updateGrade());
}
/**
*
*/
private void updateGrade() {
String selected = (String) gradeComboBox.getSelectedItem();
switch (selected) {
case "小学":
currentGrade = Grade.primary;
break;
case "初中":
currentGrade = Grade.middle;
break;
case "高中":
currentGrade = Grade.high;
break;
}
gradeLabel.setText("当前年级: " + getGradeName(currentGrade));
}
/**
*
*/
private void startQuiz() {
int questionCount = (Integer) questionSpinner.getValue();
QuizFrame quizFrame = new QuizFrame(email, currentGrade, questionCount);
quizFrame.setVisible(true);
this.dispose();
}
/**
*
*/
private void openUserSettings() {
UserSettingsDialog dialog = new UserSettingsDialog(this, email, username);
dialog.setVisible(true);
}
/**
* 退
*/
private void logout() {
int confirm = JOptionPane.showConfirmDialog(this,
"确定要退出登录吗?",
"确认",
JOptionPane.YES_NO_OPTION);
if (confirm == JOptionPane.YES_OPTION) {
LoginFrame loginFrame = new LoginFrame();
loginFrame.setVisible(true);
this.dispose();
}
}
/**
*
*
* @param grade
* @return
*/
private String getGradeName(Grade grade) {
switch (grade) {
case primary:
return "小学";
case middle:
return "初中";
case high:
return "高中";
default:
return "未知";
}
}
}

@ -0,0 +1,178 @@
package ui;
import model.UserManager;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
/**
*
*
* <p>
* 使{@link UserManager}
*
* @author
*/
public class LoginFrame extends JFrame {
private static final long serialVersionUID = 1L;
/** 邮箱/用户名输入框 */
private final JTextField accountField;
/** 密码输入框 */
private final JPasswordField passwordField;
/** 登录按钮 */
private final JButton loginButton;
/** 注册按钮 */
private final JButton registerButton;
/** 用户管理器,处理登录验证 */
private final UserManager userManager;
/**
*
*
* <p>
* {@link UserManager}
*/
public LoginFrame() {
this.userManager = new UserManager();
setTitle("数学学习软件 - 登录");
setSize(600, 450);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(true);
JPanel mainPanel = new JPanel(new GridBagLayout());
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
// 标题
JLabel titleLabel = new JLabel("小初高数学学习系统", JLabel.CENTER);
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 20));
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
mainPanel.add(titleLabel, gbc);
// 提示信息
JLabel hintLabel = new JLabel("支持邮箱或用户名登录", JLabel.CENTER);
hintLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
hintLabel.setForeground(java.awt.Color.GRAY);
gbc.gridy = 1;
mainPanel.add(hintLabel, gbc);
// 账号输入
gbc.gridwidth = 1;
gbc.gridy = 2;
mainPanel.add(new JLabel("账号:"), gbc);
accountField = new JTextField(25);
gbc.gridx = 1;
gbc.weightx = 1.0;
mainPanel.add(accountField, gbc);
gbc.weightx = 0.0;
// 密码输入
gbc.gridx = 0;
gbc.gridy = 3;
mainPanel.add(new JLabel("密码:"), gbc);
passwordField = new JPasswordField(25);
gbc.gridx = 1;
gbc.weightx = 1.0;
mainPanel.add(passwordField, gbc);
gbc.weightx = 0.0;
// 按钮面板
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 10, 0));
loginButton = new JButton("登录");
registerButton = new JButton("注册");
loginButton.setPreferredSize(new Dimension(100, 30));
registerButton.setPreferredSize(new Dimension(100, 30));
buttonPanel.add(loginButton);
buttonPanel.add(registerButton);
gbc.gridx = 0;
gbc.gridy = 4;
gbc.gridwidth = 2;
mainPanel.add(buttonPanel, gbc);
add(mainPanel);
// 事件监听
loginButton.addActionListener(e -> handleLogin());
registerButton.addActionListener(e -> handleRegister());
getRootPane().setDefaultButton(loginButton);
}
/**
*
*
* <p>
*
*/
private void handleLogin() {
String account = accountField.getText().trim();
String password = new String(passwordField.getPassword());
if (account.isEmpty() || password.isEmpty()) {
JOptionPane.showMessageDialog(this,
"账号和密码不能为空",
"错误",
JOptionPane.ERROR_MESSAGE);
return;
}
// 使用新的登录方法,支持邮箱或用户名
UserManager.UserInfo userInfo = userManager.loginWithEmailOrUsername(account, password);
if (userInfo != null) {
openGradeSelection(userInfo);
} else {
JOptionPane.showMessageDialog(this,
"账号或密码错误",
"登录失败",
JOptionPane.ERROR_MESSAGE);
}
}
/**
*
*
* <p>{@link RegisterDialog}
*/
private void handleRegister() {
RegisterDialog dialog = new RegisterDialog(this, userManager);
dialog.setVisible(true);
}
/**
*
*
* <p>{@link GradeSelectionFrame}
*
*
* @param userInfo
*/
private void openGradeSelection(UserManager.UserInfo userInfo) {
GradeSelectionFrame gradeFrame = new GradeSelectionFrame(userInfo);
gradeFrame.setVisible(true);
this.dispose();
}
}

@ -0,0 +1,24 @@
package ui;
import javax.swing.SwingUtilities;
/**
*
*
* @author
* @version 1.0
*/
public class MathLearningApp {
/**
*
*
* @param args
*/
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
LoginFrame loginFrame = new LoginFrame();
loginFrame.setVisible(true);
});
}
}

@ -0,0 +1,215 @@
package ui;
import model.Grade;
import model.UserManager;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.JTextField;
import java.awt.Dimension;
import java.awt.Frame;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.Serial;
import java.util.regex.Pattern;
/**
*
*
*/
public class PasswordSetupDialog extends JDialog {
@Serial
private static final long serialVersionUID = 1L;
private static final Pattern PASSWORD_PATTERN =
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)[a-zA-Z0-9]{6,10}$");
private final JTextField usernameField;
private final JPasswordField password1Field;
private final JPasswordField password2Field;
private final JComboBox<String> gradeComboBox;
private final JButton confirmButton;
private final UserManager userManager;
private final String email;
private boolean registrationComplete;
/**
*
*
* @param parent
* @param userManager
* @param email
*/
public PasswordSetupDialog(Frame parent, UserManager userManager, String email) {
super(parent, "设置密码和用户名", true);
this.userManager = userManager;
this.email = email;
this.registrationComplete = false;
setSize(600, 420);
setLocationRelativeTo(parent);
setResizable(true);
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
JLabel infoLabel = new JLabel("<html>密码要求:<br>" +
"1. 长度 6-10位<br>" +
"2. 必须包含大写字母<br>" +
"3. 必须包含小写字母<br>" +
"4. 必须包含数字<br>" +
"5. 不能包含其他特殊字符</html>");
infoLabel.setFont(new Font("微软雅黑", Font.PLAIN, 11));
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
panel.add(infoLabel, gbc);
gbc.gridwidth = 1;
gbc.gridy = 1;
panel.add(new JLabel("输入密码:"), gbc);
password1Field = new JPasswordField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(password1Field, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 2;
panel.add(new JLabel("确认密码:"), gbc);
password2Field = new JPasswordField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(password2Field, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 3;
panel.add(new JLabel("设置用户名:"), gbc);
usernameField = new JTextField(20);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(usernameField, gbc);
gbc.weightx = 0.0;
gbc.gridx = 0;
gbc.gridy = 4;
panel.add(new JLabel("选择年级:"), gbc);
String[] grades = {"小学", "初中", "高中"};
gradeComboBox = new JComboBox<>(grades);
gbc.gridx = 1;
gbc.weightx = 1.0;
panel.add(gradeComboBox, gbc);
gbc.weightx = 0.0;
confirmButton = new JButton("确认注册");
confirmButton.setPreferredSize(new Dimension(120, 30));
gbc.gridx = 0;
gbc.gridy = 5;
gbc.gridwidth = 2;
gbc.anchor = GridBagConstraints.CENTER;
panel.add(confirmButton, gbc);
add(panel);
confirmButton.addActionListener(e -> handleConfirm());
getRootPane().setDefaultButton(confirmButton);
}
/**
*
*/
private void handleConfirm() {
String password1 = new String(password1Field.getPassword());
String password2 = new String(password2Field.getPassword());
String username = usernameField.getText().trim();
if (username.isEmpty() || password1.isEmpty() || password2.isEmpty()) {
showError("用户名和密码不能为空");
return;
}
if (userManager.usernameExists(username)) {
showError("该用户名已被使用,请更换一个");
return;
}
if (!password1.equals(password2)) {
showError("两次输入的密码不一致");
return;
}
if (!PASSWORD_PATTERN.matcher(password1).matches()) {
showError("密码不符合要求:\n" +
"长度6-10位, 必须包含大小写字母和数字, 且不能包含特殊字符");
return;
}
Grade gradeType = getSelectedGradeType();
boolean success = userManager.register(email, password1, gradeType, username);
if (success) {
JOptionPane.showMessageDialog(this,
"注册成功!请返回登录。",
"成功",
JOptionPane.INFORMATION_MESSAGE);
registrationComplete = true;
dispose();
} else {
showError("注册失败,该邮箱可能已被注册");
}
}
/**
*
*
* @return
*/
private Grade getSelectedGradeType() {
String selected = (String) gradeComboBox.getSelectedItem();
switch (selected) {
case "小学":
return Grade.primary;
case "初中":
return Grade.middle;
case "高中":
return Grade.high;
default:
return Grade.primary;
}
}
/**
*
*
* @param message
*/
private void showError(String message) {
JOptionPane.showMessageDialog(this,
message,
"错误",
JOptionPane.ERROR_MESSAGE);
}
/**
*
*
* @return
*/
public boolean isRegistrationComplete() {
return registrationComplete;
}
}

@ -0,0 +1,210 @@
package ui;
import model.Grade;
import model.Question;
import model.QuestionMaker;
import javax.swing.BorderFactory;
import javax.swing.ButtonGroup;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.io.Serial;
import java.util.List;
/**
*
* ,
*/
public class QuizFrame extends JFrame {
@Serial
private static final long serialVersionUID = 1L;
private final String email;
private final Grade gradeType;
private final List<Question> questions;
private int currentIndex;
private int correctCount;
private JLabel questionLabel;
private JLabel progressLabel;
private JRadioButton[] optionButtons;
private JButton submitButton;
private ButtonGroup optionGroup;
/**
*
*
* @param email
* @param gradeType
* @param questionCount
*/
public QuizFrame(String email, Grade gradeType, int questionCount) {
this.email = email;
this.gradeType = gradeType;
this.currentIndex = 0;
this.correctCount = 0;
QuestionMaker generator = new QuestionMaker();
this.questions = generator.makeQuestions(gradeType, questionCount, email);
if (questions.isEmpty()) {
JOptionPane.showMessageDialog(null,
"生成题目失败,返回主界面",
"错误",
JOptionPane.ERROR_MESSAGE);
returnToGradeSelection();
return;
}
setTitle("数学学习软件 - 答题");
setSize(900, 600);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(true);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
progressLabel = new JLabel("", JLabel.CENTER);
progressLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
mainPanel.add(progressLabel, BorderLayout.NORTH);
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.fill = GridBagConstraints.HORIZONTAL;
// 新增:答案格式提示
JLabel answerFormatLabel = new JLabel("注意:所有计算结果的答案均保留两位小数。", JLabel.CENTER);
answerFormatLabel.setFont(new Font("微软雅黑", Font.ITALIC, 12));
gbc.gridx = 0;
gbc.gridy = 0;
gbc.gridwidth = 2;
centerPanel.add(answerFormatLabel, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
gbc.gridwidth = 2;
questionLabel = new JLabel();
questionLabel.setFont(new Font("Cambria Math", Font.PLAIN, 18));
questionLabel.setBorder(BorderFactory.createEmptyBorder(10, 10, 20, 10));
centerPanel.add(questionLabel, gbc);
optionGroup = new ButtonGroup();
optionButtons = new JRadioButton[4];
for (int i = 0; i < 4; i++) {
optionButtons[i] = new JRadioButton();
optionButtons[i].setFont(new Font("Cambria Math", Font.PLAIN, 14));
optionGroup.add(optionButtons[i]);
gbc.gridy = i + 2;
gbc.gridwidth = 2;
centerPanel.add(optionButtons[i], gbc);
gbc.gridwidth = 1;
}
mainPanel.add(centerPanel, BorderLayout.CENTER);
JPanel bottomPanel = new JPanel(new FlowLayout(FlowLayout.CENTER));
submitButton = new JButton("提交答案");
submitButton.setPreferredSize(new Dimension(150, 35));
submitButton.setFont(new Font("微软雅黑", Font.BOLD, 14));
bottomPanel.add(submitButton);
mainPanel.add(bottomPanel, BorderLayout.SOUTH);
add(mainPanel);
submitButton.addActionListener(e -> handleSubmit());
displayQuestion();
}
/**
*
*/
private void displayQuestion() {
if (currentIndex >= questions.size()) {
showResult();
return;
}
Question question = questions.get(currentIndex);
progressLabel.setText(String.format("第 %d/%d 题",
currentIndex + 1, questions.size()));
questionLabel.setText("<html><body style='width: 500px'>" +
question.questionText() + "</body></html>");
optionGroup.clearSelection();
String[] options = question.options();
for (int i = 0; i < 4; i++) {
optionButtons[i].setText(options[i]);
}
submitButton.setEnabled(true);
}
/**
*
*/
private void handleSubmit() {
int selectedIndex = -1;
for (int i = 0; i < 4; i++) {
if (optionButtons[i].isSelected()) {
selectedIndex = i;
break;
}
}
if (selectedIndex == -1) {
JOptionPane.showMessageDialog(this,
"请选择一个答案",
"提示",
JOptionPane.WARNING_MESSAGE);
return;
}
Question question = questions.get(currentIndex);
if (selectedIndex == question.correctAnswer()) {
correctCount++;
}
currentIndex++;
displayQuestion();
}
/**
*
*/
private void showResult() {
double percentage = (double) correctCount / questions.size() * 100;
int score = (int) percentage;
ResultFrame resultFrame = new ResultFrame(
email, gradeType, score, correctCount, questions.size());
resultFrame.setVisible(true);
this.dispose();
}
/**
*
*/
private void returnToGradeSelection() {
GradeSelectionFrame frame = new GradeSelectionFrame(email, gradeType);
frame.setVisible(true);
this.dispose();
}
}

@ -0,0 +1,275 @@
package ui;
import model.UserManager;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JDialog;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingWorker;
import java.awt.Frame;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.util.Properties;
import java.util.regex.Pattern;
import javax.mail.Authenticator;
import javax.mail.Message;
import javax.mail.MessagingException;
import javax.mail.PasswordAuthentication;
import javax.mail.Session;
import javax.mail.Transport;
import javax.mail.internet.InternetAddress;
import javax.mail.internet.MimeMessage;
/**
*
*
*/
public class RegisterDialog extends JDialog {
private static final long serialVersionUID = 1L;
private static final Pattern EMAIL_PATTERN =
Pattern.compile("^[A-Za-z0-9+_.-]+@qq\\.com$");
// 邮件配置 - 请根据实际情况修改
private static final String SMTP_HOST = "smtp.qq.com"; // SMTP服务器
private static final String SMTP_PORT = "587";
private static final String FROM_EMAIL = "3551664030@qq.com"; // 发件人邮箱
private static final String EMAIL_PASSWORD = "wvazqphcxhuqdbfa"; // 授权码
private final JTextField emailField;
private final JTextField codeField;
private final JButton sendCodeButton;
private final JButton verifyButton;
private final UserManager userManager;
private String generatedCode;
/**
*
*
* @param parent
* @param userManager
*/
public RegisterDialog(Frame parent, UserManager userManager) {
super(parent, "用户注册", true);
this.userManager = userManager;
setSize(600, 300);
setLocationRelativeTo(parent);
setResizable(false);
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(5, 5, 5, 5);
gbc.fill = GridBagConstraints.HORIZONTAL;
gbc.gridx = 0;
gbc.gridy = 0;
panel.add(new JLabel("邮箱:"), gbc);
emailField = new JTextField(25);
gbc.gridx = 1;
gbc.gridwidth = 2;
panel.add(emailField, gbc);
sendCodeButton = new JButton("发送验证码");
gbc.gridx = 3;
gbc.gridwidth = 1;
panel.add(sendCodeButton, gbc);
gbc.gridx = 0;
gbc.gridy = 1;
panel.add(new JLabel("验证码:"), gbc);
codeField = new JTextField(25);
codeField.setEnabled(false);
gbc.gridx = 1;
gbc.gridwidth = 2;
panel.add(codeField, gbc);
verifyButton = new JButton("验证");
verifyButton.setEnabled(false);
gbc.gridx = 3;
gbc.gridwidth = 1;
panel.add(verifyButton, gbc);
add(panel);
sendCodeButton.addActionListener(e -> sendVerificationCode());
verifyButton.addActionListener(e -> verifyCode());
}
/**
*
*/
private void sendVerificationCode() {
String email = emailField.getText().trim();
if (email.isEmpty()) {
showError("邮箱不能为空");
return;
}
if (!EMAIL_PATTERN.matcher(email).matches()) {
showError("请输入有效的QQ邮箱(以@qq.com结尾)");
return;
}
if (userManager.userExists(email)) {
showError("该邮箱已注册");
return;
}
generatedCode = generateCode();
// 禁用按钮,防止重复点击
sendCodeButton.setEnabled(false);
sendCodeButton.setText("发送中...");
// 使用SwingWorker在后台线程发送邮件避免阻塞UI
new SwingWorker<Boolean, Void>() {
@Override
protected Boolean doInBackground() {
return sendEmail(email, generatedCode);
}
@Override
protected void done() {
try {
boolean success = get();
if (success) {
JOptionPane.showMessageDialog(
RegisterDialog.this,
"验证码已发送到您的邮箱,请查收!",
"发送成功",
JOptionPane.INFORMATION_MESSAGE);
emailField.setEnabled(false);
codeField.setEnabled(true);
verifyButton.setEnabled(true);
sendCodeButton.setText("已发送");
} else {
showError("验证码发送失败,请检查邮箱地址或稍后重试");
sendCodeButton.setEnabled(true);
sendCodeButton.setText("发送验证码");
}
} catch (Exception e) {
showError("验证码发送失败: " + e.getMessage());
sendCodeButton.setEnabled(true);
sendCodeButton.setText("发送验证码");
}
}
}.execute();
}
/**
*
*
* @param toEmail
* @param code
* @return
*/
private boolean sendEmail(String toEmail, String code) {
try {
// 配置邮件服务器属性
Properties props = new Properties();
props.put("mail.smtp.host", SMTP_HOST);
props.put("mail.smtp.port", SMTP_PORT);
props.put("mail.smtp.auth", "true");
props.put("mail.smtp.starttls.enable", "true"); // 启用TLS
// 创建会话
Session session = Session.getInstance(props, new Authenticator() {
@Override
protected PasswordAuthentication getPasswordAuthentication() {
return new PasswordAuthentication(FROM_EMAIL, EMAIL_PASSWORD);
}
});
// 创建邮件消息
Message message = new MimeMessage(session);
message.setFrom(new InternetAddress(FROM_EMAIL));
message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(toEmail));
message.setSubject("注册验证码");
// 邮件内容
String content = String.format(
"<html><body>" +
"<h2>欢迎注册</h2>" +
"<p>您的验证码是:<strong style='font-size: 24px; color: #007bff;'>%s</strong></p>" +
"<p>验证码有效期为10分钟请及时使用。</p>" +
"<p>如果这不是您的操作,请忽略此邮件。</p>" +
"</body></html>",
code
);
message.setContent(content, "text/html; charset=utf-8");
// 发送邮件
Transport.send(message);
return true;
} catch (MessagingException e) {
e.printStackTrace();
return false;
}
}
/**
*
*/
private void verifyCode() {
String inputCode = codeField.getText().trim();
if (inputCode.isEmpty()) {
showError("请输入验证码");
return;
}
if (!inputCode.equals(generatedCode)) {
showError("验证码错误");
return;
}
openPasswordDialog();
}
/**
*
*/
private void openPasswordDialog() {
String email = emailField.getText().trim();
PasswordSetupDialog passwordDialog = new PasswordSetupDialog(
(Frame) getOwner(),
userManager,
email);
passwordDialog.setVisible(true);
if (passwordDialog.isRegistrationComplete()) {
dispose();
}
}
/**
* 6
*
* @return
*/
private String generateCode() {
return String.format("%06d", (int) (Math.random() * 1000000));
}
/**
*
*
* @param message
*/
private void showError(String message) {
JOptionPane.showMessageDialog(this,
message,
"错误",
JOptionPane.ERROR_MESSAGE);
}
}

@ -0,0 +1,127 @@
package ui;
import model.Grade;
import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
/**
*
* ,退
*/
public class ResultFrame extends JFrame {
private static final long serialVersionUID = 1L;
private final String email;
private final Grade gradeType;
/**
*
*
* @param email
* @param gradeType
* @param score
* @param correctCount
* @param totalCount
*/
public ResultFrame(String email, Grade gradeType, int score,
int correctCount, int totalCount) {
this.email = email;
this.gradeType = gradeType;
setTitle("数学学习软件 - 答题结果");
setSize(600, 420);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setLocationRelativeTo(null);
setResizable(true);
JPanel mainPanel = new JPanel(new BorderLayout(10, 10));
mainPanel.setBorder(BorderFactory.createEmptyBorder(30, 30, 30, 30));
JPanel centerPanel = new JPanel(new GridBagLayout());
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 10, 10, 10);
gbc.gridx = 0;
gbc.gridy = 0;
JLabel titleLabel = new JLabel("答题完成!", JLabel.CENTER);
titleLabel.setFont(new Font("微软雅黑", Font.BOLD, 24));
centerPanel.add(titleLabel, gbc);
gbc.gridy = 1;
JLabel scoreLabel = new JLabel("得分: " + score, JLabel.CENTER);
scoreLabel.setFont(new Font("微软雅黑", Font.BOLD, 36));
scoreLabel.setForeground(getScoreColor(score));
centerPanel.add(scoreLabel, gbc);
gbc.gridy = 2;
JLabel detailLabel = new JLabel(
String.format("答对 %d 题 / 共 %d 题", correctCount, totalCount),
JLabel.CENTER);
detailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
centerPanel.add(detailLabel, gbc);
mainPanel.add(centerPanel, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 15, 0));
JButton continueButton = new JButton("继续做题");
JButton exitButton = new JButton("退出");
continueButton.setPreferredSize(new Dimension(120, 35));
exitButton.setPreferredSize(new Dimension(120, 35));
buttonPanel.add(continueButton);
buttonPanel.add(exitButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
add(mainPanel);
continueButton.addActionListener(e -> continueQuiz());
exitButton.addActionListener(e -> exitToSelection());
}
/**
*
*
* @param score
* @return
*/
private Color getScoreColor(int score) {
if (score >= 90) {
return new Color(0, 150, 0);
} else if (score >= 60) {
return new Color(255, 140, 0);
} else {
return new Color(200, 0, 0);
}
}
/**
*
*/
private void continueQuiz() {
GradeSelectionFrame frame = new GradeSelectionFrame(email, gradeType);
frame.setVisible(true);
this.dispose();
}
/**
* 退
*/
private void exitToSelection() {
GradeSelectionFrame frame = new GradeSelectionFrame(email, gradeType);
frame.setVisible(true);
this.dispose();
}
}

@ -0,0 +1,86 @@
package ui;
import javax.swing.*;
import java.awt.*;
/**
*
*
*/
public class UserSettingsDialog extends JDialog {
private static final long serialVersionUID = 1L;
private final String email;
private final String username;
private final Frame parentFrame;
/**
*
*
* @param parent
* @param email
* @param username
*/
public UserSettingsDialog(Frame parent, String email, String username) {
super(parent, "用户设置", true);
this.parentFrame = parent;
this.email = email;
this.username = username;
setSize(400, 250);
setLocationRelativeTo(parent);
setResizable(false);
JPanel panel = new JPanel(new GridBagLayout());
panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20));
GridBagConstraints gbc = new GridBagConstraints();
gbc.insets = new Insets(10, 5, 10, 5);
gbc.anchor = GridBagConstraints.WEST;
// 显示用户名
gbc.gridx = 0;
gbc.gridy = 0;
panel.add(new JLabel("用户名:"), gbc);
gbc.gridx = 1;
JLabel usernameLabel = new JLabel(username);
usernameLabel.setFont(new Font("微软雅黑", Font.BOLD, 14));
panel.add(usernameLabel, gbc);
// 显示邮箱
gbc.gridx = 0;
gbc.gridy = 1;
panel.add(new JLabel("邮箱:"), gbc);
gbc.gridx = 1;
JLabel emailLabel = new JLabel(email);
emailLabel.setFont(new Font("微软雅黑", Font.PLAIN, 14));
panel.add(emailLabel, gbc);
// 修改密码按钮
JButton changePasswordButton = new JButton("修改密码");
changePasswordButton.setPreferredSize(new Dimension(120, 30));
gbc.gridx = 0;
gbc.gridy = 2;
gbc.gridwidth = 2;
gbc.anchor = GridBagConstraints.CENTER;
gbc.insets = new Insets(20, 5, 10, 5);
panel.add(changePasswordButton, gbc);
add(panel);
// 按钮点击事件
changePasswordButton.addActionListener(e -> openChangePasswordDialog());
}
/**
*
*/
private void openChangePasswordDialog() {
// 关闭当前对话框
this.dispose();
// 打开修改密码对话框
ChangePasswordDialog dialog = new ChangePasswordDialog(parentFrame, email);
dialog.setVisible(true);
}
}

@ -0,0 +1,8 @@
{
"1617940512@qq.com": {
"email": "1617940512@qq.com",
"password": "Qwerty1",
"grade": "primary",
"username": "zhang"
}
}
Loading…
Cancel
Save