syy_develop #1

Merged
hnu202326010322 merged 11 commits from songyangyang_branch into develop 4 months ago

@ -0,0 +1,53 @@
import json
import os
from pathlib import Path
class DataHandler:
def __init__(self):
self.base_dir = Path("math_learning_data")
self.users_file = self.base_dir / "users.json"
self.ensure_data_dir()
def ensure_data_dir(self):
"""确保数据目录和文件存在并且文件内容是有效的JSON"""
if not self.base_dir.exists():
self.base_dir.mkdir(parents=True)
if not self.users_file.exists():
with open(self.users_file, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False) # 写入空对象
else:
# 如果文件存在但为空,写入空对象
if os.path.getsize(self.users_file) == 0:
with open(self.users_file, "w", encoding="utf-8") as f:
json.dump({}, f, ensure_ascii=False)
def load_users(self):
"""加载所有用户数据"""
with open(self.users_file, "r", encoding="utf-8") as f:
try:
return json.load(f)
except json.JSONDecodeError:
# 如果文件内容损坏,返回空字典
return {}
def save_users(self, users_data):
"""保存用户数据"""
with open(self.users_file, "w", encoding="utf-8") as f:
json.dump(users_data, f, ensure_ascii=False, indent=2)
def get_user(self, email):
"""获取用户信息"""
users = self.load_users()
return users.get(email)
def add_user(self, email, user_data):
"""添加新用户"""
users = self.load_users()
users[email] = user_data
self.save_users(users)
def update_user(self, email, user_data):
"""更新用户信息"""
self.add_user(email, user_data) # 直接覆盖更新

