diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..d7a3206 --- /dev/null +++ b/doc/README.md @@ -0,0 +1,2 @@ +# math_test_generator + diff --git a/src/.vscode/c_cpp_properties.json b/src/.vscode/c_cpp_properties.json new file mode 100644 index 0000000..bc4a023 --- /dev/null +++ b/src/.vscode/c_cpp_properties.json @@ -0,0 +1,18 @@ +{ + "configurations": [ + { + "name": "windows-gcc-x64", + "includePath": [ + "${workspaceFolder}/**" + ], + "compilerPath": "D:/MinGW/bin/gcc.exe", + "cStandard": "${default}", + "cppStandard": "${default}", + "intelliSenseMode": "windows-gcc-x64", + "compilerArgs": [ + "" + ] + } + ], + "version": 4 +} \ No newline at end of file diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json new file mode 100644 index 0000000..d350390 --- /dev/null +++ b/src/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "C/C++ Runner: Debug Session", + "type": "cppdbg", + "request": "launch", + "args": [], + "stopAtEntry": false, + "externalConsole": true, + "cwd": "d:/软件导论/src", + "program": "d:/软件导论/src/build/Debug/outDebug", + "MIMode": "gdb", + "miDebuggerPath": "gdb", + "setupCommands": [ + { + "description": "Enable pretty-printing for gdb", + "text": "-enable-pretty-printing", + "ignoreFailures": true + } + ] + } + ] +} \ No newline at end of file diff --git a/src/.vscode/settings.json b/src/.vscode/settings.json new file mode 100644 index 0000000..bb879da --- /dev/null +++ b/src/.vscode/settings.json @@ -0,0 +1,59 @@ +{ + "C_Cpp_Runner.cCompilerPath": "gcc", + "C_Cpp_Runner.cppCompilerPath": "g++", + "C_Cpp_Runner.debuggerPath": "gdb", + "C_Cpp_Runner.cStandard": "", + "C_Cpp_Runner.cppStandard": "", + "C_Cpp_Runner.msvcBatchPath": "C:/Program Files/Microsoft Visual Studio/VR_NR/Community/VC/Auxiliary/Build/vcvarsall.bat", + "C_Cpp_Runner.useMsvc": false, + "C_Cpp_Runner.warnings": [ + "-Wall", + "-Wextra", + "-Wpedantic", + "-Wshadow", + "-Wformat=2", + "-Wcast-align", + "-Wconversion", + "-Wsign-conversion", + "-Wnull-dereference" + ], + "C_Cpp_Runner.msvcWarnings": [ + "/W4", + "/permissive-", + "/w14242", + "/w14287", + "/w14296", + "/w14311", + "/w14826", + "/w44062", + "/w44242", + "/w14905", + "/w14906", + "/w14263", + "/w44265", + "/w14928" + ], + "C_Cpp_Runner.enableWarnings": true, + "C_Cpp_Runner.warningsAsError": false, + "C_Cpp_Runner.compilerArgs": [], + "C_Cpp_Runner.linkerArgs": [], + "C_Cpp_Runner.includePaths": [], + "C_Cpp_Runner.includeSearch": [ + "*", + "**/*" + ], + "C_Cpp_Runner.excludeSearch": [ + "**/build", + "**/build/**", + "**/.*", + "**/.*/**", + "**/.vscode", + "**/.vscode/**" + ], + "C_Cpp_Runner.useAddressSanitizer": false, + "C_Cpp_Runner.useUndefinedSanitizer": false, + "C_Cpp_Runner.useLeakSanitizer": false, + "C_Cpp_Runner.showCompilationTime": false, + "C_Cpp_Runner.useLinkTimeOptimization": false, + "C_Cpp_Runner.msvcSecureNoWarnings": false +} \ No newline at end of file diff --git a/src/.vscode/tasks.json b/src/.vscode/tasks.json new file mode 100644 index 0000000..6c40ce7 --- /dev/null +++ b/src/.vscode/tasks.json @@ -0,0 +1,29 @@ +{ + "tasks": [ + { + "type": "cppbuild", + "label": "C/C++: gcc.exe build active file", + "command": "D:/MinGW/bin/gcc.exe", + "args": [ + "-fdiagnostics-color=always", + "-g", + "${file}", + "-o", + "${fileDirname}\\${fileBasenameNoExtension}.exe", + "" + ], + "options": { + "cwd": "D:/MinGW/bin" + }, + "problemMatcher": [ + "$gcc" + ], + "group": { + "kind": "build", + "isDefault": true + }, + "detail": "Task generated by Debugger." + } + ], + "version": "2.0.0" +} \ No newline at end of file diff --git a/src/math_test_generator.cpp b/src/math_test_generator.cpp new file mode 100644 index 0000000..1a1392f --- /dev/null +++ b/src/math_test_generator.cpp @@ -0,0 +1,520 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace math_test_generator { + +// 用户账户结构体(严格匹配文档附表1:小学/初中/高中各3个账户) +struct UserAccount { + std::string username; // 用户名(如"李四2") + std::string password; // 密码(统一为"123") + std::string user_type; // 账户类型(小学/初中/高中) +}; + +class MathTestGenerator { +public: + // 构造函数:初始化随机数与预设账户(符合文档初始化需求) + MathTestGenerator() : is_logged_in_(false) { + std::random_device rd; + rng_.seed(rd()); + InitializePresetAccounts(); + } + + // 程序主入口(按文档流程:登录→输入数量→生成/切换→保存) + void Run() { + std::cout << "=== 中小学数学卷子自动生成程序 ===" << std::endl; + while (true) { + if (!is_logged_in_) { + HandleLogin(); // 未登录:处理用户名密码输入 + } else { + HandleLoggedInOperations(); // 已登录:处理题目生成 + } + } + } + +private: + // 成员变量(按文档需求定义) + std::vector preset_accounts_; // 预设账户(文档附表1) + UserAccount current_user_; // 当前登录用户 + bool is_logged_in_; // 登录状态标记 + std::map> question_history_; // 题目去重历史(文档需求3) + std::mt19937 rng_; // 随机数生成器 + + // 常量(严格匹配文档需求3、5) + static constexpr int kMinQuestionCount = 10; // 最小题目数(文档需求3) + static constexpr int kMaxQuestionCount = 30; // 最大题目数(文档需求3) + static constexpr int kExitLoginCode = -1; // 退出登录指令(文档需求2) + static constexpr int kMaxRetryCount = 1000; // 题目去重最大尝试次数 + + // 初始化预设账户(文档附表1:小学3个/初中3个/高中3个) + void InitializePresetAccounts() { + // 小学账户 + preset_accounts_.push_back({"张三1", "123", "小学"}); + preset_accounts_.push_back({"张三2", "123", "小学"}); + preset_accounts_.push_back({"张三3", "123", "小学"}); + // 初中账户 + preset_accounts_.push_back({"李四1", "123", "初中"}); + preset_accounts_.push_back({"李四2", "123", "初中"}); + preset_accounts_.push_back({"李四3", "123", "初中"}); + // 高中账户 + preset_accounts_.push_back({"王五1", "123", "高中"}); + preset_accounts_.push_back({"王五2", "123", "高中"}); + preset_accounts_.push_back({"王五3", "123", "高中"}); + } + + // 处理登录(文档需求1:命令行输入用户名密码,空格分隔) + void HandleLogin() { + std::string username, password; + std::cout << "请输入用户名和密码(用空格隔开): "; + // 处理输入格式错误 + while (!(std::cin >> username >> password)) { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cout << "输入格式错误,请重新输入用户名和密码(用空格隔开): "; + } + std::cin.ignore(std::numeric_limits::max(), '\n'); + + // 验证登录并反馈(文档需求1:成功则显示"当前选择为XX出题") + if (VerifyLogin(username, password)) { + std::cout << "登录成功!当前选择为" << current_user_.user_type << "出题" << std::endl; + } else { + std::cout << "请输入正确的用户名、密码" << std::endl; + } + } + + // 登录验证(文档需求1:匹配成功后预创建用户文件夹,符合需求5) + bool VerifyLogin(const std::string& username, const std::string& password) { + for (const auto& account : preset_accounts_) { + if (account.username == username && account.password == password) { + current_user_ = account; + is_logged_in_ = true; + // 登录成功后立即创建用户文件夹(文档需求5:每个账号一个文件夹) + std::string user_dir = GetUserDirectory(); + if (CreateUserDirectory(user_dir)) { + LoadQuestionHistory(); // 加载历史题目(文档需求3去重) + } + return true; + } + } + return false; + } + + // 已登录操作(文档需求2/3/4:输入数量、切换类型、生成题目) + void HandleLoggedInOperations() { + int question_count = 0; + // 提示输入题目数量(文档需求2:显示"准备生成XX数学题目") + std::cout << "准备生成" << current_user_.user_type + << "数学题目,请输入生成题目数量(输入-1将退出当前用户,重新登录): "; + // 处理非数字输入 + while (!(std::cin >> question_count)) { + std::cin.clear(); + std::cin.ignore(std::numeric_limits::max(), '\n'); + std::cout << "输入格式错误,请输入有效数字(10-30或-1): "; + } + std::cin.ignore(std::numeric_limits::max(), '\n'); + + // 退出登录(文档需求2:输入-1退出) + if (question_count == kExitLoginCode) { + Logout(); + return; + } + + // 验证题目数量(文档需求3:10-30含边界) + if (!IsValidQuestionCount(question_count)) { + std::cout << "题目数量有效范围是10-30,请重新输入" << std::endl; + return; + } + + // 处理类型切换(文档需求4:输入"切换为XX"切换类型) + std::string command; + std::cout << "请输入命令(直接回车生成题目,或输入'切换为XX'切换类型): "; + std::getline(std::cin, command); + + if (!command.empty() && !HandleTypeSwitch(command)) { + std::cout << "切换失败,将按原类型生成题目" << std::endl; + } + + // 生成并保存试卷(文档需求3/5:不重复题目、账号文件夹存储) + GenerateAndSaveTestPaper(question_count); + } + + // 验证题目数量(文档需求3) + bool IsValidQuestionCount(int count) const { + return count >= kMinQuestionCount && count <= kMaxQuestionCount; + } + + // 处理类型切换(文档需求4:仅支持小学/初中/高中) + bool HandleTypeSwitch(const std::string& command) { + const std::string kSwitchPrefix = "切换为"; + // 检查命令格式 + if (command.substr(0, kSwitchPrefix.size()) != kSwitchPrefix) { + std::cout << "请输入小学、初中和高中三个选项中的一个" << std::endl; + return false; + } + + // 提取并验证目标类型 + std::string target_type = command.substr(kSwitchPrefix.size()); + if (target_type != "小学" && target_type != "初中" && target_type != "高中") { + std::cout << "请输入小学、初中和高中三个选项中的一个" << std::endl; + return false; + } + + // 切换类型并提示(文档需求4) + current_user_.user_type = target_type; + std::cout << "系统提示:准备生成" << target_type << "数学题目,请输入生成题目数量" << std::endl; + return true; + } + + // 生成试卷并保存(文档需求3/5) + void GenerateAndSaveTestPaper(int question_count) { + std::vector questions; + int retry_count = 0; + + // 生成不重复题目(文档需求3:同一老师题目不重复) + while (questions.size() < static_cast(question_count) && retry_count < kMaxRetryCount) { + std::string question = GenerateQuestionByType(current_user_.user_type); + if (!IsQuestionDuplicate(question)) { + questions.push_back(question); + AddQuestionToHistory(question); + } + retry_count++; + } + + // 处理生成不足问题 + if (questions.size() < static_cast(question_count)) { + std::cerr << "警告:无法生成足够的不重复题目,仅生成" << questions.size() << "道" << std::endl; + } + + // 保存题目(文档需求5:按格式保存到账号文件夹) + if (SaveQuestionsToFile(questions)) { + std::cout << "题目生成完成,已保存到文件" << std::endl; + } else { + std::cerr << "错误:题目保存失败" << std::endl; + } + } + + // 按类型生成题目(文档附表2:小学/初中/高中难度差异) + std::string GenerateQuestionByType(const std::string& user_type) { + if (user_type == "小学") return GeneratePrimaryQuestion(); + if (user_type == "初中") return GenerateJuniorQuestion(); + return GenerateSeniorQuestion(); // 高中 + } + + // 小学题目(文档附表2:仅+,-,*,/和(),操作数1-100) + std::string GeneratePrimaryQuestion() { + const int operand_count = GenerateRandomInt(1, 5); // 1-5个操作数(文档需求6) + std::vector operands; + for (int i = 0; i < operand_count; i++) { + operands.push_back(GenerateRandomInt(1, 100)); // 操作数范围1-100(文档需求6) + } + + const std::vector kOps = {"+", "-", "*", "/"}; + std::vector selected_ops; + for (int i = 0; i < operand_count - 1; i++) { + selected_ops.push_back(kOps[GenerateRandomInt(0, 3)]); + } + + // 构建题目(50%概率加括号,文档附表2) + std::stringstream ss; + if (operand_count > 2 && GenerateRandomInt(0, 1)) { + const int paren_start = GenerateRandomInt(0, operand_count - 3); + ss << operands[0]; + for (int i = 1; i < operand_count; i++) { + if (i == paren_start + 1) ss << selected_ops[i-1] << "(" << operands[i]; + else if (i == paren_start + 2) ss << selected_ops[i-1] << operands[i] << ")"; + else ss << selected_ops[i-1] << operands[i]; + } + } else { + ss << operands[0]; + for (int i = 1; i < operand_count; i++) { + ss << selected_ops[i-1] << operands[i]; + } + } + ss << " = ?"; + return ss.str(); + } + + // 初中题目(文档附表2:含平方/开根号,至少1个特殊运算符) + std::string GenerateJuniorQuestion() { + std::string question; + while (true) { + const int operand_count = GenerateRandomInt(1, 5); + std::vector operands; + for (int i = 0; i < operand_count; i++) { + operands.push_back(GenerateRandomInt(1, 100)); + } + + const std::vector kOps = {"+", "-", "*", "/", "^2", "√"}; + bool has_special = false; // 标记是否含平方/开根号 + std::stringstream ss; + + // 处理第一个操作数(可能带特殊运算符) + const int first_op = GenerateRandomInt(0, 2); + if (first_op == 1) { ss << operands[0] << "^2"; has_special = true; } + else if (first_op == 2) { ss << "√" << operands[0]; has_special = true; } + else ss << operands[0]; + + // 处理后续操作数 + for (int i = 1; i < operand_count; i++) { + const std::string op = kOps[GenerateRandomInt(0, 5)]; + if (op == "^2" || op == "√") has_special = true; + ss << op; + if (op == "√") ss << operands[i]; + else { ss << operands[i]; if (op == "^2") ss << "^2"; } + } + + ss << " = ?"; + question = ss.str(); + if (has_special) break; // 确保含特殊运算符(文档附表2) + } + return question; + } + + // 高中题目(文档附表2:含sin/cos/tan,至少1个三角函数) + std::string GenerateSeniorQuestion() { + std::string question; + while (true) { + const int operand_count = GenerateRandomInt(1, 5); + std::vector operands; + for (int i = 0; i < operand_count; i++) { + operands.push_back(GenerateRandomInt(0, 180)); // 角度0-180° + } + + const std::vector kOps = {"+", "-", "*", "/", "sin", "cos", "tan"}; + bool has_trig = false; // 标记是否含三角函数 + std::stringstream ss; + + // 第一个操作数必为三角函数(确保至少一个,文档附表2) + const std::string trig_op = kOps[GenerateRandomInt(4, 6)]; + ss << trig_op << "(" << operands[0] << "°)"; + has_trig = true; + + // 处理后续操作数 + for (int i = 1; i < operand_count; i++) { + const std::string op = kOps[GenerateRandomInt(0, 6)]; + if (op == "sin" || op == "cos" || op == "tan") { + ss << op << "(" << operands[i] << "°)"; + } else { + ss << op << operands[i]; + } + } + + ss << " = ?"; + question = ss.str(); + if (has_trig) break; // 确保含三角函数(文档附表2) + } + return question; + } + + // 题目去重(文档需求3:基于用户历史记录) + bool IsQuestionDuplicate(const std::string& question) const { + const auto it = question_history_.find(current_user_.username); + return it != question_history_.end() && it->second.count(question); + } + + // 添加题目到历史记录(文档需求3) + void AddQuestionToHistory(const std::string& question) { + question_history_[current_user_.username].insert(question); + } + + // 加载历史题目(文档需求3:从账号文件夹读取已有文件) + void LoadQuestionHistory() { + const std::string user_dir = GetUserDirectory(); + if (!std::filesystem::exists(user_dir)) return; + + // 遍历账号文件夹下所有txt文件(文档需求5) + for (const auto& entry : std::filesystem::directory_iterator(user_dir)) { + if (entry.is_regular_file() && entry.path().extension() == ".txt") { + LoadQuestionsFromFile(entry.path().string()); + } + } + } + + // 从文件加载题目到历史记录(文档需求3) + void LoadQuestionsFromFile(const std::string& file_path) { + std::ifstream file(file_path); + if (!file.is_open()) { + std::cerr << "警告:无法读取历史文件" << file_path << std::endl; + return; + } + + std::string line; + while (std::getline(file, line)) { + if (line.empty()) continue; // 跳过空行(文档需求5) + const size_t dot_pos = line.find('.'); + if (dot_pos != std::string::npos) { + // 提取题目内容(去除题号) + std::string q = line.substr(dot_pos + 1); + q.erase(0, q.find_first_not_of(" \t")); + q.erase(q.find_last_not_of(" \t") + 1); + if (!q.empty()) AddQuestionToHistory(q); + } + } + file.close(); + } + + // 保存题目到文件(核心修复:解决中文路径编码与错误码判断) + bool SaveQuestionsToFile(const std::vector& questions) { + const std::string user_dir = GetUserDirectory(); + // 确保用户文件夹存在(文档需求5) + if (!std::filesystem::exists(user_dir)) { + std::cerr << "错误:用户文件夹" << user_dir << "不存在" << std::endl; + return false; + } + + // 生成合规文件名(文档需求5:年-月-日-时-分-秒.txt) + const std::string timestamp = GetCurrentTimestamp(); + const std::string rel_path = user_dir + timestamp + ".txt"; + // 转为Windows宽字符路径(修复中文编码问题) + std::wstring wide_path = Utf8ToWide(rel_path); + + // 使用Windows API创建文件(避免C++标准库中文路径问题) + HANDLE file_handle = CreateFileW( + wide_path.c_str(), + GENERIC_WRITE, + 0, + nullptr, + CREATE_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + nullptr + ); + + if (file_handle == INVALID_HANDLE_VALUE) { + std::cerr << "错误:无法创建文件" << rel_path + << ",系统错误码:" << GetLastError() << std::endl; + return false; + } + + // 按文档需求5写入题目(带题号、每题空一行) + std::string content; + for (size_t i = 0; i < questions.size(); i++) { + content += std::to_string(i + 1) + ". " + questions[i] + "\r\n"; + if (i < questions.size() - 1) content += "\r\n"; // 题目间空一行 + } + + // 写入文件(UTF-8编码,避免中文乱码) + DWORD bytes_written = 0; + bool write_ok = WriteFile( + file_handle, + content.c_str(), + static_cast(content.size()), + &bytes_written, + nullptr + ); + + if (!write_ok || bytes_written != content.size()) { + std::cerr << "错误:文件写入失败,系统错误码:" << GetLastError() << std::endl; + CloseHandle(file_handle); + return false; + } + + CloseHandle(file_handle); + std::cout << "文件保存成功,路径:" << rel_path << std::endl; + return true; + } + + // 获取用户文件夹路径(文档需求5:用户名/) + std::string GetUserDirectory() const { + return current_user_.username + "/"; + } + + // 【核心修复】创建用户文件夹:解决中文路径与错误码判断 + bool CreateUserDirectory(const std::string& dir_path) const { + // 转为Windows宽字符路径(修复中文编码问题) + std::wstring wide_dir = Utf8ToWide(dir_path); + + // 使用Windows API创建目录(避免C++标准库中文问题) + if (CreateDirectoryW(wide_dir.c_str(), nullptr)) { + std::cout << "成功:创建用户文件夹" << dir_path << std::endl; + return true; + } else { + DWORD err_code = GetLastError(); + // 错误码183:目录已存在(视为成功,文档允许复用文件夹) + if (err_code == ERROR_ALREADY_EXISTS) { + std::cout << "提示:用户文件夹" << dir_path << "已存在" << std::endl; + return true; + } + // 其他错误(如权限不足) + std::cerr << "错误:创建文件夹" << dir_path << "失败,系统错误码:" << err_code << std::endl; + return false; + } + } + + // 【辅助修复】UTF-8转Windows宽字符(解决中文路径编码) + std::wstring Utf8ToWide(const std::string& utf8_str) const { + int wide_len = MultiByteToWideChar( + CP_UTF8, + 0, + utf8_str.c_str(), + -1, + nullptr, + 0 + ); + if (wide_len <= 0) return L""; + + std::wstring wide_str(wide_len, 0); + MultiByteToWideChar( + CP_UTF8, + 0, + utf8_str.c_str(), + -1, + &wide_str[0], + wide_len + ); + return wide_str; + } + + // 获取时间戳(文档需求5:年-月-日-时-分-秒,无非法字符) + std::string GetCurrentTimestamp() const { + const auto now = std::time(nullptr); + const auto local = *std::localtime(&now); + std::stringstream ss; + ss << std::put_time(&local, "%Y-%m-%d-%H-%M-%S"); // 无冒号,符合Windows命名 + return ss.str(); + } + + // 退出登录(文档需求2) + void Logout() { + is_logged_in_ = false; + current_user_ = UserAccount(); + std::cout << "已退出当前用户,请重新登录" << std::endl; + } + + // 生成随机整数 + int GenerateRandomInt(int min, int max) { + std::uniform_int_distribution dist(min, max); + return dist(rng_); + } +}; + +} // namespace math_test_generator + +// 程序入口(设置控制台UTF-8编码,解决中文显示) +int main() { + #ifdef _WIN32 + SetConsoleOutputCP(CP_UTF8); + SetConsoleCP(CP_UTF8); + #endif + + try { + math_test_generator::MathTestGenerator generator; + generator.Run(); + } catch (const std::exception& e) { + std::cerr << "程序异常退出:" << e.what() << std::endl; + return 1; + } + return 0; +} \ No newline at end of file diff --git a/src/math_test_generator.exe b/src/math_test_generator.exe new file mode 100644 index 0000000..e0eb22c Binary files /dev/null and b/src/math_test_generator.exe differ