Compare commits

..

2 Commits

Author SHA1 Message Date
HAPE f1ee857660 说明文档更新
4 months ago
HAPE 4b5aa177bb 邮箱发送验证码功能
4 months ago

@ -0,0 +1,7 @@
# double_program
程序主体使用张勇进的个人项目修改
### 发送验证码邮件功能模块
使用126邮箱发送

@ -1,6 +0,0 @@
**结对编程项目**
操作系统: Windows11 22H4
编码语言: python3.8
中文编码格式: UTF-8
个人项目参考: 张勇进的个人项目

@ -1,8 +0,0 @@
{
"python.linting.flake8Enabled": true,
"python.formatting.provider": "yapf",
"python.linting.flake8Args": ["--max-line-length=248"],
"python.linting.pylintEnabled": false,
"python-envs.defaultEnvManager": "ms-python.python:system",
"python-envs.pythonProjects": []
}

@ -0,0 +1,58 @@
import smtplib
import random
import re
from email.mime.text import MIMEText
def is_valid_email(email):
# 简单邮箱格式校验
return re.match(r"[^@]+@[^@]+\.[^@]+", email)
def send_verification_code(email, code):
smtp_server = 'smtp.126.com'
smtp_port = 25
sender_email = 'hape233@126.com'
sender_password = 'ZKcbV37TdRwgnsZC'
msg = MIMEText(f'您的验证码是:{code}')
msg['Subject'] = '登录验证码'
msg['From'] = sender_email
msg['To'] = email
try:
server = smtplib.SMTP(smtp_server, smtp_port, timeout=30)
server.starttls()
server.login(sender_email, sender_password)
server.sendmail(sender_email, [email], msg.as_string())
print("邮件发送成功")
return True
except Exception as e:
print(f"邮件发送失败:{e}")
return False
finally:
try:
server.quit()
except:
server.close()
def main():
email = input("请输入您的邮箱:")
if not is_valid_email(email):
print("邮箱格式不正确!")
return
code = str(random.randint(100000, 999999))
try:
send_verification_code(email, code)
print("验证码已发送,请查收邮箱。")
except Exception as e:
print("验证码发送失败:", e)
return
user_code = input("请输入收到的验证码:")
if user_code == code:
print("登录成功!")
else:
print("验证码错误,登录失败。")
if __name__ == "__main__":
main()

Binary file not shown.

