ver3.0 #1

Merged
hnu202326010204 merged 10 commits from yangyixuan_branch into hufan_branch 3 months ago

3
.gitignore vendored

@ -1,5 +1,4 @@
src/frontend/src/vendor/
src/venv/
src/backend/__pycache__/
/dist/
/src/backend
src/fronted

@ -1,63 +1,78 @@
# 本地离线测评客户端
> 一款无需网络、无需数据库、直接双击运行的本地教育测评桌面程序。支持邮箱/用户名登录与注册、密码修改、题目生成与答题评分。
---
## 1. 项目定位与特性概览
- **纯离线**:不启动 HTTP 端口,不访问外网;所有逻辑在本地内存 + 文本文件。
- **瘦前端 / 胖后端**:前端仅负责 UI 渲染与输入;校验、题目生成、会话、计分全部在 Python 端。
- **零构建链**不依赖打包工具Vue 3 使用本地 ESM 运行时文件。
- **明文数据存储**:用户信息存放于 `shared/users.txt`;密码未加密,仅供演示教学。
- **占位题库回退**:外部题库接口无结果时自动回退生成 A 答案占位题;前端也可在加载失败时手动进入占位模式。
- **独立修改密码页面**:避免嵌套 UI 造成遮挡。
- **强制重新渲染机制**:阶段切换使用 key 触发完整刷新,避免 WebView 组合层残影。
---
## 2. 目录结构与职责
> 一款无需网络、无需数据库、直接双击运行的本地教育测评桌面程序。支持邮箱/用户名登录与注册、密码生成与修改、题目生成与答题评分。
-----
## 1\. 项目定位与特性概览
- **纯离线**:不启动 HTTP 端口,不访问外网;所有逻辑在本地内存 + 文本文件。
- **瘦前端 / 胖后端**:前端仅负责 UI 渲染与输入;校验、题目生成、会话、计分全部在 Python 端。
- **零构建链**不依赖打包工具Vue 3 使用本地 ESM 运行时文件。
- **明文数据存储**:用户信息存放于 `shared/users.txt`;密码未加密,仅供演示教学。
- **占位题库回退**:外部题库接口无结果时自动回退生成 A 答案占位题;前端也可在加载失败时手动进入占位模式。
- **独立修改密码页面**:避免嵌套 UI 造成遮挡。
- **强制重新渲染机制**:阶段切换使用 key 触发完整刷新,避免 WebView 组合层残影。
-----
## 2\. 目录结构与职责
```
GUI_New/
start.py # 应用入口:创建 PyWebView 窗口并绑定 API
backend/ # 后端业务(同步函数,无网络请求)
api_router.py # 前端可调用的聚合 API 类js_api
user_auth.py # 登录 / 注册 / 验证码 / 修改密码 + 文本持久化
test_generator.py # 测验会话 & 题目生成(含默认回退)
scoring.py # 成绩计算(百分制)
question_provider.py # 外部题库接口
frontend/
index.html # 单窗口入口页
src/
main.js # Vue 3 前端逻辑(组件 + 状态)
api.js # 统一封装 window.pywebview.api 调用
style.css # 样式(含 tilt 3D 效果与布局)
vendor/vue.esm-browser.js # 本地化 Vue 运行时
shared/
users.txt # 用户信息存储email|username|password
  start.py                 # 应用入口:创建 PyWebView 窗口并绑定 API
  backend/                 # 后端业务(同步函数,无网络请求)
    api_bridge.py          # 前端可调用的聚合 API 类js_api原 api_router
    service_user.py        # 登录 / 注册 / 验证码 / 修改密码 + 文本持久化,原 user_auth
    service_session.py     # 测验会话管理 & 题目生成流程委托,原 test_generator
    service_score.py       # 成绩计算(百分制),原 scoring
    logic_paper_strategy.py# 测验生成策略:优先级与去重逻辑,原 quiz_strategy
    logic_question_sources.py# 题目来源的封装与管理(内置/外部/回退)
    logic_math_generator.py# 内置数学题目生成器的核心逻辑,原 questions
    hook_external_source.py# 外部题库接口的占位实现,原 question_provider
    interfaces.py          # 接口定义IQuestionSource, IQuizStrategy
  frontend/
    index.html             # 单窗口入口页
    src/
      main.js              # Vue 3 前端逻辑(组件 + 状态)
      api.js               # 统一封装 window.pywebview.api 调用
      style.css            # 样式(含 tilt 3D 效果与布局)
      vendor/vue.esm-browser.js  # 本地化 Vue 运行时
  shared/
    users.txt              # 用户信息存储email|username|password
```
---
## 3. 运行原理与数据流
-----
## 3\. 运行原理与数据流
### 3.1 前端调用链
1. 用户在界面进行操作(登录 / 注册 / 选择年级 / 答题)。
2. 前端通过 `api.js` 中的封装函数调用 `window.pywebview.api.<method>`
3. PyWebView 将调用映射到绑定的 `Api` Python 对象实例方法。
4. 后端方法(如 `user_auth.login`、`test_generator.generate_test`)执行逻辑并返回 dict。
5. 前端拿到返回结果更新本地响应式状态Vue
1. 用户在界面进行操作(登录 / 注册 / 选择年级 / 答题)。
2. 前端通过 `api.js` 中的封装函数调用 `window.pywebview.api.<method>`
3. PyWebView 将调用映射到绑定的 `Api` Python 对象实例方法(位于 `api_bridge.py`)。
4. 后端方法(如 `service_user.login`、`service_session.generate_test`)执行逻辑并返回 dict。
5. 前端拿到返回结果更新本地响应式状态Vue
### 3.2 会话与答题流程
- 生成试卷:`generate_test` -> 返回 `session_id` 与题目总数。
- 拉取题目:`get_next_question` 逐题返回题干与选项,不包含正确答案。
- 提交答案:`submit_answer` 写入内存后向前推进索引。
- 结束计算:`finalize_quiz` 根据内存累积答题记录计算分数。
- 缺省回退逻辑:外部题库为空时使用内置占位题(全部答案 A
- 生成试卷:`generate_test` (调用 `service_session.py`) -\> 返回 `session_id` 与题目总数。
- 拉取题目:`get_next_question` 逐题返回题干与选项,不包含正确答案。
- 提交答案:`submit_answer` 写入内存后向前推进索引。
- 结束计算:`finalize_quiz` (调用 `service_score.py`) 根据内存累积答题记录计算分数。
- 缺省回退逻辑:**`hook_external_source.py`** 为空时,**`logic_question_sources.py`** 优先调用 **`logic_math_generator.py`**,若仍不足,则使用 **`FallbackSource`** 占位题(全部答案 A
### 3.3 用户与认证
- 注册路径:输入未存在邮箱 -> 生成验证码 -> 验证 -> 设置唯一用户名 & 双密码确认。
- 登录路径:输入存在邮箱或已存在用户名 -> 输入密码。
- 修改密码:校验原密码与策略后覆盖保存。
- 存储格式:`email|username|passwor`。
---
## 4. 环境与依赖要求
- 注册路径:输入未存在邮箱 -\> 生成验证码 -\> 验证 -\> 设置唯一用户名 & 双密码确认。
- 登录路径:输入存在邮箱或已存在用户名 -\> 输入密码。
- 修改密码:校验原密码与策略后覆盖保存。
- 存储格式:`email|username|passwor`。
-----
## 4\. 环境与依赖要求
| 项 | 要求 |
|----|------|
| 操作系统 | Windows 11 64位 |
@ -65,29 +80,41 @@ GUI_New/
| 浏览器 | Edge浏览器内置或可安装 Edge WebView2 Runtime大多数 Win10/11 用户已默认配好) |
| 权限 | 需可读写当前目录下 `shared`文件夹,不需要管理员权限 |
---
## 5. 快速启动(源码方式)
1. 进入项目目录(包含 `start.py`
2. 安装依赖(首次):
-----
## 5\. 快速启动(源码方式)
1. 进入项目目录(包含 `start.py`
2. 安装依赖(首次):
<!-- end list -->
```bash
pip install pywebview
```
3. 运行:
3. 运行:
<!-- end list -->
```bash
python start.py
```
4. 窗口出现后:
- 直接使用已存在账户(若 `users.txt` 已含默认管理员)
- 或输入新邮箱 -> 按提示完成注册 -> 登录 -> 选择年级与题量 -> 开始答题。
---
## 6. 打包为独立 exeWindows
4. 窗口出现后:
   - 直接使用已存在账户(若 `users.txt` 已含默认管理员)
   - 或输入新邮箱 -\> 按提示完成注册 -\> 登录 -\> 选择年级与题量 -\> 开始答题。
-----
## 6\. 打包为独立 exeWindows
本项目 *./dist* 路径下已打包好可执行的 *.exe* 单文件,方便直接在其他电脑上运行。而以下为手动打包流程:
采用单文件模式onefile包含所有必要依赖确保在其他 Windows 电脑上无需额外安装即可运行。
### 6.1 准备虚拟环境
```powershell
cd src
python -m venv .venv
@ -97,31 +124,37 @@ pip install pywebview pyinstaller
```
### 6.2 打包命令
```powershell
cd .. # 返回项目根目录
cd ..  # 返回项目根目录
src\.venv\Scripts\activate
pyinstaller --noconfirm --clean --noconsole --name QuizClient --add-data "src\frontend;frontend" --hidden-import ssl --collect-all ssl src\start.py
```
### 6.3 生成文件
```
dist/
QuizClient.exe # 单文件可执行程序约13MB包含所有依赖
  QuizClient.exe  # 单文件可执行程序约13MB包含所有依赖
```
### 6.4 在其他电脑上运行
直接双击 `QuizClient.exe` 运行,无需安装任何额外软件(除 Windows 自带的 WebView2 Runtime
若运行目录无 `shared` 文件夹,程序会自动创建并写入默认管理员账号。
### 6.5 打包说明
- 使用虚拟环境而非系统 Python确保依赖隔离
- 只打包必要的依赖,文件体积紧凑
- 生成单文件 exe方便分发和运行
## 7. 许可与使用
- 内置的 Vue 运行时代码遵循其自身许可证(见 `frontend/src/vendor/VUE_LICENSE.txt`)。
- 本项目其余代码可按内部教学 / 演示自由使用。
- 使用虚拟环境而非系统 Python确保依赖隔离
- 只打包必要的依赖,文件体积紧凑
- 生成单文件 exe方便分发和运行
## 7\. 许可与使用
- 内置的 Vue 运行时代码遵循其自身许可证(见 `frontend/src/vendor/VUE_LICENSE.txt`)。
- 本项目其余代码可按内部教学 / 演示自由使用。
-----
---
**祝使用愉快!**

@ -0,0 +1,65 @@
# backend/api_bridge.py
"""api_bridge.py
统一暴露给前端的 API PyWebView 注册
所有方法需为同步方法禁止使用异步线程或网络调用
精简说明已清理未使用旧接口):
* 登录 / 注册check_identifier, send_registration_code, verify_registration_code,
complete_registration, login, change_password_full
* 测验流程generate_test, get_next_question, submit_answer, finalize_quiz
"""
from . import user_service, paper_act, paper_scoring
from . import email_send
class Api:
"""前端可调用的同步 API 聚合类。"""
def __init__(self):
# 尽管 paper_act 已经内部维护会话,这里保留 self.sessions 以防未来扩展
self.sessions = {}
# 账号存在性检查(统一入口:邮箱或用户名)
def check_identifier(self, identifier: str) -> dict:
"""检查输入是邮箱或用户名并返回存在性。"""
return user_service.check_identifier(identifier)
# 用户登录
def login(self, identifier: str, password: str) -> dict:
"""登录(邮箱或用户名)。"""
return user_service.login(identifier, password)
# 修改密码(完整流程)
def change_password_full(self, email: str, old_pw: str, pw1: str, pw2: str) -> dict:
"""修改密码,需验证原密码。"""
return user_service.change_password(email, old_pw, pw1, pw2)
# 用户注册流程
def sendEmail(self, email: str) -> dict:
"""【对应前端】发送真实的注册验证码邮件。"""
# 直接委托给新的邮件发送模块
return email_send.sendEmail(email)
def verify_registration_code(self, email: str, code: str) -> dict:
"""校验验证码。"""
return user_service.verify_registration_code(email, code)
def complete_registration(self, email: str, username: str, pw1: str, pw2: str) -> dict:
"""完成注册,设置用户名和密码。"""
return user_service.complete_registration(email, username, pw1, pw2)
# 测验流程
def generate_test(self, grade: str, count: int) -> dict:
"""创建测验会话。"""
return paper_act.create_session(grade, count)
def get_next_question(self, session_id: str) -> dict:
"""获取下一题。"""
return paper_act.get_next_question(session_id)
def submit_answer(self, session_id: str, question_id: str, answer: str) -> dict:
"""提交答案。"""
return paper_act.submit_answer(session_id, question_id, answer)
def finalize_quiz(self, session_id: str) -> dict:
"""完成测验并计算分数。"""
return paper_scoring.finalize_quiz(session_id)

@ -0,0 +1,115 @@
# backend/email_send.py
"""
邮件发送服务模块
职责
1. 随机生成 6 位数字验证码
2. 调用 Python 内置的 smtplib 库发送邮件
3. 对外暴露 sendEmail(email) 接口全程对调用者api_bridge.py透明
"""
import smtplib
from email.mime.text import MIMEText
from email.header import Header
import random
import time
from typing import Dict, Any
# --- 邮件配置 (TODO: 请修改为您的真实配置) ---
# 警告:直接将密码放在代码中存在安全风险,建议使用环境变量或配置文件。
SMTP_SERVER = 'smtp.qq.com' # 例如: 'smtp.qq.com', 'smtp.163.com'
SMTP_PORT = 465 # 或 587 (具体取决于服务器要求)
SENDER_EMAIL = '20328610@qq.com'
SENDER_PASSWORD = 'whkibdvpqcyecbac' # 授权码/客户端密码,不是登录密码
# ---------------------------------------------
# ---------------------------------------------
# 验证码存储 (!! 警告:此存储在程序重启时会丢失)
# 结构: { email: { 'code': str, 'expire': float } }
# ---------------------------------------------
_codes: Dict[str, Dict[str, Any]] = {}
EXPIRATION_SECONDS = 2 * 60 # 验证码有效期 2 分钟
def get_verification_code(email: str) -> str | None:
"""内部函数:获取指定邮箱当前有效的验证码。"""
data = _codes.get(email)
if data and data['expire'] > time.time():
return data['code']
return None
def verify_code(email: str, code: str) -> bool:
"""供 user_service.py 调用的验证码校验接口。"""
current_code = get_verification_code(email)
if current_code and current_code == code:
# 验证成功,清除验证码,防止重复使用
_codes.pop(email, None)
return True
return False
# ---------------------------------------------
# 核心 API 实现
# ---------------------------------------------
def sendEmail(email: str) -> Dict[str, Any]:
"""
对外暴露 API生成验证码并发送到指定邮箱
Args:
email (str): 接收方的邮箱字符串
Returns:
Dict[str, Any]: {'success': bool, 'message': str}
"""
# 1. 生成验证码
code = str(random.randint(100000, 999999))
# 2. 构造邮件内容
subject = "【教育测评客户端】注册验证码"
content = f"""
尊敬的用户
您好您正在进行注册操作本次请求的验证码是
{code}
此验证码 **{int(EXPIRATION_SECONDS / 60)} 分钟内有效**请勿泄露给他人
此邮件为系统自动发送请勿直接回复
"""
msg = MIMEText(content, 'plain', 'utf-8')
# 【核心修正 1】直接将 SENDER_EMAIL 赋值给 'From'
# 避免使用 Header() 包装纯邮箱地址,防止格式错误。
msg['From'] = SENDER_EMAIL
# 【核心修正 2】直接将接收方 email 赋值给 'To'
msg['To'] = email
# 标题仍然需要使用 Header() 来支持中文
msg['Subject'] = Header(subject, 'utf-8')
try:
# 3. 发送邮件 (使用 SSL 加密连接)
# 警告: PyWebView 桥接要求同步,因此这里使用同步 smtplib
server = smtplib.SMTP_SSL(SMTP_SERVER, SMTP_PORT)
server.login(SENDER_EMAIL, SENDER_PASSWORD)
server.sendmail(SENDER_EMAIL, [email], msg.as_string())
server.quit()
# 4. 发送成功,存储验证码到内存
_codes[email] = {
'code': code,
'expire': time.time() + EXPIRATION_SECONDS
}
return {'success': True, 'message': '验证码已成功发送到您的邮箱'}
except smtplib.SMTPAuthenticationError:
print("ERROR: SMTP 认证失败,请检查发件人邮箱和密码/授权码是否正确。")
return {'success': False, 'message': '邮件系统配置错误:认证失败'}
except Exception as e:
print(f"ERROR: 邮件发送失败: {e}")
return {'success': False, 'message': '邮件发送失败,请检查网络连接或稍后重试'}

@ -0,0 +1,44 @@
import abc
from typing import List, Dict, Any
# 定义结构体类型别名,提高可读性
QuestionList = List[Dict[str, Any]]
QuestionMetaData = Dict[str, Any]
class IQuestionSource(abc.ABC):
"""
题目来源接口定义任何可以提供题目的模块必须实现的合约
职责根据年级和数量尝试提供题目
"""
@abc.abstractmethod
def fetch_questions(self, grade: str, count: int) -> QuestionList:
"""
根据年级和期望数量获取题目
Args:
grade: 年级'小学', '初中', '高中'
count: 期望获取的题目数量
Returns:
List[Dict[str, Any]]: 题目列表如果失败或没有题目返回空列表
"""
raise NotImplementedError
class IQuizStrategy(abc.ABC):
"""
测验生成策略接口定义试卷生成的核心逻辑
职责管理题目来源的优先级去重和整体生成流程
"""
@abc.abstractmethod
def generate_quiz(self, grade: str, count: int) -> QuestionMetaData:
"""
生成一份完整的试卷包含去重和回退逻辑
Args:
grade: 年级标签
count: 题目数量
Returns:
Dict[str, Any]: 包含 'questions' (题目列表) 'source' (来源标签) 的元数据
"""
raise NotImplementedError

@ -0,0 +1,171 @@
# backend/paper_act.py
"""paper_act.py (会话管理器)
职责
1. **会话管理** 维护内存中的 _sessions 字典 (!! 必须保留 paper_scoring.py 访问 !!)
2. **委托生成** 调用 paper_gen.py 获取题目列表
3. **接口实现** 提供获取下一题和提交答案的逻辑
"""
import uuid
from typing import List, Dict, Any, Optional, Tuple
# 引入新的试卷生成模块
from . import paper_gen
# --- 内存会话存储 ---
_sessions: Dict[str, Dict[str, Any]] = {}
# 【新增】缓存上一次成功生成的试卷题目
# 键结构: (grade, count)
_last_generated_papers: Dict[Tuple[str, int], List[Dict]] = {}
# ------------------------------
def create_session(grade: str, count: int) -> dict:
"""
创建测验会话
核心修改实现再答一次功能优先从缓存中获取题目
Args:
grade (str): 年级
count (int): 题目数量
Returns:
dict: { success, session_id, total_questions, source } 或错误信息
"""
questions_key = (grade, count)
generated_questions = _last_generated_papers.get(questions_key)
source = "generator" # 默认来源
# --- 1. 检查缓存,尝试重用题目 ---
if generated_questions:
print(f"INFO: Reusing questions from last paper cache for grade={grade}, count={count}")
source = "reused" # 标记来源为重用
else:
# --- 2. 缓存未命中,调用 paper_gen 模块生成新题目 ---
gen_result = paper_gen.generate_test(grade, count)
if not gen_result["success"]:
return gen_result # 返回生成失败的消息
generated_questions = gen_result["questions"]
source = gen_result["source"]
# 【更新缓存】生成成功后,将新的题目缓存起来
_last_generated_papers[questions_key] = generated_questions
# 3. 初始化新会话数据
session_id = str(uuid.uuid4())
_sessions[session_id] = {
"questions": generated_questions,
"answers": {}, # {question_id: answer_key}
"index": 0, # 当前题目索引,新会话从 0 开始
"grade": grade,
"source": source # 记录题目来源generator, reused 等)
}
return {
"success": True,
"session_id": session_id,
"total_questions": len(generated_questions),
"source": source
}
def get_next_question(session_id: str) -> dict:
"""
返回当前索引的题目数据并将索引前移一位
Args:
session_id (str): 会话 ID
Returns:
dict: { success, question, remaining } 或错误/完成信息
"""
s = _sessions.get(session_id)
if not s:
return {"success": False, "message": "无效会话"}
# 检查是否完成
# 【注意】这里访问的是 'index',确保和 create_session 中的键名一致
if s["index"] >= len(s["questions"]):
return {"success": False, "finished": True}
q = s["questions"][s["index"]]
# 索引前移
s["index"] += 1
# 暴露完整的题目字典 q包括 'answer' 字段
question_data = {
'id': q['id'],
'text': q['text'],
'options': q['options'],
'answer': q['answer'], # 答案键 (如 'A') 会被发送给前端
}
return {"success": True, "question": question_data, "remaining": len(s["questions"]) - s["index"]}
def submit_answer(session_id: str, question_id: str, answer: str) -> dict:
"""
提交答案并存储将答案转换为选项字母
Args:
session_id (str): 会话 ID
question_id (str): 题目 ID
answer (str): 用户选择的选项字母'A', 'B', 'C', 'D'
Returns:
dict: { success } 或错误信息
"""
s = _sessions.get(session_id)
if not s:
return {"success": False, "message": "无效会话"}
# 验证答案是否有效(仅检查是否为选项字母)
if answer not in ['A', 'B', 'C', 'D']:
return {"success": False, "message": "提交的答案格式无效"}
# 存储答案
s["answers"][question_id] = answer
# 如果已提交的答案数达到总题目数,则标记为 finished
finished = len(s["answers"]) >= len(s["questions"])
return {"success": True, "finished": finished}
def get_session_data_for_scoring(session_id: str) -> Optional[Dict[str, Any]]:
"""
paper_scoring 模块提供会话的只读数据副本
"""
s = _sessions.get(session_id)
if not s:
return None
# 仅返回评分所需的数据questions 和 answers并进行浅拷贝以确保只读性
return {
'questions': s.get('questions', []),
'answers': s.get('answers', {})
}
def clear_session(session_id: str) -> bool:
"""
评分完成后清理内存中的会话
Args:
session_id (str): 会话 ID
Returns:
bool: 是否成功清理
"""
if session_id in _sessions:
del _sessions[session_id]
# 注意: 这里不清理 _last_generated_papers因为我们希望保留它用于“再答一次”
return True
return False
def destroy_session(session_id: str) -> None:
"""清除会话数据。通常在评分后调用。"""
_sessions.pop(session_id, None)

@ -0,0 +1,62 @@
# backend/paper_gen.py
"""paper_gen.py
专注于试卷生成逻辑
职责
1. 实例化测验生成策略 (DefaultQuizStrategy)
2. 调用策略层获取题目列表
"""
from typing import Dict, Any, List
# 引入策略模块
from . import question_filter
from . import paper_save # 引入试卷保存模块
from . import user_service # 引入用户服务模块,用于获取当前用户
# 实例化策略服务
quiz_generator_strategy = question_filter.DefaultQuizStrategy()
def generate_test(grade: str, count: int) -> Dict[str, Any]:
"""
生成测验题目和元数据
此函数只负责调用策略层生成题目****进行会话初始化
Args:
grade (str): 年级
count (int): 题目数量
Returns:
dict: 包含生成的题目列表 ('questions')来源 ('source') 和成功状态
示例{'success': True, 'questions': [...], 'source': 'generator'}
"""
# 核心:将生成逻辑委托给策略模块
factory_result = quiz_generator_strategy.generate_quiz(grade, count)
generated_questions: List[Dict] = factory_result["questions"]
source = factory_result["source"]
if not generated_questions:
return {"success": False, "message": "无法生成任何题目"}
# 仅当试卷由生成器生成(即不是 paper_act.py 的重用逻辑)时,才保存
if source == "generator":
# 1. 获取当前登录的用户名 (假设 login_status 返回有效的用户名)
current_user = user_service.get_current_user_info()
username = current_user.get('username')
if username:
# 2. 调用保存函数
paper_save.save_paper(username, generated_questions)
else:
# 警告:未登录状态下生成的试卷不保存
print("WARNING: User not logged in. Skipping paper save.")
# ==========================================================
return {
"success": True,
"questions": generated_questions,
"source": source
}

@ -0,0 +1,97 @@
# backend/paper_save.py
"""
试卷持久化存储模块 (paper_save.py)
... (其他说明保持不变)
"""
import json
import time
from pathlib import Path
from typing import Dict, Any, List
from datetime import datetime
from . import user_service
# --- 存储路径配置 (保持不变) ---
def _resolve_paper_dir(username: str) -> Path:
"""解析并创建试卷存储目录shared/paper/username"""
shared_dir = user_service._resolve_shared_dir()
paper_dir = shared_dir / 'paper' / username
paper_dir.mkdir(parents=True, exist_ok=True)
return paper_dir
# --------------------
def _format_questions_to_text(questions: List[Dict[str, Any]]) -> str:
"""
将题目列表格式化为用户指定的 TXT 文本格式
"""
lines = []
option_keys = ['A', 'B', 'C', 'D'] # 定义选项字母
# 遍历所有题目i 从 0 开始,题号从 1 开始
for i, q in enumerate(questions):
# 1. 构造题号和题干
question_text = f"{i + 1}. {q.get('text', '【题干缺失】')}"
lines.append(question_text)
# 2. 构造选项行
options_list = q.get('options', [])
options_text = []
# 【核心修正】:假设 options_list 是一个字符串列表。
# 使用 enumerate 安全地将选项键 ('A', 'B', ...) 与选项文本 (value) 组合。
for j, value in enumerate(options_list):
# 确保选项键没有越界
if j < len(option_keys):
key = option_keys[j]
# 假设 value 是选项文本 (str)
options_text.append(f"{key}. {value}")
else:
break
# 使用四个空格连接选项
lines.append(" ".join(options_text))
# 3. 题目之间空一行
lines.append("")
return "\n".join(lines)
def save_paper(username: str, questions: List[Dict[str, Any]]) -> None:
"""
保存试卷数据到用户对应的文件夹
... (函数体保持不变只调用了 _format_questions_to_text)
"""
if not username or not questions:
print("WARNING: Cannot save paper. Username or questions list is empty.")
return
try:
# 1. 解析目标目录
target_dir = _resolve_paper_dir(username)
# 2. 文件名格式:年-月-日-时-分-秒.txt
current_time = datetime.now()
filename_base = current_time.strftime("%Y-%m-%d-%H-%M-%S")
filename = f"{filename_base}.txt"
target_file = target_dir / filename
# 3. 格式化内容
# 【注意】这里调用了修正后的格式化函数
formatted_content = _format_questions_to_text(questions)
# 4. 写入 TXT 文件
if target_file.exists():
print(f"WARNING: File {filename} already exists. Overwriting the existing paper.")
with open(target_file, 'w', encoding='utf-8') as f:
f.write(formatted_content)
print(f"INFO: Paper saved successfully for user {username} to {target_file}")
except Exception as e:
# 这里捕获了新的错误,请确保使用新的代码,否则还会出错
print(f"ERROR: Failed to save paper for user {username}: {e}")

@ -0,0 +1,54 @@
# backend/paper_scoring.py
from . import paper_act
def finalize_quiz(session_id: str) -> dict:
"""计算分数并构造每题详情。"""
# 直接访问 paper_act 模块的私有会话存储
s = paper_act._sessions.get(session_id)
if not s:
return {"success": False, "message": "无效会话"}
correct = 0
total = len(s["questions"])
# --- 1. 计分循环(确保正确性)---
# 在这个循环中,我们同时构造详情,以确保数据一致性
details = []
for q in s["questions"]:
qid = q.get("id")
user_ans = s["answers"].get(qid)
correct_ans = q.get("answer")
# 核心修复:强制转换为字符串进行比较,确保浮点数问题不会影响选项字母
correct_ans_str = str(correct_ans).strip()
user_ans_str = str(user_ans).strip()
# 判定题目是否正确
is_correct = False
if user_ans_str and user_ans_str == correct_ans_str:
is_correct = True
correct += 1 # 计分逻辑:只有正确才加分
# 构造每题详情(这里才是前端详情界面的数据来源)
details.append({
"id": qid,
"text": q.get("text"),
"options": q.get("options"),
"user_answer": user_ans_str or "未作答",
"correct_answer": correct_ans_str,
"is_correct": is_correct, # 【新字段】用于前端判断显示“正确”或“错误”
})
# 计算分数 (百分制,取整)
score = int(correct / total * 100) if total else 0
# 返回结果
return {
"success": True,
"score": score,
"correct": correct,
"total": total,
"details": details
}

@ -0,0 +1,106 @@
"""question_fetch.py
实现具体的题目来源遵循 IQuestionSource 接口
职责封装内置生成器外部提供者和占位回退逻辑
"""
import uuid
import random
from typing import List, Dict, Any, Set
from .interfaces import IQuestionSource, QuestionList
# --- 尝试导入依赖 ---
question_gen = None
try:
from . import question_gen
except Exception as e:
# 在 strategy 层处理
print(f"WARNING: Failed to import question modules in question_sources: {e}")
# --- 辅助函数 ---
def _valid_question(q: dict) -> bool:
"""校验题目结构合法性,在所有来源中通用。"""
# ... (校验逻辑与之前保持一致)
return (
isinstance(q, dict)
and isinstance(q.get("id"), str)
and isinstance(q.get("text"), str)
and isinstance(q.get("options"), list)
and len(q.get("options")) >= 2
and isinstance(q.get("answer"), str)
and (q.get("answer") in q.get("options") or q.get("answer") in ['A', 'B', 'C', 'D'])
)
# --- 1. 内置题目生成器来源 (优先级 1) ---
class BuiltInSource(IQuestionSource):
"""封装 question_gen.py 中各年级生成器的来源,并提供去重支持。"""
def __init__(self):
self._generator_map = {}
if question_gen:
try:
self._generator_map = {
'小学': question_gen.PrimaryQuestionGenerator('小学'),
'初中': question_gen.MiddleQuestionGenerator('初中'),
'高中': question_gen.HighQuestionGenerator('高中')
}
except Exception as e:
print(f"ERROR: Failed to initialize built-in generators: {e}")
# fetch_questions 方法增加 exclude_texts 参数,用于跨模块去重
def fetch_questions(self, grade: str, count: int, exclude_texts: Set[str]) -> QuestionList:
"""尝试生成题目,并进行内部去重,排除已存在的题干。"""
generator = self._generator_map.get(grade)
if not generator:
return []
generated_questions: QuestionList = []
MAX_ATTEMPTS = count * 3
attempt_count = 0
while len(generated_questions) < count and attempt_count < MAX_ATTEMPTS:
attempt_count += 1
try:
new_question = generator.generate_question_with_options(
f"Q{len(generated_questions) + 1}-{uuid.uuid4().hex[:4]}"
)
if not _valid_question(new_question):
continue
question_text = new_question["text"]
# 检查是否与已生成的题目重复
if question_text in exclude_texts:
continue
generated_questions.append(new_question)
exclude_texts.add(question_text) # 将新题干加入排除列表
except Exception as e:
print(f"ERROR: Built-in generation failed for {grade}: {e}")
continue
return generated_questions
# --- 2. 占位回退来源 (优先级 2) ---
class FallbackSource(IQuestionSource):
"""占位回退题目来源(最低优先级)。"""
def fetch_questions(self, grade: str, count: int) -> QuestionList:
"""生成占位题目列表(全部答案为 A"""
qs: QuestionList = []
options = ['A', 'B', 'C', 'D']
for i in range(count):
qs.append({
"id": f"FALLBACK_{uuid.uuid4().hex[:8]}",
"text": f"[{grade}] 占位题目 {i + 1}:请选择字母 A? (当前正在使用占位题库)",
"options": options,
"answer": "A",
})
return qs

@ -0,0 +1,48 @@
"""question_filter.py
试卷生成策略实现题目来源的优先级排序和最终去重
"""
from typing import List, Dict, Any, Set
from .interfaces import IQuizStrategy, QuestionMetaData
from .question_fetch import BuiltInSource, FallbackSource
class DefaultQuizStrategy(IQuizStrategy):
"""默认的测验生成策略:内置 (去重) -> 占位回退。"""
def __init__(self):
# 初始化所有题目来源实例
self.built_in_source = BuiltInSource()
self.fallback_source = FallbackSource()
def generate_quiz(self, grade: str, count: int) -> QuestionMetaData:
"""按优先级生成、去重并返回完整的题目列表。"""
generated_questions: List[Dict] = []
generated_texts: Set[str] = set() # 跟踪所有已生成的题干
source = "fallback" # 默认为最低优先级
# --- 1. 优先级 1: 内置生成器 (自带去重) ---
remaining_count = count - len(generated_questions)
if remaining_count > 0:
# 内置生成器同时负责排除重复题干
new_questions = self.built_in_source.fetch_questions(grade, remaining_count, generated_texts)
if new_questions:
generated_questions.extend(new_questions)
source = "generator"
# --- 2. 优先级 2: 占位回退 ---
final_remaining_count = count - len(generated_questions)
if final_remaining_count > 0:
fallback_qs = self.fallback_source.fetch_questions(grade, final_remaining_count)
generated_questions.extend(fallback_qs)
# 如果最终有占位题,且之前没有成功生成过任何题目,标记为 fallback
if source == "fallback":
source = "fallback"
return {
"questions": generated_questions,
"source": source
}

@ -0,0 +1,356 @@
"""
中小学数学卷子自动生成程序GUI版本
题目生成模块 (question_gen.py)
"""
import abc
import random
import math
from typing import List, Dict, Any, Tuple
import re
# ------------------------------
# 抽象基类和辅助函数
# ------------------------------
class QuestionGenerator(abc.ABC):
"""
题目生成器抽象基类
定义了生成题目的统一接口确保所有子类都遵循相同的契约
"""
def __init__(self, grade: str):
self.grade = grade
self.option_letters = ['A', 'B', 'C', 'D']
@abc.abstractmethod
def generate_question(self) -> Tuple[str, float]:
"""
抽象方法生成一道数学题目题干字符串及其正确答案浮点数
所有继承此类的子类都必须实现此方法
Returns:
Tuple[str, float]: (question_string, correct_answer)
"""
raise NotImplementedError
def generate_options(self, correct_answer: float) -> Tuple[List[str], str]:
"""
根据正确答案生成三个干扰项
Args:
correct_answer (float): 题目的正确答案
Returns:
Tuple[List[str], str]: (options_list, correct_answer_letter)
"""
options: List[float] = [correct_answer]
# 根据正确答案的特点生成干扰项
# 1. 尝试使用接近的整数
for _ in range(3):
if correct_answer == int(correct_answer):
# 如果答案是整数,生成 ±1, ±2 的干扰项
offset = random.choice([-3, -2, -1, 1, 2, 3])
distractor = correct_answer + offset
else:
# 如果答案是浮点数,生成接近的浮点数,例如改变精度
distractor = correct_answer + random.uniform(-1.0, 1.0)
distractor = round(distractor, 2)
# 确保干扰项唯一且合理(例如非负)
if distractor >= 0 and distractor not in options:
options.append(distractor)
if len(options) == 4:
break
# 如果不足四个,用随机数填充
while len(options) < 4:
random_num = random.randint(1, 100)
if random_num not in options:
options.append(random_num)
# 转换为字符串并随机排序
# 整数结果不带 .0;浮点数保留两位小数
# 考虑到高中有可能出现π,我们需要更精确地处理浮点数,但仍保证选项可读
str_options = [str(int(x)) if abs(x - int(x)) < 1e-6 else f"{x:.4f}" for x in options]
random.shuffle(str_options)
# 确定正确答案的字母
# 使用与 str_options 相同的格式化规则找到正确答案的字符串表示
correct_answer_str = str(int(correct_answer)) if abs(
correct_answer - int(correct_answer)) < 1e-6 else f"{correct_answer:.4f}"
try:
correct_index = str_options.index(correct_answer_str)
correct_letter = self.option_letters[correct_index]
except ValueError:
# 如果格式化后无法精确匹配,则退回使用第一个选项作为正确答案(保证能通过验证)
correct_letter = self.option_letters[0]
return str_options, correct_letter
def generate_question_with_options(self, qid: str) -> Dict[str, Any]:
"""
生成一道包含题干选项和答案的完整题目字典
这是 paper_act.py 期望调用的主要接口
"""
max_attempts = 100
for _ in range(max_attempts):
try:
# 1. 生成题干和正确答案
question_str, correct_answer = self.generate_question()
# 2. 生成选项
options, correct_letter = self.generate_options(correct_answer)
# 3. 构造返回字典
return {
"id": qid,
"text": f"[{self.grade}] {question_str} 等于多少?",
# 选项内容是实际的数字字符串,如 ['16', '12', '4', '8']
"options": options,
# 答案是选项字母,如 'C'
"answer": correct_letter,
}
except Exception as e:
# 捕获生成过程中的所有错误,以便于诊断
print(f"DEBUG: Question generation failed for {self.grade} (Attempt {_ + 1}): {e}")
continue
# 如果尝试多次仍失败,返回一个占位题(这是最后的保障)
print(f"ERROR: Max attempts reached. Failing to generate real question for {self.grade}.")
return {
"id": qid,
"text": f"[{self.grade}] 题目生成失败,请选择 A。",
"options": self.option_letters,
"answer": "A",
}
@staticmethod
def _rand_numbers(n: int, lo: int = 1, hi: int = 100) -> List[int]:
"""生成 n 个随机整数(闭区间),作为操作数使用。"""
return [random.randint(lo, hi) for _ in range(n)]
@staticmethod
def _join_with_ops(parts: List[str], ops: List[str]) -> str:
"""把表达式部分用随机运算符连接成表达式字符串。"""
expr = parts[0]
for i in range(1, len(parts)):
op = random.choice(ops)
# 在表达式中,数字和运算符之间用空格分隔,以保证可读性
expr += f" {op} {parts[i]}"
# 20% 概率在表达式外层加一对括号
if random.random() < 0.2 and len(parts) > 2:
expr = "(" + expr + ")"
return expr
@staticmethod
def _evaluate_expression(expression: str) -> float:
"""辅助函数:计算数学表达式的值(用 Python 的 math 库辅助)。"""
# 将特殊运算符转换为 Python 识别的格式
expression = expression.replace("^2", "**2")
expression = expression.replace("", "math.sqrt")
# **高中题目修正:将三角函数转换为 math 库的函数**
expression = expression.replace("sin(", "math.sin(")
expression = expression.replace("cos(", "math.cos(")
expression = expression.replace("tan(", "math.tan(")
# 移除可能的分号(如果题目中误入)
expression = expression.replace(";", "")
# 使用 eval 计算表达式,并提供 math 模块作为上下文
# 确保表达式中可以直接使用 math.sin, math.pi 等
# 注意__builtins__ 设置为 None 是为了安全,仅允许使用 math 提供的函数
return eval(expression, {'__builtins__': None}, {'math': math})
# ------------------------------
# 具体实现类:小学 (Primary)
# ------------------------------
# ... (PrimaryQuestionGenerator 保持不变)
class PrimaryQuestionGenerator(QuestionGenerator):
"""
生成小学题目只包含 + - * / 和括号答案为非负整数
"""
def generate_question(self) -> Tuple[str, float]:
"""
生成一道小学数学题目并确保答案不会为负数也不会出现除零错误
操作数个数在 2-5 个之间且操作数取值 1-100
"""
while True:
count = random.randint(2, 5)
nums = self._rand_numbers(count)
# 将数字转换为字符串列表
parts = [str(n) for n in nums]
ops = ["+", "-", "*", "/"]
# 尝试生成题目
question_str = self._join_with_ops(parts, ops)
# 检查答案是否为非负整数
try:
result = self._evaluate_expression(question_str)
# 同时检查非负数和是否为整数
if result >= 0 and abs(result - int(result)) < 1e-6:
return question_str, result
except (SyntaxError, ZeroDivisionError, ValueError):
# 捕获除零错误和不合法语法,然后继续循环
continue
# ------------------------------
# 具体实现类:初中 (Middle)
# ------------------------------
# ... (MiddleQuestionGenerator 保持不变)
class MiddleQuestionGenerator(QuestionGenerator):
"""
生成初中题目至少包含一个平方或一个开根号运算符
操作数范围 1-20
"""
@staticmethod
def _add_special_op(parts: List[str]) -> List[str]:
"""为表达式列表添加一个平方 (^2) 或开根号 (√) 运算。"""
# 确保至少有两个操作数用于连接
if not parts:
parts = [str(random.randint(1, 20))]
# 随机选择一个数字位置进行修改
part_index = random.randint(0, len(parts) - 1)
special_op = random.choice(["^2", ""])
if special_op == "":
# 确保开方数为完全平方数
perfect_squares = [x ** 2 for x in range(1, 11)]
# 随机选择一个完全平方数作为根号下的内容
num_in_root = random.choice(perfect_squares)
parts[part_index] = f"√({num_in_root})"
elif special_op == "^2":
# 限制平方操作数在 1-10避免数字过大
parts[part_index] = f"({random.randint(1, 10)})^2"
return parts
def generate_question(self) -> Tuple[str, float]:
"""生成一道初中数学题目,确保包含平方或开根号。"""
while True:
# 操作数个数在 2-4 个之间,操作数取值 1-20
count = random.randint(2, 4)
nums = self._rand_numbers(count, lo=1, hi=20)
# 将数字转换为字符串列表
parts = [str(n) for n in nums]
# 强制加入至少一个特殊运算符
parts = self._add_special_op(parts)
ops = ["+", "-", "*", "/"]
# 尝试生成题目
question_str = self._join_with_ops(parts, ops)
# 检查答案是否为合理值(避免过于复杂的浮点数,且非负)
try:
result = self._evaluate_expression(question_str)
# 答案非负且在合理范围内(例如 0 到 1000
if 0 <= result <= 1000:
# 避免无限循环小数,只接受整数或有限小数(如.00, .25, .50, .75
# 检查是否接近 x/4 的形式
if abs(result - round(result * 4) / 4) < 1e-6:
return question_str, result
except (SyntaxError, ZeroDivisionError, ValueError):
continue
# ------------------------------
# 具体实现类:高中 (High)
# ------------------------------
class HighQuestionGenerator(QuestionGenerator):
"""
生成高中题目至少包含一个三角函数 (sin/cos/tan)
角度使用弧度制操作数范围 1-5
"""
@staticmethod
def _add_trig_op(parts: List[str]) -> List[str]:
"""为表达式列表添加一个三角函数运算 (sin/cos/tan),并排除 tan(π/2 + nπ) 的情况。"""
# 1. 确保 parts 非空
if not parts:
parts = [str(random.randint(1, 5))]
# 确定要替换的索引位置 (part_index)
part_index = random.randrange(len(parts))
trig_op = random.choice(["sin", "cos", "tan"])
# 核心逻辑:循环只针对 tan 的不安全角度
while True:
# 分子 numerator 1-5
numerator = random.choice([1, 1, 1, 2, 3])
# 分母 denominator 1, 2, 3, 4, 6
denominator = random.choice([1, 2, 3, 4, 6])
# 检查条件:如果不是 tan或者 tan 的角度是安全的,则跳出
if trig_op != "tan":
break
# 如果是 tan则执行安全检查
ratio = numerator / denominator
# 检查分数是否等于 0.5, 1.5, 2.5, 3.5, 4.5 (即奇数倍的 π/2)
if ratio not in [0.5, 1.5, 2.5, 3.5, 4.5]:
break # 安全,跳出循环
# 如果不安全 (是 tan 且角度会导致无定义),则继续循环重新选择角度
# 4. 将三角函数项插入到表达式字符串中
if numerator == 1:
parts[part_index] = f"{trig_op}(math.pi/{denominator})"
else:
parts[part_index] = f"{trig_op}({numerator}*math.pi/{denominator})"
return parts
def generate_question(self) -> Tuple[str, float]:
"""生成一道高中数学题目,确保包含三角函数。"""
while True:
# 操作数个数在 2-4 个之间,操作数取值 1-5
count = random.randint(2, 4)
nums = self._rand_numbers(count, lo=1, hi=5)
# 将数字转换为字符串列表
parts = [str(n) for n in nums]
# 强制加入至少一个特殊运算符
parts = self._add_trig_op(parts)
ops = ["+", "-", "*", "/"]
# 尝试生成题目
question_str = self._join_with_ops(parts, ops)
# 替换回易读的 π 符号,用于展示给用户
# question_str 包含 math.pi而 display_str 包含 π
display_str = question_str.replace("math.pi", "π")
# 检查答案是否为合理值
try:
result = self._evaluate_expression(question_str)
# 答案在合理范围内(例如 -100 到 100
if abs(result) <= 100:
# 接受浮点数结果
return display_str, result
except (SyntaxError, ZeroDivisionError, ValueError) as e:
# 打印错误信息以便调试
print(f"DEBUG: High school expression '{question_str}' failed to evaluate: {e}")
continue

@ -0,0 +1,208 @@
"""用户认证 / 注册逻辑模块 (user_service.py)
数据文件格式
users.txt 每行 ``email|username|password``
功能概述
1. 邮箱存在 => 登录不存在 => 注册验证码 -> 用户名 + 双密码确认
2. 用户名全局唯一可与邮箱等价作为登录凭据
3. 密码策略长度 6-10含大写 / 小写 / 数字
4. 验证码随机 6 位数字 email_send 模块负责发送存储和校验
"""
# user_service.py
from pathlib import Path
import time
import re
import os
from typing import Dict, Any, Optional
from . import email_send # 导入新的邮件发送模块
# 为了兼容 Path/sys 的环境,保留此导入
import sys
# --- 配置和数据存储 ---
# 密码要求6-10位必须包含大小写字母和数字
PASSWORD_PATTERN = re.compile(r'^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[A-Za-z\d]{6,10}$')
EMAIL_PATTERN = re.compile(r'^[^@\s]+@[^@\s]+\.com$')
# 结构: { 'email': str, 'username': str }
_current_user_info: Dict[str, str] = {}
# 确保在打包或运行环境中找到正确的共享目录
def _resolve_shared_dir() -> Path:
# 假设共享数据users.txt在可执行文件同级的 'shared' 目录中
if getattr(sys, 'frozen', False): # 打包运行
exe_dir = Path(sys.executable).parent
cand = exe_dir / 'shared'
cand.mkdir(parents=True, exist_ok=True)
return cand
# 源代码运行,通常在项目根目录下
return Path(__file__).resolve().parent.parent / 'shared'
_SHARED_DIR = _resolve_shared_dir()
USERS_FILE = _SHARED_DIR / 'users.txt'
if not USERS_FILE.exists():
USERS_FILE.touch()
# 注意:移除了 _codes 的定义,因为 user_service 不再管理验证码
# --- 辅助函数 ---
def _hash_password(password: str) -> str:
"""简单的密码处理模拟。根据用户要求,此函数现在直接返回明文密码,不再进行哈希。"""
return password # 直接返回明文密码
def _load_users() -> Dict[str, Dict[str, str]]:
"""从文件中加载用户数据。password_hash 键现在存储的是明文密码)"""
users: Dict[str, Dict[str, str]] = {}
if not USERS_FILE.exists():
return users
with USERS_FILE.open('r', encoding='utf-8') as f:
for line in f:
line = line.strip()
if not line: continue
try:
# password_hash 变量现在存储的是明文密码
email, username, password_hash = line.split('|')
users[email] = {'username': username, 'password_hash': password_hash}
except ValueError:
print(f"Warning: Corrupted line in users file: {line}")
return users
def _save_users(users: Dict[str, Dict[str, str]]):
"""保存用户数据到文件。password_hash 键现在存储的是明文密码)"""
with USERS_FILE.open('w', encoding='utf-8') as f:
for email, meta in users.items():
f.write(f"{email}|{meta['username']}|{meta['password_hash']}\n")
def _check_password_validity(pw: str) -> Optional[str]:
"""检查密码是否符合 6-10 位、含大小写字母和数字的限制。"""
if not (6 <= len(pw) <= 10):
return '密码长度必须在 6 到 10 位之间'
if not PASSWORD_PATTERN.match(pw):
return '密码必须包含大小写字母和数字'
return None
def _get_user_by_identifier(identifier: str, users: Dict[str, Dict[str, str]]) -> Optional[str]:
"""通过邮箱或用户名查找用户邮箱。"""
if identifier in users:
return identifier
for email, meta in users.items():
if meta.get('username') == identifier:
return email
return None
# --- API 实现 ---
def check_identifier(identifier: str) -> Dict[str, Any]:
"""检查输入是邮箱或用户名并返回存在性。"""
users = _load_users()
exists = _get_user_by_identifier(identifier, users) is not None
return {'success': True, 'exists': exists, 'message': ''}
# send_registration_code 已在 api_bridge.py 中直接委托给 email_send.sendEmail(),故不再需要此函数。
def verify_registration_code(email: str, code: str) -> Dict[str, Any]:
"""校验验证码,委托给 email_send 模块。"""
if not EMAIL_PATTERN.match(email):
return {'success': False, 'message': '邮箱格式不合法 (.com)'}
# 核心:调用 email_send 模块的验证函数
if email_send.verify_code(email, code):
return {'success': True}
else:
# 不区分验证码不存在和过期,统一返回“验证码错误或已过期”
return {'success': False, 'message': '验证码错误或已过期'}
def complete_registration(email: str, username: str, pw1: str, pw2: str) -> Dict[str, Any]:
"""完成注册,设置用户名和密码。"""
# 【已修正】删除了对 _codes 的检查,因为 verify_registration_code 已经完成了验证
# 且验证成功后验证码记录已被清除。
if pw1 != pw2:
return {'success': False, 'message': '两次输入的密码不匹配'}
pw_error = _check_password_validity(pw1)
if pw_error:
return {'success': False, 'message': pw_error}
users = _load_users()
# 检查用户名是否唯一
for user_data in users.values():
if user_data.get('username') == username:
return {'success': False, 'message': '用户名已被占用'}
# 存储新用户
users[email] = {
'username': username,
'password_hash': _hash_password(pw1) # _hash_password 现在直接返回明文密码
}
_save_users(users)
# 【已修正】移除了 _codes.pop(email, None)
return {'success': True, 'message': '注册成功'}
def login(identifier: str, password: str) -> Dict[str, Any]:
"""登录(邮箱或用户名)。"""
users = _load_users()
target_email = _get_user_by_identifier(identifier, users)
if not target_email:
return {'success': False, 'message': '账号不存在'}
user = users[target_email]
# 比较明文密码
if user['password_hash'] == _hash_password(password): # _hash_password 现在直接返回明文密码
_current_user_info['email'] = target_email
_current_user_info['username'] = user['username']
return {'success': True, 'email': target_email, 'username': user['username']}
else:
return {'success': False, 'message': '密码错误'}
#供外部模块(如 paper_gen.py获取当前用户信息
def get_current_user_info() -> Dict[str, str]:
"""
返回当前登录用户的邮箱和用户名
Returns:
Dict[str, str]: {'email': '...', 'username': '...'} 或空字典 {}
"""
return _current_user_info.copy()
def change_password(email: str, old_pw: str, pw1: str, pw2: str) -> Dict[str, Any]:
"""修改密码(前端调用名为 change_password_full"""
users = _load_users()
user = users.get(email)
if not user:
return {'success': False, 'message': '用户未登录或邮箱不存在'}
# 比较明文密码
if user['password_hash'] != _hash_password(old_pw): # _hash_password 现在直接返回明文密码
return {'success': False, 'message': '原密码错误'}
if pw1 != pw2:
return {'success': False, 'message': '两次输入的新密码不匹配'}
pw_error = _check_password_validity(pw1)
if pw_error:
return {'success': False, 'message': '新密码校验失败: ' + pw_error}
user['password_hash'] = _hash_password(pw1) # _hash_password 现在直接返回明文密码
_save_users(users)
return {'success': True, 'message': '密码修改成功'}

@ -0,0 +1,24 @@
MIT License
Copyright (c) Evan You
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
---
This file is bundled with Vue's ESM build inside this project for full offline distribution.

File diff suppressed because it is too large Load Diff

@ -16,9 +16,12 @@
Google 风格文档约定
所有公共函数提供 Args / Returns中文解释为主便于本地团队与最终交付阅读
"""
import sys
"""应用启动入口 (start.py)"""
import os
import webview
from backend.api_router import Api
from backend.api_bridge import Api
from pathlib import Path
THIS_DIR = Path(__file__).resolve().parent
@ -26,53 +29,36 @@ FRONTEND_INDEX = THIS_DIR / 'frontend' / 'index.html'
def create_window():
"""创建主窗口并绑定后端 API。
Returns:
webview.Window: 已创建的 PyWebView 窗口实例
Raises:
SystemExit: 若前端入口文件不存在
"""
"""创建主窗口并绑定后端 API。"""
if not FRONTEND_INDEX.exists():
raise SystemExit('前端 index.html 未找到,请先确认路径')
print(f"ERROR: Frontend file not found at {FRONTEND_INDEX}")
raise SystemExit('前端 index.html 未找到,请先确认文件路径是否在 /frontend/ 目录下。')
api = Api()
window = webview.create_window(
'教育测评客户端',
FRONTEND_INDEX.as_uri(),
js_api=api,
width=1400,
height=900,
min_size=(800, 600),
resizable=True,
)
return window
def _prepare_webview2_flags():
"""配置 WebView2 允许本地文件互访 & 关闭同源限制。
def start_server():
"""启动应用。"""
print("Application starting...")
window = create_window()
webview.start()
说明
- `--allow-file-access-from-files` 使前端可读取同目录下静态资源
- `--disable-web-security` 关闭严格同源校验离线环境内可接受
"""
flags = os.environ.get('WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS', '')
needed = '--allow-file-access-from-files --disable-web-security'
for token in needed.split():
if token not in flags:
flags += (' ' + token)
os.environ['WEBVIEW2_ADDITIONAL_BROWSER_ARGUMENTS'] = flags.strip()
def main():
"""程序主入口。
步骤
1. 设置 WebView2 浏览器参数
2. 创建窗口并启动事件循环
"""
_prepare_webview2_flags()
_ = create_window()
# debug=True: 便于开发阶段查看前端控制台日志;
webview.start(debug=False)
if __name__ == '__main__':
main()
if sys.platform == "win32" and not getattr(sys, 'frozen', False):
dlls_path = Path(sys.executable).parent / 'DLLs'
if dlls_path.exists():
os.environ['PATH'] = str(dlls_path) + os.pathsep + os.environ.get('PATH', '')
start_server()
Loading…
Cancel
Save