diff --git a/README.md b/README.md deleted file mode 100644 index c0cc583..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# smarthomework - diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..da4ee2d --- /dev/null +++ b/doc/README.md @@ -0,0 +1,22 @@ +# 中小学数学卷子自动生成程序 + +## 功能概览 +- 账户登录:支持小学、初中、高中各三位教师账号,登录成功提示当前难度。 +- 出题控制:支持输入 10-30 道题的数量限制,-1 退出当前登录。 +- 题目生成:根据难度生成符合运算规则的题目,并保证不与历史题库重复。 +- 难度切换:输入 `切换为小学|初中|高中` 可即时切换出题类型。 +- 结果保存:按 `YYYY-MM-DD-HH-mm-ss.txt` 格式保存在 `generated_questions/<用户名>/` 目录下,题号与空行符合要求。 + +## 使用方式 +1. 在项目根目录执行 `javac $(find src -name '*.java') -d out` 完成编译。 +2. 执行 `java -cp out com.personalproject.MathExamApplication` 启动程序。 +3. 根据提示输入用户名、密码(以空格分隔)。 +4. 输入题目数量或切换命令生成试题,生成完成后可继续出题或输入 `-1` 退出当前账号。 +5. 登录新账号或结束程序(直接回车结束输入即可退出)。 + +## 目录结构 +``` +src/ Java 源码(遵循 Google Java Style) +doc/README.md 项目说明文档 +generated_questions/ 程序运行后生成的题目文件夹 +``` diff --git a/src/com/personalproject/MathExamApplication.java b/src/com/personalproject/MathExamApplication.java new file mode 100644 index 0000000..498143a --- /dev/null +++ b/src/com/personalproject/MathExamApplication.java @@ -0,0 +1,172 @@ +package com.personalproject; + +import com.personalproject.auth.AccountRepository; +import com.personalproject.auth.UserAccount; +import com.personalproject.generator.HighSchoolQuestionGenerator; +import com.personalproject.generator.MiddleSchoolQuestionGenerator; +import com.personalproject.generator.PrimaryQuestionGenerator; +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import com.personalproject.service.QuestionGenerationService; +import com.personalproject.storage.QuestionStorageService; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; + +/** + * 数学试卷自动生成命令行程序的入口。 + */ +public final class MathExamApplication { + private final AccountRepository accountRepository = new AccountRepository(); + private final QuestionGenerationService questionGenerationService; + private final QuestionStorageService questionStorageService = new QuestionStorageService(); + + public MathExamApplication() { + Map generatorMap = new EnumMap<>(DifficultyLevel.class); + generatorMap.put(DifficultyLevel.PRIMARY, new PrimaryQuestionGenerator()); + generatorMap.put(DifficultyLevel.MIDDLE, new MiddleSchoolQuestionGenerator()); + generatorMap.put(DifficultyLevel.HIGH, new HighSchoolQuestionGenerator()); + questionGenerationService = new QuestionGenerationService(generatorMap); + } + + public static void main(String[] args) { + MathExamApplication application = new MathExamApplication(); + application.run(); + } + + private void run() { + Scanner scanner = new Scanner(System.in, StandardCharsets.UTF_8); + while (true) { + UserAccount account = promptLogin(scanner); + if (account == null) { + return; + } + handleSession(scanner, account); + } + } + + private UserAccount promptLogin(Scanner scanner) { + while (true) { + System.out.print("请输入用户名和密码(以空格分隔):"); + if (!scanner.hasNextLine()) { + return null; + } + String input = scanner.nextLine().trim(); + if (input.isEmpty()) { + continue; + } + String[] parts = input.split("\\s+"); + if (parts.length != 2) { + System.out.println("请输入正确的用户名、密码"); + continue; + } + Optional account = accountRepository.authenticate(parts[0], parts[1]); + if (account.isPresent()) { + DifficultyLevel level = account.get().difficultyLevel(); + System.out.println("当前选择为" + level.getDisplayName() + "出题"); + return account.get(); + } + System.out.println("请输入正确的用户名、密码"); + } + } + + private void handleSession(Scanner scanner, UserAccount account) { + DifficultyLevel currentLevel = account.difficultyLevel(); + Set existingQuestions = loadExistingQuestions(account.username()); + while (true) { + System.out.print( + "准备生成" + + currentLevel.getDisplayName() + + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):"); + if (!scanner.hasNextLine()) { + return; + } + String input = scanner.nextLine().trim(); + if (input.isEmpty()) { + continue; + } + if (input.startsWith("切换为")) { + currentLevel = processSwitchCommand(input, currentLevel); + continue; + } + if ("-1".equals(input)) { + System.out.println("已退出当前用户。"); + return; + } + Integer questionCount = parseQuestionCount(input); + if (questionCount == null) { + continue; + } + generateAndPersistQuestions(account, currentLevel, questionCount, existingQuestions); + } + } + + private DifficultyLevel processSwitchCommand(String input, DifficultyLevel currentLevel) { + String targetName = input.substring("切换为".length()).trim(); + Optional targetLevel = DifficultyLevel.fromDisplayName(targetName); + if (targetLevel.isEmpty()) { + System.out.println("请输入小学、初中和高中三个选项中的一个"); + return currentLevel; + } + DifficultyLevel level = targetLevel.get(); + System.out.println( + "系统提示: 准备生成" + + level.getDisplayName() + + "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录):"); + return level; + } + + private Integer parseQuestionCount(String input) { + try { + int value = Integer.parseInt(input); + if (value < 10 || value > 30) { + System.out.println("题目数量必须在10到30之间,请重新输入。"); + return null; + } + return value; + } catch (NumberFormatException exception) { + System.out.println("请输入有效的数字或切换命令。"); + return null; + } + } + + private void generateAndPersistQuestions( + UserAccount account, + DifficultyLevel level, + int questionCount, + Set existingQuestions) { + try { + var questions = + questionGenerationService + .generateUniqueQuestions(level, questionCount, existingQuestions); + Path outputPath = questionStorageService.saveQuestions(account.username(), questions); + existingQuestions.addAll(questions); + System.out.println( + "已生成" + + questionCount + + "道" + + level.getDisplayName() + + "数学题目,保存在:" + + outputPath); + } catch (IllegalStateException exception) { + System.out.println("生成题目失败,请稍后重试。"); + } catch (IOException exception) { + System.out.println("保存题目失败,请检查存储目录是否可写。"); + } + } + + private Set loadExistingQuestions(String username) { + try { + return questionStorageService.loadExistingQuestions(username); + } catch (IOException exception) { + System.out.println("读取历史题目失败,将仅针对本次生成查重。"); + return new HashSet<>(); + } + } +} diff --git a/src/com/personalproject/auth/AccountRepository.java b/src/com/personalproject/auth/AccountRepository.java new file mode 100644 index 0000000..9c2f379 --- /dev/null +++ b/src/com/personalproject/auth/AccountRepository.java @@ -0,0 +1,51 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +/** + * 存放预设教师账号并提供认证查询能力。 + */ +public final class AccountRepository { + private final Map accounts = new HashMap<>(); + + public AccountRepository() { + registerPrimaryAccounts(); + registerMiddleSchoolAccounts(); + registerHighSchoolAccounts(); + } + + public Optional authenticate(String username, String password) { + if (username == null || password == null) { + return Optional.empty(); + } + UserAccount account = accounts.get(username.trim()); + if (account == null) { + return Optional.empty(); + } + if (!account.password().equals(password.trim())) { + return Optional.empty(); + } + return Optional.of(account); + } + + private void registerPrimaryAccounts() { + accounts.put("张三1", new UserAccount("张三1", "123", DifficultyLevel.PRIMARY)); + accounts.put("张三2", new UserAccount("张三2", "123", DifficultyLevel.PRIMARY)); + accounts.put("张三3", new UserAccount("张三3", "123", DifficultyLevel.PRIMARY)); + } + + private void registerMiddleSchoolAccounts() { + accounts.put("李四1", new UserAccount("李四1", "123", DifficultyLevel.MIDDLE)); + accounts.put("李四2", new UserAccount("李四2", "123", DifficultyLevel.MIDDLE)); + accounts.put("李四3", new UserAccount("李四3", "123", DifficultyLevel.MIDDLE)); + } + + private void registerHighSchoolAccounts() { + accounts.put("王五1", new UserAccount("王五1", "123", DifficultyLevel.HIGH)); + accounts.put("王五2", new UserAccount("王五2", "123", DifficultyLevel.HIGH)); + accounts.put("王五3", new UserAccount("王五3", "123", DifficultyLevel.HIGH)); + } +} diff --git a/src/com/personalproject/auth/UserAccount.java b/src/com/personalproject/auth/UserAccount.java new file mode 100644 index 0000000..08d11ef --- /dev/null +++ b/src/com/personalproject/auth/UserAccount.java @@ -0,0 +1,9 @@ +package com.personalproject.auth; + +import com.personalproject.model.DifficultyLevel; + +/** + * 不可变的账号定义。 + */ +public record UserAccount(String username, String password, DifficultyLevel difficultyLevel) { +} diff --git a/src/com/personalproject/generator/HighSchoolQuestionGenerator.java b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java new file mode 100644 index 0000000..3240cf5 --- /dev/null +++ b/src/com/personalproject/generator/HighSchoolQuestionGenerator.java @@ -0,0 +1,36 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成至少包含一种三角函数的高中难度题目表达式。 + */ +public final class HighSchoolQuestionGenerator implements QuestionGenerator { + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + private static final String[] TRIG_FUNCTIONS = {"sin", "cos", "tan"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + String function = TRIG_FUNCTIONS[random.nextInt(TRIG_FUNCTIONS.length)]; + operands[specialIndex] = function + '(' + operands[specialIndex] + ')'; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java new file mode 100644 index 0000000..4b3dba9 --- /dev/null +++ b/src/com/personalproject/generator/MiddleSchoolQuestionGenerator.java @@ -0,0 +1,38 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含平方或开根号运算的初中难度题目表达式。 + */ +public final class MiddleSchoolQuestionGenerator implements QuestionGenerator { + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + String[] operands = new String[operandCount]; + for (int index = 0; index < operandCount; index++) { + operands[index] = String.valueOf(random.nextInt(100) + 1); + } + int specialIndex = random.nextInt(operandCount); + if (random.nextBoolean()) { + operands[specialIndex] = '(' + operands[specialIndex] + ")^2"; + } else { + operands[specialIndex] = "sqrt(" + operands[specialIndex] + ')'; + } + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + builder.append(operands[index]); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/PrimaryQuestionGenerator.java b/src/com/personalproject/generator/PrimaryQuestionGenerator.java new file mode 100644 index 0000000..e0e0080 --- /dev/null +++ b/src/com/personalproject/generator/PrimaryQuestionGenerator.java @@ -0,0 +1,29 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 生成包含基础四则运算的小学难度题目表达式。 + */ +public final class PrimaryQuestionGenerator implements QuestionGenerator { + private static final String[] OPERATORS = {"+", "-", "*", "/"}; + + @Override + public String generateQuestion(Random random) { + int operandCount = random.nextInt(5) + 1; + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < operandCount; index++) { + if (index > 0) { + String operator = OPERATORS[random.nextInt(OPERATORS.length)]; + builder.append(' ').append(operator).append(' '); + } + int value = random.nextInt(100) + 1; + builder.append(value); + } + String expression = builder.toString(); + if (operandCount > 1 && random.nextBoolean()) { + return '(' + expression + ')'; + } + return expression; + } +} diff --git a/src/com/personalproject/generator/QuestionGenerator.java b/src/com/personalproject/generator/QuestionGenerator.java new file mode 100644 index 0000000..9f346cd --- /dev/null +++ b/src/com/personalproject/generator/QuestionGenerator.java @@ -0,0 +1,10 @@ +package com.personalproject.generator; + +import java.util.Random; + +/** + * 负责生成单条数学题目的表达式。 + */ +public interface QuestionGenerator { + String generateQuestion(Random random); +} diff --git a/src/com/personalproject/model/DifficultyLevel.java b/src/com/personalproject/model/DifficultyLevel.java new file mode 100644 index 0000000..5592801 --- /dev/null +++ b/src/com/personalproject/model/DifficultyLevel.java @@ -0,0 +1,38 @@ +package com.personalproject.model; + +import java.util.Optional; + +/** + * 系统支持的出题难度级别。 + */ +public enum DifficultyLevel { + PRIMARY("小学"), + MIDDLE("初中"), + HIGH("高中"); + + private final String displayName; + + DifficultyLevel(String displayName) { + this.displayName = displayName; + } + + public String getDisplayName() { + return displayName; + } + + public static Optional fromDisplayName(String name) { + if (name == null) { + return Optional.empty(); + } + String trimmed = name.trim(); + if (trimmed.isEmpty()) { + return Optional.empty(); + } + for (DifficultyLevel level : values()) { + if (level.displayName.equals(trimmed)) { + return Optional.of(level); + } + } + return Optional.empty(); + } +} diff --git a/src/com/personalproject/service/QuestionGenerationService.java b/src/com/personalproject/service/QuestionGenerationService.java new file mode 100644 index 0000000..ca49ed2 --- /dev/null +++ b/src/com/personalproject/service/QuestionGenerationService.java @@ -0,0 +1,55 @@ +package com.personalproject.service; + +import com.personalproject.generator.QuestionGenerator; +import com.personalproject.model.DifficultyLevel; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +/** + * 负责批量生成题目并避免与历史题库重复。 + */ +public final class QuestionGenerationService { + private static final int MAX_ATTEMPTS = 10_000; + private final Map generators; + private final Random random = new SecureRandom(); + + public QuestionGenerationService(Map generatorMap) { + generators = new EnumMap<>(DifficultyLevel.class); + generators.putAll(generatorMap); + } + + public List generateUniqueQuestions( + DifficultyLevel level, int count, Set existingQuestions) { + QuestionGenerator generator = generators.get(level); + if (generator == null) { + throw new IllegalArgumentException("Unsupported difficulty level: " + level); + } + Set produced = new HashSet<>(); + List results = new ArrayList<>(); + int attempts = 0; + while (results.size() < count) { + if (attempts >= MAX_ATTEMPTS) { + throw new IllegalStateException("Unable to generate enough unique questions."); + } + attempts++; + String question = generator.generateQuestion(random).trim(); + if (question.isEmpty()) { + continue; + } + if (existingQuestions.contains(question)) { + continue; + } + if (!produced.add(question)) { + continue; + } + results.add(question); + } + return results; + } +} diff --git a/src/com/personalproject/storage/QuestionStorageService.java b/src/com/personalproject/storage/QuestionStorageService.java new file mode 100644 index 0000000..4ce319e --- /dev/null +++ b/src/com/personalproject/storage/QuestionStorageService.java @@ -0,0 +1,85 @@ +package com.personalproject.storage; + +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.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Stream; + +/** + * 负责保存生成的题目并维护查重信息。 + */ +public final class QuestionStorageService { + private static final String BASE_DIRECTORY = "generated_questions"; + private static final DateTimeFormatter FORMATTER = + DateTimeFormatter.ofPattern("yyyy-MM-dd-HH-mm-ss"); + + public Set loadExistingQuestions(String username) throws IOException { + Path accountDirectory = getAccountDirectory(username); + Set questions = new HashSet<>(); + if (!Files.exists(accountDirectory)) { + return questions; + } + try (Stream paths = Files.list(accountDirectory)) { + paths + .filter(path -> path.getFileName().toString().endsWith(".txt")) + .sorted() + .forEach(path -> readQuestionsFromFile(path, questions)); + } + return questions; + } + + public Path saveQuestions(String username, List questions) throws IOException { + Path accountDirectory = getAccountDirectory(username); + Files.createDirectories(accountDirectory); + String fileName = FORMATTER.format(LocalDateTime.now()) + ".txt"; + Path outputFile = accountDirectory.resolve(fileName); + StringBuilder builder = new StringBuilder(); + for (int index = 0; index < questions.size(); index++) { + String question = questions.get(index); + builder + .append(index + 1) + .append(". ") + .append(question) + .append(System.lineSeparator()) + .append(System.lineSeparator()); + } + Files.writeString( + outputFile, builder.toString(), StandardCharsets.UTF_8, StandardOpenOption.CREATE_NEW); + return outputFile; + } + + private Path getAccountDirectory(String username) { + return Paths.get(BASE_DIRECTORY, username); + } + + private void readQuestionsFromFile(Path path, Set questions) { + try { + List lines = Files.readAllLines(path, StandardCharsets.UTF_8); + for (String line : lines) { + String trimmed = line.trim(); + if (trimmed.isEmpty()) { + continue; + } + int dotIndex = trimmed.indexOf('.'); + if (dotIndex >= 0 && dotIndex + 1 < trimmed.length()) { + String question = trimmed.substring(dotIndex + 1).trim(); + if (!question.isEmpty()) { + questions.add(question); + } + } else { + questions.add(trimmed); + } + } + } catch (IOException exception) { + // 继续处理其他文件,如有需要可在此处添加日志。 + } + } +}