diff --git a/README.md b/README.md deleted file mode 100644 index 3c25037..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# Software-Introduction-Personal-Project - diff --git a/doc/类的设计.md b/doc/类的设计.md new file mode 100644 index 0000000..d8683b5 --- /dev/null +++ b/doc/类的设计.md @@ -0,0 +1,298 @@ +# 试卷生成系统类设计说明文档 + +本说明文档详细描述系统中各个核心类的设计目的、职责、关键属性及方法实现逻辑。 + +--- + +## 1. User 类 + +### 功能概述 +User 是用户信息的封装类,用于存储单个用户的登录凭证和所属学段类型。 + +### 属性 +- `username`:字符串类型,表示用户名。 +- `password`:字符串类型,表示用户密码。 +- `userType`:字符串类型,表示用户类型,取值为“小学”、“初中”或“高中”。 + +### 方法 +- **构造方法** `User(String username, String password, String userType)` + 初始化用户对象,设置用户名、密码和用户类型。 + +- `getUsername()` / `setUsername(String username)` + 获取或设置用户名。 + +- `getPassword()` / `setPassword(String password)` + 获取或设置密码。 + +- `getUserType()` / `setUserType(String userType)` + 获取或设置用户类型。 + +- `validateCredentials(String inputUsername, String inputPassword)` + 验证输入的用户名和密码是否与当前用户匹配。返回布尔值,用于登录判断。 + +--- + +## 2. UserManager 类 + +### 功能概述 +UserManager 负责管理所有用户账户、处理登录逻辑,并维护当前登录用户状态。 + +### 属性 +- `users`:`Map` 类型,以用户名为键存储所有预设用户。 +- `currentUser`:`User` 类型,表示当前已登录的用户。 +- `currentType`:`String` 类型,缓存当前出题类型。 + +### 方法 +- **构造方法** `UserManager()` + 调用 `initializeUsers()` 初始化9个预设用户(3小学 + 3初中 + 3高中)。 + +- `initializeUsers()` + 向 users 映射中添加预设用户对象,用户名如“张三1”,密码均为“123”,类型按学段分配。 + +- `login(String username, String password)` + 实现登录逻辑: + - 若用户名不存在,提示“该账号不存在”; + - 若密码错误,提示“密码错误”; + - 验证成功,设置 `currentUser` 和 `currentType`,返回 true。 + +- `logout()` + 清空当前用户状态(设为 null)。 + +- `switchUserType(String newType)` + 允许在已登录状态下切换用户类型(实际项目中此方法未被使用,主流程通过重新获取 QuestionSetting 实现切换)。 + +- `isLoggedIn()` + 判断是否有用户登录(检查 `currentUser != null`)。 + +- `getCurrentUser()` + 返回当前登录的 `User` 对象,供其他模块获取用户信息。 + +--- + +## 3. QuestionSetting 接口 + +### 功能概述 +定义题目生成器的统一契约,确保所有学段题目生成器具有相同调用方式。 + +### 方法 +- `Expression setQuestion(int count)` + 根据指定的操作数数量 `count`,生成一个符合当前学段要求的数学表达式对象。返回 `Expression` 实例。 + +--- + +## 4. AbstractQuestionSetting 抽象类 + +### 功能概述 +作为所有题目生成器的基类,提供通用工具方法,减少代码重复。 + +### 属性 +- `RANDOM`:静态 Random 实例,用于生成随机数,保证线程安全且全局一致。 + +### 方法 +- `getRandomOperator()` + 从 `{"+", "-", "*", "/"}` 中随机返回一个四则运算符。 + +- `getRandomNumber()` + 生成 1 到 99 的随机整数(含1和99),并返回其字符串形式。 + +- `getPriority(String operator)` + 返回运算符的优先级数值,用于括号判断: + - 三角函数、平方、开方:优先级 3 + - 乘、除:优先级 2 + - 加、减:优先级 1 + - 其他或 null:返回 0 或 -1 + +- `addParenthesesIfNeeded(Expression child, String parentOperator, boolean isRightChild)` + 根据运算优先级决定是否为子表达式添加括号: + - 子表达式无主运算符(即为纯数字),不加括号 + - 子优先级 < 父优先级,加括号 + - 若为右子表达式,且父运算符为 - 或 /,即使优先级相等也加括号(如 a - (b - c) 而非 a - b - c) + +--- + +## 5. PrimaryQuestionSetting 类(小学) + +### 功能概述 +实现小学阶段的题目生成逻辑,仅包含四则运算,并保证题目合理性。 + +### 方法 +- `setQuestion(int count)` + 递归生成表达式: + - 若 count == 1,返回一个随机数字; + - 否则将操作数拆分为左右两部分,递归生成左右子表达式; + - 随机选择运算符; + - 若为除法且右值为0,重新生成右子表达式; + - 若为减法且左值 < 右值,交换左右子表达式以保证结果非负; + - 使用 `addParenthesesIfNeeded` 处理括号; + - 返回最终表达式对象。 + +小学题目不包含任何一元运算符(如平方、三角函数),也不强制包含特定符号。 + +--- + +## 6. MiddleQuestionSetting 类(初中) + +### 功能概述 +在小学基础上增加平方(²)和平方根(√)运算,并确保每道题至少包含其中一种。 + +### 新增方法 +- `applyUnaryOperator(Expression child, String operator)` + 对子表达式应用一元运算符: + - 平方(²):若子表达式为纯数字,则直接拼接“²”;否则加括号后拼接,如 `(a + b)²` + - 平方根(√):若子值为负,替换为新随机正数;若为纯数字,前缀“√”;否则写为 `√(表达式)` + +- `applyProbability(Expression result)` + 以 30% 概率对表达式应用平方或平方根运算,并更新主运算符。 + +- `generateFirstQuestion(int count)` + 与小学类似,但每次生成后调用 `applyProbability` 尝试添加一元运算。 + +- `setQuestion(int count)` + 循环调用 `generateFirstQuestion`,直到生成的表达式包含“²”或“√”,确保题目符合初中要求。 + +--- + +## 7. HighQuestionSetting 类(高中) + +### 功能概述 +在初中基础上增加三角函数(sin、cos、tan),并确保每道题至少包含一个三角函数。 + +### 新增方法 +- `applyUnaryOperator(Expression child, String operator)` + 扩展支持三角函数: + - `sin`/`cos`:直接包裹为 `sin(表达式)` 或 `cos(表达式)`,值为 `Math.round(Math.sin(Math.toRadians(value)))` + - `tan`:若输入角度为 90 + 180k(无定义点),则替换为新随机数,避免异常 + +- `applyProbability(Expression result)` + 以 30% 概率从 `{"²", "√", "sin", "cos", "tan"}` 中随机选择一元运算符应用。 + +- `generateFirstQuestion(int count)` + 逻辑同初中,但支持更多运算符。 + +- `setQuestion(int count)` + 循环生成,直到表达式包含 `sin`、`cos` 或 `tan` 中的任意一个。 + ⚠️ 三角函数结果被强制转换为整数,可能损失精度,但简化了题目形式。 + +--- + +## 8. Expression 类 + +### 功能概述 +封装一个数学表达式的文本形式、计算值及其主运算符,用于递归构建和值传递。 + +### 属性 +- `expression`:表达式的字符串表示(如 `"5 + 3"` 或 `"√(16)"`) +- `value`:表达式计算结果的整数值 +- `mainOperator`:表达式的主运算符(如 `"+"`、`"²"`、`"sin"`),若为纯数字则为 null + +### 方法 +- 构造方法 `Expression(String expression, int value, String mainOperator)` + 初始化表达式对象 + +- `getExpression()` / `setExpression(String expression)` + 获取或设置表达式字符串 + +- `getValue()` / `setValue(int value)` + 获取或设置计算值 + +- `getMainOperator()` / `setMainOperator(String mainOperator)` + 获取或设置主运算符 + +> 该类是表达式树的节点,支持在生成过程中传递结构与值。 + +--- + +## 9. QuestionSettingFactory 类 + +### 功能概述 +工厂类,根据用户类型字符串动态创建对应的题目生成器实例。 + +### 方法 +- `getQuestionSetting(String type)` + 使用 switch 表达式: + - `"小学"` → 返回 `new PrimaryQuestionSetting()` + - `"初中"` → 返回 `new MiddleQuestionSetting()` + - `"高中"` → 返回 `new HighQuestionSetting()` + - 其他 → 打印“类型错误”,返回 `null` + +> 该类解耦了用户类型与具体实现,便于扩展新学段。 + +--- + +## 10. FileManager 类 + +### 功能概述 +负责试卷的本地存储与题目重复性检查。 + +### 常量 +- `ROOT_PATH`:根目录路径,值为 `"./试卷/"` + +### 方法 +- `savePaper(User user, List questions)` + - 创建用户专属目录(如 `./试卷/张三1/`) + - 生成时间戳文件名(格式:`yyyy-MM-dd-HH-mm-ss.txt`) + - 以 UTF-8 编码写入题目,格式为 `1. 表达式\n\n2. 表达式\n\n...` + +- `checkQuestion(String question, User user)` + - 若用户目录不存在,返回 true(无需去重) + - 遍历该用户所有历史试卷文件 + - 对每行去除题号,与历史文件进行匹配,如果相同则重新生成(查重功能) +> 去重基于字符串精确匹配,不进行数学等价判断。 + +--- + +## 11. AllSystem 类 + +### 功能概述 +系统的主控制器,协调用户交互、流程跳转和模块调用。 + +### 属性 +- `scanner`:`Scanner` 实例,用于读取用户输入 +- `userManager`:用户管理器 +- `fileManager`:文件管理器 +- `questionSettingFactory`:题目生成器工厂 +- `questionSetting`:当前使用的题目生成器 +- `currentMode`:当前学段类型(如“小学”) +- `isFirstLogin`:标志是否首次登录,用于提示信息 +- `tipMessage`:临时提示信息(如“切换成功”) + +### 核心方法 +- `clearScreen()` + 调用系统命令 `cls`(Windows)清屏,失败时打印堆栈(不影响主流程) + +- `initialize()` + 初始化所有组件和状态变量 + +- `showHomeMenu()` + 显示欢迎界面,循环处理登录输入,成功后进入 `setPaper()` + +- `setPaper()` + 提示用户输入题目数量(10~30),调用 `handleFirstInput` + +- `handleFirstInput(String input)` + - 输入 `-1`:返回登录界面 + - 输入 10~30:循环生成题目,调用 `checkQuestion` 去重,显示并保存 + - 其他输入:提示重试 + +- `showChoiceMenu()` + 显示主菜单,循环处理用户选择 + +- `handleSecondInput(String input)` + - 输入 `-1`:返回登录 + - 输入 `1`:重新生成试卷 + - 输入 `切换为XX`:解析类型,调用工厂切换 `questionSetting`,进入生成流程 + - 其他输入:提示重试 + +--- + +## 12. Main 类 + +### 功能概述 +程序入口类,启动整个系统。 + +### 方法 +- `main(String[] args)` + 创建 `AllSystem` 实例,调用 `showHomeMenu()` 启动交互流程 + 捕获 `IOException` 并转为运行时异常抛出 + +--- \ No newline at end of file diff --git a/doc/说明文档.md b/doc/说明文档.md new file mode 100644 index 0000000..e570e98 --- /dev/null +++ b/doc/说明文档.md @@ -0,0 +1,209 @@ +# 试卷生成系统说明文档 + +## 目录 +- [1. 项目概述](#1-项目概述) +- [2. 功能特性](#2-功能特性) + - [2.1 多学段支持](#21-多学段支持) + - [2.2 智能题目生成](#22-智能题目生成) + - [2.3 题目去重机制](#23-题目去重机制) + - [2.4 用户认证与管理](#24-用户认证与管理) + - [2.5 试卷保存](#25-试卷保存) + - [2.6 交互式菜单](#26-交互式菜单) +- [3. 系统架构](#3-系统架构) + - [3.1 核心模块](#31-核心模块) + - [3.2 数据流](#32-数据流) + - [3.3 扩展性](#33-扩展性) +- [4. 使用说明](#4-使用说明) + - [4.1 启动系统](#41-启动系统) + - [4.2 登录](#42-登录) + - [4.3 生成试卷](#43-生成试卷) + - [4.4 主菜单操作](#44-主菜单操作) + - [4.5 退出](#45-退出) +- [5. 文件存储结构](#5-文件存储结构) + - [5.1 目录结构](#51-目录结构) + - [5.2 文件内容格式](#52-文件内容格式) + - [5.3 去重机制](#53-去重机制) +- [6. 技术栈](#6-技术栈) +- [7. 注意事项](#7-注意事项) + - [7.1 数学精度问题](#71-数学精度问题) + - [7.2 题目去重局限性](#72-题目去重局限性) + - [7.3 系统环境要求](#73-系统环境要求) + - [7.4 安全性](#74-安全性) + - [7.5 扩展建议](#75-扩展建议) + +--- + +## 1. 项目概述 +本项目是一个面向小学、初中和高中三个学段的数学试卷自动生成系统,采用 Java 语言开发,运行于命令行环境。系统根据用户所属学段,智能生成符合该学段知识体系的数学题目,支持题目去重、试卷保存、用户登录及学段切换等功能。 + +系统内置三类题目生成策略: + +- **小学**:仅包含加、减、乘、除四则运算,确保减法结果非负、除法无零除。 +- **初中**:在小学基础上引入平方(²)与平方根(√)运算,题目中至少包含一项。 +- **高中**:进一步引入三角函数(sin、cos、tan),题目中至少包含一个三角函数表达式。 + +所有题目均以表达式形式呈现,系统自动处理运算优先级与括号逻辑,确保表达式语义清晰、格式规范。生成的试卷按用户独立存储,避免题目重复,便于教师或学生长期使用。 + +--- + +## 2. 功能特性 + +### 2.1 多学段支持 +- 支持小学、初中、高中三种用户类型。 +- 每种类型对应不同的题目生成规则与运算符集合。 +- 用户可在登录后动态切换学段(如从小学切换为高中)。 + +### 2.2 智能题目生成 +- 题目基于递归表达式树构建,结构灵活。 +- 自动处理运算优先级,合理添加括号(如 `a + b * c` 不加括号,`a * (b + c)` 加括号)。 +- 小学题目保证减法非负、除法无零除。 +- 初中/高中题目强制包含特定运算符(²/√ 或 sin/cos/tan),确保题目符合学段要求。 + +### 2.3 题目去重机制 +- 系统在生成每道题目前,会扫描该用户历史所有试卷。 +- 若发现完全相同的表达式字符串,则重新生成,确保试卷内及跨试卷无重复题目。 + +### 2.4 用户认证与管理 +- 内置 9 个预设用户(3 小学 + 3 初中 + 3 高中),用户名与密码固定。 +- 登录时验证账号密码,失败可重试。 +- 支持退出当前用户并重新登录。 + +### 2.5 试卷保存 +- 试卷以纯文本格式保存至本地文件系统。 +- 文件按用户隔离,按生成时间命名(`yyyy-MM-dd-HH-mm-ss.txt`)。 +- 每题编号并空行分隔,格式清晰易读。 + +### 2.6 交互式菜单 +提供清晰的命令行菜单,支持: +- 重新生成试卷 +- 切换学段(输入“切换为初中”等) +- 退出当前用户 +- 输入校验严格,非法输入会提示重试。 + +--- + +## 3. 系统架构 + +系统采用分层 + 工厂模式设计,结构清晰,易于扩展。 + +### 3.1 核心模块 +| 模块 | 类/接口 | 职责 | +|------------|---------|------| +| 用户管理 | User, UserManager | 管理用户信息、登录验证、当前用户状态 | +| 题目生成 | QuestionSetting(接口)
PrimaryQuestionSetting
MiddleQuestionSetting
HighQuestionSetting | 定义题目生成规范,三类实现分别对应小学、初中、高中 | +| 表达式模型 | Expression | 封装表达式字符串、计算值、主运算符 | +| 公共逻辑 | AbstractQuestionSetting | 提供随机数、运算符、优先级、括号处理等通用方法 | +| 工厂模式 | QuestionSettingFactory | 根据用户类型创建对应题目生成器 | +| 文件管理 | FileManager | 负责试卷保存与题目去重检查 | +| 主控流程 | AllSystem | 控制登录、菜单、生成、保存等全流程 | +| 程序入口 | Main | 启动系统 | + +### 3.2 数据流 +用户登录 → UserManager 验证 → 设置 currentUser +→ AllSystem 调用 QuestionSettingFactory 获取对应 QuestionSetting +→ 循环调用 `setQuestion()` 生成表达式 +→ 每题调用 `FileManager.checkQuestion()` 去重 +→ 题目集齐后调用 `FileManager.savePaper()` 保存 + +### 3.3 扩展性 +- 新增学段:实现 QuestionSetting 接口,注册到工厂类。 +- 新增运算符:扩展 `getPriority()` 和 `applyUnaryOperator()`。 +- 修改题目结构:调整 `generateFirstQuestion()` 递归逻辑。 + +--- + +## 4. 使用说明 + +### 4.1 启动系统 +编译并运行 `Main.java`,系统自动进入登录界面。 + +### 4.2 登录 +输入格式:`用户名 密码`(中间一个空格) + +预设账号: +- 小学:张三1、张三2、张三3(密码均为 `123`) +- 初中:李四1、李四2、李四3(密码均为 `123`) +- 高中:王五1、王五2、王五3(密码均为 `123`) + +输入 `exit` 可直接退出程序(仅在登录界面有效)。 + +### 4.3 生成试卷 +- 登录成功后,系统提示输入题目数量。 +- 输入 10 到 30 之间的整数(如 `15`)。 +- 系统逐题生成并显示,完成后提示“试卷生成完成”。 + +### 4.4 主菜单操作 +生成完成后进入主菜单,可执行以下操作: +- 输入 **1**:重新生成试卷(使用当前学段) +- 输入 **切换为小学 / 切换为初中 / 切换为高中**:切换学段并立即进入生成流程 +- 输入 **-1**:退出当前用户,返回登录界面 + +⚠️ 注意:切换学段命令必须完整输入“切换为XX”,XX只能是“小学”“初中”“高中”之一。 + +### 4.5 退出 +- 在主菜单输入 `-1` 可重新登录。 +- 在登录界面输入 `exit` 可彻底退出程序。 + +--- + +## 5. 文件存储结构 + +所有试卷保存在项目根目录下的 `./试卷/` 文件夹中,按用户隔离存储。 + +### 5.1 目录结构 + ./试卷/ + ├── 张三1/ + │ ├── 2025-09-29-14-30-22.txt + │ └── 2025-09-29-15-01-45.txt + ├── 李四1/ + │ └── 2025-09-29-14-35-10.txt + └── 王五1/ + └── 2025-09-29-14-40-33.txt +### 5.2 文件内容格式 +每份试卷为纯文本文件,内容格式如下: +45 + 23 +√(16) * 5 +sin(30) + 10 + +- 每题以 `序号. 表达式` 开头 +- 每题后空一行,便于阅读 +- 编码为 UTF-8,支持中文与数学符号 + +### 5.3 去重机制 +- 系统在保存前遍历该用户所有历史试卷文件。 +- 对每一行去除题号前缀(如 `1. `)后与新题目比对。 +- 完全相同的表达式将被跳过,重新生成新题。 + +--- + +## 6. 技术栈 + +| 类别 | 技术/工具 | +|--------|-----------| +| 编程语言 | Java 17(或兼容版本) | +| 开发范式 | 面向对象编程(OOP) | +| 设计模式 | 工厂模式、抽象类、接口 | +| 核心特性 | 递归表达式生成、运算符优先级处理、文件 I/O | +| 依赖库 | 仅使用 Java 标准库(`java.util`, `java.io`, `java.nio`, `java.time`) | +| 运行环境 | 支持 Java 的命令行终端(Windows/Linux/macOS) | +| 构建方式 | 无需构建工具,直接编译 `.java` 文件即可运行 | + +项目无第三方依赖,可直接通过 `javac` 和 `java` 命令编译运行。 + +--- + +## 7. 注意事项 + +### 7.1 数学精度问题 +- 三角函数结果被四舍五入为整数(如 `sin(30°) ≈ 0.5 → 1`),这在数学上不严谨,但便于题目简洁。实际教学中建议保留小数或使用符号形式。 +- 平方根仅对非负整数开方,负数会自动替换为新随机正数。 +- 除法为整数除法(`/` 运算符),结果向下取整(如 `7 / 3 = 2`)。 + +### 7.2 题目去重局限性 +- 去重基于字符串完全匹配,无法识别数学等价但形式不同的表达式(如 `2 + 3` 与 `3 + 2` 被视为不同题目)。 +- 若需更智能去重,需引入表达式解析与标准化模块。 + +### 7.3 系统环境要求 +- 需要 **写权限** 创建 `./试卷/` 目录及子文件。 +- 在 Windows 上使用 `cls` 清屏,Linux/macOS 用户可能看到清屏失败(可忽略或修改 `clearScreen()` 方法)。 + diff --git a/src/AbstractQuestionSetting.java b/src/AbstractQuestionSetting.java new file mode 100644 index 0000000..5b29b43 --- /dev/null +++ b/src/AbstractQuestionSetting.java @@ -0,0 +1,55 @@ +// AbstractQuestionSetting.java +import java.util.Random; + +public abstract class AbstractQuestionSetting implements QuestionSetting { + protected static final Random RANDOM = new Random(); + + public String getRandomOperator() { + String[] operators = {"+", "-", "*", "/"}; + int index = RANDOM.nextInt(4); + return operators[index]; + } + + public String getRandomNumber() { + int number = RANDOM.nextInt(1, 100); + return String.valueOf(number); + } + + public int getPriority(String operator) { + if (operator == null) { + return -1; + } + if (operator.equals("²") || operator.equals("√") + || operator.equals("sin") || operator.equals("cos") || operator.equals("tan")) { + return 3; + } + if (operator.equals("+") || operator.equals("-")) { + return 1; + } + if (operator.equals("*") || operator.equals("/")) { + return 2; + } + return 0; + } + + public String addParenthesesIfNeeded(Expression child, String parentOperator, boolean isRightChild) { + if (child.getMainOperator() == null) { + return child.getExpression(); + } + + int parentPriority = getPriority(parentOperator); + int childPriority = getPriority(child.getMainOperator()); + + if (childPriority < parentPriority) { + return "(" + child.getExpression() + ")"; + } + + if (isRightChild && (parentOperator.equals("-") || parentOperator.equals("/"))) { + if (parentPriority == childPriority) { + return "(" + child.getExpression() + ")"; + } + } + + return child.getExpression(); + } +} \ No newline at end of file diff --git a/src/AllSystem.java b/src/AllSystem.java new file mode 100644 index 0000000..c29cc19 --- /dev/null +++ b/src/AllSystem.java @@ -0,0 +1,174 @@ +// AllSystem.java +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Scanner; + +public class AllSystem { + private static final String EXIT_COMMAND = "exit"; + private static final String SWITCH_PREFIX = "切换为"; + + private Scanner scanner; + private UserManager userManager; + private FileManager fileManager; + private QuestionSettingFactory questionSettingFactory; + private QuestionSetting questionSetting; + private String tipMessage; + private String currentMode; + private boolean isFirstLogin; + + public static void clearScreen() { + try { + new ProcessBuilder("cmd", "/c", "cls").inheritIO().start().waitFor(); + } catch (IOException | InterruptedException exception) { + exception.printStackTrace(); + } + } + + public void initialize() { + tipMessage = ""; + currentMode = ""; + isFirstLogin = true; + this.scanner = new Scanner(System.in); + userManager = new UserManager(); + fileManager = new FileManager(); + questionSettingFactory = new QuestionSettingFactory(); + questionSetting = null; + } + + public void showHomeMenu() throws IOException { + initialize(); + clearScreen(); + System.out.println("**************欢迎来到试卷生成系统**************"); + System.out.println("————————————————————————————————————————————————"); + System.out.println("请输入账号和密码(中间用空格分开,输入exit退出):"); + boolean loginSuccessful = false; + while (!loginSuccessful) { + String input = scanner.nextLine(); + if (input.isEmpty()) { + System.out.println("您的输入不符合要求,请重试"); + continue; + } + if (input.equals(EXIT_COMMAND)) { + System.exit(0); + } + String[] inputs = input.split(" "); + if (inputs.length != 2) { + System.out.println("您的输入不符合要求,请重试"); + continue; + } + if (userManager.login(inputs[0], inputs[1])) { + loginSuccessful = true; + } + } + currentMode = userManager.getCurrentUser().getUserType(); + this.questionSetting = questionSettingFactory.getQuestionSetting( + userManager.getCurrentUser().getUserType()); + setPaper(); + } + + public void setPaper() throws IOException { + clearScreen(); + if (isFirstLogin) { + System.out.println("准备生成 " + currentMode + " 数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录)"); + isFirstLogin = false; + } else { + System.out.println("准备生成 " + currentMode + " 数学题目,请输入生成题目数量"); + } + String input = scanner.nextLine(); + handleFirstInput(input); + } + + public void handleFirstInput(String input) throws IOException { + boolean continueProcessing = true; + while (continueProcessing) { + try { + int questionCount = Integer.parseInt(input); + if (questionCount == -1) { + continueProcessing = false; + showHomeMenu(); + } else if (10 <= questionCount && questionCount <= 30) { + List questions = new ArrayList<>(); + int remainingQuestions = questionCount; + while (remainingQuestions > 0) { + String question = questionSetting.setQuestion(getRandomNumber()).getExpression(); + if (fileManager.checkQuestion(question, userManager.getCurrentUser())) { + questions.add(question); + System.out.println(questionCount - remainingQuestions + 1 + ". " + question); + System.out.println(); + remainingQuestions--; + } + } + fileManager.savePaper(userManager.getCurrentUser(), questions); + tipMessage = "试卷生成成功,"; + continueProcessing = false; + System.out.println("试卷生成完成!按回车键返回主菜单..."); + scanner.nextLine(); + showChoiceMenu(); + } else { + System.out.println("您的输入不符合要求,请重试"); + input = scanner.nextLine(); + } + } catch (NumberFormatException exception) { + System.out.println("您的输入不符合要求,请重试"); + input = scanner.nextLine(); + } + } + } + + public void showChoiceMenu() throws IOException { + boolean running = true; + while (running) { + clearScreen(); + System.out.println("****************欢迎光临," + userManager.getCurrentUser().getUsername() + + "****************"); + System.out.println("————————————————————————————————————————————————"); + System.out.println("当前模式是: " + currentMode); + System.out.println("-1. 退出(输入-1)"); + System.out.println("1. 生成试卷(输入1)"); + System.out.println("2. 切换类型(输入:切换为XX)(XX为\"初中\",\"小学\",\"高中\")"); + System.out.println(tipMessage + "请选择接下来操作:"); + + String input = scanner.nextLine(); + running = handleSecondInput(input); + } + } + + public boolean handleSecondInput(String input) throws IOException { + if (input.equals("-1")) { + clearScreen(); + showHomeMenu(); + return false; + } else if (input.equals("1")) { + clearScreen(); + setPaper(); + return true; + } else if (input.length() >= 4 && input.startsWith(SWITCH_PREFIX)) { + String type = input.substring(3); + tipMessage = "切换成功,"; + if (type.equals("小学") || type.equals("初中") || type.equals("高中")) { + questionSetting = questionSettingFactory.getQuestionSetting(type); + currentMode = type; + setPaper(); + return true; + } else { + System.out.println("请输入小学、初中和高中三个选项中的一个,请按回车重试"); + scanner.nextLine(); + return true; + } + } else { + System.out.println("输入不符合要求,请按回车重试"); + scanner.nextLine(); + return true; + } + } + public int getRandomNumber() { + int count = (int) (Math.random() * 5) + 1; + if (currentMode.equals("小学")) { + if (count == 1) { + count = (int) (Math.random() * 4) + 2; + } + } + return count; + } +} \ No newline at end of file diff --git a/src/Expression.java b/src/Expression.java new file mode 100644 index 0000000..7b130a7 --- /dev/null +++ b/src/Expression.java @@ -0,0 +1,36 @@ +// Expression.java +public class Expression { + private String expression; + private int value; + private String mainOperator; + + public Expression(String expression, int value, String mainOperator) { + this.expression = expression; + this.value = value; + this.mainOperator = mainOperator; + } + + public String getExpression() { + return expression; + } + + public void setExpression(String expression) { + this.expression = expression; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + + public String getMainOperator() { + return mainOperator; + } + + public void setMainOperator(String mainOperator) { + this.mainOperator = mainOperator; + } +} \ No newline at end of file diff --git a/src/FileManager.java b/src/FileManager.java new file mode 100644 index 0000000..2238d16 --- /dev/null +++ b/src/FileManager.java @@ -0,0 +1,57 @@ +// FileManager.java +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.List; + +public class FileManager { + public static final String ROOT_PATH = "./试卷/"; + + public void savePaper(User user, List questions) throws IOException { + String userDirectory = ROOT_PATH + user.getUsername(); + Path userPath = Paths.get(userDirectory); + if (!Files.exists(userPath)) { + Files.createDirectories(userPath); + } + + String fileName = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss")) + ".txt"; + Path filePath = userPath.resolve(fileName); + try (PrintWriter writer = new PrintWriter(new FileWriter(filePath.toFile(), StandardCharsets.UTF_8))) { + for (int index = 0; index < questions.size(); index++) { + writer.printf("%d. %s%n%n", index + 1, questions.get(index)); + } + } + } + + public boolean checkQuestion(String question, User user) throws IOException { + String userDirectory = ROOT_PATH + user.getUsername(); + Path userPath = Paths.get(userDirectory); + if (!Files.exists(userPath)) { + return true; + } else { + File directory = new File("./试卷/" + user.getUsername()); + File[] files = directory.listFiles(); + if (files != null) { + for (File file : files) { + for (String line : Files.readAllLines(file.toPath())) { + if (!line.isEmpty()) { + line = line.replaceFirst("^\\d+\\.\\s*", ""); + if (line.equals(question)) { + System.out.println("有道一样的题"); + return false; + } + } + } + } + } + } + return true; + } +} \ No newline at end of file diff --git a/src/HighQuestionSetting.java b/src/HighQuestionSetting.java new file mode 100644 index 0000000..acfa2f9 --- /dev/null +++ b/src/HighQuestionSetting.java @@ -0,0 +1,123 @@ +// HighQuestionSetting.java +public class HighQuestionSetting extends AbstractQuestionSetting { + + @Override + public String addParenthesesIfNeeded(Expression child, String parentOperator, boolean isRightChild) { + if (child.getMainOperator() == null + || child.getMainOperator().equals("²") || child.getMainOperator().equals("√") + || child.getMainOperator().equals("sin") || child.getMainOperator().equals("cos") + || child.getMainOperator().equals("tan")) { + return child.getExpression(); + } + + int parentPriority = getPriority(parentOperator); + int childPriority = getPriority(child.getMainOperator()); + + if (childPriority < parentPriority) { + return "(" + child.getExpression() + ")"; + } + + if (isRightChild && (parentOperator.equals("-") || parentOperator.equals("/"))) { + if (parentPriority == childPriority) { + return "(" + child.getExpression() + ")"; + } + } + + return child.getExpression(); + } + + public Expression applyUnaryOperator(Expression child, String operator) { + switch (operator) { + case "²": + if (child.getMainOperator() == null) { + return new Expression(child.getExpression() + "²", child.getValue() * child.getValue(), "²"); + } + return new Expression("(" + child.getExpression() + ")²", child.getValue() * child.getValue(), "²"); + case "√": + if (child.getValue() < 0) { + String numberString = getRandomNumber(); + child = new Expression(numberString, Integer.parseInt(numberString), null); + } + if (child.getMainOperator() == null) { + return new Expression("√" + child.getExpression(), (int) Math.sqrt(child.getValue()), "√"); + } + return new Expression("√(" + child.getExpression() + ")", (int) Math.sqrt(child.getValue()), "√"); + case "sin": + return new Expression("sin(" + child.getExpression() + ")", + (int) Math.round(Math.sin(Math.toRadians(child.getValue()))), "sin"); + case "cos": + return new Expression("cos(" + child.getExpression() + ")", + (int) Math.round(Math.cos(Math.toRadians(child.getValue()))), "cos"); + case "tan": + while (child.getValue() % 180 == 90) { + String numberString = getRandomNumber(); + child = new Expression(numberString, Integer.parseInt(numberString), null); + } + return new Expression("tan(" + child.getExpression() + ")", + (int) Math.round(Math.tan(Math.toRadians(child.getValue()))), "tan"); + default: + return child; + } + } + + @Override + public Expression setQuestion(int count) { + Expression result = generateFirstQuestion(count); + while (!result.getExpression().contains("sin") && !result.getExpression().contains("cos") + && !result.getExpression().contains("tan")) { + result = generateFirstQuestion(count); + } + return result; + } + + public Expression applyProbability(Expression result) { + if (RANDOM.nextDouble() < 0.3) { + String[] unaryOperators = {"²", "√", "sin", "cos", "tan"}; + String unaryOperator = unaryOperators[RANDOM.nextInt(unaryOperators.length)]; + result = applyUnaryOperator(result, unaryOperator); + result.setMainOperator(unaryOperator); + } + return result; + } + + public Expression generateFirstQuestion(int count) { + if (count == 1) { + String numberString = getRandomNumber(); + int number = Integer.parseInt(numberString); + Expression expression = new Expression(numberString, number, null); + expression = applyProbability(expression); + return expression; + } + int leftCount = 1 + RANDOM.nextInt(count - 1); + int rightCount = count - leftCount; + Expression left = generateFirstQuestion(leftCount); + Expression right = generateFirstQuestion(rightCount); + String operator = getRandomOperator(); + int value = 0; + switch (operator) { + case "+": + value = left.getValue() + right.getValue(); + break; + case "-": + value = left.getValue() - right.getValue(); + break; + case "*": + value = left.getValue() * right.getValue(); + break; + case "/": + if (right.getValue() == 0) { + return generateFirstQuestion(rightCount); + } + value = left.getValue() / right.getValue(); + break; + } + + String leftExpression = addParenthesesIfNeeded(left, operator, false); + String rightExpression = addParenthesesIfNeeded(right, operator, true); + + Expression result = new Expression(leftExpression + " " + operator + " " + rightExpression, (int) value, operator); + + result = applyProbability(result); + return result; + } +} \ No newline at end of file diff --git a/src/Main.java b/src/Main.java new file mode 100644 index 0000000..5f799d8 --- /dev/null +++ b/src/Main.java @@ -0,0 +1,13 @@ +import java.io.IOException; + +public class Main { + + public static void main(String[] args) { + AllSystem allSystem = new AllSystem(); + try { + allSystem.showHomeMenu(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/MiddleQuestionSetting.java b/src/MiddleQuestionSetting.java new file mode 100644 index 0000000..95df76e --- /dev/null +++ b/src/MiddleQuestionSetting.java @@ -0,0 +1,106 @@ +// MiddleQuestionSetting.java +public class MiddleQuestionSetting extends AbstractQuestionSetting { + + public Expression applyUnaryOperator(Expression child, String operator) { + switch (operator) { + case "²": + if (child.getMainOperator() == null) { + return new Expression(child.getExpression() + "²", child.getValue() * child.getValue(), "²"); + } + return new Expression("(" + child.getExpression() + ")²", child.getValue() * child.getValue(), "²"); + case "√": + if (child.getValue() < 0) { + String numberString = getRandomNumber(); + child = new Expression(numberString, Integer.parseInt(numberString), null); + } + if (child.getMainOperator() == null) { + return new Expression("√" + child.getExpression(), child.getValue() * child.getValue(), "√"); + } + return new Expression("√(" + child.getExpression() + ")", (int) Math.sqrt(child.getValue()), "√"); + default: + return child; + } + } + + @Override + public String addParenthesesIfNeeded(Expression child, String parentOperator, boolean isRightChild) { + if (child.getMainOperator() == null || child.getMainOperator().equals("²") || child.getMainOperator().equals("√")) { + return child.getExpression(); + } + + int parentPriority = getPriority(parentOperator); + int childPriority = getPriority(child.getMainOperator()); + + if (childPriority < parentPriority) { + return "(" + child.getExpression() + ")"; + } + + if (isRightChild && (parentOperator.equals("-") || parentOperator.equals("/"))) { + if (parentPriority == childPriority) { + return "(" + child.getExpression() + ")"; + } + } + + return child.getExpression(); + } + + @Override + public Expression setQuestion(int count) { + Expression result = generateFirstQuestion(count); + while (!result.getExpression().contains("²") && !result.getExpression().contains("√")) { + result = generateFirstQuestion(count); + } + return result; + } + + public Expression generateFirstQuestion(int count) { + if (count == 1) { + String numberString = getRandomNumber(); + int number = Integer.parseInt(numberString); + Expression expression = new Expression(numberString, number, null); + expression = applyProbability(expression); + return expression; + } + int leftCount = 1 + RANDOM.nextInt(count - 1); + int rightCount = count - leftCount; + Expression left = generateFirstQuestion(leftCount); + Expression right = generateFirstQuestion(rightCount); + String operator = getRandomOperator(); + int value = 0; + switch (operator) { + case "+": + value = left.getValue() + right.getValue(); + break; + case "-": + value = left.getValue() - right.getValue(); + break; + case "*": + value = left.getValue() * right.getValue(); + break; + case "/": + while (right.getValue() == 0) { + right = generateFirstQuestion(rightCount); + } + value = left.getValue() / right.getValue(); + break; + } + + String leftExpression = addParenthesesIfNeeded(left, operator, false); + String rightExpression = addParenthesesIfNeeded(right, operator, true); + + Expression result = new Expression(leftExpression + " " + operator + " " + rightExpression, value, operator); + + result = applyProbability(result); + return result; + } + + public Expression applyProbability(Expression result) { + if (RANDOM.nextDouble() < 0.3) { + String[] unaryOperators = {"²", "√"}; + String unaryOperator = unaryOperators[RANDOM.nextInt(unaryOperators.length)]; + result = applyUnaryOperator(result, unaryOperator); + result.setMainOperator(unaryOperator); + } + return result; + } +} \ No newline at end of file diff --git a/src/PrimaryQuestionSetting.java b/src/PrimaryQuestionSetting.java new file mode 100644 index 0000000..72b5cd5 --- /dev/null +++ b/src/PrimaryQuestionSetting.java @@ -0,0 +1,37 @@ +// PrimaryQuestionSetting.java +public class PrimaryQuestionSetting extends AbstractQuestionSetting { + + @Override + public Expression setQuestion(int count) { + if (count == 1) { + String expression = getRandomNumber(); + return new Expression(expression, Integer.parseInt(expression), null); + } + int leftCount = 1 + RANDOM.nextInt(count - 1); + int rightCount = count - leftCount; + Expression left = setQuestion(leftCount); + Expression right = setQuestion(rightCount); + String operator = getRandomOperator(); + while (operator.equals("/") && right.getValue() == 0) { + right = setQuestion(rightCount); + } + if (operator.equals("-") && left.getValue() < right.getValue()) { + Expression temporary = left; + left = right; + right = temporary; + } + + String leftExpression = addParenthesesIfNeeded(left, operator, false); + String rightExpression = addParenthesesIfNeeded(right, operator, true); + + int value = switch (operator) { + case "+" -> left.getValue() + right.getValue(); + case "-" -> left.getValue() - right.getValue(); + case "*" -> left.getValue() * right.getValue(); + case "/" -> left.getValue() / right.getValue(); + default -> 0; + }; + + return new Expression(leftExpression + " " + operator + " " + rightExpression, value, operator); + } +} \ No newline at end of file diff --git a/src/QuestionSetting.java b/src/QuestionSetting.java new file mode 100644 index 0000000..db95619 --- /dev/null +++ b/src/QuestionSetting.java @@ -0,0 +1,4 @@ +public interface QuestionSetting { + + Expression setQuestion(int count); +} diff --git a/src/QuestionSettingFactory.java b/src/QuestionSettingFactory.java new file mode 100644 index 0000000..97e68c4 --- /dev/null +++ b/src/QuestionSettingFactory.java @@ -0,0 +1,20 @@ +public class QuestionSettingFactory { + + public QuestionSetting getQuestionSetting(String type) { + switch (type) { + case "小学" -> { + return new PrimaryQuestionSetting(); + } + case "初中" -> { + return new MiddleQuestionSetting(); + } + case "高中" -> { + return new HighQuestionSetting(); + } + default -> { + System.out.println("类型错误"); + return null; + } + } + } +} diff --git a/src/User.java b/src/User.java new file mode 100644 index 0000000..e98be23 --- /dev/null +++ b/src/User.java @@ -0,0 +1,41 @@ +public class User { + + private String username; // 用户名 + private String password; // 密码 + private String userType; //(小学/初中/高中) + + public User(String username, String password, String userType) { + this.username = username; + this.password = password; + this.userType = userType; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getUserType() { + return userType; + } + + public void setUserType(String userType) { + this.userType = userType; + } + + public boolean validateCredentials(String inputUsername, String inputPassword) { + // 验证用户名和密码是否匹配 + return this.username.equals(inputUsername) && this.password.equals(inputPassword); + } +} diff --git a/src/UserManager.java b/src/UserManager.java new file mode 100644 index 0000000..5658400 --- /dev/null +++ b/src/UserManager.java @@ -0,0 +1,63 @@ +// UserManager.java +import java.util.HashMap; +import java.util.Map; + +public class UserManager { + private Map users = new HashMap<>(); + private User currentUser; + private String currentType; + + public UserManager() { + initializeUsers(); + } + + public boolean login(String username, String password) { + if (users.containsKey(username)) { + User user = users.get(username); + if (user.validateCredentials(username, password)) { + currentUser = user; + currentType = user.getUserType(); + System.out.println("登录成功"); + return true; + } else { + System.out.println("密码错误,请重试"); + } + } else { + System.out.println("该账号不存在,请重试"); + } + return false; + } + + public void logout() { + currentUser = null; + currentType = null; + } + + public boolean switchUserType(String newType) { + if (currentUser != null) { + currentType = newType; + return true; + } + return false; + } + + public boolean isLoggedIn() { + return currentUser != null; + } + + public User getCurrentUser() { + return currentUser; + } + + private void initializeUsers() { + users.put("张三1", new User("张三1", "123", "小学")); + users.put("张三2", new User("张三2", "123", "小学")); + users.put("张三3", new User("张三3", "123", "小学")); + users.put("李四1", new User("李四1", "123", "初中")); + users.put("李四2", new User("李四2", "123", "初中")); + users.put("李四3", new User("李四3", "123", "初中")); + users.put("王五1", new User("王五1", "123", "高中")); + users.put("王五2", new User("王五2", "123", "高中")); + users.put("王五3", new User("王五3", "123", "高中")); + } +} \ No newline at end of file