diff --git a/README.md b/README.md deleted file mode 100644 index c8e97a6..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# mathpaper - diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..34124b9 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,148 @@ +# 数学试卷生成系统 + +## 项目简介 + +这是一个智能数学试卷生成系统,能够根据不同年级(小学、初中、高中)自动生成相应难度的数学题目,并保存为试卷文件。 + +## 系统特性 + +### 🎯 核心功能 +- **多年级支持**:支持小学、初中、高中三个年级的题目生成 +- **智能题目生成**:根据年级自动调整题目难度和类型 +- **重复检测**:防止生成重复题目,确保试卷质量 +- **文件保存**:自动保存试卷到指定目录,支持时间戳命名 + +### 🚀 性能优化 +- **高效缓存机制**:智能缓存用户题目,支持LRU和时间过期策略 +- **优化文件I/O**:使用NIO和缓冲区提升文件操作性能 +- **内存管理**:自动清理过期缓存,防止内存泄漏 +- **异常处理**:完善的错误处理和降级机制 + +### 🎨 用户体验 +- **美观界面**:现代化的控制台界面设计 +- **进度显示**:题目生成过程中显示实时进度条 +- **即时预览**:生成完成后立即预览题目内容 +- **友好提示**:清晰的操作指引和状态反馈 + +## 题目类型 + +### 小学数学 +- 基础四则运算(加减乘除) +- 简单括号表达式 +- 数值范围:1-100 + +### 初中数学 +- 复杂四则运算 +- 多层括号嵌套 +- 平方运算 +- 数值范围:1-1000 + +### 高中数学 +- 三角函数(sin, cos, tan) +- 根式运算(√) +- 幂运算(x²) +- 复合函数表达式 +- 数值范围:1-10000 + +## 系统架构 + +``` +src/com/mathpaper/ +├── Main.java # 主程序入口 +├── model/ +│ └── User.java # 用户模型 +├── generator/ +│ ├── QuestionGenerator.java # 题目生成器接口 +│ ├── PrimaryGenerator.java # 小学题目生成器 +│ ├── JuniorGenerator.java # 初中题目生成器 +│ └── SeniorGenerator.java # 高中题目生成器 +└── util/ + ├── FileUtil.java # 文件操作工具 + └── DuplicateChecker.java # 重复检测工具 +``` + +## 使用方法 + +### 启动程序 +```bash +# Windows PowerShell +.\start.ps1 + +# 或直接运行 +java com.mathpaper.Main +``` + +### 操作流程 +1. **登录**:输入用户名(3-20位字母、数字或下划线) +2. **选择年级**:选择小学、初中或高中 +3. **生成试卷**:输入题目数量(1-50道) +4. **查看结果**:系统自动保存并预览生成的试卷 + +### 文件结构 +``` +doc/paper/用户名/时间戳.txt +``` +例如:`doc/paper/test123/2025-09-28-22-50-30.txt` + +## 技术特点 + +### 代码质量 +- **模块化设计**:清晰的分层架构,职责分离 +- **设计模式**:使用工厂模式、策略模式等 +- **异常处理**:完善的错误处理和用户友好的提示 +- **代码规范**:统一的编码风格和命名规范 + +### 性能优化 +- **缓存策略**: + - LRU缓存淘汰算法 + - 时间过期自动清理 + - 最大缓存用户数限制 +- **文件I/O优化**: + - NIO文件操作 + - 8KB缓冲区 + - UTF-8编码支持 +- **内存管理**: + - 自动垃圾回收 + - 资源及时释放 + - 内存泄漏防护 + +### 用户体验 +- **响应式界面**:实时进度反馈 +- **智能提示**:操作指引和错误提示 +- **数据验证**:输入参数严格验证 +- **容错处理**:优雅的错误恢复机制 + +## 开发环境 + +- **Java版本**:Java 8+ +- **编码格式**:UTF-8 +- **操作系统**:Windows/Linux/macOS +- **依赖**:无外部依赖,纯Java实现 + +## 版本历史 + +### v2.1 (当前版本) +- ✅ 完成性能优化和代码重构 +- ✅ 优化用户界面和交互体验 +- ✅ 增强缓存机制和内存管理 +- ✅ 改进文件I/O操作效率 +- ✅ 完善异常处理和错误恢复 + +### v2.0 +- ✅ 重构代码架构,提升可维护性 +- ✅ 优化题目生成算法 +- ✅ 增加重复检测功能 +- ✅ 改进用户界面设计 + +### v1.0 +- ✅ 基础功能实现 +- ✅ 多年级题目生成 +- ✅ 文件保存功能 + +## 贡献指南 + +欢迎提交Issue和Pull Request来改进这个项目! + +## 许可证 + +本项目采用MIT许可证。 \ No newline at end of file diff --git a/doc/bin/start.bat b/doc/bin/start.bat new file mode 100644 index 0000000..e86b85c --- /dev/null +++ b/doc/bin/start.bat @@ -0,0 +1,15 @@ +@echo off +chcp 65001 >nul +echo 启动数学试卷生成系统... +echo. +echo === 可用用户账号 === +echo 小学用户: 张三1, 张三2, 张三3, zhangsan1, zhangsan2, zhangsan3 +echo 初中用户: 李四1, 李四2, 李四3, lisi1, lisi2, lisi3 +echo 高中用户: 王五1, 王五2, 王五3, wangwu1, wangwu2, wangwu3 +echo 测试账号: test123, 测试 +echo =============================== +echo. +echo 正在启动程序... +cd /d "%~dp0..\.." +java -cp src com.mathpaper.Main +pause \ No newline at end of file diff --git a/doc/bin/start.ps1 b/doc/bin/start.ps1 new file mode 100644 index 0000000..bab752a --- /dev/null +++ b/doc/bin/start.ps1 @@ -0,0 +1,28 @@ +# Math Paper Generation System Startup Script +# Automatically set UTF-8 encoding and start the program + +Write-Host "启动数学试卷生成系统..." -ForegroundColor Green + +# Set console encoding to UTF-8 +Write-Host "设置字符编码为UTF-8..." -ForegroundColor Yellow +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 +[Console]::InputEncoding = [System.Text.Encoding]::UTF8 + +Write-Host "编码设置完成!" -ForegroundColor Green +Write-Host "" + +# Display available user accounts +Write-Host "=== 可用用户账号 ===" -ForegroundColor Cyan +Write-Host "小学用户: 张三1, 张三2, 张三3, zhangsan1, zhangsan2, zhangsan3" -ForegroundColor White +Write-Host "初中用户: 李四1, 李四2, 李四3, lisi1, lisi2, lisi3" -ForegroundColor White +Write-Host "高中用户: 王五1, 王五2, 王五3, wangwu1, wangwu2, wangwu3" -ForegroundColor White +Write-Host "测试账号: test123, 测试" -ForegroundColor White +Write-Host "===============================" -ForegroundColor Cyan +Write-Host "" + +# Change to project root directory +Set-Location (Join-Path $PSScriptRoot "..\..") + +# Start Java program +Write-Host "正在启动程序..." -ForegroundColor Green +java -cp src com.mathpaper.Main \ No newline at end of file diff --git a/doc/paper/abc/2025-09-28-23-33-37.txt b/doc/paper/abc/2025-09-28-23-33-37.txt new file mode 100644 index 0000000..b875614 --- /dev/null +++ b/doc/paper/abc/2025-09-28-23-33-37.txt @@ -0,0 +1,5 @@ +1. 82 + 16 + 23 * (35 + 94) + +2. 56 + +3. 23 + 90 - 15 * (76 + 26) \ No newline at end of file diff --git a/doc/paper/admin/2025-09-28-23-33-35.txt b/doc/paper/admin/2025-09-28-23-33-35.txt new file mode 100644 index 0000000..b427967 --- /dev/null +++ b/doc/paper/admin/2025-09-28-23-33-35.txt @@ -0,0 +1,5 @@ +1. 42 - 60 + +2. (19 * 60 + 9) + 64 + 89 + +3. 79 \ No newline at end of file diff --git a/doc/paper/admin/2025-09-28-23-33-37.txt b/doc/paper/admin/2025-09-28-23-33-37.txt new file mode 100644 index 0000000..f4ae68d --- /dev/null +++ b/doc/paper/admin/2025-09-28-23-33-37.txt @@ -0,0 +1,5 @@ +1. 69 + 54 * 22 + +2. 6 + +3. 32 / 24 / 47 \ No newline at end of file diff --git a/doc/paper/admin123/2025-09-28-23-33-36.txt b/doc/paper/admin123/2025-09-28-23-33-36.txt new file mode 100644 index 0000000..d7682ed --- /dev/null +++ b/doc/paper/admin123/2025-09-28-23-33-36.txt @@ -0,0 +1,5 @@ +1. 31 / (37 / 66) + +2. 61 / 34 + (91 * 94) + 78 + +3. 93 + 77 * 2 + 54 \ No newline at end of file diff --git a/doc/paper/admin123/2025-09-28-23-33-37.txt b/doc/paper/admin123/2025-09-28-23-33-37.txt new file mode 100644 index 0000000..eb0bed2 --- /dev/null +++ b/doc/paper/admin123/2025-09-28-23-33-37.txt @@ -0,0 +1,5 @@ +1. 47 * 35 + +2. (25 + 39 / 16) * 2 - 93 + +3. 28 + 60 \ No newline at end of file diff --git a/doc/paper/null/2025-09-28-23-33-36.txt b/doc/paper/null/2025-09-28-23-33-36.txt new file mode 100644 index 0000000..843d11f --- /dev/null +++ b/doc/paper/null/2025-09-28-23-33-36.txt @@ -0,0 +1,5 @@ +1. 20 * 32 + +2. 74 + 35 + +3. 95 * 14 \ No newline at end of file diff --git a/doc/paper/primary1/2025-09-28-21-58-30.txt b/doc/paper/primary1/2025-09-28-21-58-30.txt new file mode 100644 index 0000000..379d618 --- /dev/null +++ b/doc/paper/primary1/2025-09-28-21-58-30.txt @@ -0,0 +1,23 @@ +1. 100 - 55 + 32 + +2. 80 + +3. 92 + +4. 76 * 100 / 2 / 73 * 6 + +5. 37 * 52 + 90 + +6. 36 - 73 + +7. 26 * (95 * 90) + 60 + +8. 100 + +9. 82 - 2 + +10. 67 + +11. 27 * 22 / 30 * 52 + +12. 98 - 41 / (95 / 83) \ No newline at end of file diff --git a/doc/paper/test123/2025-09-28-22-48-11.txt b/doc/paper/test123/2025-09-28-22-48-11.txt new file mode 100644 index 0000000..e3a3038 --- /dev/null +++ b/doc/paper/test123/2025-09-28-22-48-11.txt @@ -0,0 +1,9 @@ +1. 77 + +2. 10 - (98 * 46) + +3. (63 + 61 * 45) + +4. 10 + 98 + +5. (51 - 48 + 2) \ No newline at end of file diff --git a/doc/paper/test123/2025-09-28-22-50-30.txt b/doc/paper/test123/2025-09-28-22-50-30.txt new file mode 100644 index 0000000..c860a04 --- /dev/null +++ b/doc/paper/test123/2025-09-28-22-50-30.txt @@ -0,0 +1,39 @@ +1. sin(3) * tan(3) + +2. tan(√(5+2)) + tan(5) - 8 / 10 + 4 + +3. sin(6) / sin(2) + 6 * 4^2 + √7 + +4. tan(7) - 7^2 - (3 * 1 * tan(3)) + +5. cos(5) - √2 / 4^2 + +6. tan(√(8+4)) - 1 + +7. sin(√(8+2)) + √7 * 8 + +8. tan(√(4+4)) - tan(9) + √3 + √3 + +9. cos(3) - 5 + +10. sin(2) / tan(2) / 7 + +11. cos(3) * 4 * 5 + +12. sin(6) / sin(4) - 6^2 + 7 + +13. tan(√(8+3)) - tan(9) / 10 / 7 + +14. cos(6) - √9 - 9^2 + 2^2 * 2^2 + +15. tan(10) - 7 (* tan(6) + 9) + +16. tan(√(10+2)) + cos(3) + +17. cos(√(4+1)) * 7 + +18. tan(10) - √2 / tan(√(10+3)) / 4 / 7^2 + +19. sin(6) - sin(2) - 1 / √8 - 8 + +20. tan(3) / cos(5) \ No newline at end of file diff --git a/doc/paper/test123/2025-09-28-23-32-54.txt b/doc/paper/test123/2025-09-28-23-32-54.txt new file mode 100644 index 0000000..5da9266 --- /dev/null +++ b/doc/paper/test123/2025-09-28-23-32-54.txt @@ -0,0 +1,5 @@ +1. 66 / 95 + 87 + +2. (27 / 36 / 19) * 68 + +3. 1 / 2 \ No newline at end of file diff --git a/doc/paper/user/2025-09-28-23-33-36.txt b/doc/paper/user/2025-09-28-23-33-36.txt new file mode 100644 index 0000000..cd20eeb --- /dev/null +++ b/doc/paper/user/2025-09-28-23-33-36.txt @@ -0,0 +1,5 @@ +1. 33 + +2. 27 + 99 - (24 + 51 / 26) + +3. (25 + 26) + 71 \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-28-22-02-48.txt b/doc/paper/张三1/2025-09-28-22-02-48.txt new file mode 100644 index 0000000..6ffe6ba --- /dev/null +++ b/doc/paper/张三1/2025-09-28-22-02-48.txt @@ -0,0 +1,31 @@ +1. 39 + +2. 76 - 14 / 8 + +3. 49 + +4. 11 - 76 * (58 / 38 - 73) + +5. 16 - 61 * 69 + +6. (47 / 82 / 65) + +7. (62 / 70) / 10 + +8. 94 * 27 + 50 - 10 + +9. 44 - (2 + 76 + 58) - 2 + +10. 27 + +11. 57 / 60 + 23 + +12. 90 * 63 + +13. 72 + 81 + 54 * 1 + +14. 46 / 92 + +15. 58 + 12 + +16. (49 * 19 * 69 * 9) \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-28-22-03-02.txt b/doc/paper/张三1/2025-09-28-22-03-02.txt new file mode 100644 index 0000000..07c7565 --- /dev/null +++ b/doc/paper/张三1/2025-09-28-22-03-02.txt @@ -0,0 +1,23 @@ +1. 54 + +2. 100 / 73 + 69 * 11 / 60 + +3. 8 + 4 * 40 * 60 + +4. 91 + +5. 88 / 58 - 77 + +6. 2 + +7. 89 * 87 - 99 + +8. 89 - 36 + 12 + +9. 90 / 21 + +10. 61 - 16 / 93 * 57 * 68 + +11. 33 * (96 / 45) + +12. 14 + (63 + 26) \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-28-22-59-11.txt b/doc/paper/张三1/2025-09-28-22-59-11.txt new file mode 100644 index 0000000..93f214d --- /dev/null +++ b/doc/paper/张三1/2025-09-28-22-59-11.txt @@ -0,0 +1,25 @@ +1. tan(2) / 2 + 6 - √7 - tan(√(2+1)) + +2. sin(6) * 6^2 / tan(4) + +3. sin(1) * cos(10) / √5 + 1 + +4. tan(√(10+5)) + cos(6) / 10^2 * √6 + 7 + +5. tan(8) / (4 / 9 + 1) + +6. (tan(8) / 9^2 - 6^2 /) 2 + +7. tan(1) * √6 - sin(7) * 5 * √5 + +8. cos(8) / cos(8) / √5 - √2 / 10 + +9. cos(√(1+2)) * 10 / tan(2) / 5 + 3 + +10. cos(√(1+1)) * 5 + 6 + 6 + +11. sin(5) * 9^2 - tan(7) / 4 * 10 + +12. sin(10) / √8 + 8^2 * 6^2 - 5 + +13. tan(4) * 10 \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-28-22-59-26.txt b/doc/paper/张三1/2025-09-28-22-59-26.txt new file mode 100644 index 0000000..041d96b --- /dev/null +++ b/doc/paper/张三1/2025-09-28-22-59-26.txt @@ -0,0 +1,25 @@ +1. √23 * 31 + +2. 18^2 - 1 + √23 / 18 + +3. 23^2 - (33 / 25) + +4. 27^2 - 9^2 + +5. √10 - 22^2 * 7 + +6. 40^2 * √25 * 12 - 16 + +7. √25 / √1 * 18 * 8 + 32 + +8. 7^2 / √24 / (50 * 35) + +9. 5^2 (* 21 + 35) / 9 * 33 + +10. √25 - 48^2 + 9 + +11. 20^2 * 48 + 39 + +12. √25 / 38 - 21 * 28 * 32 + +13. 4^2 / √2 * 15 \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-28-22-59-37.txt b/doc/paper/张三1/2025-09-28-22-59-37.txt new file mode 100644 index 0000000..e88cec6 --- /dev/null +++ b/doc/paper/张三1/2025-09-28-22-59-37.txt @@ -0,0 +1,29 @@ +1. 65 / 18 + +2. 28 * 78 + +3. 22 + 93 * 63 * 98 + +4. 25 - 33 - (35 + 18 / 82) + +5. 59 + +6. 98 + 52 * 94 + +7. 100 * (87 + 51) + +8. (15 / 44 / 55) + +9. 37 - 48 * 84 + +10. 10 * 38 + +11. 76 + +12. 67 / (23 * 85) + +13. 38 - 100 * 77 + +14. 94 * 33 + +15. 38 \ No newline at end of file diff --git a/doc/paper/张三1/2025-09-29-00-09-16.txt b/doc/paper/张三1/2025-09-29-00-09-16.txt new file mode 100644 index 0000000..245915c --- /dev/null +++ b/doc/paper/张三1/2025-09-29-00-09-16.txt @@ -0,0 +1,27 @@ +1. 10^2 + 42 - 20 + +2. √5 * 20 + +3. (√20 - √25 + 11) / 26 + 29 + +4. √23 * 6 / 32 + +5. 31^2 * (35 - √25) + +6. √25 + 41 * 11 * 8 + +7. 48^2 / 7 + +8. √25 / 17 + +9. 14^2 / 3 + +10. √25 / 1 + √3 * 21 + +11. √25 * (17 / 32^2) + +12. 26^2 / 49 - 15^2 + +13. 43^2 + (49 - 18^2) + +14. 49^2 / 27 - 28^2 / 13 + 5 \ No newline at end of file diff --git a/doc/paper/王五2/2025-09-28-22-26-06.txt b/doc/paper/王五2/2025-09-28-22-26-06.txt new file mode 100644 index 0000000..494c07b --- /dev/null +++ b/doc/paper/王五2/2025-09-28-22-26-06.txt @@ -0,0 +1,25 @@ +1. cos(1) * 4 * tan(√(6+5)) * 2 / √6 + +2. cos(10) * sin(√(10+2)) / √1 + +3. sin(√(3+3)) + sin(√(5+2)) + 6 / 10 + 9 + +4. cos(√(9+2)) / √1 * 9^2 + +5. sin(9) + tan(√(2+5)) + +6. cos(8) - 1 - sin(√(10+2)) + 8 + +7. tan(2) - 5 + tan(9) / 1 + +8. tan(3) / 10 * tan(4) + 7 + 7^2 + +9. tan(√(9+4)) - tan(√(7+4)) + +10. tan(1) / sin(√(3+2)) / 4 + +11. tan(10) / 3 * 3 - cos(√(2+5)) * sin(10) + +12. sin(3) + √3 / 5 + +13. tan(5) / cos(7) / sin(√(6+3)) - tan(3) + 2 \ No newline at end of file diff --git a/src/com/mathpaper/Main.class b/src/com/mathpaper/Main.class new file mode 100644 index 0000000..d203cca Binary files /dev/null and b/src/com/mathpaper/Main.class differ diff --git a/src/com/mathpaper/Main.java b/src/com/mathpaper/Main.java new file mode 100644 index 0000000..1d74e91 --- /dev/null +++ b/src/com/mathpaper/Main.java @@ -0,0 +1,469 @@ +package com.mathpaper; + +import com.mathpaper.model.User; +import com.mathpaper.generator.QuestionGenerator; +import com.mathpaper.generator.PrimaryGenerator; +import com.mathpaper.generator.JuniorGenerator; +import com.mathpaper.generator.SeniorGenerator; +import com.mathpaper.util.FileUtil; +import com.mathpaper.util.DuplicateChecker; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Scanner; + +/** + * 数学试卷生成程序主类 + * 负责用户登录、交互和试卷生成的主要流程 + * + * @author 系统 + * @version 2.0 + */ +public class Main { + + // 常量定义 + private static final int MIN_QUESTIONS = 1; + private static final int MAX_QUESTIONS = 100; + private static final int PREVIEW_COUNT = 5; + private static final int MAX_ATTEMPTS_MULTIPLIER = 10; + private static final String GRADE_SWITCH_PREFIX = "切换为"; + private static final String EXIT_COMMAND = "-1"; + + // 当前年级(全局变量) + private static String currentGrade = "小学"; + + // 界面美化常量 + private static final String SEPARATOR = "=" + "=".repeat(50); + private static final String MENU_SEPARATOR = "=" + "=".repeat(50); + private static final int PROGRESS_BAR_LENGTH = 30; + + // 预设用户账号 + private static final Map USERS = new HashMap<>(); + + static { + initializeUsers(); + } + + /** + * 初始化用户账号 + */ + private static void initializeUsers() { + // 小学用户(中文和英文用户名) + addUser("张三1", "123", "小学"); + addUser("张三2", "123", "小学"); + addUser("张三3", "123", "小学"); + addUser("zhangsan1", "123", "小学"); + addUser("zhangsan2", "123", "小学"); + addUser("zhangsan3", "123", "小学"); + addUser("primary1", "123", "小学"); + addUser("primary2", "123", "小学"); + + // 初中用户(中文和英文用户名) + addUser("李四1", "123", "初中"); + addUser("李四2", "123", "初中"); + addUser("李四3", "123", "初中"); + addUser("lisi1", "123", "初中"); + addUser("lisi2", "123", "初中"); + addUser("lisi3", "123", "初中"); + addUser("junior1", "123", "初中"); + addUser("junior2", "123", "初中"); + + // 高中用户(中文和英文用户名) + addUser("王五1", "123", "高中"); + addUser("王五2", "123", "高中"); + addUser("王五3", "123", "高中"); + addUser("wangwu1", "123", "高中"); + addUser("wangwu2", "123", "高中"); + addUser("wangwu3", "123", "高中"); + addUser("senior1", "123", "高中"); + addUser("senior2", "123", "高中"); + + // 测试账号 + addUser("test123", "123", "小学"); + addUser("测试", "123", "小学"); + } + + /** + * 添加用户到系统 + */ + private static void addUser(String username, String password, String grade) { + // 由于User类已简化,这里只存储用户名 + try { + USERS.put(username, new User(username)); + } catch (IllegalArgumentException e) { + System.err.println("添加用户失败: " + username + " - " + e.getMessage()); + } + } + + /** + * 程序主入口 + */ + public static void main(String[] args) { + Scanner scanner = new Scanner(System.in); + + try { + // 定期清理过期缓存 + DuplicateChecker.cleanupExpiredCache(); + + // 用户登录 + User user = performLogin(scanner); + if (user == null) { + System.out.println("登录失败,程序退出"); + return; + } + + // 主交互循环 + runMainLoop(scanner, user); + + } catch (Exception e) { + System.err.println("程序运行出现异常: " + e.getMessage()); + } finally { + scanner.close(); + } + } + + /** + * 用户登录 + */ + private static User performLogin(Scanner scanner) { + System.out.println("=== 用户登录 ==="); + System.out.println("请输入用户名和密码进行登录"); + System.out.println(); + + while (true) { + System.out.print("用户名: "); + String username = scanner.nextLine().trim(); + + // 验证用户名格式 + String validationError = User.getValidationError(username); + if (validationError != null) { + System.out.println("❌ " + validationError); + System.out.println("请重新输入:"); + continue; + } + + // 检查用户是否存在 + if (!USERS.containsKey(username)) { + System.out.println("❌ 用户名不存在,请检查输入或使用以下用户名:"); + System.out.println("中文用户:张三1, 张三2, 张三3, 李四1, 李四2, 李四3, 王五1, 王五2, 王五3, 测试"); + System.out.println("英文用户:zhangsan1, zhangsan2, zhangsan3, lisi1, lisi2, lisi3, wangwu1, wangwu2, wangwu3, test123"); + System.out.println("请重新输入:"); + continue; + } + + System.out.print("密码: "); + String password = scanner.nextLine().trim(); + + // 验证密码(所有用户密码都是123) + if (!"123".equals(password)) { + System.out.println("❌ 密码错误!所有用户的密码都是 123"); + System.out.println("请重新输入:"); + continue; + } + + try { + User user = new User(username); + System.out.println("✅ 登录成功!欢迎," + username + "!"); + System.out.println(); + return user; + } catch (IllegalArgumentException e) { + System.out.println("❌ " + e.getMessage()); + System.out.println("请重新输入:"); + } + } + } + + /** + * 验证登录信息(简化版本) + */ + private static User validateLogin(String input) { + String[] parts = input.split("\\s+"); + + if (parts.length != 2) { + return null; + } + + String username = parts[0].trim(); + String password = parts[1].trim(); + + // 简单验证:检查用户是否存在 + User user = USERS.get(username); + if (user != null) { + return user; + } + + return null; + } + + /** + * 运行主交互循环 + */ + private static void runMainLoop(Scanner scanner, User user) { + QuestionGenerator generator = getGenerator(currentGrade); + + while (true) { + displayMainMenu(user); + String choice = scanner.nextLine().trim(); + + switch (choice) { + case "1": + handleGradeSwitch(scanner, user); + generator = getGenerator(currentGrade); + break; + case "2": + handleQuestionGeneration(scanner, user, generator); + break; + case "3": + displayExitMessage(); + return; + default: + System.out.println("❌ 无效选择,请重新输入!"); + } + } + } + + /** + * 显示欢迎界面 + */ + private static void displayWelcomeScreen(User user) { + System.out.println(SEPARATOR); + System.out.println("🎓 数学题目生成系统"); + System.out.println("👤 用户:" + user.getUsername()); + System.out.println("📚 当前年级:" + currentGrade); + System.out.println(SEPARATOR); + } + + /** + * 显示主菜单 + */ + private static void displayMainMenu(User user) { + System.out.println(MENU_SEPARATOR); + System.out.println("📚 当前年级:" + currentGrade); + System.out.println("👤 当前用户:" + user.getUsername()); + System.out.println(); + System.out.println("1. 切换年级"); + System.out.println("2. 生成试卷"); + System.out.println("3. 退出系统"); + System.out.println(MENU_SEPARATOR); + System.out.print("\n🔹 请选择操作(当前:" + currentGrade + "):"); + } + + /** + * 显示分隔线 + */ + private static void displaySeparator() { + System.out.println("\n" + "-".repeat(30) + "\n"); + } + + /** + * 显示退出消息 + */ + private static void displayExitMessage() { + System.out.println("\n" + SEPARATOR); + System.out.println("👋 感谢使用数学题目生成系统!"); + System.out.println("🎉 祝您学习愉快,再见!"); + System.out.println(SEPARATOR); + } + + /** + * 根据年级获取对应的题目生成器 + */ + private static QuestionGenerator getGenerator(String grade) { + switch (grade) { + case "小学": + return new PrimaryGenerator(); + case "初中": + return new JuniorGenerator(); + case "高中": + return new SeniorGenerator(); + default: + System.err.println("未知年级: " + grade + ",使用小学模式"); + return new PrimaryGenerator(); + } + } + + /** + * 验证年级是否有效 + */ + private static boolean isValidGrade(String grade) { + return "小学".equals(grade) || "初中".equals(grade) || "高中".equals(grade); + } + + /** + * 生成并保存试卷 + */ + private static void generateAndSavePaper(QuestionGenerator generator, int count, User user) { + try { + List questions = generateQuestionsWithProgress(generator, count, user.getUsername()); + + if (questions.isEmpty()) { + System.out.println("❌ 生成失败:无法生成题目,请稍后重试"); + return; + } + + if (questions.size() < count) { + System.out.println("⚠️ 注意:由于查重限制,实际生成了 " + questions.size() + " 道题目"); + } + + System.out.print("💾 正在保存试卷..."); + String path = FileUtil.getSavePath(user.getUsername()); + FileUtil.savePaper(path, questions); + System.out.println(" 完成!"); + System.out.println("✅ 试卷已保存至:" + path); + + displayQuestionPreview(questions); + + } catch (Exception e) { + System.err.println("❌ 生成试卷时出现错误: " + e.getMessage()); + } + } + + /** + * 带进度提示的题目生成 + */ + private static List generateQuestionsWithProgress(QuestionGenerator generator, int count, String username) { + List questions = new ArrayList<>(); + generator.clearCurrentQuestions(); + + int attempts = 0; + int maxAttempts = count * MAX_ATTEMPTS_MULTIPLIER; + + System.out.println("📊 生成进度:"); + + while (questions.size() < count && attempts < maxAttempts) { + try { + String question = generator.generateUniqueQuestion(); + + if (question != null && !question.trim().isEmpty()) { + // 检查是否与历史题目重复 + if (!DuplicateChecker.isDuplicate(question, username)) { + questions.add(question); + + // 显示进度 + displayProgress(questions.size(), count); + } + } + + } catch (Exception e) { + System.err.println("⚠️ 生成题目时出现错误: " + e.getMessage()); + } + + attempts++; + } + + if (questions.size() < count && attempts >= maxAttempts) { + System.out.println("\n⚠️ 提示:已达到最大尝试次数,可能存在题目重复过多的情况"); + } else if (questions.size() == count) { + System.out.println("\n🎉 题目生成完成!"); + } + + return questions; + } + + /** + * 显示生成进度 + */ + private static void displayProgress(int current, int total) { + int percentage = (int) ((double) current / total * 100); + int progressLength = (int) ((double) current / total * PROGRESS_BAR_LENGTH); + + StringBuilder progressBar = new StringBuilder(); + progressBar.append("["); + + for (int i = 0; i < PROGRESS_BAR_LENGTH; i++) { + if (i < progressLength) { + progressBar.append("█"); + } else { + progressBar.append("░"); + } + } + + progressBar.append("]"); + + System.out.print("\r" + progressBar.toString() + " " + current + "/" + total + " (" + percentage + "%)"); + + if (current == total) { + System.out.println(); // 换行 + } + } + + /** + * 显示题目预览 + */ + private static void displayQuestionPreview(List questions) { + System.out.println("\n📝 题目预览"); + System.out.println("─".repeat(40)); + + int displayCount = Math.min(PREVIEW_COUNT, questions.size()); + for (int i = 0; i < displayCount; i++) { + System.out.println((i + 1) + ". " + questions.get(i)); + } + + if (questions.size() > PREVIEW_COUNT) { + System.out.println(" ... 还有 " + (questions.size() - PREVIEW_COUNT) + " 道题目"); + } + + System.out.println("─".repeat(40)); + System.out.println("📊 总计:" + questions.size() + " 道题目"); + } + + /** + * 处理年级切换 + */ + private static void handleGradeSwitch(Scanner scanner, User user) { + System.out.println("\n📚 请选择年级:"); + System.out.println("1. 小学"); + System.out.println("2. 初中"); + System.out.println("3. 高中"); + System.out.print("请输入选择(1-3):"); + + String choice = scanner.nextLine().trim(); + String newGrade = null; + + switch (choice) { + case "1": + newGrade = "小学"; + break; + case "2": + newGrade = "初中"; + break; + case "3": + newGrade = "高中"; + break; + default: + System.out.println("❌ 无效选择!"); + return; + } + + if (!newGrade.equals(currentGrade)) { + currentGrade = newGrade; + System.out.println("✅ 已切换到" + currentGrade + "模式"); + } else { + System.out.println("ℹ️ 当前已是" + currentGrade + "模式"); + } + } + + /** + * 处理题目生成 + */ + private static void handleQuestionGeneration(Scanner scanner, User user, QuestionGenerator generator) { + System.out.print("\n📝 请输入要生成的题目数量(1-50):"); + String input = scanner.nextLine().trim(); + + try { + int count = Integer.parseInt(input); + if (count < 1 || count > 50) { + System.out.println("❌ 题目数量必须在1-50之间!"); + return; + } + + System.out.println("\n🔄 开始生成" + count + "道" + currentGrade + "数学题目..."); + generateAndSavePaper(generator, count, user); + + } catch (NumberFormatException e) { + System.out.println("❌ 请输入有效的数字!"); + } + } +} \ No newline at end of file diff --git a/src/com/mathpaper/generator/JuniorGenerator.class b/src/com/mathpaper/generator/JuniorGenerator.class new file mode 100644 index 0000000..3296504 Binary files /dev/null and b/src/com/mathpaper/generator/JuniorGenerator.class differ diff --git a/src/com/mathpaper/generator/JuniorGenerator.java b/src/com/mathpaper/generator/JuniorGenerator.java new file mode 100644 index 0000000..e1f6d72 --- /dev/null +++ b/src/com/mathpaper/generator/JuniorGenerator.java @@ -0,0 +1,151 @@ +package com.mathpaper.generator; + +import java.util.ArrayList; +import java.util.List; + +/** + * 初中题目生成器 + * 生成2-5个操作数的题目,至少含1个平方(^2)或开根号(√),支持嵌套括号 + * + * @author 系统 + * @version 2.0 + */ +public class JuniorGenerator extends QuestionGenerator { + + // 常量定义 + private static final int MIN_OPERANDS = 2; + private static final int MAX_OPERANDS = 5; + private static final int MIN_NUMBER = 1; + private static final int MAX_NUMBER = 50; // 避免数字过大 + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final double ADVANCED_OP_PROBABILITY = 0.3; // 30%概率添加高级运算 + private static final double BRACKET_PROBABILITY = 0.4; // 40%概率添加括号 + + /** + * 构造函数 + */ + public JuniorGenerator() { + super("初中"); + } + + @Override + public String generateQuestion() { + int numCount = randomInt(MIN_OPERANDS, MAX_OPERANDS); + List nums = generateNumbers(numCount); + List ops = generateOperators(numCount - 1); + return addAdvancedOperations(nums, ops); + } + + /** + * 生成指定数量的随机数(1-50,避免过大) + * @param count 数字数量 + * @return 数字列表 + */ + private List generateNumbers(int count) { + List nums = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + nums.add(randomInt(MIN_NUMBER, MAX_NUMBER)); + } + return nums; + } + + /** + * 生成指定数量的运算符 + * @param count 运算符数量 + * @return 运算符列表 + */ + private List generateOperators(int count) { + List ops = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + ops.add(randomChoice(OPERATORS)); + } + return ops; + } + + /** + * 添加高级运算(平方、开根号)和嵌套括号 + * @param nums 数字列表 + * @param ops 运算符列表 + * @return 包含高级运算的表达式字符串 + */ + private String addAdvancedOperations(List nums, List ops) { + StringBuilder sb = new StringBuilder(nums.size() * 12); // 预估容量 + + // 确保至少有一个平方或开根号 + boolean hasAdvanced = false; + int advancedCount = 0; + + for (int i = 0; i < nums.size(); i++) { + int num = nums.get(i); + + // 决定是否添加高级运算 + boolean shouldAddAdvanced = !hasAdvanced || + (advancedCount < 2 && random.nextDouble() < ADVANCED_OP_PROBABILITY); + + if (shouldAddAdvanced) { + if (random.nextBoolean()) { + // 添加平方 + sb.append(num).append("^2"); + } else { + // 添加开根号(选择较小的数避免复杂计算) + int sqrtNum = Math.min(num, 25); // 限制开根号的数不超过25 + sb.append("√").append(sqrtNum); + } + hasAdvanced = true; + advancedCount++; + } else { + sb.append(num); + } + + if (i < ops.size()) { + sb.append(" ").append(ops.get(i)).append(" "); + } + } + + // 添加嵌套括号 + return addNestedBrackets(sb.toString()); + } + + /** + * 添加嵌套括号 + * @param expression 原始表达式 + * @return 带括号的表达式 + */ + private String addNestedBrackets(String expression) { + if (random.nextDouble() >= BRACKET_PROBABILITY) { + return expression; + } + + String[] parts = expression.split(" "); + if (parts.length < 3) { + return expression; + } + + // 选择括号范围(确保至少包含一个完整的运算) + int maxStart = parts.length - 3; // 至少需要 数字 运算符 数字 + int start = randomInt(0, maxStart); + int minEnd = start + 2; // 至少包含3个部分 + int maxEnd = Math.min(start + 6, parts.length - 1); // 最多包含7个部分 + int end = randomInt(minEnd, maxEnd); + + StringBuilder result = new StringBuilder(expression.length() + 4); + + for (int i = 0; i < parts.length; i++) { + if (i == start) { + result.append("("); + } + + result.append(parts[i]); + + if (i == end) { + result.append(")"); + } + + if (i < parts.length - 1) { + result.append(" "); + } + } + + return result.toString(); + } +} \ No newline at end of file diff --git a/src/com/mathpaper/generator/PrimaryGenerator.class b/src/com/mathpaper/generator/PrimaryGenerator.class new file mode 100644 index 0000000..b5aad32 Binary files /dev/null and b/src/com/mathpaper/generator/PrimaryGenerator.class differ diff --git a/src/com/mathpaper/generator/PrimaryGenerator.java b/src/com/mathpaper/generator/PrimaryGenerator.java new file mode 100644 index 0000000..c3357b0 --- /dev/null +++ b/src/com/mathpaper/generator/PrimaryGenerator.java @@ -0,0 +1,130 @@ +package com.mathpaper.generator; + +import java.util.ArrayList; +import java.util.List; + +/** + * 小学题目生成器 + * 生成1-5个操作数的四则运算题目,支持最多1层括号 + * + * @author 系统 + * @version 2.0 + */ +public class PrimaryGenerator extends QuestionGenerator { + + // 常量定义 + private static final int MIN_OPERANDS = 1; + private static final int MAX_OPERANDS = 5; + private static final int MIN_NUMBER = 1; + private static final int MAX_NUMBER = 100; + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final double BRACKET_PROBABILITY = 0.5; // 50%概率添加括号 + + /** + * 构造函数 + */ + public PrimaryGenerator() { + super("小学"); + } + + @Override + public String generateQuestion() { + int numCount = randomInt(MIN_OPERANDS, MAX_OPERANDS); + List nums = generateNumbers(numCount); + List ops = generateOperators(numCount - 1); + return addBrackets(nums, ops); + } + + /** + * 生成指定数量的随机数(1-100) + * @param count 数字数量 + * @return 数字列表 + */ + private List generateNumbers(int count) { + List nums = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + nums.add(randomInt(MIN_NUMBER, MAX_NUMBER)); + } + return nums; + } + + /** + * 生成指定数量的运算符 + * @param count 运算符数量 + * @return 运算符列表 + */ + private List generateOperators(int count) { + List ops = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + ops.add(randomChoice(OPERATORS)); + } + return ops; + } + + /** + * 添加括号(最多1层,无嵌套) + * @param nums 数字列表 + * @param ops 运算符列表 + * @return 带括号的表达式字符串 + */ + private String addBrackets(List nums, List ops) { + // 随机决定是否添加括号(需要至少3个操作数) + if (nums.size() >= 3 && random.nextDouble() < BRACKET_PROBABILITY) { + return addSimpleBrackets(nums, ops); + } + + return buildBasicExpression(nums, ops); + } + + /** + * 构建基本表达式(无括号) + * @param nums 数字列表 + * @param ops 运算符列表 + * @return 表达式字符串 + */ + private String buildBasicExpression(List nums, List ops) { + StringBuilder sb = new StringBuilder(nums.size() * 8); // 预估容量 + + for (int i = 0; i < nums.size(); i++) { + sb.append(nums.get(i)); + if (i < ops.size()) { + sb.append(" ").append(ops.get(i)).append(" "); + } + } + + return sb.toString(); + } + + /** + * 添加简单括号(不嵌套) + * @param nums 数字列表 + * @param ops 运算符列表 + * @return 带括号的表达式字符串 + */ + private String addSimpleBrackets(List nums, List ops) { + StringBuilder sb = new StringBuilder(nums.size() * 10); // 预估容量(包含括号) + + // 随机选择括号位置(确保至少包含2个操作数) + int maxStart = nums.size() - 2; + int bracketStart = randomInt(0, maxStart); + int bracketEnd = randomInt(bracketStart + 1, nums.size() - 1); + + for (int i = 0; i < nums.size(); i++) { + if (i == bracketStart) { + sb.append("("); + } + + sb.append(nums.get(i)); + + if (i == bracketEnd) { + sb.append(")"); + } + + if (i < ops.size()) { + sb.append(" ").append(ops.get(i)).append(" "); + } + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/src/com/mathpaper/generator/QuestionGenerator.class b/src/com/mathpaper/generator/QuestionGenerator.class new file mode 100644 index 0000000..1a42a0e Binary files /dev/null and b/src/com/mathpaper/generator/QuestionGenerator.class differ diff --git a/src/com/mathpaper/generator/QuestionGenerator.java b/src/com/mathpaper/generator/QuestionGenerator.java new file mode 100644 index 0000000..be5244a --- /dev/null +++ b/src/com/mathpaper/generator/QuestionGenerator.java @@ -0,0 +1,194 @@ +package com.mathpaper.generator; + +import java.util.HashSet; +import java.util.Set; +import java.util.Random; +import java.util.concurrent.ThreadLocalRandom; + +/** + * 题目生成器抽象基类 + * 定义题目生成的基本结构和通用方法 + * + * @author 系统 + * @version 2.0 + */ +public abstract class QuestionGenerator { + + // 常量定义 + protected static final int MAX_GENERATION_ATTEMPTS = 50; + protected static final int INITIAL_SET_CAPACITY = 64; + + protected final String grade; + protected final Set currentQuestions; // 本次生成的题目(防重复) + protected final Random random; + + // 性能统计 + private int totalAttempts = 0; + private int successfulGenerations = 0; + + /** + * 构造函数 + * @param grade 年级 + */ + public QuestionGenerator(String grade) { + if (grade == null || grade.trim().isEmpty()) { + throw new IllegalArgumentException("年级不能为空"); + } + + this.grade = grade.trim(); + this.currentQuestions = new HashSet<>(INITIAL_SET_CAPACITY); + this.random = ThreadLocalRandom.current(); // 使用线程安全的随机数生成器 + } + + /** + * 抽象方法:生成一道题目 + * 子类必须实现此方法 + * @return 生成的题目字符串 + */ + public abstract String generateQuestion(); + + /** + * 生成不重复的题目(优化版本) + * @return 生成的题目字符串,如果无法生成返回null + */ + public String generateUniqueQuestion() { + String question = null; + int attempts = 0; + + while (attempts < MAX_GENERATION_ATTEMPTS) { + try { + question = generateQuestion(); + totalAttempts++; + + if (question != null && !question.trim().isEmpty()) { + String normalizedQuestion = normalizeQuestion(question); + + if (!currentQuestions.contains(normalizedQuestion)) { + currentQuestions.add(normalizedQuestion); + successfulGenerations++; + return question; + } + } + + } catch (Exception e) { + System.err.println("生成题目时发生错误: " + e.getMessage()); + } + + attempts++; + } + + // 如果达到最大尝试次数仍未成功,记录警告 + if (attempts >= MAX_GENERATION_ATTEMPTS) { + System.err.println("警告:" + grade + "题目生成器达到最大尝试次数(" + MAX_GENERATION_ATTEMPTS + ")"); + } + + return question; // 返回最后一次生成的题目,即使可能重复 + } + + /** + * 标准化题目格式,用于重复检查 + * @param question 原始题目 + * @return 标准化后的题目 + */ + protected String normalizeQuestion(String question) { + if (question == null) { + return ""; + } + + return question.trim() + .replaceAll("\\s+", " ") // 统一空格 + .toLowerCase(); // 转小写(如果需要) + } + + /** + * 清空当前题目集合 + */ + public void clearCurrentQuestions() { + currentQuestions.clear(); + // 重置统计信息 + totalAttempts = 0; + successfulGenerations = 0; + } + + /** + * 获取年级 + * @return 年级字符串 + */ + public String getGrade() { + return grade; + } + + /** + * 获取当前已生成的题目数量 + * @return 题目数量 + */ + public int getCurrentQuestionCount() { + return currentQuestions.size(); + } + + /** + * 检查题目是否已存在 + * @param question 要检查的题目 + * @return 如果题目已存在返回true + */ + public boolean isQuestionExists(String question) { + if (question == null || question.trim().isEmpty()) { + return false; + } + + String normalized = normalizeQuestion(question); + return currentQuestions.contains(normalized); + } + + /** + * 获取生成效率统计 + * @return 成功率(0.0-1.0) + */ + public double getGenerationEfficiency() { + if (totalAttempts == 0) { + return 0.0; + } + return (double) successfulGenerations / totalAttempts; + } + + /** + * 获取统计信息字符串 + * @return 统计信息 + */ + public String getStatistics() { + return String.format("生成器统计[%s]: 总尝试=%d, 成功=%d, 效率=%.2f%%", + grade, totalAttempts, successfulGenerations, + getGenerationEfficiency() * 100); + } + + /** + * 生成指定范围内的随机整数(包含边界) + * @param min 最小值 + * @param max 最大值 + * @return 随机整数 + */ + protected int randomInt(int min, int max) { + if (min > max) { + throw new IllegalArgumentException("最小值不能大于最大值"); + } + + if (min == max) { + return min; + } + + return random.nextInt(max - min + 1) + min; + } + + /** + * 从数组中随机选择一个元素 + * @param array 数组 + * @return 随机选择的元素 + */ + protected T randomChoice(T[] array) { + if (array == null || array.length == 0) { + throw new IllegalArgumentException("数组不能为空"); + } + + return array[random.nextInt(array.length)]; + } +} \ No newline at end of file diff --git a/src/com/mathpaper/generator/SeniorGenerator.class b/src/com/mathpaper/generator/SeniorGenerator.class new file mode 100644 index 0000000..66e8b4e Binary files /dev/null and b/src/com/mathpaper/generator/SeniorGenerator.class differ diff --git a/src/com/mathpaper/generator/SeniorGenerator.java b/src/com/mathpaper/generator/SeniorGenerator.java new file mode 100644 index 0000000..83e199c --- /dev/null +++ b/src/com/mathpaper/generator/SeniorGenerator.java @@ -0,0 +1,216 @@ +package com.mathpaper.generator; + +import java.util.ArrayList; +import java.util.List; + +/** + * 高中题目生成器 + * 生成2-5个操作数的题目,至少含1个sin/cos/tan(弧度制),支持函数嵌套 + * + * @author 系统 + * @version 2.0 + */ +public class SeniorGenerator extends QuestionGenerator { + + // 常量定义 + private static final int MIN_OPERANDS = 2; + private static final int MAX_OPERANDS = 5; + private static final int MIN_NUMBER = 1; + private static final int MAX_NUMBER = 10; // 避免过大的弧度值 + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + private static final double TRIG_PROBABILITY = 0.4; // 40%概率添加三角函数 + private static final double NESTED_PROBABILITY = 0.3; // 30%概率函数嵌套 + private static final double ADVANCED_OP_PROBABILITY = 0.3; // 30%概率添加高级运算 + private static final double BRACKET_PROBABILITY = 0.3; // 30%概率添加括号 + + /** + * 构造函数 + */ + public SeniorGenerator() { + super("高中"); + } + + @Override + public String generateQuestion() { + int numCount = randomInt(MIN_OPERANDS, MAX_OPERANDS); + List nums = generateNumbers(numCount); + List ops = generateOperators(numCount - 1); + return addTrigonometricFunctions(nums, ops); + } + + /** + * 生成指定数量的随机数(1-10,避免过大的弧度值) + * @param count 数字数量 + * @return 数字列表 + */ + private List generateNumbers(int count) { + List nums = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + nums.add(randomInt(MIN_NUMBER, MAX_NUMBER)); + } + return nums; + } + + /** + * 生成指定数量的运算符 + * @param count 运算符数量 + * @return 运算符列表 + */ + private List generateOperators(int count) { + List ops = new ArrayList<>(count); + for (int i = 0; i < count; i++) { + ops.add(randomChoice(OPERATORS)); + } + return ops; + } + + /** + * 添加三角函数和函数嵌套 + * @param nums 数字列表 + * @param ops 运算符列表 + * @return 包含三角函数的表达式字符串 + */ + private String addTrigonometricFunctions(List nums, List ops) { + StringBuilder sb = new StringBuilder(nums.size() * 15); // 预估容量 + + // 确保至少有一个三角函数 + boolean hasTrig = false; + int trigCount = 0; + + for (int i = 0; i < nums.size(); i++) { + int num = nums.get(i); + + // 决定是否添加三角函数 + boolean shouldAddTrig = !hasTrig || + (trigCount < 2 && random.nextDouble() < TRIG_PROBABILITY); + + if (shouldAddTrig) { + String func = randomChoice(TRIG_FUNCTIONS); + + // 决定是否嵌套函数 + if (random.nextDouble() < NESTED_PROBABILITY) { + // 函数嵌套,如 sin(√(5+3)) + int innerNum = randomInt(1, 5); + sb.append(func).append("(√(").append(num) + .append("+").append(innerNum).append("))"); + } else { + // 简单函数,如 sin(3) + sb.append(func).append("(").append(num).append(")"); + } + hasTrig = true; + trigCount++; + } else { + // 添加其他高级运算或普通数字 + if (random.nextDouble() < ADVANCED_OP_PROBABILITY) { + if (random.nextBoolean()) { + sb.append(num).append("^2"); + } else { + // 限制开根号的数值避免过于复杂 + int sqrtNum = Math.min(num, 9); + sb.append("√").append(sqrtNum); + } + } else { + sb.append(num); + } + } + + if (i < ops.size()) { + sb.append(" ").append(ops.get(i)).append(" "); + } + } + + // 添加复杂括号 + return addComplexBrackets(sb.toString()); + } + + /** + * 添加复杂括号结构 + * @param expression 原始表达式 + * @return 带括号的表达式 + */ + private String addComplexBrackets(String expression) { + // 如果表达式中已有函数调用的括号,减少额外括号的概率 + boolean hasFunctionBrackets = expression.contains("sin(") || + expression.contains("cos(") || + expression.contains("tan("); + + double actualBracketProb = hasFunctionBrackets ? + BRACKET_PROBABILITY * 0.5 : BRACKET_PROBABILITY; + + if (random.nextDouble() >= actualBracketProb) { + return expression; + } + + String[] parts = expression.split(" "); + if (parts.length < 5) { + return expression; + } + + // 选择括号范围,避免破坏函数调用结构 + int maxStart = parts.length - 4; + int start = randomInt(0, maxStart); + int minEnd = start + 3; + int maxEnd = Math.min(start + 5, parts.length - 1); + int end = randomInt(minEnd, maxEnd); + + // 检查是否会破坏函数调用结构 + StringBuilder testBuilder = new StringBuilder(); + for (int i = start; i <= end; i++) { + testBuilder.append(parts[i]); + if (i < end) testBuilder.append(" "); + } + String testSection = testBuilder.toString(); + + // 如果选中的部分包含不完整的函数调用,跳过添加括号 + if (containsIncompleteFunction(testSection)) { + return expression; + } + + StringBuilder result = new StringBuilder(expression.length() + 4); + + for (int i = 0; i < parts.length; i++) { + if (i == start) { + result.append("("); + } + + result.append(parts[i]); + + if (i == end) { + result.append(")"); + } + + if (i < parts.length - 1) { + result.append(" "); + } + } + + return result.toString(); + } + + /** + * 检查字符串是否包含不完整的函数调用 + * @param text 要检查的文本 + * @return 如果包含不完整的函数调用返回true + */ + private boolean containsIncompleteFunction(String text) { + for (String func : TRIG_FUNCTIONS) { + if (text.contains(func + "(") && !text.contains(")")) { + return true; + } + if (text.contains(")") && !text.contains(func + "(")) { + // 检查是否有孤立的右括号 + int leftCount = 0; + int rightCount = 0; + for (char c : text.toCharArray()) { + if (c == '(') leftCount++; + if (c == ')') rightCount++; + } + if (rightCount > leftCount) { + return true; + } + } + } + return false; + } +} \ No newline at end of file diff --git a/src/com/mathpaper/model/User.class b/src/com/mathpaper/model/User.class new file mode 100644 index 0000000..af09c72 Binary files /dev/null and b/src/com/mathpaper/model/User.class differ diff --git a/src/com/mathpaper/model/User.java b/src/com/mathpaper/model/User.java new file mode 100644 index 0000000..52de919 --- /dev/null +++ b/src/com/mathpaper/model/User.java @@ -0,0 +1,118 @@ +package com.mathpaper.model; + +import java.util.regex.Pattern; + +/** + * 用户实体类 + * 表示系统中的用户信息 + * + * @author 系统 + * @version 2.0 + */ +public class User { + + // 用户名验证正则表达式:允许字母、数字、下划线和中文字符,长度2-20 + private static final Pattern USERNAME_PATTERN = Pattern.compile("^[a-zA-Z0-9_\\u4e00-\\u9fa5]{2,20}$"); + + // 用户名长度限制 + private static final int MIN_USERNAME_LENGTH = 2; + private static final int MAX_USERNAME_LENGTH = 20; + + private String username; + + /** + * 构造函数 + * @param username 用户名 + * @throws IllegalArgumentException 如果用户名无效 + */ + public User(String username) { + setUsername(username); + } + + /** + * 获取用户名 + * @return 用户名 + */ + public String getUsername() { + return username; + } + + /** + * 设置用户名(带验证) + * @param username 用户名 + * @throws IllegalArgumentException 如果用户名无效 + */ + public void setUsername(String username) { + if (!isValidUsername(username)) { + throw new IllegalArgumentException("用户名无效:必须是2-20位字母、数字、下划线或中文字符"); + } + this.username = username.trim(); + } + + /** + * 验证用户名是否有效 + * @param username 要验证的用户名 + * @return 如果用户名有效返回true,否则返回false + */ + public static boolean isValidUsername(String username) { + if (username == null || username.trim().isEmpty()) { + return false; + } + + String trimmed = username.trim(); + + // 检查长度 + if (trimmed.length() < MIN_USERNAME_LENGTH || trimmed.length() > MAX_USERNAME_LENGTH) { + return false; + } + + // 检查格式 + return USERNAME_PATTERN.matcher(trimmed).matches(); + } + + /** + * 获取用户名验证错误信息 + * @param username 要验证的用户名 + * @return 错误信息,如果用户名有效则返回null + */ + public static String getValidationError(String username) { + if (username == null || username.trim().isEmpty()) { + return "用户名不能为空"; + } + + String trimmed = username.trim(); + + if (trimmed.length() < MIN_USERNAME_LENGTH) { + return "用户名长度不能少于" + MIN_USERNAME_LENGTH + "位"; + } + + if (trimmed.length() > MAX_USERNAME_LENGTH) { + return "用户名长度不能超过" + MAX_USERNAME_LENGTH + "位"; + } + + if (!USERNAME_PATTERN.matcher(trimmed).matches()) { + return "用户名只能包含字母、数字、下划线和中文字符"; + } + + return null; // 验证通过 + } + + @Override + public String toString() { + return "User{username='" + username + "'}"; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) return true; + if (obj == null || getClass() != obj.getClass()) return false; + + User user = (User) obj; + return username != null ? username.equals(user.username) : user.username == null; + } + + @Override + public int hashCode() { + return username != null ? username.hashCode() : 0; + } +} \ No newline at end of file diff --git a/src/com/mathpaper/util/DuplicateChecker.class b/src/com/mathpaper/util/DuplicateChecker.class new file mode 100644 index 0000000..e04e51c Binary files /dev/null and b/src/com/mathpaper/util/DuplicateChecker.class differ diff --git a/src/com/mathpaper/util/DuplicateChecker.java b/src/com/mathpaper/util/DuplicateChecker.java new file mode 100644 index 0000000..8531601 --- /dev/null +++ b/src/com/mathpaper/util/DuplicateChecker.java @@ -0,0 +1,281 @@ +package com.mathpaper.util; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * 查重工具类 + * 负责检查题目是否在历史文件中重复 + * + * @author 系统 + * @version 2.0 + */ +public class DuplicateChecker { + + // 缓存用户的历史题目,提高查重性能 + private static final ConcurrentHashMap> userQuestionCache = new ConcurrentHashMap<>(); + + // 缓存的最大用户数,防止内存溢出 + private static final int MAX_CACHED_USERS = 10; + + // 缓存访问时间记录,用于LRU清理 + private static final ConcurrentHashMap cacheAccessTime = new ConcurrentHashMap<>(); + + // 缓存清理阈值(毫秒) + private static final long CACHE_EXPIRY_TIME = 30 * 60 * 1000; // 30分钟 + + /** + * 检查题目是否在历史文件中存在 + * @param question 要检查的题目 + * @param username 用户名 + * @return 如果题目重复返回true,否则返回false + */ + public static boolean isDuplicate(String question, String username) { + if (question == null || question.trim().isEmpty()) { + return false; + } + + if (username == null || username.trim().isEmpty()) { + return false; + } + + try { + String cleanQuestion = cleanQuestionText(question); + Set userQuestions = getUserQuestions(username); + return userQuestions.contains(cleanQuestion); + + } catch (Exception e) { + System.err.println("查重检查时发生错误: " + e.getMessage()); + return false; // 出错时默认不重复,避免阻塞题目生成 + } + } + + /** + * 获取用户的所有历史题目(带缓存) + */ + private static Set getUserQuestions(String username) { + // 检查缓存 + Set cachedQuestions = userQuestionCache.get(username); + if (cachedQuestions != null) { + // 更新访问时间 + cacheAccessTime.put(username, System.currentTimeMillis()); + return cachedQuestions; + } + + // 缓存未命中,从文件加载 + Set questions = loadUserQuestionsFromFiles(username); + + // 缓存管理:如果缓存已满,清理最久未使用的条目 + if (userQuestionCache.size() >= MAX_CACHED_USERS) { + cleanupOldestCache(); + } + + // 添加到缓存 + userQuestionCache.put(username, questions); + cacheAccessTime.put(username, System.currentTimeMillis()); + + return questions; + } + + /** + * 从文件加载用户的历史题目 + */ + private static Set loadUserQuestionsFromFiles(String username) { + Set questions = new HashSet<>(); + + try { + String userDir = FileUtil.getUserPaperDir(username); + Path userPath = Paths.get(userDir); + + if (!Files.exists(userPath)) { + return questions; + } + + // 使用NIO遍历用户目录下的所有文件 + Files.walk(userPath) + .filter(Files::isRegularFile) + .filter(path -> path.toString().endsWith(".txt")) + .forEach(path -> { + try { + Files.lines(path, StandardCharsets.UTF_8) + .map(String::trim) + .filter(line -> !line.isEmpty()) + .map(DuplicateChecker::cleanQuestionText) + .forEach(questions::add); + } catch (IOException e) { + System.err.println("读取文件失败: " + path + " - " + e.getMessage()); + } + }); + + } catch (Exception e) { + System.err.println("加载用户题目失败: " + e.getMessage()); + } + + return questions; + } + + /** + * 清理最久未使用的缓存条目 + */ + private static void cleanupOldestCache() { + if (cacheAccessTime.isEmpty()) { + return; + } + + // 找到最久未访问的用户 + String oldestUser = cacheAccessTime.entrySet().stream() + .min((e1, e2) -> Long.compare(e1.getValue(), e2.getValue())) + .map(entry -> entry.getKey()) + .orElse(null); + + if (oldestUser != null) { + userQuestionCache.remove(oldestUser); + cacheAccessTime.remove(oldestUser); + } + } + + /** + * 定期清理过期缓存 + */ + public static void cleanupExpiredCache() { + long currentTime = System.currentTimeMillis(); + + cacheAccessTime.entrySet().removeIf(entry -> { + if (currentTime - entry.getValue() > CACHE_EXPIRY_TIME) { + userQuestionCache.remove(entry.getKey()); + return true; + } + return false; + }); + } + + /** + * 从单个文件加载题目 + */ + private static void loadQuestionsFromFile(Path filePath, Set questions) { + try (BufferedReader reader = Files.newBufferedReader(filePath, StandardCharsets.UTF_8)) { + String line; + while ((line = reader.readLine()) != null) { + String cleanLine = cleanQuestionText(line); + if (!cleanLine.isEmpty()) { + questions.add(cleanLine); + } + } + } catch (IOException e) { + System.err.println("读取文件时发生错误: " + filePath + " - " + e.getMessage()); + } + } + + /** + * 清理题目文本,统一格式用于比较 + * @param text 原始文本 + * @return 清理后的题目内容 + */ + private static String cleanQuestionText(String text) { + if (text == null || text.trim().isEmpty()) { + return ""; + } + + String cleaned = text.trim(); + + // 去除题号(如"1. "、"2) "等格式) + cleaned = cleaned.replaceFirst("^\\d+[.)、]\\s*", ""); + + // 去除多余的空白字符 + cleaned = cleaned.replaceAll("\\s+", " ").trim(); + + return cleaned; + } + + /** + * 批量检查题目列表是否有重复 + * @param questions 题目列表 + * @param username 用户名 + * @return 重复的题目数量 + */ + public static int countDuplicates(String[] questions, String username) { + if (questions == null || questions.length == 0) { + return 0; + } + + if (username == null || username.trim().isEmpty()) { + return 0; + } + + int duplicateCount = 0; + try { + for (String question : questions) { + if (isDuplicate(question, username)) { + duplicateCount++; + } + } + } catch (Exception e) { + System.err.println("批量查重时发生错误: " + e.getMessage()); + } + + return duplicateCount; + } + + /** + * 获取用户历史试卷文件数量 + * @param username 用户名 + * @return 历史试卷文件数量 + */ + public static int getHistoryFileCount(String username) { + if (username == null || username.trim().isEmpty()) { + return 0; + } + + try { + String userDir = FileUtil.getUserPaperDir(username); + Path dirPath = Paths.get(userDir); + + if (!Files.exists(dirPath) || !Files.isDirectory(dirPath)) { + return 0; + } + + return (int) Files.list(dirPath) + .filter(path -> Files.isRegularFile(path)) + .filter(path -> path.toString().endsWith(".txt")) + .count(); + + } catch (IOException e) { + System.err.println("获取历史文件数量时发生错误: " + e.getMessage()); + return 0; + } + } + + /** + * 清理指定用户的缓存 + * @param username 用户名 + */ + public static void clearUserCache(String username) { + if (username != null) { + userQuestionCache.remove(username); + } + } + + /** + * 清理所有缓存 + */ + public static void clearAllCache() { + userQuestionCache.clear(); + } + + /** + * 获取当前缓存的用户数量(用于监控) + * @return 缓存的用户数量 + */ + public static int getCachedUserCount() { + return userQuestionCache.size(); + } +} \ No newline at end of file diff --git a/src/com/mathpaper/util/FileUtil.class b/src/com/mathpaper/util/FileUtil.class new file mode 100644 index 0000000..0d88b4d Binary files /dev/null and b/src/com/mathpaper/util/FileUtil.class differ diff --git a/src/com/mathpaper/util/FileUtil.java b/src/com/mathpaper/util/FileUtil.java new file mode 100644 index 0000000..838daa7 --- /dev/null +++ b/src/com/mathpaper/util/FileUtil.java @@ -0,0 +1,290 @@ +package com.mathpaper.util; + +import java.io.BufferedWriter; +import java.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; + +/** + * 文件操作工具类 + * 负责试卷文件的保存和路径处理 + * + * @author 系统 + * @version 2.1 - 优化资源管理和性能 + */ +public class FileUtil { + + // 基础目录常量 + private static final String BASE_DIR = "doc"; + private static final String PAPER_DIR = "paper"; + private static final String FILE_EXTENSION = ".txt"; + + // 性能优化常量 + private static final int BUFFER_SIZE = 8192; // 8KB缓冲区 + private static final int MAX_FILENAME_LENGTH = 255; + + // 时间戳格式 + private static final SimpleDateFormat TIMESTAMP_FORMAT = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); + + /** + * 生成保存路径:doc/paper/用户名/时间戳.txt + * @param username 用户名 + * @return 文件保存路径 + * @throws IllegalArgumentException 如果用户名无效 + */ + public static String getSavePath(String username) { + validateUsername(username); + + try { + String sanitizedUsername = sanitizeFilename(username.trim()); + Path userDir = Paths.get(BASE_DIR, PAPER_DIR, sanitizedUsername); + + // 确保目录存在 + Files.createDirectories(userDir); + + String timestamp = TIMESTAMP_FORMAT.format(new Date()); + Path filePath = userDir.resolve(timestamp + FILE_EXTENSION); + + return filePath.toString(); + + } catch (IOException e) { + System.err.println("创建保存路径时发生错误: " + e.getMessage()); + // 降级到传统方式 + return getFallbackSavePath(username); + } + } + + /** + * 验证用户名 + */ + private static void validateUsername(String username) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("用户名不能为空"); + } + if (username.trim().length() > MAX_FILENAME_LENGTH) { + throw new IllegalArgumentException("用户名过长"); + } + } + + /** + * 降级保存路径生成方法 + */ + private static String getFallbackSavePath(String username) { + String sanitizedUsername = sanitizeFilename(username.trim()); + String baseDir = BASE_DIR + File.separator + PAPER_DIR + File.separator + sanitizedUsername; + File dir = new File(baseDir); + if (!dir.exists() && !dir.mkdirs()) { + throw new RuntimeException("无法创建目录: " + baseDir); + } + + String timestamp = TIMESTAMP_FORMAT.format(new Date()); + return baseDir + File.separator + timestamp + FILE_EXTENSION; + } + + /** + * 清理文件名中的非法字符 + */ + private static String sanitizeFilename(String filename) { + if (filename == null) { + return "default"; + } + + // 替换Windows文件名中的非法字符 + String sanitized = filename.replaceAll("[<>:\"/\\\\|?*]", "_") + .replaceAll("\\s+", "_") + .trim(); + + // 确保文件名不为空且不超过最大长度 + if (sanitized.isEmpty()) { + sanitized = "default"; + } + if (sanitized.length() > MAX_FILENAME_LENGTH) { + sanitized = sanitized.substring(0, MAX_FILENAME_LENGTH); + } + + return sanitized; + } + + /** + * 保存试卷到文件(带题号和空行) + * @param path 文件路径 + * @param questions 题目列表 + * @throws IllegalArgumentException 如果参数无效 + * @throws RuntimeException 如果保存失败 + */ + public static void savePaper(String path, List questions) { + validateSaveParameters(path, questions); + + try { + Path filePath = Paths.get(path); + + // 确保父目录存在 + Path parentDir = filePath.getParent(); + if (parentDir != null) { + Files.createDirectories(parentDir); + } + + // 使用NIO写入文件,指定UTF-8编码和缓冲区大小 + try (BufferedWriter writer = Files.newBufferedWriter(filePath, StandardCharsets.UTF_8, + StandardOpenOption.CREATE, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING)) { + + writeQuestionsToFile(writer, questions); + } + + } catch (IOException e) { + System.err.println("保存文件时发生错误: " + e.getMessage()); + // 尝试降级保存 + savePaperFallback(path, questions); + } catch (Exception e) { + System.err.println("保存试卷时发生未知错误: " + e.getMessage()); + throw new RuntimeException("保存试卷失败", e); + } + } + + /** + * 验证保存参数 + */ + private static void validateSaveParameters(String path, List questions) { + if (path == null || path.trim().isEmpty()) { + throw new IllegalArgumentException("文件路径不能为空"); + } + if (questions == null || questions.isEmpty()) { + throw new IllegalArgumentException("题目列表不能为空"); + } + } + + /** + * 将题目写入文件 + */ + private static void writeQuestionsToFile(BufferedWriter writer, List questions) throws IOException { + for (int i = 0; i < questions.size(); i++) { + String question = questions.get(i); + if (question != null && !question.trim().isEmpty()) { + writer.write((i + 1) + ". " + question.trim()); + + // 题间空一行,但最后一题不加空行 + if (i < questions.size() - 1) { + writer.newLine(); + writer.newLine(); + } + } + } + writer.flush(); + } + + /** + * 降级保存方法(使用传统IO) + */ + private static void savePaperFallback(String path, List questions) { + try (BufferedWriter bw = new BufferedWriter( + new java.io.FileWriter(path, StandardCharsets.UTF_8), BUFFER_SIZE)) { + + writeQuestionsToFile(bw, questions); + + } catch (IOException e) { + System.err.println("降级保存也失败: " + e.getMessage()); + throw new RuntimeException("文件保存完全失败", e); + } + } + + /** + * 检查文件是否存在 + * @param path 文件路径 + * @return 文件是否存在 + */ + public static boolean fileExists(String path) { + if (path == null || path.trim().isEmpty()) { + return false; + } + + try { + Path filePath = Paths.get(path); + return Files.exists(filePath) && Files.isRegularFile(filePath); + } catch (Exception e) { + // 降级到传统方式 + File file = new File(path); + return file.exists() && file.isFile(); + } + } + + /** + * 创建目录 + * @param dirPath 目录路径 + * @return 是否创建成功 + */ + public static boolean createDirectory(String dirPath) { + if (dirPath == null || dirPath.trim().isEmpty()) { + return false; + } + + try { + Path path = Paths.get(dirPath); + Files.createDirectories(path); + return true; + } catch (IOException e) { + System.err.println("创建目录失败: " + e.getMessage()); + // 降级到传统方式 + File dir = new File(dirPath); + return dir.mkdirs() || dir.exists(); + } + } + + /** + * 获取用户试卷目录 + * @param username 用户名 + * @return 用户试卷目录路径 + */ + public static String getUserPaperDir(String username) { + if (username == null || username.trim().isEmpty()) { + throw new IllegalArgumentException("用户名不能为空"); + } + + String sanitizedUsername = sanitizeFilename(username.trim()); + return BASE_DIR + File.separator + PAPER_DIR + File.separator + sanitizedUsername; + } + + /** + * 获取文件大小(字节) + * @param path 文件路径 + * @return 文件大小,如果文件不存在返回-1 + */ + public static long getFileSize(String path) { + if (path == null || path.trim().isEmpty()) { + return -1; + } + + try { + Path filePath = Paths.get(path); + return Files.exists(filePath) ? Files.size(filePath) : -1; + } catch (IOException e) { + System.err.println("获取文件大小失败: " + e.getMessage()); + return -1; + } + } + + /** + * 删除文件 + * @param path 文件路径 + * @return 是否删除成功 + */ + public static boolean deleteFile(String path) { + if (path == null || path.trim().isEmpty()) { + return false; + } + + try { + Path filePath = Paths.get(path); + return Files.deleteIfExists(filePath); + } catch (IOException e) { + System.err.println("删除文件失败: " + e.getMessage()); + return false; + } + } +} \ No newline at end of file diff --git a/start.bat b/start.bat new file mode 100644 index 0000000..4d8ac97 --- /dev/null +++ b/start.bat @@ -0,0 +1,6 @@ +@echo off +REM Convenience startup script - redirects to the main startup script +REM 便捷启动脚本 - 重定向到主启动脚本 + +echo Redirecting to main startup script... +call "doc\bin\start.bat" \ No newline at end of file diff --git a/start.ps1 b/start.ps1 new file mode 100644 index 0000000..1d51490 --- /dev/null +++ b/start.ps1 @@ -0,0 +1,5 @@ +# Convenience startup script - redirects to the main startup script +# 便捷启动脚本 - 重定向到主启动脚本 + +Write-Host "Redirecting to main startup script..." -ForegroundColor Yellow +& ".\doc\bin\start.ps1" \ No newline at end of file