@ -0,0 +1,167 @@
import random
import time
import smtplib
import logging
from email.mime.text import MIMEText
from email.header import Header
from email.utils import formataddr
from tkinter import messagebox
from typing import Dict
from smtplib import SMTPResponseException
# 配置日志
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class EmailService:
def __init__(self, smtp_server: str, smtp_port: int, sender_email: str, sender_password: str):
self.smtp_server = smtp_server
self.smtp_port = smtp_port
self.sender_email = sender_email
self.sender_password = sender_password
self.verification_codes: Dict[str, tuple[str, float]] = {} # 邮箱: (验证码, 过期时间)
self.code_validity = 60 # 验证码有效期1分钟
self.last_send_time: Dict[str, float] = {} # 记录上次发送时间
self.send_interval = 60 # 发送间隔60秒
def generate_code(self) -> str:
"""生成6位数字注册码"""
return str(random.randint(100000, 999999))
def can_send_code(self, email: str) -> bool:
"""检查是否可以发送注册码(防止频繁发送)"""
if email in self.last_send_time:
elapsed = time.time() - self.last_send_time[email]
return elapsed >= self.send_interval
return True
def send_verification_code(self, email: str, username: str = "用户") -> bool:
"""发送验证码到用户邮箱。
Args:
email: 目标邮箱地址
username: 用户昵称用于邮件正文
Returns:
成功返回 True失败返回 False
"""
if not self.can_send_code(email):
logger.warning("发送过于频繁,邮箱: %s", email)
return False
code = self.generate_code()
now = time.time()
self.verification_codes[email] = (code, now + self.code_validity)
self.last_send_time[email] = now
subject = "数学学习软件 - 注册验证码"
formatted_email = self._format_email(email)
body = self._build_email_body(username, code)
try:
self._send_email(email, subject, body)
logger.info("注册码已发送到 %s", email)
return True
except smtplib.SMTPAuthenticationError as e:
logger.error("邮箱认证失败: %s", e)
self._show_warning("邮箱认证失败", "请检查邮箱账号和授权码是否正确")
return False
except smtplib.SMTPConnectError as e:
logger.error("无法连接到SMTP服务器: %s", e)
self._show_warning("连接失败", "无法连接到邮件服务器,请检查网络连接")
return False
except Exception as e:
logger.error("邮件发送失败: %s", e, exc_info=True)
self._show_warning("发送失败", f"邮件发送失败: {e}")
return False
def _format_email(self, email: str) -> str:
"""隐藏部分邮箱字符,保护隐私。"""
try:
at_index = email.index('@')
if at_index >= 4:
return email[:3] + '...' + email[at_index - 4:at_index]
return email
except ValueError:
return email
def _build_email_body(self, username: str, code: str) -> str:
"""构建HTML邮件正文。"""
return f"""
<html>
<body style="font-family: 'Microsoft YaHei', Arial, sans-serif;
line-height: 1.6; color: #333;">
<div style="max-width: 600px; margin: 0 auto; padding: 20px;
border: 1px solid #e0e0e0; border-radius: 8px;">
<div style="text-align: center;
border-bottom: 2px solid #4a90e2;
padding-bottom: 15px; margin-bottom: 20px;">
<h1 style="color: #4a90e2; margin: 0;">数学学习软件</h1>
<p style="color: #666; margin: 5px 0 0 0;">注册验证码</p>
</div>
<div style="background-color: #f8f9fa; padding: 20px;
border-radius: 5px; margin: 20px 0;">
<p>亲爱的 <strong>{username}</strong>您好</p>
<p>您正在注册数学学习软件请使用以下注册码完成注册</p>
<div style="text-align: center; margin: 25px 0;">
<span style="font-size: 32px; font-weight: bold; color: #e74c3c;
letter-spacing: 5px; background: #f8f9fa;
padding: 10px 20px; border: 2px dashed #e74c3c;
border-radius: 5px; display: inline-block;">
{code}
</span>
</div>
<p style="color: #e74c3c; font-weight: bold;">
注册码有效期1分钟请尽快完成注册
</p>
</div>
<div style="color: #999; font-size: 12px; text-align: center;
margin-top: 30px; padding-top: 15px;
border-top: 1px solid #e0e0e0;">
<p>如果这不是您的操作请忽略此邮件</p>
<p>此为系统邮件请勿回复</p>
<p style="font-weight: bold;">数学学习软件团队</p>
</div>
</div>
</body>
</html>
"""
def _send_email(self, to_email: str, subject: str, body: str) -> None:
"""发送邮件。"""
msg = MIMEText(body, "html", "utf-8")
msg["Subject"] = Header(subject, "utf-8")
msg["From"] = formataddr(("数学学习软件", self.sender_email))
msg["To"] = to_email
if self.smtp_port == 587:
server = smtplib.SMTP(self.smtp_server, self.smtp_port, timeout=10)
server.starttls()
else:
server = smtplib.SMTP_SSL(self.smtp_server, self.smtp_port, timeout=10)
server.set_debuglevel(1)
server.login(self.sender_email, self.sender_password)
server.sendmail(self.sender_email, to_email, msg.as_string())
server.quit()
def _show_warning(self, title: str, message: str) -> None:
"""弹出警告框。"""
messagebox.warning(None, title, message)
def verify_code(self, email: str, code: str) -> bool:
"""验证注册码是否有效"""
if email not in self.verification_codes:
return False
stored_code, expire_time = self.verification_codes[email]
if time.time() > expire_time:
del self.verification_codes[email]
return False
return stored_code == code

