From 96e7c358ebf8befd1168123eeafa9f9595d815a8 Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Wed, 24 Sep 2025 21:42:32 +0800 Subject: [PATCH 1/3] second --- src/main.py | 83 ++++++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 69 insertions(+), 14 deletions(-) diff --git a/src/main.py b/src/main.py index 3600257..cfcdf23 100644 --- a/src/main.py +++ b/src/main.py @@ -12,15 +12,64 @@ import os import random from datetime import datetime from typing import Callable, List, Set +import sqlite3 +import os -# ------------------------------ -# 常量:预设账户(来自需求文档附表) -# ------------------------------ -ACCOUNTS = { - "小学": {"张三1": "123", "张三2": "123", "张三3": "123"}, - "初中": {"李四1": "123", "李四2": "123", "李四3": "123"}, - "高中": {"王五1": "123", "王五2": "123", "王五3": "123"}, -} +DB_NAME = "accounts.db" + + +def init_db(): + """初始化数据库和账户表,并导入预设数据。""" + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + # 创建表:如果不存在就创建 + cursor.execute(""" + CREATE TABLE IF NOT EXISTS users ( + username TEXT PRIMARY KEY, + password TEXT NOT NULL, + level TEXT NOT NULL + ) + """) + conn.commit() + + # 预设数据,如果表中没有就插入 + predefined_accounts = { + "小学": {"张三1": "123", "张三2": "123", "张三3": "123"}, + "初中": {"李四1": "123", "李四2": "123", "李四3": "123"}, + "高中": {"王五1": "123", "王五2": "123", "王五3": "123"}, + } + + for level, users in predefined_accounts.items(): + for username, password in users.items(): + # 检查用户是否已存在,如果不存在则插入 + cursor.execute("SELECT 1 FROM users WHERE username = ?", (username,)) + if cursor.fetchone() is None: + cursor.execute("INSERT INTO users (username, password, level) VALUES (?, ?, ?)", + (username, password, level)) + + conn.commit() + conn.close() + + +def authenticate_user(username, password): + """ + 通过查询数据库验证用户名和密码。 + 返回: (level, username) 或 (None, None) + """ + conn = sqlite3.connect(DB_NAME) + cursor = conn.cursor() + + cursor.execute("SELECT level FROM users WHERE username = ? AND password = ?", + (username, password)) + + result = cursor.fetchone() + conn.close() + + if result: + return result[0], username # 返回用户类型和用户名 + else: + return None, None # 验证失败 # ------------------------------ # 工具:随机表达式生成(控制简单易懂) @@ -256,12 +305,17 @@ def login_prompt() -> (str, str): if len(parts) != 2: print("输入格式错误,请输入:用户名 密码 (中间以空格隔开)") continue + username, password = parts - for level, users in ACCOUNTS.items(): - if username in users and users[username] == password: - print(f"当前选择为 {level} 出题") - return level, username - print("请输入正确的用户名、密码") + + # 使用数据库函数进行验证 + level, auth_username = authenticate_user(username, password) + + if level: + print(f"当前选择为 {level} 出题") + return level, auth_username + else: + print("请输入正确的用户名、密码") def main_loop() -> None: """主循环:登录->出题->可切换/退出登录""" @@ -311,10 +365,11 @@ def main_loop() -> None: def main() -> None: """程序入口""" + init_db() try: main_loop() except KeyboardInterrupt: print("\n程序已被用户中断,退出。") -main() \ No newline at end of file +main()git remote -v \ No newline at end of file -- 2.34.1 From cc461f35f50d30dcfc688bbfb10c6d1d00817beb Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Thu, 25 Sep 2025 16:58:26 +0800 Subject: [PATCH 2/3] =?UTF-8?q?README=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- doc/README.md | 84 ++++++++++++++++++++------------------------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/doc/README.md b/doc/README.md index be2d36f..8a96b68 100644 --- a/doc/README.md +++ b/doc/README.md @@ -1,66 +1,48 @@ # 软件2302班_杨逸轩_个人项目 - 中小学数学卷子自动生成程序 -## 一、项目说明(概述) +## 一、项目概览 -本项目为命令行程序,用于为小学、初中、高中自动生成数学试题卷子(仅生成题目文本,不包含答案)。程序实现了登录(预设账户)、题目生成、查重、题目类型切换、按账号保存卷子等功能,符合课程个人项目要求。详细需求参见提交的需求文档。(开发参考:已阅读并参考课程需求文档与 Google Python 风格指南。) +本项目是一个命令行程序,旨在为小学、初中、高中不同年级的用户自动生成数学试卷。程序实现了用户登录、根据账户类型生成指定数量的题目、题目查重、以及按用户账户保存卷子等核心功能。所有账户信息都通过一个轻量级的 **SQLite 数据库**进行持久化管理,确保数据的稳定性和可扩展性。 -## 二、目录结构 +## 二、项目目录结构 -X班_姓名_个人项目/ +X班_姓名_个人项目/├── src/│ └── main.py # 程序主入口├── doc/│ └── README.md # 项目说明文档└── papers/ # 自动生成的试卷保存目录 (无需提交该目录)├── 张三1/│ └── 2025-09-25-16-30-01.txt└── ... -├── src/ +## 三、运行环境与安装 -│ └── main.py # 主程序(Python) +* **运行环境**: Python 3.8 或更高版本。 +* **依赖**: 本项目仅使用 Python 标准库,无需安装任何额外依赖。 +* **启动**: 在终端中,进入 `src` 目录的上层,执行以下命令即可启动程序:`python3 src/main.py` 或 `python src/main.py` -├── doc/ +## 四、预设账户 -│  └── README.md # 本文件 +程序启动时会自动初始化一个名为 `accounts.db` 的数据库文件,并导入以下预设账户。所有账户的密码均为 `123`。 +| 账户类型 | 用户名 | 密码 | +| --- | --- | --- | +| 小学 | 张三1, 张三2, 张三3 | 123 | +| 初中 | 李四1, 李四2, 李四3 | 123 | +| 高中 | 王五1, 王五2, 王五3 | 123 | -## 三、运行环境与要求 +**登录示例**:在命令行提示符下,输入 `张三1 123` 并回车即可登录。 -- Python 版本:推荐 Python 3.8+(向后兼容 3.x); +## 五、程序交互与功能说明 -- 无需额外第三方库,仅使用标准库; - -- 在终端中执行:`python3 src/main.py` 或 `python src/main.py`。 - - - -## 四、预设账户(登录示例) - - 账户类型与账号(密码均为 `123`): - -- 小学:`张三1`、`张三2`、`张三3` - -- 初中:`李四1`、`李四2`、`李四3` - -- 高中:`王五1`、`王五2`、`王五3` - -- 登录输入示例(命令行):`张三1 123` - - - -## 五、使用说明(交互流程) - -1.启动程序后,按提示输入 `用户名 密码`(中间以空格隔开)。 - -2.登录成功后将显示 `当前选择为 XX 出题`(XX 为 小学/初中/高中)。 - -3.程序会提示:`准备生成 XX 数学题目,请输入生成题目数量(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):` - 输入整数 `n`(10 ≤ n ≤ 30)表示生成 n 道题并保存; - 输入 `-1` 退出当前用户,返回登录界面; - 输入 `切换为 初中`(或 小学/高中)可在登录状态下切换出题类型。 - -4卷子将保存在 `papers/<用户名>/年-月-日-时-分-秒.txt`,每题有题号,题目之间空一行。 - -5.程序在生成题目时会避免与 `papers/<用户名>` 下已有卷子中的题目重复(查重功能)。 - -## 六、题型规则(来自需求) - -- 小学题:仅包含 `+`, `-`, `*`, `/` 和括号 `()`。 - -- 初中题:题目中至少包含一个平方 `^2` 或一个开根号 `√()`。 - -- 高中题:题目中至少包含一个三角函数 `sin`, `cos`, 或 `tan`。 题目中操作数个数为 1-5,数值范围 1-100。详见课程需求附表。 - - +1. **登录**: 程序启动后会提示输入用户名和密码。成功登录后,系统会显示当前选择的出题类型。 +2. **题目数量**: 登录后,程序会提示输入生成题目的数量。有效范围为 **10-30** 题。 +3. **题目生成**: + * **小学题**: 题目只包含加、减、乘、除和括号运算符。 + * **初中题**: 题目中至少包含一个平方或一个开根号运算符。 + * **高中题**: 题目中至少包含 `sin`, `cos`, 或 `tan` 三角函数运算符。 + * **操作数**: 每道题目包含的操作数在 **1-5** 个之间,数值范围为 **1-100**。 +4. **查重**: 生成题目时,程序会自动检查并避免与该账户已生成的卷子中的题目重复。 +5. **卷子保存**: 生成的卷子文件将自动保存至 `papers/<用户名>/` 目录下,文件名格式为 `年-月-日-时-分-秒.txt`。每道题目都有题号,且题目之间空一行,以确保格式清晰。 +6. **命令操作**: + * 输入 `-1` : 退出当前用户,返回登录界面。 + * 输入 `切换为 XX` : 在登录状态下切换出题类型,`XX` 选项为 `小学`、`初中` 或 `高中`。 +## 六、开发说明 +* 本项目遵循 Google Python 风格指南。 +* 主要代码逻辑位于 `src/main.py` 文件中,所有功能均在该文件中实现。 +* 项目满足课程需求文档中的各项功能和规范。 \ No newline at end of file -- 2.34.1 From 0f54d29d172e16711ec80a993c0522a5cc1ab685 Mon Sep 17 00:00:00 2001 From: yyx <20328610@qq.com> Date: Thu, 25 Sep 2025 23:11:57 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=B8=A4=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main.py | 175 ++++++----------------------------------------- src/questions.py | 94 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+), 155 deletions(-) create mode 100644 src/questions.py diff --git a/src/main.py b/src/main.py index cfcdf23..725de5c 100644 --- a/src/main.py +++ b/src/main.py @@ -1,3 +1,5 @@ +# src/main.py + """ 中小学数学卷子自动生成程序(命令行版) 功能要点: @@ -9,21 +11,25 @@ """ import os -import random +import sqlite3 from datetime import datetime from typing import Callable, List, Set -import sqlite3 -import os -DB_NAME = "accounts.db" +# 从 questions 模块导入题目生成函数 +from .questions import generate_primary_question, generate_middle_question, generate_high_question +# ------------------------------ +# 数据库管理 +# ------------------------------ + +DB_NAME = "accounts.db" +VALID_LEVELS = ["小学", "初中", "高中"] def init_db(): """初始化数据库和账户表,并导入预设数据。""" conn = sqlite3.connect(DB_NAME) cursor = conn.cursor() - # 创建表:如果不存在就创建 cursor.execute(""" CREATE TABLE IF NOT EXISTS users ( username TEXT PRIMARY KEY, @@ -33,7 +39,6 @@ def init_db(): """) conn.commit() - # 预设数据,如果表中没有就插入 predefined_accounts = { "小学": {"张三1": "123", "张三2": "123", "张三3": "123"}, "初中": {"李四1": "123", "李四2": "123", "李四3": "123"}, @@ -42,7 +47,6 @@ def init_db(): for level, users in predefined_accounts.items(): for username, password in users.items(): - # 检查用户是否已存在,如果不存在则插入 cursor.execute("SELECT 1 FROM users WHERE username = ?", (username,)) if cursor.fetchone() is None: cursor.execute("INSERT INTO users (username, password, level) VALUES (?, ?, ?)", @@ -67,125 +71,9 @@ def authenticate_user(username, password): conn.close() if result: - return result[0], username # 返回用户类型和用户名 + return result[0], username else: - return None, None # 验证失败 - -# ------------------------------ -# 工具:随机表达式生成(控制简单易懂) -# 每个生成函数尽量短小(符合风格指南对函数长度的建议) -# ------------------------------ - -def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]: - """生成 n 个随机整数(闭区间),作为操作数使用。""" - return [random.randint(lo, hi) for _ in range(n)] - -def _join_with_ops(numbers: List[int], ops: List[str]) -> str: - """把数字列表用随机运算符连接成表达式字符串,可能加入括号。""" - expr = str(numbers[0]) - for num in numbers[1:]: - op = random.choice(ops) - expr += f" {op} {num}" - # 20% 概率在表达式外层加一对括号 - if random.random() < 0.2: - expr = "(" + expr + ")" - return expr - -def generate_primary_question() -> str: - """ - 生成小学题(仅包含 + - * / 和可能的括号)。 - 操作数个数在 1-5 个之间,且操作数取值 1-100。 - 返回字符串形式的题目表达式(不计算答案)。 - """ - count = random.randint(2, 5) - nums = _rand_numbers(count) - ops = ["+", "-", "*", "/"] - return _join_with_ops(nums, ops) - - -def generate_middle_question() -> str: - """ - 生成初中题:在小学题的基础上,题目中至少包含一个平方或一个开根号运算符。 - 我们用简单的字符串形式表示:x^2 或 √(expr)。 - """ - # 确保题目中至少有一个平方或开根号运算符 - ops = ["+", "-", "*", "/"] - special_ops = ["^2", "√"] - - # 随机生成操作数个数(2-5个) - num_count = random.randint(2, 5) - numbers = _rand_numbers(num_count) - - # 将数字和运算符交替拼接成列表 - parts = [] - for i in range(num_count): - parts.append(str(numbers[i])) - if i < num_count - 1: - parts.append(random.choice(ops)) - - # 随机选择一个位置插入特殊运算符 - # 尝试将特殊运算符插入到某个数字前或某个子表达式前 - special_op_pos = random.randint(0, len(parts) // 2) * 2 - - # 获取要应用特殊运算的部分 - part_to_modify = parts[special_op_pos] - special_op = random.choice(special_ops) - - # 如果是开方,确保开方内部有括号,除非是单个数字 - if special_op == "√": - if num_count > 1: - # 找到一个子表达式进行开方 - # 我们可以简单地选择一个操作数和它后面的运算符+操作数 - # 为了简化,这里只对一个数字进行开方 - parts[special_op_pos] = f"√({parts[special_op_pos]})" - else: - parts[special_op_pos] = f"√{parts[special_op_pos]}" - # 如果是平方,且不是单个数字,则加上括号 - elif special_op == "^2": - if num_count > 1: - parts[special_op_pos] = f"({parts[special_op_pos]})^2" - else: - parts[special_op_pos] = f"{parts[special_op_pos]}^2" - - return "".join(parts) - - -def generate_high_question() -> str: - """ - 生成高中题:题目中至少包含 sin/cos/tan。 - 形式示例:sin(expr) 或 cos(expr) - """ - # 确保题目中至少有一个三角函数运算符 - ops = ["+", "-", "*", "/"] - trig_ops = ["sin", "cos", "tan"] - - # 随机生成操作数个数(2-5个) - num_count = random.randint(2, 5) - numbers = _rand_numbers(num_count) - - # 将数字和运算符交替拼接成列表 - parts = [] - for i in range(num_count): - parts.append(str(numbers[i])) - if i < num_count - 1: - parts.append(random.choice(ops)) - - # 随机选择一个位置插入三角函数 - # 尝试将三角函数插入到某个数字前 - trig_op_pos = random.randint(0, len(parts) // 2) * 2 - - # 获取要应用三角函数的部分 - part_to_modify = parts[trig_op_pos] - trig_op = random.choice(trig_ops) - - # 将三角函数应用到该部分,并加上括号 - parts[trig_op_pos] = f"{trig_op}({part_to_modify})" - - # 50%的概率在表达式外层加上括号 - if random.random() < 0.5: - return "(" + "".join(parts) + ")" - - return "".join(parts) + return None, None # ------------------------------ # 文件与查重相关函数 @@ -197,8 +85,7 @@ def _ensure_dir(path: str) -> None: def _read_existing_questions(folder: str) -> Set[str]: """ - 读取指定文件夹下所有文本文件,返回其中已存在的题目集合(去重并去空行)。 - 这样在生成新卷子时能避免题目重复。 + 读取指定文件夹下所有文本文件,返回其中已存在的题目集合。 """ questions = set() if not os.path.isdir(folder): @@ -206,18 +93,14 @@ def _read_existing_questions(folder: str) -> Set[str]: for fname in os.listdir(folder): if not fname.lower().endswith(".txt"): continue - # 跳过非题目文件 fpath = os.path.join(folder, fname) try: with open(fpath, "r", encoding="utf-8") as f: for line in f: s = line.strip() - # 跳过题号行前的序号(如 "1. "),提取实际内容 if not s: continue - # 如果行以数字点空格开头,去掉序号(兼容已有文件格式) if s.split(".", 1)[0].isdigit() and s.count(".") >= 1: - # 分割一次获取题目主体 parts = s.split(".", 1) if len(parts) == 2: content = parts[1].strip() @@ -228,7 +111,6 @@ def _read_existing_questions(folder: str) -> Set[str]: if content: questions.add(content) except Exception: - # 忽略单个文件读取错误(但不抛出),继续处理其他文件 continue return questions @@ -238,10 +120,8 @@ def _read_existing_questions(folder: str) -> Set[str]: def generate_paper(level: str, count: int, folder: str) -> str: """ - 根据 level(小学/初中/高中)和题目数量 count 生成一个卷子并保存到 folder。 - 返回生成的文件路径(成功)或空字符串(失败)。 + 根据 level 和题目数量 count 生成一个卷子并保存。 """ - # 目录与参数校验 _ensure_dir(folder) if level == "小学": @@ -253,31 +133,25 @@ def generate_paper(level: str, count: int, folder: str) -> str: else: raise ValueError("level 必须是:小学、初中或高中") - # 读取已有题目以便查重 existing = _read_existing_questions(folder) - # 生成题目(去重) new_questions: List[str] = [] - attempt_limit = count * 20 # 防止死循环:尝试上限 + attempt_limit = count * 20 attempts = 0 while len(new_questions) < count and attempts < attempt_limit: attempts += 1 q = gen_func() - # 对题目主体做简单规范化(去掉多余空白) q_norm = " ".join(q.split()) if q_norm not in existing and q_norm not in new_questions: new_questions.append(q_norm) if len(new_questions) < count: - # 如果没生成足够题目,提示并返回空 print("提示:未能生成足够的不重复题目,请稍后再试或清理旧卷子。") return "" - # 文件名按 年-月-日-时-分-秒.txt filename = datetime.now().strftime("%Y-%m-%d-%H-%M-%S") + ".txt" filepath = os.path.join(folder, filename) - # 写入文件:每题前带题号,题目之间空一行 try: with open(filepath, "w", encoding="utf-8") as f: for idx, q in enumerate(new_questions, start=1): @@ -297,7 +171,6 @@ def login_prompt() -> (str, str): """ 命令行登录提示: 要求格式:"用户名 密码"(用空格分开)。 - 返回: (level, username) """ while True: raw = input("请输入用户名和密码(空格隔开):").strip() @@ -308,7 +181,6 @@ def login_prompt() -> (str, str): username, password = parts - # 使用数据库函数进行验证 level, auth_username = authenticate_user(username, password) if level: @@ -321,46 +193,39 @@ def main_loop() -> None: """主循环:登录->出题->可切换/退出登录""" while True: level, username = login_prompt() - user_folder = os.path.join("papers", username) # 每个账号一个文件夹 + user_folder = os.path.join("papers", username) while True: prompt = f"准备生成 {level} 数学题目,请输入生成题目数量(输入 -1 退出当前用户,或输入'切换为 XX'切换类型):" inp = input(prompt).strip() - # 处理切换命令 if inp.startswith("切换为"): new_level = inp.replace("切换为", "").strip() - if new_level in ACCOUNTS: + if new_level in VALID_LEVELS: level = new_level print(f"已切换,准备生成 {level} 数学题目") else: print("请输入小学、初中和高中三个选项中的一个") continue - # 退出当前用户,返回登录界面 if inp == "-1": print("退出当前用户,返回登录界面。") break - # 解析数字 try: count = int(inp) except ValueError: print("输入有误,请输入整数或指令(例如:切换为 初中,或 -1)") continue - # 数量范围校验:10-30(含) if not (10 <= count <= 30): print("题目数量必须在 10-30 之间(包含 10 和 30)") continue - # 生成题目 res = generate_paper(level, count, user_folder) if not res: - # 生成失败(如查重过多),允许用户重试或退出 print("生成失败,请尝试更小的题目数量或清理已有卷子后重试。") else: - # 生成成功后,继续允许该用户再次出题或切换/退出(满足评分细则:每次登录可出题多次) pass def main() -> None: @@ -371,5 +236,5 @@ def main() -> None: except KeyboardInterrupt: print("\n程序已被用户中断,退出。") - -main()git remote -v \ No newline at end of file +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/questions.py b/src/questions.py new file mode 100644 index 0000000..9a36699 --- /dev/null +++ b/src/questions.py @@ -0,0 +1,94 @@ +import random +from typing import List + +# ------------------------------ +# 工具:随机表达式生成(控制简单易懂) +# ------------------------------ + +def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]: + """生成 n 个随机整数(闭区间),作为操作数使用。""" + return [random.randint(lo, hi) for _ in range(n)] + +def _join_with_ops(numbers: List[int], ops: List[str]) -> str: + """把数字列表用随机运算符连接成表达式字符串,可能加入括号。""" + expr = str(numbers[0]) + for num in numbers[1:]: + op = random.choice(ops) + expr += f" {op} {num}" + # 20% 概率在表达式外层加一对括号 + if random.random() < 0.2: + expr = "(" + expr + ")" + return expr + +def generate_primary_question() -> str: + """ + 生成小学题(仅包含 + - * / 和可能的括号)。 + 操作数个数在 1-5 个之间,且操作数取值 1-100。 + """ + count = random.randint(2, 5) + nums = _rand_numbers(count) + ops = ["+", "-", "*", "/"] + return _join_with_ops(nums, ops) + +def generate_middle_question() -> str: + """ + 生成初中题:题目中至少包含一个平方或一个开根号运算符。 + """ + ops = ["+", "-", "*", "/"] + special_ops = ["^2", "√"] + + num_count = random.randint(2, 5) + numbers = _rand_numbers(num_count) + + parts = [] + for i in range(num_count): + parts.append(str(numbers[i])) + if i < num_count - 1: + parts.append(random.choice(ops)) + + special_op_pos = random.randint(0, len(parts) // 2) * 2 + + part_to_modify = parts[special_op_pos] + special_op = random.choice(special_ops) + + if special_op == "√": + if num_count > 1: + parts[special_op_pos] = f"√({parts[special_op_pos]})" + else: + parts[special_op_pos] = f"√{parts[special_op_pos]}" + elif special_op == "^2": + if num_count > 1: + parts[special_op_pos] = f"({parts[special_op_pos]})^2" + else: + parts[special_op_pos] = f"{parts[special_op_pos]}^2" + + return "".join(parts) + + +def generate_high_question() -> str: + """ + 生成高中题:题目中至少包含 sin/cos/tan。 + """ + ops = ["+", "-", "*", "/"] + trig_ops = ["sin", "cos", "tan"] + + num_count = random.randint(2, 5) + numbers = _rand_numbers(num_count) + + parts = [] + for i in range(num_count): + parts.append(str(numbers[i])) + if i < num_count - 1: + parts.append(random.choice(ops)) + + trig_op_pos = random.randint(0, len(parts) // 2) * 2 + + part_to_modify = parts[trig_op_pos] + trig_op = random.choice(trig_ops) + + parts[trig_op_pos] = f"{trig_op}({part_to_modify})" + + if random.random() < 0.5: + return "(" + "".join(parts) + ")" + + return "".join(parts) \ No newline at end of file -- 2.34.1