diff --git a/README.md b/README.md deleted file mode 100644 index 4bc6aba..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# MathStudySystem - diff --git a/doc/README.md b/doc/README.md new file mode 100644 index 0000000..e69de29 diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0f26f21 --- /dev/null +++ b/requirements.txt @@ -0,0 +1 @@ +PyQt6==6.7.0 \ No newline at end of file diff --git a/src/__pycache__/app.cpython-311.pyc b/src/__pycache__/app.cpython-311.pyc new file mode 100644 index 0000000..563b10c Binary files /dev/null and b/src/__pycache__/app.cpython-311.pyc differ diff --git a/src/app.py b/src/app.py new file mode 100644 index 0000000..38bee2f --- /dev/null +++ b/src/app.py @@ -0,0 +1,14 @@ +from PyQt6.QtWidgets import QApplication +from src.ui.main_window import MainWindow + + +def main() -> None: + """应用程序入口:创建 QApplication,显示主窗口,并进入事件循环。""" + app = QApplication([]) + window = MainWindow() + window.show() + app.exec() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/src/services/__init__.py b/src/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/services/__pycache__/__init__.cpython-311.pyc b/src/services/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..c2d809d Binary files /dev/null and b/src/services/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/services/__pycache__/email_service.cpython-311.pyc b/src/services/__pycache__/email_service.cpython-311.pyc new file mode 100644 index 0000000..da4c53f Binary files /dev/null and b/src/services/__pycache__/email_service.cpython-311.pyc differ diff --git a/src/services/__pycache__/question_service.cpython-311.pyc b/src/services/__pycache__/question_service.cpython-311.pyc new file mode 100644 index 0000000..8921db7 Binary files /dev/null and b/src/services/__pycache__/question_service.cpython-311.pyc differ diff --git a/src/services/__pycache__/storage_service.cpython-311.pyc b/src/services/__pycache__/storage_service.cpython-311.pyc new file mode 100644 index 0000000..a9bbf84 Binary files /dev/null and b/src/services/__pycache__/storage_service.cpython-311.pyc differ diff --git a/src/services/__pycache__/user_service.cpython-311.pyc b/src/services/__pycache__/user_service.cpython-311.pyc new file mode 100644 index 0000000..af9ce87 Binary files /dev/null and b/src/services/__pycache__/user_service.cpython-311.pyc differ diff --git a/src/services/email_service.py b/src/services/email_service.py new file mode 100644 index 0000000..2d05d0c --- /dev/null +++ b/src/services/email_service.py @@ -0,0 +1,66 @@ +""" +邮件服务模块:基于 SMTP 发送验证码邮件。支持 TLS/SSL。 +""" +import smtplib +import ssl +from email.message import EmailMessage +from typing import Dict + +from .storage_service import StorageService + + +class EmailService: + """负责发送邮件验证码的服务类。""" + + def __init__(self, storage: StorageService) -> None: + """初始化邮件服务,依赖存储服务获取 SMTP 配置。""" + self.storage = storage + + def send_verification_code(self, to_email: str, code: str) -> bool: + """发送验证码到指定邮箱。 + + 参数: + to_email: 收件人邮箱。 + code: 验证码字符串。 + 返回: + True 表示发送成功,False 表示失败。 + """ + config = self.storage.load_config().get('smtp', {}) + server = config.get('server', '') + port = int(config.get('port', 587)) + username = config.get('username', '') + password = config.get('password', '') + use_tls = bool(config.get('use_tls', True)) + use_ssl = bool(config.get('use_ssl', False)) + sender_name = config.get('sender_name', 'Math Study App') + + if not server or not username or not password: + return False + + msg = EmailMessage() + msg['Subject'] = '数学学习软件注册验证码' + msg['From'] = f"{sender_name} <{username}>" + msg['To'] = to_email + msg.set_content(f"您的注册验证码为:{code}\n该验证码10分钟内有效。") + + try: + if use_ssl: + context = ssl.create_default_context() + with smtplib.SMTP_SSL(server, port, context=context) as smtp: + smtp.login(username, password) + smtp.send_message(msg) + else: + with smtplib.SMTP(server, port) as smtp: + if use_tls: + smtp.starttls(context=ssl.create_default_context()) + smtp.login(username, password) + smtp.send_message(msg) + return True + except Exception: + return False + + def update_smtp_config(self, config: Dict) -> None: + """更新并保存 SMTP 配置。""" + data = self.storage.load_config() + data['smtp'] = config + self.storage.save_config(data) \ No newline at end of file diff --git a/src/services/question_service.py b/src/services/question_service.py new file mode 100644 index 0000000..7265765 --- /dev/null +++ b/src/services/question_service.py @@ -0,0 +1,96 @@ +""" +试题服务模块:按小学/初中/高中生成选择题试卷,并保证同一试卷题目不重复。 +""" +import random +from typing import List, Dict + + +class QuestionService: + """负责生成不同年级难度的选择题。""" + + def __init__(self) -> None: + """初始化题目服务。""" + random.seed() + + def generate_questions(self, level: str, count: int) -> List[Dict]: + """生成指定年级与数量的题目列表。 + + 参数: + level: 'primary'|'middle'|'high' 三选一。 + count: 题目数量。 + 返回: + 题目字典列表,每个字典包含 stem, options, answer_index。 + """ + generators = { + 'primary': self._gen_primary, + 'middle': self._gen_middle, + 'high': self._gen_high, + } + gen = generators.get(level) + if not gen: + raise ValueError('无效的年级选项') + seen = set() + questions: List[Dict] = [] + while len(questions) < count: + q = gen() + if q['stem'] in seen: + continue + seen.add(q['stem']) + questions.append(q) + return questions + + def _gen_primary(self) -> Dict: + """生成一题小学难度的加减法选择题。""" + a = random.randint(1, 50) + b = random.randint(1, 50) + op = random.choice(['+', '-']) + if op == '+': + ans = a + b + stem = f"计算:{a} + {b} = ?" + else: + ans = a - b + stem = f"计算:{a} - {b} = ?" + options = self._make_options(ans) + return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _gen_middle(self) -> Dict: + """生成一题初中难度的一元一次方程选择题。""" + a = random.randint(2, 9) + b = random.randint(1, 20) + # ax + b = 0 => x = -b / a + x = -b / a + stem = f"解方程:{a}x + {b} = 0,x = ?" + options = self._make_options(x, float_mode=True) + correct = f"{x:.2f}" + return {'stem': stem, 'options': options, 'answer_index': options.index(correct)} + + def _gen_high(self) -> Dict: + """生成一题高中难度的导数计算选择题:f(x)=ax^2+bx+c 在 x0 处的导数。""" + a = random.randint(1, 5) + b = random.randint(-5, 5) + c = random.randint(-10, 10) + x0 = random.randint(-5, 5) + # f'(x) = 2ax + b => f'(x0) = 2a*x0 + b + ans = 2 * a * x0 + b + stem = f"设 f(x)={a}x^2+{b}x+{c},求 f'({x0}) = ?" + options = self._make_options(ans) + return {'stem': stem, 'options': options, 'answer_index': options.index(str(ans))} + + def _make_options(self, answer, float_mode: bool = False) -> List[str]: + """根据正确答案生成 4 个选项(包含正确答案与3个干扰项)。""" + opts = set() + if float_mode: + correct = f"{answer:.2f}" + opts.add(correct) + while len(opts) < 4: + delta = random.uniform(-5, 5) + opts.add(f"{answer + delta:.2f}") + else: + correct = str(answer) + opts.add(correct) + while len(opts) < 4: + delta = random.randint(-10, 10) + opts.add(str(answer + delta)) + options = list(opts) + random.shuffle(options) + return options \ No newline at end of file diff --git a/src/services/storage_service.py b/src/services/storage_service.py new file mode 100644 index 0000000..7c41bd4 --- /dev/null +++ b/src/services/storage_service.py @@ -0,0 +1,85 @@ +""" +存储服务模块:使用 JSON 文件持久化数据(不使用数据库)。 +提供用户数据与配置数据的读写接口。 +""" +import json +import os +from typing import Dict, Any, List + + +class StorageService: + """使用 JSON 文件进行数据持久化的服务类。""" + + def __init__(self) -> None: + """初始化存储路径并确保必要文件存在。""" + base_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + self.storage_dir = os.path.join(base_dir, 'storage') + self.users_file = os.path.join(self.storage_dir, 'users.json') + self.config_file = os.path.join(self.storage_dir, 'config.json') + self._ensure_files() + + def _ensure_files(self) -> None: + """确保存储目录与文件存在,如不存在则创建。""" + os.makedirs(self.storage_dir, exist_ok=True) + if not os.path.exists(self.users_file): + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump({'users': []}, f, ensure_ascii=False, indent=2) + if not os.path.exists(self.config_file): + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump({ + 'smtp': { + 'server': '', + 'port': 587, + 'username': '', + 'password': '', + 'use_tls': True, + 'use_ssl': False, + 'sender_name': 'Math Study App' + } + }, f, ensure_ascii=False, indent=2) + + def load_users(self) -> Dict[str, List[Dict[str, Any]]]: + """读取用户列表数据。返回字典 {'users': [...]}。""" + with open(self.users_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_users(self, data: Dict[str, List[Dict[str, Any]]]) -> None: + """写入用户列表数据到文件。""" + with open(self.users_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def load_config(self) -> Dict[str, Any]: + """读取配置数据(包含 SMTP 配置)。""" + with open(self.config_file, 'r', encoding='utf-8') as f: + return json.load(f) + + def save_config(self, data: Dict[str, Any]) -> None: + """写入配置数据到文件。""" + with open(self.config_file, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=2) + + def get_user(self, email: str) -> Dict[str, Any] | None: + """根据邮箱获取用户字典,不存在返回 None。""" + users = self.load_users().get('users', []) + for u in users: + if u.get('email') == email: + return u + return None + + def upsert_user(self, user: Dict[str, Any]) -> None: + """插入或更新用户字典,并立即持久化。""" + data = self.load_users() + users = data.get('users', []) + found = False + for i, u in enumerate(users): + if u.get('email') == user.get('email'): + users[i] = user + found = True + break + if not found: + users.append(user) + self.save_users({'users': users}) + + def user_exists(self, email: str) -> bool: + """判断指定邮箱的用户是否存在。""" + return self.get_user(email) is not None \ No newline at end of file diff --git a/src/services/user_service.py b/src/services/user_service.py new file mode 100644 index 0000000..2006ff6 --- /dev/null +++ b/src/services/user_service.py @@ -0,0 +1,123 @@ +""" +用户服务模块:处理注册、验证码、设置密码、登录与修改密码逻辑。 +""" +import time +from typing import Any, Dict + +from ..utils.security_utils import ( + validate_email, + validate_password_strength, + generate_verification_code, + hash_password, + verify_password, +) +from .storage_service import StorageService +from .email_service import EmailService + + +class UserService: + """封装用户相关业务逻辑的服务类。""" + + def __init__(self, storage: StorageService, email_service: EmailService) -> None: + """初始化用户服务,注入存储与邮件服务。""" + self.storage = storage + self.email_service = email_service + + def request_registration(self, email: str) -> tuple[bool, str]: + """发起注册请求:校验邮箱,生成验证码并发送邮件。 + + 参数: + email: 用户邮箱。 + 返回: + (success, message) 二元组,表示结果与提示信息。 + """ + if not validate_email(email): + return False, '邮箱格式不正确' + existing = self.storage.get_user(email) + if existing and existing.get('verified'): + return False, '该邮箱已注册' + code = generate_verification_code() + salt, code_hash = hash_password(code) + user = existing or {'email': email} + user.update({ + 'verified': False, + 'code_salt': salt.hex(), + 'code_hash': code_hash.hex(), + 'code_time': int(time.time()), + 'password_salt': '', + 'password_hash': '', + 'created_at': user.get('created_at') or int(time.time()) + }) + self.storage.upsert_user(user) + sent = self.email_service.send_verification_code(email, code) + if not sent: + return False, '验证码发送失败,请检查SMTP设置' + return True, '验证码已发送,请查收邮箱' + + def verify_code(self, email: str, code: str) -> tuple[bool, str]: + """校验验证码并标记邮箱已验证。 + + 参数: + email: 用户邮箱。 + code: 用户输入的验证码。 + 返回: + (success, message)。 + """ + user = self.storage.get_user(email) + if not user: + return False, '用户不存在,请先注册' + # 10 分钟有效期 + if int(time.time()) - int(user.get('code_time', 0)) > 600: + return False, '验证码已过期,请重新获取' + salt_hex = user.get('code_salt', '') + hash_hex = user.get('code_hash', '') + if not salt_hex or not hash_hex: + return False, '未找到验证码信息' + salt = bytes.fromhex(salt_hex) + hash_bytes = bytes.fromhex(hash_hex) + if not verify_password(code, salt, hash_bytes): + return False, '验证码不正确' + user['verified'] = True + # 清除验证码信息 + user['code_salt'] = '' + user['code_hash'] = '' + self.storage.upsert_user(user) + return True, '邮箱验证成功,请设置密码' + + def set_password(self, email: str, password: str, confirm: str) -> tuple[bool, str]: + """设置或重置密码:校验强度、确认一致并持久化哈希。""" + if password != confirm: + return False, '两次输入的密码不一致' + if not validate_password_strength(password): + return False, '密码需为6-10位且包含大小写字母与数字' + user = self.storage.get_user(email) + if not user or not user.get('verified'): + return False, '邮箱未验证或用户不存在' + salt, pwd_hash = hash_password(password) + user['password_salt'] = salt.hex() + user['password_hash'] = pwd_hash.hex() + self.storage.upsert_user(user) + return True, '密码设置成功' + + def login(self, email: str, password: str) -> tuple[bool, str]: + """登录:校验邮箱存在与密码匹配。""" + user = self.storage.get_user(email) + if not user or not user.get('password_hash'): + return False, '用户不存在或未设置密码' + salt_hex = user.get('password_salt', '') + hash_hex = user.get('password_hash', '') + ok = verify_password(password, bytes.fromhex(salt_hex), bytes.fromhex(hash_hex)) + if not ok: + return False, '密码不正确' + return True, '登录成功' + + def change_password(self, email: str, old_password: str, new_password: str, confirm: str) -> tuple[bool, str]: + """修改密码:校验原密码正确并设置新密码。""" + user = self.storage.get_user(email) + if not user: + return False, '用户不存在' + salt_hex = user.get('password_salt', '') + hash_hex = user.get('password_hash', '') + if not verify_password(old_password, bytes.fromhex(salt_hex), bytes.fromhex(hash_hex)): + return False, '原密码不正确' + return self.set_password(email, new_password, confirm) \ No newline at end of file diff --git a/src/storage/config.json b/src/storage/config.json new file mode 100644 index 0000000..e358e09 --- /dev/null +++ b/src/storage/config.json @@ -0,0 +1,11 @@ +{ + "smtp": { + "server": "", + "port": 587, + "username": "", + "password": "", + "use_tls": true, + "use_ssl": false, + "sender_name": "Math Study App" + } +} \ No newline at end of file diff --git a/src/storage/users.json b/src/storage/users.json new file mode 100644 index 0000000..4ef6c8c --- /dev/null +++ b/src/storage/users.json @@ -0,0 +1,3 @@ +{ + "users": [] +} \ No newline at end of file diff --git a/src/ui/__init__.py b/src/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/ui/__pycache__/__init__.cpython-311.pyc b/src/ui/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..a868991 Binary files /dev/null and b/src/ui/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/ui/__pycache__/main_window.cpython-311.pyc b/src/ui/__pycache__/main_window.cpython-311.pyc new file mode 100644 index 0000000..3d0fd81 Binary files /dev/null and b/src/ui/__pycache__/main_window.cpython-311.pyc differ diff --git a/src/ui/main_window.py b/src/ui/main_window.py new file mode 100644 index 0000000..1888ab6 --- /dev/null +++ b/src/ui/main_window.py @@ -0,0 +1,141 @@ +from PyQt6.QtWidgets import ( + QMainWindow, QWidget, QStackedWidget, QVBoxLayout, QHBoxLayout, + QLabel, QLineEdit, QPushButton, QMessageBox, QCheckBox +) +from PyQt6.QtCore import Qt + +from src.services.storage_service import StorageService +from src.services.email_service import EmailService +from src.services.user_service import UserService +from src.services.question_service import QuestionService + + +class MainWindow(QMainWindow): + """应用主窗口:组织各个页面并承载业务服务。""" + + def __init__(self) -> None: + """初始化主窗口并构建基础页面。""" + super().__init__() + self.setWindowTitle("数学学习软件") + self.setFixedSize(900, 600) + + # 业务服务初始化 + self.storage_service = StorageService() + self.email_service = EmailService(self.storage_service) + self.user_service = UserService(self.storage_service, self.email_service) + self.question_service = QuestionService() + + # 页面容器 + self.stack = QStackedWidget() + self.setCentralWidget(self.stack) + + # 先构建最基础的两个页面:登录与SMTP设置 + self._build_login_page() + self._build_smtp_page() + + # 默认显示登录页 + self.stack.setCurrentIndex(0) + + def _build_login_page(self) -> None: + """构建登录页面:邮箱+密码+登录按钮。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("登录") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.login_email = QLineEdit() + self.login_email.setPlaceholderText("请输入邮箱") + layout.addWidget(self.login_email) + + self.login_password = QLineEdit() + self.login_password.setPlaceholderText("请输入密码") + self.login_password.setEchoMode(QLineEdit.EchoMode.Password) + layout.addWidget(self.login_password) + + btn_row = QHBoxLayout() + self.btn_login = QPushButton("登录") + self.btn_to_smtp = QPushButton("SMTP设置") + btn_row.addWidget(self.btn_login) + btn_row.addWidget(self.btn_to_smtp) + layout.addLayout(btn_row) + + self.btn_login.clicked.connect(self._on_login) + self.btn_to_smtp.clicked.connect(lambda: self.stack.setCurrentIndex(1)) + + self.stack.addWidget(page) + + def _on_login(self) -> None: + """处理登录逻辑:校验输入并调用用户服务。""" + email = (self.login_email.text() or "").strip() + password = self.login_password.text() or "" + if not email or not password: + QMessageBox.warning(self, "提示", "邮箱与密码均不能为空") + return + ok, msg = self.user_service.login(email, password) + if ok: + QMessageBox.information(self, "提示", "登录成功") + else: + QMessageBox.warning(self, "提示", msg or "登录失败") + + def _build_smtp_page(self) -> None: + """构建 SMTP 设置页面:服务器、端口、账号、授权码、TLS/SSL、发件人名。""" + page = QWidget() + layout = QVBoxLayout(page) + layout.setAlignment(Qt.AlignmentFlag.AlignTop) + + title = QLabel("SMTP 设置") + title.setAlignment(Qt.AlignmentFlag.AlignCenter) + title.setStyleSheet("font-size: 22px; font-weight: bold; margin: 16px 0;") + layout.addWidget(title) + + self.smtp_server = QLineEdit(); self.smtp_server.setPlaceholderText("服务器,例如 smtp.qq.com") + self.smtp_port = QLineEdit(); self.smtp_port.setPlaceholderText("端口,通常 465 或 587") + self.smtp_user = QLineEdit(); self.smtp_user.setPlaceholderText("邮箱账号,例如 123456@qq.com") + self.smtp_pass = QLineEdit(); self.smtp_pass.setPlaceholderText("邮箱授权码/应用密码") + self.smtp_pass.setEchoMode(QLineEdit.EchoMode.Password) + self.smtp_tls = QCheckBox("启用 TLS") + self.smtp_ssl = QCheckBox("启用 SSL") + self.smtp_sender = QLineEdit(); self.smtp_sender.setPlaceholderText("发件人显示名称,可选") + + layout.addWidget(self.smtp_server) + layout.addWidget(self.smtp_port) + layout.addWidget(self.smtp_user) + layout.addWidget(self.smtp_pass) + layout.addWidget(self.smtp_tls) + layout.addWidget(self.smtp_ssl) + layout.addWidget(self.smtp_sender) + + btn_row = QHBoxLayout() + self.btn_save_smtp = QPushButton("保存设置") + self.btn_back_login = QPushButton("返回登录") + btn_row.addWidget(self.btn_save_smtp) + btn_row.addWidget(self.btn_back_login) + layout.addLayout(btn_row) + + self.btn_save_smtp.clicked.connect(self._on_save_smtp) + self.btn_back_login.clicked.connect(lambda: self.stack.setCurrentIndex(0)) + + self.stack.addWidget(page) + + def _on_save_smtp(self) -> None: + """保存 SMTP 配置到本地 JSON,以支持验证码发送。""" + try: + port_val = int(self.smtp_port.text().strip() or '587') + except ValueError: + QMessageBox.warning(self, '提示', '端口必须为数字') + return + config = { + 'server': self.smtp_server.text().strip(), + 'port': port_val, + 'username': self.smtp_user.text().strip(), + 'password': self.smtp_pass.text(), + 'use_tls': self.smtp_tls.isChecked(), + 'use_ssl': self.smtp_ssl.isChecked(), + 'sender_name': self.smtp_sender.text().strip() or 'Math Study App', + } + self.email_service.update_smtp_config(config) + QMessageBox.information(self, '提示', 'SMTP设置已保存') \ No newline at end of file diff --git a/src/utils/__init__.py b/src/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/__pycache__/__init__.cpython-311.pyc b/src/utils/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 0000000..98fa030 Binary files /dev/null and b/src/utils/__pycache__/__init__.cpython-311.pyc differ diff --git a/src/utils/__pycache__/security_utils.cpython-311.pyc b/src/utils/__pycache__/security_utils.cpython-311.pyc new file mode 100644 index 0000000..b6e3f39 Binary files /dev/null and b/src/utils/__pycache__/security_utils.cpython-311.pyc differ diff --git a/src/utils/security_utils.py b/src/utils/security_utils.py new file mode 100644 index 0000000..4e6dc0b --- /dev/null +++ b/src/utils/security_utils.py @@ -0,0 +1,79 @@ +""" +安全工具模块:提供邮箱校验、密码强度校验、验证码生成、密码哈希与校验。 +遵循 Google Python 风格指南,所有函数均提供中文函数级注释。 +""" +import re +import secrets +import string +import hashlib +from typing import Tuple + + +def validate_email(email: str) -> bool: + """验证邮箱格式是否正确。 + + 参数: + email: 待验证的邮箱字符串。 + 返回: + True 表示格式合法,False 表示格式不合法。 + """ + pattern = r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$" + return re.match(pattern, email) is not None + + +def validate_password_strength(password: str) -> bool: + """验证密码是否满足强度要求:6-10位,必须包含大小写字母和数字。 + + 参数: + password: 待验证的密码字符串。 + 返回: + True 表示满足要求,False 表示不满足。 + """ + if not (6 <= len(password) <= 10): + return False + has_upper = any(c.isupper() for c in password) + has_lower = any(c.islower() for c in password) + has_digit = any(c.isdigit() for c in password) + return has_upper and has_lower and has_digit + + +def generate_verification_code(length: int = 6) -> str: + """生成数字验证码字符串。 + + 参数: + length: 验证码长度,默认为 6。 + 返回: + 由数字组成的验证码字符串。 + """ + return ''.join(secrets.choice(string.digits) for _ in range(length)) + + +def hash_password(password: str, salt: bytes | None = None) -> Tuple[bytes, bytes]: + """对密码进行 PBKDF2 哈希。 + + 参数: + password: 原始密码。 + salt: 可选的盐值;若未提供则自动生成。 + 返回: + (salt, pwd_hash) 二元组,其中 salt 为随机盐,pwd_hash 为哈希值。 + """ + if salt is None: + salt = secrets.token_bytes(16) + pwd_hash = hashlib.pbkdf2_hmac( + 'sha256', password.encode('utf-8'), salt, 100_000 + ) + return salt, pwd_hash + + +def verify_password(password: str, salt: bytes, pwd_hash: bytes) -> bool: + """校验密码是否与存储的哈希匹配。 + + 参数: + password: 用户输入的密码。 + salt: 存储的盐值。 + pwd_hash: 存储的密码哈希。 + 返回: + True 表示匹配,False 表示不匹配。 + """ + calc_hash = hashlib.pbkdf2_hmac('sha256', password.encode('utf-8'), salt, 100_000) + return secrets.compare_digest(calc_hash, pwd_hash) \ No newline at end of file diff --git a/~$工程导论-结对编程项目需求-2025.docx b/~$工程导论-结对编程项目需求-2025.docx new file mode 100644 index 0000000..4718cf0 Binary files /dev/null and b/~$工程导论-结对编程项目需求-2025.docx differ diff --git a/软件工程导论-结对编程项目需求-2025.docx b/软件工程导论-结对编程项目需求-2025.docx new file mode 100644 index 0000000..8cf9001 Binary files /dev/null and b/软件工程导论-结对编程项目需求-2025.docx differ