@ -1,791 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
单文件桌面应用小初高数学练习Tkinter
主要修改按用户要求
- 每道题中出现的数字不超过 5 操作数数目为 1~5
- 小学题不出现负数操作数为正生成表达式尽量避免负结果
- 初中题题目显示使用根号符号 (...)幂显示为 x^2内部仍用 sqrt()/** 计算
- 高中题题目中不出现 pi 字符串三角函数以角度表示 sin(30°)内部计算将角度转为弧度
其它功能保留注册邮箱验证码用户名/邮箱登录修改密码题库自动生成与保存PBKDF2 密码哈希等
注意替换 SMTP 配置为可用的邮箱/授权码或改为测试模式
"""
import tkinter as tk
from tkinter import messagebox, simpledialog, scrolledtext
import json
import os
import secrets
import hashlib
import binascii
import random
import re
import math
import threading
from datetime import datetime, timedelta
import smtplib
from email.mime.text import MIMEText
# ---------------- 配置 ----------------
USERS_FILE = "users.json"
QUESTION_FILES = {
"小学": "questions_primary.json",
"初中": "questions_middle.json",
"高中": "questions_high.json",
}
LOG_FILE = "app_log.txt"
# SMTP 配置 —— 请替换为可用的 SMTP 服务账号/密码/端口
SMTP_SERVER = "smtp.126.com"
SMTP_PORT = 25
SENDER_EMAIL = "hape233@126.com" # <-- 请替换
SENDER_PASSWORD = "ZKcbV37TdRwgnsZC" # <-- 请替换或使用授权码
CODE_TTL_SECONDS = 300 # 验证码有效期 5 分钟
SEND_COOLDOWN = 60 # 发送按钮冷却 60 秒
MAX_QUESTIONS = 20 # 题目上限
DEFAULT_QUESTIONS = 3 # 默认题目数
WINDOW_W = 820
WINDOW_H = 620
# 题目生成参数
MIN_OPERANDS = 1
MAX_OPERANDS = 5 # 至多 5 个操作数(数字)
OPERAND_MIN = 1
OPERAND_MAX = 100
# --------------------------------------
# ----------------- 辅助与存储 -----------------
def log(msg: str):
with open(LOG_FILE, "a", encoding="utf-8") as f:
f.write(f"{datetime.utcnow().isoformat()} {msg}\n")
def ensure_files_exist():
if not os.path.exists(USERS_FILE):
with open(USERS_FILE, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=2)
for k, p in QUESTION_FILES.items():
if not os.path.exists(p):
with open(p, "w", encoding="utf-8") as f:
json.dump([], f, ensure_ascii=False, indent=2)
# User store
class UserStore:
def __init__(self, path=USERS_FILE):
self.path = path
self._load()
def _load(self):
try:
with open(self.path, "r", encoding="utf-8") as f:
self.users = json.load(f)
except Exception:
self.users = []
def _save(self):
with open(self.path, "w", encoding="utf-8") as f:
json.dump(self.users, f, ensure_ascii=False, indent=2)
def find_by_email(self, email: str):
for u in self.users:
if u.get("email") == email:
return u
return None
def find_by_username(self, username: str):
for u in self.users:
if u.get("username") == username:
return u
return None
def find(self, key: str):
u = self.find_by_email(key)
if u:
return u
return self.find_by_username(key)
def add_or_update(self, user: dict):
existing = self.find_by_email(user["email"])
if existing:
existing.update(user)
else:
self.users.append(user)
self._save()
# Question store
class QuestionStore:
def __init__(self, files_map=QUESTION_FILES):
self.files_map = files_map
def load(self, grade: str):
p = self.files_map.get(grade)
try:
with open(p, "r", encoding="utf-8") as f:
return json.load(f)
except Exception:
return []
def save(self, grade: str, qs):
p = self.files_map.get(grade)
with open(p, "w", encoding="utf-8") as f:
json.dump(qs, f, ensure_ascii=False, indent=2)
# ----------------- 验证码管理 & 密码哈希 -----------------
EMAIL_RE = re.compile(r"^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$")
def is_valid_email(e: str) -> bool:
return bool(EMAIL_RE.match(e or ""))
class VerificationManager:
def __init__(self):
self.store = {} # email -> (code, expiry)
def gen(self, email):
code = f"{random.randint(100000, 999999)}"
expiry = datetime.utcnow() + timedelta(seconds=CODE_TTL_SECONDS)
self.store[email] = (code, expiry)
return code
def verify(self, email, code):
rec = self.store.get(email)
if not rec:
return False, "未发送验证码"
real, expiry = rec
if datetime.utcnow() > expiry:
return False, "验证码已过期"
if not secrets.compare_digest(real, code):
return False, "验证码不匹配"
del self.store[email]
return True, "验证成功"
verif_mgr = VerificationManager()
def hash_password(password: str, salt: bytes = None):
if salt is None:
salt = secrets.token_bytes(16)
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, 100000)
return binascii.hexlify(dk).decode("utf-8"), binascii.hexlify(salt).decode("utf-8")
def verify_password_hash(stored_hash, stored_salt, attempt):
try:
salt = binascii.unhexlify(stored_salt)
at_hash, _ = hash_password(attempt, salt)
return secrets.compare_digest(at_hash, stored_hash)
except Exception:
return False
# ----------------- SMTP 发送 -----------------
def send_verification_email(target_email: str, code: str):
msg = MIMEText(f"您的注册码6位{code}\n有效期 {CODE_TTL_SECONDS//60} 分钟。")
msg['Subject'] = "注册码"
msg['From'] = SENDER_EMAIL
msg['To'] = target_email
try:
server = smtplib.SMTP(SMTP_SERVER, SMTP_PORT, timeout=30)
try:
server.starttls()
except Exception:
pass
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, [target_email], msg.as_string())
try:
server.quit()
except Exception:
server.close()
log(f"Sent code {code} to {target_email}")
return True, "发送成功"
except Exception as e:
try:
server.close()
except:
pass
log(f"Failed send code to {target_email}: {e}")
return False, str(e)
# ----------------- 题目生成相关辅助 -----------------
def gen_id(prefix: str) -> str:
return f"{prefix}_{secrets.token_hex(4)}"
def mk_options_from_number(correct_val, kind="int"):
opts = []
if kind == "int":
corr = int(round(correct_val))
opts = [str(corr)]
attempts = 0
while len(opts) < 4 and attempts < 200:
delta = random.choice([-10, -6, -4, -3, -2, -1, 1, 2, 3, 4, 5, 8, 10])
candidate = corr + delta
if candidate >= -9999 and str(candidate) not in opts:
opts.append(str(candidate))
attempts += 1
while len(opts) < 4:
opts.append(str(corr + random.randint(11, 50)))
random.shuffle(opts)
return opts, opts.index(str(corr))
else:
corr = float(correct_val)
corr_s = f"{corr:.3f}"
opts = [corr_s]
attempts = 0
while len(opts) < 4 and attempts < 200:
delta = random.choice([-0.8, -0.5, -0.3, -0.2, -0.1, 0.1, 0.2, 0.3, 0.5, 0.8])
cand = corr + delta
s = f"{cand:.3f}"
if s not in opts:
opts.append(s)
attempts += 1
while len(opts) < 4:
opts.append(f"{(corr + random.uniform(1.0,3.0)):.3f}")
random.shuffle(opts)
return opts, opts.index(corr_s)
# 受限 eval 环境用于计算(内部使用 math 的函数)
SAFE_EVAL_ENV = {"__builtins__": None, "sqrt": math.sqrt, "sin": math.sin, "cos": math.cos, "tan": math.tan, "pi": math.pi}
def safe_eval(expr: str):
# expr 为 Python 可 eval 的表达式(外层 display 与内部计算分开)
expr = expr.replace("^", "**")
return eval(expr, SAFE_EVAL_ENV, {})
# ----------------- 题目生成(符合新要求) -----------------
# 小学1~5 个操作数,操作数为正,尽量避免负数结果
def generate_primary_question():
# 生成 1~5 个正整数操作数
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
# 选择运算符n-1 个)
ops_list = []
for _ in range(max(0, n - 1)):
ops_list.append(random.choice(['+', '-', '*', '/']))
# 为避免出现负数结果,策略:
# - 对于减法,确保左边操作数大于或等于右边(在构建顺序时调整)
# - 对于除法,尽量设置可整除关系(将右边替换为因数)
# 构造表达式逐步调整
def build_expr(nums, ops):
parts = []
for i, a in enumerate(nums):
parts.append(str(a))
if i < len(ops):
parts.append(ops[i])
return "".join(parts)
# 调整减法与除法以尽量得到非负整数
for _ in range(100):
nums_copy = nums[:]
ops_copy = ops_list[:]
# adjust for division: make divisor divide dividend when possible
for i, op in enumerate(ops_copy):
if op == '/':
a = nums_copy[i]
b = nums_copy[i+1]
if b == 0:
nums_copy[i+1] = random.randint(OPERAND_MIN, OPERAND_MAX)
# try to set b as a divisor of a by replacing b
divisors = [d for d in range(1, OPERAND_MAX+1) if a % d == 0]
if divisors:
nums_copy[i+1] = random.choice(divisors)
else:
# fallback: change op to + to avoid fractions
ops_copy[i] = random.choice(['+', '-','*'])
# adjust for subtraction to avoid negatives: ensure left >= right when directly subtracting
for i, op in enumerate(ops_copy):
if op == '-':
if nums_copy[i] < nums_copy[i+1]:
# swap operands to attempt non-negative
nums_copy[i], nums_copy[i+1] = nums_copy[i+1], nums_copy[i]
expr = build_expr(nums_copy, ops_copy)
try:
val = safe_eval(expr)
# if float, round to int
if isinstance(val, float):
if abs(val - round(val)) < 1e-9:
val = int(round(val))
else:
val = int(round(val))
# ensure non-negative (小学要求)
if isinstance(val, (int, float)) and val >= 0:
nums = nums_copy
ops_list = ops_copy
break
except Exception:
# regenerate small changes and retry
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
continue
# final expression
expr_display = build_expr(nums, ops_list)
# Evaluate final value
try:
val = safe_eval(expr_display)
if isinstance(val, float):
if abs(val - round(val)) < 1e-9:
val = int(round(val))
else:
val = int(round(val))
except Exception:
val = 0
opts, idx = mk_options_from_number(val, kind="int")
# Ensure no negative options for primary (replace negatives)
opts = [o if not (o.startswith("-")) else str(abs(int(o))) for o in opts]
# ensure answer_index updated if changed due to replacement
if str(val) in opts:
answer_index = opts.index(str(val))
else:
# if val changed by making negative->positive, recompute index
answer_index = 0
for i,o in enumerate(opts):
if int(o) == int(val):
answer_index = i
break
return {"id": gen_id("pri"), "grade": "小学", "stem": f"{expr_display} = ?", "options": opts, "answer_index": answer_index}
# 初中1~5 个操作数,题目显示中用 √(...)表示根号,幂显示为 x^2
def generate_middle_question():
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
# ensure at least one sqrt or square
use_sqrt = random.choice([True, False])
idx = random.randint(0, n - 1)
expr_parts = []
# prepare a copy for computation (python expression)
comp_nums = [str(x) for x in nums]
if use_sqrt:
# prefer perfect square inside sometimes
inner_base = random.randint(1, 50)
inner = inner_base * inner_base if random.choice([True, False]) else nums[idx]*nums[idx]
comp_nums[idx] = f"sqrt({inner})"
display_num = f"√({inner})"
else:
comp_nums[idx] = f"({nums[idx]}**2)"
display_num = f"{nums[idx]}^2"
# build display expression and compute expression separately
display_parts = []
comp_parts = []
for i in range(n):
display_parts.append(display_num if i == idx else str(nums[i]))
comp_parts.append(comp_nums[i])
if i < len(ops_list):
display_parts.append(ops_list[i])
comp_parts.append(ops_list[i])
display_expr = "".join(display_parts)
comp_expr = "".join(comp_parts)
# evaluate
try:
val = safe_eval(comp_expr)
except Exception:
# fallback to simpler expression
comp_expr = "1+1"
display_expr = "1+1"
val = 2
# format options
if isinstance(val, float) and abs(val - round(val)) > 1e-9:
opts, idx_ans = mk_options_from_number(val, kind="float")
else:
opts, idx_ans = mk_options_from_number(val, kind="int")
return {"id": gen_id("mid"), "grade": "初中", "stem": f"{display_expr} = ?", "options": opts, "answer_index": idx_ans}
# 高中1~5 个操作数,三角函数以角度表示(如 sin(30°)),题目显示不含 pi
def generate_high_question():
func = random.choice(["sin", "cos", "tan"])
n = random.randint(MIN_OPERANDS, MAX_OPERANDS)
nums = [random.randint(OPERAND_MIN, OPERAND_MAX) for _ in range(n)]
ops_list = [random.choice(['+', '-', '*', '/']) for _ in range(max(0, n - 1))]
idx = random.randint(0, n - 1)
# choose angle in degrees (常见角优先)
angle = random.choice([0, 30, 45, 60, 90, 120, 135, 150, 180, 210, 225, 240, 270, 300, 315, 330])
# comp expression uses radians: func(pi*angle/180)
comp_nums = [str(x) for x in nums]
comp_nums[idx] = f"{func}(pi*{angle}/180)"
display_nums = [str(x) for x in nums]
display_nums[idx] = f"{func}({angle}°)" # show degrees, no pi
# build expressions
comp_parts = []
display_parts = []
for i in range(n):
comp_parts.append(comp_nums[i])
display_parts.append(display_nums[i])
if i < len(ops_list):
comp_parts.append(ops_list[i])
display_parts.append(ops_list[i])
comp_expr = "".join(comp_parts)
display_expr = "".join(display_parts)
# evaluate
try:
val = safe_eval(comp_expr)
except Exception:
# fallback
comp_expr = "0.0"
display_expr = f"{func}({angle}°)"
val = 0.0
# options as float (3 decimals)
opts, idx_ans = mk_options_from_number(val, kind="float")
return {"id": gen_id("high"), "grade": "高中", "stem": f"{display_expr} = ? (保留3位小数)", "options": opts, "answer_index": idx_ans}
# auto-generate to ensure enough questions
def ensure_questions_for_grade(grade: str, needed: int, qstore: QuestionStore):
qs = qstore.load(grade)
if len(qs) >= needed:
return qs
to_gen = needed - len(qs)
new = []
for _ in range(to_gen):
if grade == "小学":
new.append(generate_primary_question())
elif grade == "初中":
new.append(generate_middle_question())
else:
new.append(generate_high_question())
qs.extend(new)
qstore.save(grade, qs)
log(f"Auto-generated {len(new)} questions for {grade}")
return qs
# ----------------- 服务逻辑(注册/激活/登录/改密) -----------------
class AuthService:
def __init__(self, store: UserStore):
self.store = store
def send_code(self, email: str):
if not is_valid_email(email):
return False, "邮箱格式不正确"
u = self.store.find_by_email(email)
if u and u.get("activated"):
return False, "该邮箱已注册并激活"
code = verif_mgr.gen(email)
ok, msg = send_verification_email(email, code)
return ok, msg
def register_with_code(self, email: str, code: str, username: str, password: str):
if not is_valid_email(email):
return False, "邮箱格式不正确"
if not username:
return False, "用户名不能为空"
existing_user = self.store.find_by_username(username)
if existing_user and existing_user.get("email") != email:
return False, "用户名已被占用"
if not (6 <= len(password) <= 10 and any(c.isupper() for c in password) and any(c.islower() for c in password) and any(c.isdigit() for c in password)):
return False, "密码规则不满足6-10位含大小写字母和数字"
ok, msg = verif_mgr.verify(email, code)
if not ok:
return False, msg
ph, salt = hash_password(password)
rec = {
"email": email,
"username": username,
"password_hash": ph,
"salt": salt,
"activated": True,
"created_at": datetime.utcnow().isoformat()
}
self.store.add_or_update(rec)
log(f"user registered: {email} / {username}")
return True, "注册并设置密码成功"
def login(self, key: str, password: str):
u = self.store.find(key)
if not u:
return False, "未找到该用户"
if not u.get("activated"):
return False, "该账号未激活"
if not verify_password_hash(u.get("password_hash"), u.get("salt"), password):
return False, "密码错误"
return True, u
def change_password(self, email: str, old_pw: str, new_pw: str):
u = self.store.find_by_email(email)
if not u:
return False, "未找到用户"
if not verify_password_hash(u.get("password_hash"), u.get("salt"), old_pw):
return False, "原密码错误"
if not (6 <= len(new_pw) <= 10 and any(c.isupper() for c in new_pw) and any(c.islower() for c in new_pw) and any(c.isdigit() for c in new_pw)):
return False, "新密码不符合规则"
ph, salt = hash_password(new_pw)
u.update({"password_hash": ph, "salt": salt})
self.store._save()
log(f"user changed password: {email}")
return True, "修改成功"
# ----------------- GUI -----------------
class MathApp:
def __init__(self, root):
self.root = root
self.root.title("小初高数学练习")
self.root.geometry(f"{WINDOW_W}x{WINDOW_H}")
ensure_files_exist()
self.user_store = UserStore()
self.qstore = QuestionStore()
self.auth = AuthService(self.user_store)
self.current_user = None
self.current_exam = None
self.build_main()
def clear(self):
for w in self.root.winfo_children():
w.destroy()
def build_main(self):
self.clear()
frm = tk.Frame(self.root, padx=20, pady=20)
frm.pack(fill="both", expand=True)
tk.Label(frm, text="数学学习应用", font=("Arial", 18)).pack(pady=12)
tk.Button(frm, text="注册", width=40, command=self.ui_register_flow).pack(pady=6)
tk.Button(frm, text="登录", width=40, command=self.ui_login).pack(pady=6)
tk.Button(frm, text="退出", width=40, command=self.root.quit).pack(pady=6)
# 注册流程
def ui_register_flow(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text="邮箱(注册邮箱):").grid(row=0, column=0, sticky="e")
email_var = tk.StringVar()
email_entry = tk.Entry(fr, textvariable=email_var, width=36)
email_entry.grid(row=0, column=1, padx=6)
send_btn = tk.Button(fr, text="发送验证码")
send_btn.grid(row=0, column=2, padx=6)
tk.Label(fr, text="验证码:").grid(row=1, column=0, sticky="e")
code_var = tk.StringVar()
code_entry = tk.Entry(fr, textvariable=code_var, width=20)
code_entry.grid(row=1, column=1, padx=6)
tk.Label(fr, text="用户名:").grid(row=2, column=0, sticky="e")
username_var = tk.StringVar()
username_entry = tk.Entry(fr, textvariable=username_var, width=30)
username_entry.grid(row=2, column=1, padx=6)
tk.Label(fr, text="密码:").grid(row=3, column=0, sticky="e")
pw_var = tk.StringVar()
pw_entry = tk.Entry(fr, textvariable=pw_var, show="*", width=30)
pw_entry.grid(row=3, column=1, padx=6)
tk.Label(fr, text="确认密码:").grid(row=4, column=0, sticky="e")
pw2_var = tk.StringVar()
pw2_entry = tk.Entry(fr, textvariable=pw2_var, show="*", width=30)
pw2_entry.grid(row=4, column=1, padx=6)
btn_fr = tk.Frame(fr)
btn_fr.grid(row=6, column=0, columnspan=3, pady=18)
tk.Button(btn_fr, text="确定", width=12, command=lambda: on_confirm()).pack(side="left", padx=8)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=8)
fr.grid_columnconfigure(1, weight=1)
cooldown = {"left": 0, "timer": None}
def update_send_btn():
if cooldown["left"] > 0:
send_btn.config(text=f"重新发送({cooldown['left']})", state="disabled")
else:
send_btn.config(text="发送验证码", state="normal")
def countdown_tick():
if cooldown["left"] <= 0:
update_send_btn()
return
cooldown["left"] -= 1
update_send_btn()
cooldown["timer"] = self.root.after(1000, countdown_tick)
def on_send():
email = email_var.get().strip()
if not is_valid_email(email):
messagebox.showwarning("邮箱错误", "请输入正确的邮箱地址", parent=self.root)
return
def worker():
send_btn.config(state="disabled", text="发送中...")
ok, msg = self.auth.send_code(email)
if ok:
messagebox.showinfo("发送成功", f"验证码已发送到 {email}(若未收到, 请检查垃圾邮件)。", parent=self.root)
cooldown["left"] = SEND_COOLDOWN
countdown_tick()
else:
messagebox.showerror("发送失败", f"发送失败: {msg}", parent=self.root)
update_send_btn()
threading.Thread(target=worker, daemon=True).start()
send_btn.config(command=on_send)
def on_confirm():
email = email_var.get().strip()
code = code_var.get().strip()
username = username_var.get().strip()
pw = pw_var.get()
pw2 = pw2_var.get()
if not is_valid_email(email):
messagebox.showwarning("错误", "请输入合法邮箱", parent=self.root); return
if not code:
messagebox.showwarning("错误", "请输入验证码", parent=self.root); return
if not username:
messagebox.showwarning("错误", "请输入用户名", parent=self.root); return
if not pw or not pw2:
messagebox.showwarning("错误", "请输入密码并确认", parent=self.root); return
if pw != pw2:
messagebox.showwarning("错误", "两次密码不一致", parent=self.root); return
if not (6 <= len(pw) <= 10 and any(c.isupper() for c in pw) and any(c.islower() for c in pw) and any(c.isdigit() for c in pw)):
messagebox.showwarning("错误", "密码需6-10位且包含大写、小写字母与数字", parent=self.root); return
ok, msg = self.auth.register_with_code(email, code, username, pw)
if not ok:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "注册并设置密码成功,跳转到选择界面", parent=self.root)
self.current_user = self.user_store.find_by_email(email)
self.build_choice_screen()
# 登录(用户名或邮箱)
def ui_login(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text="登录", font=("Arial", 16)).pack(pady=8)
tk.Label(fr, text="用户名/邮箱:").pack(anchor="w")
key_var = tk.StringVar()
tk.Entry(fr, textvariable=key_var, width=50).pack(pady=4)
tk.Label(fr, text="密码:").pack(anchor="w")
pw_var = tk.StringVar()
tk.Entry(fr, textvariable=pw_var, show="*", width=50).pack(pady=4)
btn_fr = tk.Frame(fr); btn_fr.pack(pady=12)
def do_login():
key = key_var.get().strip()
pw = pw_var.get()
if not key:
messagebox.showwarning("输入错误", "请输入用户名或邮箱", parent=self.root); return
ok, res = self.auth.login(key, pw)
if not ok:
messagebox.showerror("登录失败", res, parent=self.root); return
self.current_user = res
messagebox.showinfo("登录成功", f"欢迎,{self.current_user.get('username')}", parent=self.root)
self.build_choice_screen()
tk.Button(btn_fr, text="登录", width=12, command=do_login).pack(side="left", padx=6)
tk.Button(btn_fr, text="返回", width=12, command=self.build_main).pack(side="left", padx=6)
# 选择学段与题量
def build_choice_screen(self):
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
uname = self.current_user.get("username") if self.current_user else ""
email = self.current_user.get("email") if self.current_user else ""
tk.Label(fr, text=f"已登录: {uname} ({email})", font=("Arial", 12)).pack(anchor="w")
tk.Button(fr, text="修改密码", command=self.ui_change_password).pack(anchor="w", pady=4)
tk.Button(fr, text="注销", command=self.logout).pack(anchor="w", pady=4)
tk.Label(fr, text="请选择学段并输入题目数量数量最高为20:", font=("Arial", 12)).pack(pady=8, anchor="w")
grade_var = tk.StringVar(value="小学")
for g in QUESTION_FILES.keys():
tk.Radiobutton(fr, text=g, variable=grade_var, value=g).pack(anchor="w")
num_entry = tk.Entry(fr, width=8)
num_entry.pack(pady=6, anchor="w")
num_entry.delete(0, "end"); num_entry.insert(0, str(DEFAULT_QUESTIONS))
def start():
try:
n = int(num_entry.get())
if n <= 0:
raise ValueError()
if n > MAX_QUESTIONS:
messagebox.showwarning("超过上限", f"题目数量上限为 {MAX_QUESTIONS}", parent=self.root); return
except Exception:
messagebox.showwarning("输入错误", "请输入正整数题目数量", parent=self.root); return
grade = grade_var.get()
qs = self.qstore.load(grade)
if len(qs) < n:
qs = ensure_questions_for_grade(grade, n, self.qstore)
chosen = random.sample(qs, n)
self.current_exam = {"grade": grade, "questions": chosen, "index": 0, "answers": [None]*n}
self.build_question_screen()
tk.Button(fr, text="开始做题", command=start).pack(pady=8)
def logout(self):
self.current_user = None
self.current_exam = None
self.build_main()
def ui_change_password(self):
if not self.current_user:
messagebox.showwarning("未登录", "请先登录", parent=self.root); return
old = simpledialog.askstring("修改密码", "请输入原密码:", show="*", parent=self.root)
if old is None:
return
new1 = simpledialog.askstring("新密码", "请输入新密码6-10位含大小写字母与数字", show="*", parent=self.root)
if new1 is None:
return
new2 = simpledialog.askstring("确认新密码", "请再次输入新密码:", show="*", parent=self.root)
if new2 is None:
return
if new1 != new2:
messagebox.showwarning("不一致", "两次新密码不一致", parent=self.root); return
ok, msg = self.auth.change_password(self.current_user.get("email"), old, new1)
if not ok:
messagebox.showerror("失败", msg, parent=self.root); return
messagebox.showinfo("成功", "密码修改成功", parent=self.root)
# 题目界面
def build_question_screen(self):
self.clear()
exam = self.current_exam
idx = exam["index"]
q = exam["questions"][idx]
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
header = tk.Label(fr, text=f"学段:{exam['grade']} 题目 {idx+1}/{len(exam['questions'])}", font=("Arial", 12))
header.pack(anchor="w")
stem = tk.Label(fr, text=q["stem"], font=("Arial", 14), wraplength=WINDOW_W-80, justify="left")
stem.pack(pady=8, anchor="w")
chosen_var = tk.IntVar(value=-1 if exam["answers"][idx] is None else exam["answers"][idx])
for i, opt in enumerate(q["options"]):
tk.Radiobutton(fr, text=opt, variable=chosen_var, value=i).pack(anchor="w")
nav = tk.Frame(fr); nav.pack(pady=10, anchor="w")
def save_choice():
v = chosen_var.get()
exam["answers"][idx] = None if v == -1 else int(v)
return True
def prev_q():
if idx == 0:
messagebox.showinfo("提示", "已经是第一题", parent=self.root); return
save_choice(); exam["index"] -= 1; self.build_question_screen()
def next_or_finish():
save_choice()
if idx + 1 < len(exam["questions"]):
exam["index"] += 1
self.build_question_screen()
else:
self.show_result_screen()
tk.Button(nav, text="上一题", command=prev_q).pack(side="left", padx=6)
tk.Button(nav, text="下一题/提交", command=next_or_finish).pack(side="left", padx=6)
tk.Button(nav, text="放弃并返回", command=self.build_choice_screen).pack(side="left", padx=6)
# 结果界面
def show_result_screen(self):
exam = self.current_exam
total = len(exam["questions"])
correct = 0
for i, q in enumerate(exam["questions"]):
ans = exam["answers"][i]
if ans is not None and ans == q["answer_index"]:
correct += 1
percent = round(correct / total * 100, 2) if total > 0 else 0.0
self.clear()
fr = tk.Frame(self.root, padx=12, pady=12)
fr.pack(fill="both", expand=True)
tk.Label(fr, text=f"考试结束 - 得分:{percent}%", font=("Arial", 14)).pack(pady=6)
tk.Label(fr, text=f"答对:{correct} / {total}").pack(pady=4)
def show_details():
win = tk.Toplevel(self.root); win.title("答题详情")
txt = scrolledtext.ScrolledText(win, width=100, height=30)
txt.pack(fill="both", expand=True)
for i, q in enumerate(exam["questions"]):
chosen = exam["answers"][i]
chosen_text = q["options"][chosen] if chosen is not None and 0 <= chosen < len(q["options"]) else "未作答"
correct_text = q["options"][q["answer_index"]]
status = "正确" if chosen == q["answer_index"] else "错误"
txt.insert("end", f"{i+1}: {q['stem']}\n你的答案: {chosen_text}\n正确答案: {correct_text} [{status}]\n\n")
txt.config(state="disabled")
tk.Button(fr, text="查看答题详情", command=show_details).pack(pady=6)
tk.Button(fr, text="继续做题", command=self._continue_again).pack(pady=6)
tk.Button(fr, text="退出登录并返回主界面", command=self._logout_and_return).pack(pady=6)
def _continue_again(self):
self.current_exam = None
self.build_choice_screen()
def _logout_and_return(self):
self.current_user = None
self.current_exam = None
self.build_main()
# ----------------- main -----------------
def main():
ensure_files_exist()
root = tk.Tk()
app = MathApp(root)
root.mainloop()
if __name__ == "__main__":
main()
Loading…
Cancel
Save