@ -0,0 +1,276 @@
# -*- coding: utf-8 -*-
"""数学学习软件 - 题目生成与试卷管理模块
提供小学初中高中三个学段的数学选择题自动生成查重保存功能
"""
from __future__ import annotations
import json
import math
import os
import random
from datetime import datetime
from typing import List, Tuple
# 模块级常量
PRIMARY_OPS = ("+", "-", "*", "/")
MIDDLE_OPS = ("+", "-", "*", "/", "^2", "sqrt")
HIGH_OPS = ("+", "-", "*", "/", "sin", "cos", "tan")
PRIMARY_RANGE = (1, 50)
MIDDLE_RANGE = (1, 100)
HIGH_ANGLE_RANGE = (0, 90) # 角度制
MAX_OPTS = 4
MAX_TRIES = 100
PAPERS_DIR = "generated_papers"
class Question:
"""单道选择题."""
__slots__ = ("content", "options", "answer")
def __init__(self, content: str, options: List[str], answer: str):
"""初始化.
Args:
content: 题干含表达式
options: 四个选项文本
answer: 正确答案文本
"""
self.content = content
self.options = options
self.answer = answer
class QuestionBank:
"""题库:按学段生成不重复的选择题,并保存为文本试卷."""
def __init__(self) -> None:
"""初始化."""
self.operators = {
"primary": PRIMARY_OPS,
"middle": MIDDLE_OPS,
"high": HIGH_OPS,
}
self.generated_questions: List[Question] = []
self.papers_dir = PAPERS_DIR
os.makedirs(self.papers_dir, exist_ok=True)
# ------------------------------------------------------------------
# 公共接口
# ------------------------------------------------------------------
def generate_question(self, level: str) -> Question:
for attempt in range(MAX_TRIES):
try:
expr, value = self._make_expression(level)
if not self._is_valid_value(value) or expr in [q.content for q in self.generated_questions]:
continue
opts, ans = self._make_options(value, level)
return Question(f"计算:{expr}", opts, ans)
except Exception as e:
print(f"[WARNING] 题目生成失败,尝试 {attempt + 1}/{MAX_TRIES},错误:{e}")
continue
# 兜底题目
print("[WARNING] 使用兜底题目")
expr, value = self._fallback_expression(level)
opts, ans = self._make_options(value, level)
return Question(f"计算:{expr}", opts, ans)
def generate_paper(self, level: str, count: int, username: str = "unknown") -> List[Question]:
"""生成整张试卷并落盘.
Args:
level: 学段
count: 题目数量
username: 用户昵称
Returns:
题目列表
"""
self.generated_questions.clear()
questions = [self.generate_question(level) for _ in range(count)]
self._save_paper(questions, level, username)
return questions
def calculate_score(self, answers: List[bool]) -> int:
"""计算百分制得分.
Args:
answers: 每题是否正确
Returns:
0~100
"""
if not answers:
return 0
return round(sum(answers) / len(answers) * 100)
# ------------------------------------------------------------------
# 私有辅助
# ------------------------------------------------------------------
def _make_expression(self, level: str) -> Tuple[str, float]:
"""返回表达式字符串与数值结果."""
if level == "primary":
return self._primary_expr()
if level == "middle":
return self._middle_expr()
return self._high_expr()
def _primary_expr(self) -> Tuple[str, float]:
"""小学表达式:整数结果."""
nums = [random.randint(*PRIMARY_RANGE) for _ in range(random.randint(2, 3))]
ops = list(random.choices(PRIMARY_OPS, k=len(nums) - 1))
self._ensure_int_div_sub(nums, ops)
parts = [str(nums[0])]
for o, n in zip(ops, nums[1:]):
parts.extend([o, str(n)])
expr = self._add_parentheses(parts)
val = self._safe_int_eval(expr)
if val is not None:
return expr, val
a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2))
op = random.choice(["+", "-", "*"])
expr = f"{a} {op} {b}"
return expr, eval(expr)
def _middle_expr(self) -> Tuple[str, float]:
"""初中表达式:含平方或开方."""
base = random.randint(*MIDDLE_RANGE)
if random.choice([True, False]):
inner = f"{base}^2"
val = base ** 2
else:
inner = f"sqrt({base})"
val = math.sqrt(base)
if random.choice([True, False]):
n2 = random.randint(*MIDDLE_RANGE)
op = random.choice(PRIMARY_OPS)
inner = f"({inner} {op} {n2})"
val = eval(f"{val} {op} {n2}")
return inner, val
def _high_expr(self) -> Tuple[str, float]:
"""高中表达式:含三角函数."""
angle = random.randint(*HIGH_ANGLE_RANGE)
func = random.choice(["sin", "cos", "tan"])
expr = f"{func}({angle})"
val = getattr(math, func)(math.radians(angle))
if random.choice([True, False]):
n2 = random.randint(1, 90)
op = random.choice(PRIMARY_OPS)
expr = f"({expr} {op} {n2})"
val = eval(f"{val} {op} {n2}")
return expr, val
def _fallback_expression(self, level: str) -> Tuple[str, float]:
"""兜底简单表达式."""
if level == "primary":
a, b = sorted(random.randint(*PRIMARY_RANGE) for _ in range(2))
expr = f"{a} + {b}"
return expr, eval(expr)
if level == "middle":
n = random.randint(1, 10)
expr = f"{n}^2"
return expr, n ** 2
angle = random.randint(1, 89)
expr = f"sin({angle})"
return expr, math.sin(math.radians(angle))
# -- 工具 -----------------------------------------------------------
@staticmethod
def _ensure_int_div_sub(nums: List[int], ops: List[str]) -> None:
"""调整 nums/ops 保证整数结果."""
for i, op in enumerate(ops):
if op == "/":
nums[i] *= nums[i + 1]
elif op == "-" and nums[i] < nums[i + 1]:
nums[i], nums[i + 1] = nums[i + 1], nums[i]
@staticmethod
def _add_parentheses(parts: List[str]) -> str:
"""随机给表达式加括号."""
if len(parts) >= 5 and random.choice([True, False]):
start = random.randint(0, len(parts) - 4)
if start % 2 == 0:
parts.insert(start, "(")
parts.insert(start + 4, ")")
return " ".join(parts)
@staticmethod
def _safe_int_eval(expr: str) -> float | None:
"""安全计算并返回整数结果."""
try:
val = eval(expr)
if abs(val - round(val)) < 1e-4:
return round(val)
except Exception:
pass
return None
@staticmethod
def _is_valid_value(val: float) -> bool:
"""检查数值是否合法."""
return not (math.isnan(val) or math.isinf(val) or abs(val) > 1e10)
# -- 选项 & 保存 ------------------------------------------------------
def _make_options(self, correct: float, level: str) -> Tuple[List[str], str]:
"""生成四个选项1正确+3干扰"""
if level == "primary":
correct_val = int(round(correct))
else:
correct_val = round(correct, 2)
opts = [correct_val]
while len(opts) < 4:
distractor = self._make_distractor(opts, correct_val, level)
if distractor not in opts:
opts.append(distractor)
random.shuffle(opts)
# ✅ 确保答案是从 opts 中获取的,而不是原始浮点数
ans = str(opts[opts.index(correct_val)])
return [str(o) for o in opts], ans
def _make_distractor(self, existing: List[float], correct: float, level: str) -> float:
"""生成一个不重复的干扰项."""
while True:
if level == "primary":
d = int(correct) + random.randint(-5, 5)
if d != correct and d > 0:
return d
else:
delta = abs(correct) * 0.3 if correct else 1
d = round(correct + random.uniform(-delta, delta), 2)
if d not in existing:
return d
def _save_paper(self, questions: List[Question], level: str, username: str) -> None:
"""试卷落盘."""
level_name = {"primary": "小学", "middle": "初中", "high": "高中"}.get(level, level)
timestamp = datetime.now().strftime("%Y-%m-%d-%H-%M-%S")
path = os.path.join(self.papers_dir, f"{timestamp}.txt")
with open(path, "w", encoding="utf-8") as f:
f.write(f"数学学习软件 - {level_name}数学试卷\n")
f.write(f"生成时间:{datetime.now()}\n")
f.write("=" * 50 + "\n\n")
for idx, q in enumerate(questions, 1):
f.write(f"{idx}题:{q.content}\n选项:\n")
for i, opt in enumerate(q.options):
f.write(f" {chr(65+i)}. {opt}\n")
f.write(f"正确答案:{q.answer}\n\n")
f.write("=" * 50 + "\n")
f.write(f"{len(questions)}\n")
print(f"试卷已保存到:{path}")

