Compare commits

...

4 Commits

Author SHA1 Message Date
hewendi23 941a3a18a6 Update README.md
4 months ago
hewendi23 62fce3ef92 1.2
4 months ago
hewendi23 775d13b300 1.2
4 months ago
hewendi23 60cb6b0623 1.1
4 months ago

@ -1,2 +0,0 @@
# wang2

@ -0,0 +1,7 @@
何文迪的个人项目
app.py main.py quiz_generator.py user_manager.py都是项目文件运行main.py即可
users.json存储了用户信息
src/dist中的main.exe是项目打包.exe文件可直接运行

@ -1,4 +1,3 @@
# src/app.py
import customtkinter as ctk
import random
import string
@ -9,25 +8,21 @@ class MathApp(ctk.CTk):
def __init__(self):
super().__init__()
self.title("小初高数学学习软件")
self.geometry("500x450")
self.geometry("500x550") # 增加了窗口高度以适应新页面
ctk.set_appearance_mode("System")
self.user_manager = UserManager()
self.current_user = None
container = ctk.CTkFrame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frames = {}
# 还原:只包含基础的五个页面
for F in (LoginPage, RegisterPage, MainPage, QuizPage, ScorePage):
# 添加了新的 ForgotPasswordPage
for F in (LoginPage, RegisterPage, ForgotPasswordPage, MainPage, QuizPage, ScorePage):
page_name = F.__name__
frame = F(parent=container, controller=self)
self.frames[page_name] = frame
frame.grid(row=0, column=0, sticky="nsew")
self.show_frame("LoginPage")
def show_frame(self, page_name):
@ -46,7 +41,13 @@ class LoginPage(ctk.CTkFrame):
self.error_label = ctk.CTkLabel(self, text="", text_color="red")
self.error_label.pack(pady=5)
ctk.CTkButton(self, text="登录", command=self.login).pack(pady=10)
ctk.CTkButton(self, text="没有账户?去注册", fg_color="transparent", command=lambda: controller.show_frame("RegisterPage")).pack()
# 登录和注册按钮的容器
bottom_frame = ctk.CTkFrame(self, fg_color="transparent")
bottom_frame.pack(pady=5)
ctk.CTkButton(bottom_frame, text="没有账户?去注册", fg_color="transparent", command=lambda: controller.show_frame("RegisterPage")).pack(side="left", padx=5)
# 新增忘记密码按钮
ctk.CTkButton(bottom_frame, text="忘记密码?", fg_color="transparent", command=lambda: controller.show_frame("ForgotPasswordPage")).pack(side="left", padx=5)
def login(self):
email, password = self.email_entry.get(), self.password_entry.get()
@ -78,7 +79,7 @@ class RegisterPage(ctk.CTkFrame):
self.info_label.pack(pady=5)
ctk.CTkButton(self, text="注册", command=self.register).pack(pady=10)
ctk.CTkButton(self, text="返回登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack()
def update_cooldown(self):
if self.cooldown_time > 0:
self.send_code_button.configure(text=f"{self.cooldown_time}秒后可重试")
@ -95,12 +96,10 @@ class RegisterPage(ctk.CTkFrame):
return
if self.controller.user_manager.is_email_registered(email):
self.info_label.configure(text="该邮箱已被注册。", text_color="red")
return
return
self.code = ''.join(random.choices(string.digits, k=6))
self.info_label.configure(text="正在发送邮件,请稍候...", text_color="gray")
success = send_verification_email(email, self.code)
success = send_verification_email(email, self.code)
if success:
self.info_label.configure(text="邮件验证码已发送至您的邮箱,请查收。", text_color="green")
self.cooldown_time = 60
@ -125,6 +124,76 @@ class RegisterPage(ctk.CTkFrame):
self.info_label.configure(text=msg, text_color="green" if success else "red")
if success: self.code = ""
# 新增的“忘记密码”页面
class ForgotPasswordPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
self.controller = controller
self.code = ""
self.cooldown_time = 0
ctk.CTkLabel(self, text="重设密码", font=ctk.CTkFont(size=24, weight="bold")).pack(pady=10)
self.email_entry = ctk.CTkEntry(self, placeholder_text="输入已注册的邮箱", width=250)
self.email_entry.pack(pady=5)
self.send_code_button = ctk.CTkButton(self, text="发送验证码", command=self.send_code, width=250)
self.send_code_button.pack(pady=5)
self.code_entry = ctk.CTkEntry(self, placeholder_text="输入收到的邮件验证码", width=250)
self.code_entry.pack(pady=5)
self.password_entry = ctk.CTkEntry(self, placeholder_text="新密码 (6-10位,含大小写和数字)", show="*", width=250)
self.password_entry.pack(pady=5)
self.confirm_password_entry = ctk.CTkEntry(self, placeholder_text="确认新密码", show="*", width=250)
self.confirm_password_entry.pack(pady=5)
self.info_label = ctk.CTkLabel(self, text="", text_color="red")
self.info_label.pack(pady=5)
ctk.CTkButton(self, text="确认重设", command=self.reset_password).pack(pady=10)
ctk.CTkButton(self, text="返回登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack()
def update_cooldown(self):
if self.cooldown_time > 0:
self.send_code_button.configure(text=f"{self.cooldown_time}秒后可重试")
self.cooldown_time -= 1
self.after(1000, self.update_cooldown)
else:
self.send_code_button.configure(text="发送验证码", state="normal")
def send_code(self):
if self.cooldown_time > 0: return
email = self.email_entry.get().strip()
if not email:
self.info_label.configure(text="请输入邮箱地址。", text_color="red")
return
if not self.controller.user_manager.is_email_registered(email):
self.info_label.configure(text="该邮箱未注册。", text_color="red")
return
self.code = ''.join(random.choices(string.digits, k=6))
self.info_label.configure(text="正在发送邮件,请稍候...", text_color="gray")
success = send_verification_email(email, self.code)
if success:
self.info_label.configure(text="验证码已发送至您的邮箱。", text_color="green")
self.cooldown_time = 60
self.send_code_button.configure(state="disabled")
self.update_cooldown()
else:
self.info_label.configure(text="邮件发送失败,请检查邮箱或网络。", text_color="red")
self.code = ""
def reset_password(self):
email, user_code, new_password, confirm_password = self.email_entry.get().strip(), self.code_entry.get().strip(), self.password_entry.get(), self.confirm_password_entry.get()
if not self.code or user_code != self.code:
self.info_label.configure(text="邮件验证码错误", text_color="red")
return
if new_password != confirm_password:
self.info_label.configure(text="两次输入的密码不一致", text_color="red")
return
if not self.controller.user_manager.validate_password_format(new_password):
self.info_label.configure(text="新密码格式不符合要求", text_color="red")
return
success, msg = self.controller.user_manager.reset_password(email, new_password)
self.info_label.configure(text=msg, text_color="green" if success else "red")
if success:
self.code = ""
self.after(2000, lambda: self.controller.show_frame("LoginPage"))
class MainPage(ctk.CTkFrame):
def __init__(self, parent, controller):
super().__init__(parent)
@ -136,14 +205,19 @@ class MainPage(ctk.CTkFrame):
ctk.CTkButton(self, text="退出登录", fg_color="transparent", command=lambda: controller.show_frame("LoginPage")).pack(side="bottom", pady=20)
def start_quiz(self, level):
dialog = ctk.CTkInputDialog(text="请输入题目数量 (1-20):", title="设置题目数量")
# 修改了题目数量提示和范围
dialog = ctk.CTkInputDialog(text="请输入题目数量 (10-30):", title="设置题目数量")
num_str = dialog.get_input()
if num_str:
try:
num = int(num_str)
if 1 <= num <= 20:
# 修改了题目数量验证逻辑
if 10 <= num <= 30:
self.controller.frames["QuizPage"].setup_quiz(level, num)
self.controller.show_frame("QuizPage")
else:
# 可以添加一个提示,告知用户输入范围错误
pass
except ValueError: pass
class QuizPage(ctk.CTkFrame):
@ -160,23 +234,27 @@ class QuizPage(ctk.CTkFrame):
self.radio_buttons.append(rb)
self.submit_button = ctk.CTkButton(self, text="下一题", command=self.next_question)
self.submit_button.pack(pady=20)
def setup_quiz(self, level, num_questions):
self.questions = get_generator(level).generate_bunch(num_questions)
self.current_question_index = 0
self.user_answers = []
self.display_question()
def display_question(self):
self.radio_var.set(-1)
q = self.questions[self.current_question_index]
self.question_label.configure(text=f"{self.current_question_index + 1}. {q.text} = ?")
for i, option in enumerate(q.options): self.radio_buttons[i].configure(text=str(option))
self.submit_button.configure(text="提交试卷" if self.current_question_index == len(self.questions) - 1 else "下一题")
def next_question(self):
if self.radio_var.get() == -1: return
self.user_answers.append(self.radio_var.get())
self.current_question_index += 1
if self.current_question_index < len(self.questions): self.display_question()
else: self.calculate_score()
def calculate_score(self):
correct_count = sum(1 for i, q in enumerate(self.questions) if self.user_answers[i] == q.correct_answer_index)
total = len(self.questions)
@ -192,5 +270,6 @@ class ScorePage(ctk.CTkFrame):
self.score_label.pack(pady=40, padx=20)
ctk.CTkButton(self, text="继续做题", command=lambda: controller.show_frame("MainPage")).pack(pady=10)
ctk.CTkButton(self, text="退出程序", command=controller.quit).pack(pady=10)
def show_score(self, correct, total, percent):
self.score_label.configure(text=f"答对了 {correct} / {total}\n\n得分: {percent:.2f}%")

BIN
src/dist/main.exe vendored

Binary file not shown.

@ -1,5 +1,4 @@
from app import MathApp
if __name__ == "__main__":
app = MathApp()
app.mainloop()

@ -1,11 +1,20 @@
import random
import math
from abc import ABC, abstractmethod
# 安全的表达式求值函数
def safe_eval(expression):
"""
Safely evaluates a mathematical expression, allowing access to specific math functions.
"""
allowed_names = {
"sin": math.sin,
"cos": math.cos,
"tan": math.tan,
"sqrt": math.sqrt,
"pi": math.pi,
}
try:
return eval(expression, {"__builtins__": None}, {})
return eval(expression, {"__builtins__": None}, allowed_names)
except Exception:
return None
@ -19,9 +28,15 @@ class QuestionGenerator(ABC):
def generate_bunch(self, count):
questions = []
generated_texts = set()
while len(questions) < count:
# 增加一个保险措施,防止因某种原因卡在循环里
max_attempts = count * 10
attempts = 0
while len(questions) < count and attempts < max_attempts:
text, answer = self._create_question_and_answer()
if text not in generated_texts:
attempts += 1
# 确保题目是新的,且答案是有效的整数
if text not in generated_texts and answer is not None and answer == int(answer):
answer = int(answer)
options = self._generate_options(answer)
correct_index = options.index(answer)
@ -32,9 +47,10 @@ class QuestionGenerator(ABC):
def _generate_options(self, correct_answer):
options = {correct_answer}
while len(options) < 4:
offset = random.randint(-10, 10)
# 扩大随机偏移范围以适应更复杂的计算结果
offset = random.randint(-20, 20)
if offset == 0:
offset = random.choice([-1, 1]) * 10
offset = random.choice([-1, 1]) * random.randint(1, 20)
distractor = correct_answer + offset
options.add(distractor)
@ -49,40 +65,142 @@ class QuestionGenerator(ABC):
class PrimarySchoolGenerator(QuestionGenerator):
def _create_question_and_answer(self):
ops = ['+', '-', '*', '/']
op = random.choice(ops)
if op == '+':
num1, num2 = random.randint(1, 100), random.randint(1, 100)
expr = f"{num1} + {num2}"
elif op == '-':
num1, num2 = random.randint(1, 100), random.randint(1, 100)
if num1 < num2: num1, num2 = num2, num1
expr = f"{num1} - {num2}"
elif op == '*':
num1, num2 = random.randint(1, 20), random.randint(1, 20)
expr = f"{num1} * {num2}"
else: # op == '/'
divisor = random.randint(2, 10)
answer = random.randint(2, 20)
dividend = divisor * answer
expr = f"{dividend} / {divisor}"
return expr, int(safe_eval(expr))
class MiddleSchoolGenerator(PrimarySchoolGenerator):
"""
生成包含 +, -, *, / 和括号的小学题目确保答案是整数
模板示例: a + (b * c) a * (b - c)
"""
while True:
op1 = random.choice(['+', '-', '*'])
op2 = random.choice(['+', '-', '*', '/'])
num_a, num_b, num_c = random.randint(1, 10), random.randint(2, 10), random.randint(2, 10)
# 优先处理除法,确保可以整除
if op2 == '/':
inner_result = random.randint(2, 10)
num_b = inner_result * num_c
else:
inner_result = safe_eval(f"{num_b} {op2} {num_c}")
# 避免减法出现负数
if op1 == '-' and num_a < inner_result:
num_a, inner_result = inner_result + random.randint(1,10), num_a
expr_text = f"{num_a} {op1} ({num_b} {op2} {num_c})"
answer = safe_eval(expr_text)
if answer is not None and answer == int(answer):
return expr_text, int(answer)
class MiddleSchoolGenerator(QuestionGenerator):
def _create_question_and_answer(self):
if random.choice([True, False]):
return super()._create_question_and_answer()
else:
base = random.randint(2, 10)
expr = f"{base}**2"
return expr, int(safe_eval(expr))
"""
生成至少包含一个平方或开方且有多个运算数的初中题目
模板示例: a * (b**2 - c) (a + sqrt(b)) / c
"""
while True:
# 随机选择一个模板
template = random.choice(['op_first', 'op_second'])
if template == 'op_first': # 特殊运算在括号内
op1 = random.choice(['+', '-', '*'])
num_a = random.randint(2, 20)
if random.choice([True, False]): # 使用平方
base = random.randint(2, 10)
op2 = random.choice(['+', '-'])
num_c = random.randint(1, 20)
inner_expr = f"{base}**2 {op2} {num_c}"
else: # 使用开方
perfect_squares = [4, 9, 16, 25, 36, 49, 64, 81, 100]
base = random.choice(perfect_squares)
op2 = random.choice(['+', '-', '*'])
num_c = random.randint(2, 10)
inner_expr = f"sqrt({base}) {op2} {num_c}"
expr_text = f"{num_a} {op1} ({inner_expr})"
else: # op_second, 特殊运算在括号外
op1 = random.choice(['+', '-', '*'])
num_c = random.randint(2, 15)
num_a, num_b = random.randint(1, 10), random.randint(1, 10)
inner_expr = f"({num_a} + {num_b})"
if random.choice([True, False]): # 使用平方
expr_text = f"{inner_expr}**2 {op1} {num_c}"
else: # 使用开方
# 确保括号内的和是平方数
target_sum = random.choice([4, 9, 16, 25, 36, 49, 64, 81, 100])
num_a = random.randint(1, target_sum - 1)
num_b = target_sum - num_a
expr_text = f"sqrt({num_a} + {num_b}) {op1} {num_c}"
# 检查表达式和答案
eval_expr = expr_text.replace('sqrt', 'math.sqrt')
answer = safe_eval(expr_text)
if answer is not None and answer == int(answer):
return expr_text, int(answer)
class HighSchoolGenerator(MiddleSchoolGenerator):
def _create_question_and_answer(self):
# 简化版高中题,可扩展为三角函数等
return super()._create_question_and_answer()
def _create_question_and_answer(self):
"""
生成至少包含一个sin, cos, tan且有多个运算数的高中题目
通过组合已知结果的三角函数来确保最终答案是整数
"""
while True:
# (显示文本, 求值表达式, 值)
trig_facts = [
("sin(0)", "sin(0)", 0), ("cos(0)", "cos(0)", 1),
("sin(π/6)", "sin(pi/6)", 0.5), ("cos(π/3)", "cos(pi/3)", 0.5),
("tan(π/4)", "tan(pi/4)", 1), ("sin(π/2)", "sin(pi/2)", 1),
("cos(π)", "cos(pi)", -1),
]
fact1_text, fact1_expr, fact1_val = random.choice(trig_facts)
# 随机选择一个更复杂的模板
template = random.choice(['template1', 'template2'])
if template == 'template1':
# 模板: A * (B op trig_func)
num_a = random.randint(2, 10)
num_b = random.randint(5, 20)
op = random.choice(['+', '-'])
# 如果三角函数值为0.5,需要特殊处理确保括号内结果易于计算
if fact1_val == 0.5 and op == '+':
# 让A成为偶数确保最终结果是整数
num_a = random.randint(1, 5) * 2
expr_text = f"{num_a} * ({num_b} {op} {fact1_text})"
expr_eval = f"{num_a} * ({num_b} {op} {fact1_expr})"
else: # template2
# 模板: (A op trig_func1) * (B op trig_func2)
fact2_text, fact2_expr, fact2_val = random.choice(trig_facts)
num_a = random.randint(5, 15)
num_b = random.randint(2, 10)
op1 = random.choice(['+', '-'])
op2 = random.choice(['+', '-'])
# 确保两个括号内的值都不是分数,或者它们相乘后是整数
term1_val = num_a + (fact1_val if op1 == '+' else -fact1_val)
term2_val = num_b + (fact2_val if op2 == '+' else -fact2_val)
# 如果其中一个term含有.5让另一个term的系数是偶数
if term1_val != int(term1_val):
num_b = random.randint(1, 5) * 2
elif term2_val != int(term2_val):
num_a = random.randint(1, 5) * 2
expr_text = f"({num_a} {op1} {fact1_text}) * ({num_b} {op2} {fact2_text})"
expr_eval = f"({num_a} {op1} {fact1_expr}) * ({num_b} {op2} {fact2_expr})"
answer = safe_eval(expr_eval)
if answer is not None and answer == int(answer):
return expr_text, int(answer)
def get_generator(level):
generators = {

@ -1,4 +1,3 @@
# src/user_manager.py
import json
import hashlib
import os
@ -8,38 +7,30 @@ import ssl
from email.message import EmailMessage
def send_verification_email(recipient_email, code):
"""
发送验证码邮件的函数
"""
# --- 配置区域 ---
SENDER_EMAIL = "876413020@qq.com"
APP_PASSWORD = "wbeyuimdbvkxbdgf"
SMTP_SERVER = "smtp.qq.com"
SMTP_PORT = 465
# --- 构建邮件内容 ---
msg = EmailMessage()
msg['Subject'] = "【数学学习软件】您的注册验证码"
msg['Subject'] = "【数学学习软件】您的验证码"
msg['From'] = SENDER_EMAIL
msg['To'] = recipient_email
msg.set_content(
f"您好!\n\n"
f"感谢您注册数学学习软件。您的验证码是:\n\n"
f"您正在进行身份验证操作。您的验证码是:\n\n"
f"{code}\n\n"
f"请在5分钟内使用。如果不是您本人操作请忽略此邮件。"
)
# --- 发送邮件(健壮的结构)---
smtp_server = None
try:
context = ssl.create_default_context()
smtp_server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT, context=context)
smtp_server.login(SENDER_EMAIL, APP_PASSWORD)
smtp_server.send_message(msg)
print(f"邮件已成功发送至 {recipient_email}")
return True
except smtplib.SMTPAuthenticationError:
print("邮件发送失败: 认证错误。请检查邮箱或授权码是否正确。")
return False
@ -77,7 +68,6 @@ class UserManager:
return email in self.users
def validate_password_format(self, password):
"""密码必须 6-10 位,包含大小写字母和数字"""
if 6 <= len(password) <= 10 and \
re.search(r"[a-z]", password) and \
re.search(r"[A-Z]", password) and \
@ -87,8 +77,7 @@ class UserManager:
def register_user(self, email, password):
if self.is_email_registered(email):
return False, "该邮箱已被注册。"
return False, "该邮箱已被注册。"
salt = os.urandom(16).hex()
password_hash = self._hash_password(password, salt)
self.users[email] = {"password_hash": password_hash, "salt": salt}
@ -97,9 +86,22 @@ class UserManager:
def validate_login(self, email, password):
if not self.is_email_registered(email):
return False
return False
user_data = self.users[email]
salt = user_data["salt"]
password_hash = self._hash_password(password, salt)
return password_hash == user_data["password_hash"]
return password_hash == user_data["password_hash"]
# 新增的重设密码方法
def reset_password(self, email, new_password):
if not self.is_email_registered(email):
return False, "该邮箱未注册。"
# 为新密码生成新的盐和哈希值
salt = os.urandom(16).hex()
password_hash = self._hash_password(new_password, salt)
self.users[email]['password_hash'] = password_hash
self.users[email]['salt'] = salt
self._save_users()
return True, "密码重设成功!"

@ -0,0 +1,6 @@
{
"3589843135@qq.com": {
"password_hash": "1ee2d6f5cd5c2c82cf93ac817a0fdbaf78a632343a2d12d4d04cb072b07c5c98",
"salt": "22399dc52bc6b25b6b4357f0b9260189"
}
}
Loading…
Cancel
Save