@ -0,0 +1,163 @@
import re
import json
import os
import sys
from typing import Dict, Optional
from .email_service import EmailService
# 获取exe所在目录的绝对路径
if getattr(sys, 'frozen', False):
# 如果是打包后的exe
BASE_DIR = os.path.dirname(sys.executable)
else:
# 如果是开发环境
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
USER_DATA_FILE = os.path.join(BASE_DIR, "data", "users.json")
class UserSystem:
def __init__(self, email_service: EmailService):
self.email_service = email_service
# 确保data目录存在
os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True)
self.users: Dict[str, dict] = self.load_users()
self.current_user: Optional[str] = None
self.current_level: Optional[str] = None
def load_users(self) -> Dict[str, dict]:
"""从文件加载用户数据"""
print(f"尝试加载用户文件: {USER_DATA_FILE}") # 调试信息
print(f"文件是否存在: {os.path.exists(USER_DATA_FILE)}") # 调试信息
if os.path.exists(USER_DATA_FILE):
try:
with open(USER_DATA_FILE, "r", encoding="utf-8") as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"加载用户文件失败: {e}") # 调试信息
return {}
else:
print("用户文件不存在,创建空用户字典") # 调试信息
return {}
def save_users(self):
"""保存用户数据到文件"""
print(f"保存用户数据到: {USER_DATA_FILE}") # 调试信息
# 确保目录存在
os.makedirs(os.path.dirname(USER_DATA_FILE), exist_ok=True)
try:
with open(USER_DATA_FILE, "w", encoding="utf-8") as f:
json.dump(self.users, f, ensure_ascii=False, indent=4)
print("用户数据保存成功") # 调试信息
except Exception as e:
print(f"保存用户数据失败: {e}") # 调试信息
# 其他方法保持不变...
def is_valid_email(self, email: str) -> bool:
"""验证邮箱格式"""
pattern = r"^[\w\.-]+@[\w\.-]+\.\w+$"
return re.match(pattern, email) is not None
def is_valid_password(self, password: str) -> bool:
"""密码要求6-10位包含大小写字母和数字"""
if len(password) < 6 or 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 is_valid_username(self, username: str) -> bool:
"""用户名要求2-10位中文、字母或数字"""
if len(username) < 2 or len(username) > 10:
return False
pattern = r"^[\u4e00-\u9fa5a-zA-Z0-9]+$"
return re.match(pattern, username) is not None
def is_username_exists(self, username: str) -> bool:
"""检查用户名是否已存在"""
return username in self.users
def send_verification(self, email: str, username: str = "用户") -> bool:
"""发送验证码"""
# 检查邮箱是否已被其他用户使用
for user_data in self.users.values():
if user_data.get("email") == email:
return False
return self.email_service.send_verification_code(email, username)
def register(self, username: str, email: str, code: str, password: str) -> bool:
"""注册新用户"""
if self.is_username_exists(username):
return False
if not self.is_valid_password(password):
return False
if not self.is_valid_username(username):
return False
if not self.is_valid_email(email):
return False
# 检查邮箱是否已被使用
for user_data in self.users.values():
if user_data.get("email") == email:
return False
if not self.email_service.verify_code(email, code):
return False
self.users[username] = {
"password": password,
"email": email,
"level": None
}
self.save_users()
return True
def login(self, username: str, password: str) -> bool:
"""登录验证 - 使用用户名登录"""
user = self.users.get(username)
if user and user["password"] == password:
self.current_user = username
self.current_level = user.get("level")
return True
return False
def change_password(self, old_password: str, new_password: str) -> bool:
"""修改当前登录用户密码"""
if not self.current_user:
return False
user = self.users.get(self.current_user)
if not user or user["password"] != old_password:
return False
if not self.is_valid_password(new_password):
return False
user["password"] = new_password
self.save_users()
return True
def set_username(self, new_username: str) -> bool:
"""修改当前用户名"""
if not self.current_user:
return False
if not self.is_valid_username(new_username):
return False
if self.is_username_exists(new_username):
return False
# 更新用户名
user_data = self.users.pop(self.current_user)
self.users[new_username] = user_data
self.current_user = new_username
self.save_users()
return True
def set_level(self, level: str):
"""设置当前用户学段"""
if self.current_user:
self.users[self.current_user]["level"] = level
self.current_level = level
self.save_users()
def get_user_email(self, username: str) -> Optional[str]:
"""获取用户的邮箱"""
user = self.users.get(username)
return user.get("email") if user else None
Loading…
Cancel
Save