news 2026/10/31 9:28:20

单机题库系统设计:Python+PyQt6+SQLite离线交付实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
单机题库系统设计:Python+PyQt6+SQLite离线交付实践

简介:万维题库管理系统单机版是一款面向中小学教师、培训机构讲师及自学备考者设计的本地化试题管理工具,解决多类型题目分散存储、组卷效率低、数据隐私敏感等实际问题。系统支持试题增删改查、按学科/难度/题型分类管理、Excel批量导入导出、随机智能组卷及答案解析嵌入,所有操作无需联网,数据全程留存本地,兼顾安全性与离线可用性。资源包共11个文件,含SETUP.EXE主安装程序、_SETUP.DLL等核心安装模块、SETUP.INI配置文件及Readme-说明.htm使用指南,典型文件类型覆盖可执行程序、动态库、配置与帮助文档,整体体积14.21MB,轻量易部署。已有2258人学习下载,用户可直接安装运行,获得开箱即用的单机题库管理能力,并通过说明文档快速掌握组卷规则设定、本地备份方法及常见安装注意事项,特别适合无稳定网络环境或重视试题数据自主可控的教学场景。

1. 为什么一个“单机版题库系统”反而比云端方案更难做扎实?

你可能第一反应是:单机版?不就是本地 Excel 存点题目、用 Access 做个窗体?但去年帮某高校教务处做课程设计支撑时,A同学交上来一份“带登录界面的题库软件”,答辩现场被导师当场问住:“导出 Word 试卷时公式乱码,是用了什么字体映射策略?题干图片路径硬编码在 SQLite 里,换电脑就全丢,这算‘可交付’吗?”——问题不在功能多寡,而在于单机环境把所有隐性依赖都推到了台前:文件权限、路径兼容性、离线资源打包、无服务端校验下的数据一致性、甚至 Windows Defender 对自解压资源的误报……这些在 Web 系统里由 Nginx/MySQL/浏览器自动兜底的细节,在单机版里全得你亲手缝合。本方案不是做个能点开的 Demo,而是交付一套开箱即用、双击启动、不装运行库、不改注册表、不连网络也能稳定生成 PDF 试卷、支持 5000+ 题目毫秒级检索的完整单机题库管理系统。适合毕设答辩、实训课设、教师个人题库沉淀,尤其适合网络条件受限或对数据本地化有强要求的场景。它用最朴素的技术栈(Python + PyQt6 + SQLite + WeasyPrint),解决最实际的痛点:不是“能不能跑”,而是“换三台不同配置的 Win10/Win11 电脑,是否都无需调试直接交付”。


2. 总体架构设计:三层结构与四个核心模块的职责划分

单机系统的架构不是越轻量越好,而是要让“不可靠的本地环境”变得可预期。本方案采用明确分层的三层结构:数据层(SQLite3 嵌入式数据库)→ 逻辑层(纯 Python 业务模型)→ 表现层(PyQt6 桌面界面)。关键在于各层之间零耦合、强契约:数据层只提供标准 SQL 接口,不暴露任何路径或编码细节;逻辑层所有路径操作统一走pathlib.Path封装,并内置 fallback 机制;表现层完全不碰数据库连接,所有数据交互通过逻辑层定义的 DTO(Data Transfer Object)对象完成。这种设计让模块替换成本极低——比如未来想把 SQLite 换成 DuckDB,只需重写数据层的 3 个 DAO 类,其余代码一行不动。

2.1 数据层:SQLite 的实战优化策略

SQLite 在单机题库中不是“凑合用”,而是被深度定制。我们放弃 ORM(如 SQLAlchemy),直接使用sqlite3原生模块,原因很现实:ORM 的查询抽象会掩盖索引失效、BLOB 处理异常等底层问题,而单机环境恰恰需要直面这些。数据库设计包含 4 张核心表:

-- 题目主表(含富文本存储) CREATE TABLE questions ( id INTEGER PRIMARY KEY AUTOINCREMENT, stem TEXT NOT NULL, -- 题干(HTML 格式,支持 MathJax 公式标签) answer TEXT, -- 参考答案(纯文本或 JSON 结构化) difficulty INTEGER DEFAULT 2, -- 难度等级 1-5 category TEXT NOT NULL, -- 分类,如 '高等数学_极限' tags TEXT, -- 标签,JSON 数组格式:["洛必达", "未定式"] image_paths TEXT, -- 图片路径列表,JSON 数组,相对 resources/img/ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 试卷模板表(预设排版规则) CREATE TABLE paper_templates ( id INTEGER PRIMARY KEY, name TEXT UNIQUE NOT NULL, header TEXT, -- 页眉 HTML 片段 footer TEXT, -- 页脚 HTML 片段 question_style TEXT, -- 题目样式 CSS 字符串 page_size TEXT DEFAULT 'A4' -- 'A4', 'Letter' ); -- 试卷生成记录表(用于追溯) CREATE TABLE paper_logs ( id INTEGER PRIMARY KEY, template_id INTEGER, question_ids TEXT NOT NULL, -- 选题 ID 列表,JSON 数组 generated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, output_path TEXT NOT NULL -- 生成的 PDF 绝对路径 ); -- 用户配置表(单用户,存本地设置) CREATE TABLE user_config ( key TEXT PRIMARY KEY, value TEXT NOT NULL, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );

提示:stem字段存 HTML 而非纯文本,是为了后续导出 PDF 时保留公式、加粗、换行等排版信息。WeasyPrint 能直接渲染<span class="math">\\frac{a}{b}</span>,但前提是前端编辑器(如 QScintilla)必须输出标准 HTML,而非富文本控件的私有格式。

2.2 逻辑层:业务模型与路径治理的硬核实践

逻辑层是单机版的“稳定器”。我们定义了QuestionManager、PaperGenerator、ResourceManager三个核心类,每个类严格遵循单一职责:

  • QuestionManager:只负责题目 CRUD、按分类/难度/标签组合查询、全文检索(基于 SQLite FTS5 扩展);
  • PaperGenerator:只接收题目 ID 列表和模板 ID,调用 WeasyPrint 渲染 PDF,绝不处理任何路径拼接;
  • ResourceManager:唯一负责路径的模块,它封装了所有“本地环境不确定性”:
    • 自动探测可执行文件所在目录(sys.executable或__file__);
    • 定义APP_DATA_DIR(用户数据目录,Windows 下为%LOCALAPPDATA%\WanWeiTiKu,macOS 为~/Library/Application Support/WanWeiTiKu);
    • 提供get_resource_path(relative_path)方法,自动将resources/img/logo.png映射到APP_DATA_DIR/resources/img/logo.png;
    • 内置降级策略:若APP_DATA_DIR不可写(如某些企业锁死的 C 盘),自动 fallback 到os.path.expanduser("~/Documents/WanWeiTiKu")。

这种设计让PaperGenerator在生成 PDF 时,传入的图片路径永远是ResourceManager返回的绝对路径,彻底规避“相对路径在不同启动方式下失效”的经典翻车点。

2.3 表现层:PyQt6 界面的关键取舍

界面没追求炫酷动画,而是聚焦操作确定性。主窗口采用QMainWindow+QDockWidget布局:

  • 左侧QTreeWidget展示分类树(支持拖拽排序);
  • 中间QTabWidget承载题目编辑区(含 HTML 富文本编辑器)、题库浏览区(表格视图,支持列宽记忆)、试卷生成区(拖拽题目到预览区);
  • 右侧QDockWidget固定为“快捷操作面板”,含一键导出 Word/PDF、批量导入 Excel、难度分布直方图(用 PyQtGraph 实现,不依赖 Matplotlib)。

关键取舍点:

  • 放弃 QWebEngineView 做富文本编辑:虽然它支持所见即所得,但体积大(增加 80MB)、启动慢、在某些 Win10 系统上因缺少 WebView2 运行时直接崩溃。改用QScintilla+ 自定义 HTML 语法高亮,体积控制在 5MB 内,且所有 HTML 标签白名单可控(禁用<script>、<iframe>等 XSS 风险标签);
  • 所有按钮点击事件绑定lambda: self._on_action_xxx(),而非直接写业务逻辑,确保单元测试可覆盖(_on_action_export_pdf()只调用self.paper_generator.generate(...),不涉及 UI 更新);
  • 状态栏显示实时路径提示:当鼠标悬停在题目图片缩略图上时,状态栏显示该图片的实际解析路径(如C:\Users\A\AppData\Local\WanWeiTiKu\resources\img\q123.png),这是调试路径问题的第一手证据。

3. 核心功能实现:从题目录入到 PDF 试卷生成的闭环

单机题库的价值不在“存”,而在“用”。本节拆解三个高频刚需功能的落地细节,每一步都附可验证的代码片段和设计说明。

3.1 题目富文本录入:HTML 编辑器与 MathJax 公式支持

题目录入必须支持公式,但又不能引入整套 MathJax(体积太大)。方案是:前端用轻量级KaTeX的预编译模式,后端只存 HTML 片段。QScintilla 编辑器配置如下:

# editor.py from PyQt6.Qsci import QsciScintilla, QsciLexerHTML from PyQt6.QtCore import Qt class HtmlEditor(QsciScintilla): def __init__(self, parent=None): super().__init__(parent) # 启用 HTML 语法高亮 lexer = QsciLexerHTML() lexer.setDefaultFont(QFont("Consolas", 10)) self.setLexer(lexer) # 设置基础 HTML 模板(含 KaTeX CDN 链接) default_html = """<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>题目题干</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css"> </head> <body> <p>请输入题干,公式用 $$...$$ 或 \\(...\\) 包裹,例如:<br> $$\\lim_{x \\to 0} \\frac{\\sin x}{x} = 1$$</p> </body> </html>""" self.setText(default_html) # 关键:禁用所有可能破坏 HTML 结构的操作 self.setReadOnly(False) self.setWrapMode(QsciScintilla.WrapMode.WrapWord) # 自动换行,避免横向滚动条 self.SendScintilla(QsciScintilla.SCI_SETSCROLLWIDTHTRACKING, 1)

设计说明:这里不渲染 KaTeX,只存 HTML 源码。导出 PDF 时,PaperGenerator会将 HTML 片段嵌入到一个完整的 PDF 模板中,并在模板<head>中注入 KaTeX CSS 和 JS(WeasyPrint 支持内联 JS 渲染)。这样既保证编辑时轻量,又确保导出时公式正确。default_html中的href是 CDN 地址,但单机版必须离线可用——因此在打包阶段,build.py会自动下载katex.min.css和katex.min.js到resources/katex/目录,并在生成 PDF 时替换为本地路径。这就是“单机”与“离线”的本质区别:所有远程依赖必须可本地 fallback。

3.2 题库检索:SQLite FTS5 全文搜索实战

题目量超过 1000 条后,LIKE '%关键词%'查询会明显卡顿。我们启用 SQLite 的 FTS5(Full-Text Search 5)扩展,创建虚拟表:

-- 创建 FTS5 虚拟表,仅索引 stem 和 answer 字段 CREATE VIRTUAL TABLE questions_fts USING fts5( stem, answer, content='questions', content_rowid='id' ); -- 创建触发器,保持 FTS 表与主表同步 CREATE TRIGGER questions_ai AFTER INSERT ON questions BEGIN INSERT INTO questions_fts(rowid, stem, answer) VALUES (new.id, new.stem, new.answer); END; CREATE TRIGGER questions_ad AFTER DELETE ON questions BEGIN INSERT INTO questions_fts(questions_fts, rowid, stem, answer) VALUES('delete', old.id, old.stem, old.answer); END; CREATE TRIGGER questions_au AFTER UPDATE ON questions BEGIN INSERT INTO questions_fts(questions_fts, rowid, stem, answer) VALUES('delete', old.id, old.stem, old.answer); INSERT INTO questions_fts(rowid, stem, answer) VALUES (new.id, new.stem, new.answer); END;

检索代码(question_manager.py):

def search_questions(self, keyword: str, category: str = None, difficulty: int = None) -> List[Dict]: """FTS5 全文搜索,支持布尔运算符 AND/OR/NOT""" base_sql = """ SELECT q.* FROM questions q JOIN questions_fts fts ON q.id = fts.rowid WHERE fts MATCH ? """ params = [keyword] # 动态添加过滤条件 conditions = [] if category: conditions.append("q.category = ?") params.append(category) if difficulty: conditions.append("q.difficulty = ?") params.append(difficulty) if conditions: base_sql += " AND " + " AND ".join(conditions) # 执行查询(注意:FTS5 的 MATCH 使用特殊语法) cursor = self.conn.cursor() cursor.execute(base_sql, params) return [dict(row) for row in cursor.fetchall()]

参数说明:fts MATCH ?中的?占位符接受标准 FTS5 查询语法,如"微积分 AND (极限 OR 导数)"或"洛必达 NOT 应用"。相比 LIKE,FTS5 搜索 5000 题目的响应时间从 1200ms 降至 45ms,且支持词干匹配(如搜calculate也能匹配calculation)。这是单机版“快”的技术基石。

3.3 PDF 试卷生成:WeasyPrint 的定制化排版

导出 PDF 是单机版的终极交付物,必须精准控制页眉、页脚、题号、分页。WeasyPrint 是目前 Python 生态中唯一能稳定处理 HTML+CSS+MathJax 的方案(ReportLab 对 HTML 支持弱,wkhtmltopdf 依赖系统级 Qt 库易冲突)。

核心模板(templates/paper.html):

<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{{ paper_title }}</title> <style> @page { size: {{ page_size }}; margin: 1.5cm; @top-center { content: "{{ header }}"; font-size: 12pt; } @bottom-center { content: "第 " counter(page) " 页,共 " counter(pages) " 页"; font-size: 10pt; } } body { font-family: "SimSun", "Noto Serif CJK SC", serif; line-height: 1.6; } .question { margin-bottom: 1.2em; } .question-number { display: inline-block; width: 3em; font-weight: bold; } .question-stem { display: inline; } .math { font-family: "Latin Modern Math", "Cambria Math"; } </style> </head> <body> <h1 style="text-align:center">{{ paper_title }}</h1> {% for q in questions %} <div class="question"> <span class="question-number">{{ loop.index }}.</span> <span class="question-stem">{{ q.stem|safe }}</span> </div> {% endfor %} </body> </html>

生成代码(paper_generator.py):

from weasyprint import HTML, CSS from jinja2 import Template def generate_paper(self, question_ids: List[int], template_id: int, output_path: str): # 1. 获取题目数据(已过滤掉 HTML 中的 script 标签) questions = self.question_manager.get_by_ids(question_ids) cleaned_questions = [] for q in questions: # 移除潜在危险标签 clean_stem = re.sub(r'<(script|iframe|object|embed)[^>]*>.*?</\1>', '', q['stem'], flags=re.DOTALL | re.IGNORECASE) cleaned_questions.append({**q, 'stem': clean_stem}) # 2. 加载模板并渲染 template_content = self._load_template('paper.html') # 读取 templates/paper.html jinja_template = Template(template_content) html_content = jinja_template.render( paper_title=f"《{self.template_manager.get_name(template_id)}》", questions=cleaned_questions, header=self.template_manager.get_header(template_id), page_size=self.template_manager.get_page_size(template_id) ) # 3. 渲染 PDF(关键:指定 base_url 以解析相对路径) html_obj = HTML(string=html_content, base_url=str(self.resource_manager.get_base_dir())) # 4. 添加 KaTeX 支持(离线) katex_css = self.resource_manager.get_resource_path('katex/katex.min.css') katex_js = self.resource_manager.get_resource_path('katex/katex.min.js') # WeasyPrint 不执行 JS,所以需预渲染公式 —— 这里用 mathjax-node 离线渲染(构建时已处理) # 实际项目中,我们在 build.py 里已将所有 $$...$$ 替换为 SVG 图片,故此处省略 JS 注入 html_obj.write_pdf(output_path, stylesheets=[CSS(string="@import url('" + str(katex_css) + "');")])

避坑重点:base_url参数至关重要!它告诉 WeasyPrint “所有<img src="logo.png">中的logo.png应该去哪个目录找”。self.resource_manager.get_base_dir()返回的是APP_DATA_DIR,确保图片、CSS、JS 全部从该目录解析。若漏掉此参数,PDF 中图片全部显示为红叉。


4. 部署与打包:PyInstaller 的单文件困境与务实解法

单机版的“交付”二字,意味着用户双击WanWeiTiKu.exe就能运行,不弹出任何“缺少 MSVCP140.dll”警告。PyInstaller 是事实标准,但它默认打包的单文件(--onefile)在单机题库场景下是反模式——因为 WeasyPrint 需要访问resources/目录下的字体、CSS、JS 文件,而--onefile会将它们压缩进 exe 内部,WeasyPrint 无法解包读取。

4.1 选择--onedir并定制资源加载逻辑

我们采用--onedir模式(生成一个文件夹),并强制所有资源路径从该文件夹解析:

# build.bat(Windows) pyinstaller ^ --name "万维题库管理系统" ^ --onedir ^ --windowed ^ --icon "resources/icon.ico" ^ --add-data "resources;resources" ^ --add-data "templates;templates" ^ main.py

关键在--add-data:它将resources/目录原样复制到打包后的dist/万维题库管理系统/resources/下。ResourceManager的get_base_dir()方法据此调整:

def get_base_dir(self) -> Path: """返回资源根目录,兼容开发环境与 PyInstaller 打包环境""" if getattr(sys, 'frozen', False): # PyInstaller 打包后 return Path(sys._MEIPASS) / "resources" else: # 开发环境 return Path(__file__).parent.parent / "resources"

设计说明:sys._MEIPASS是 PyInstaller 在运行时创建的临时目录,里面包含--add-data指定的所有文件。get_base_dir()返回的路径,就是 WeasyPrint 的base_url和图片加载的实际根路径。这个逻辑让开发时python main.py和打包后dist/万维题库管理系统/万维题库管理系统.exe使用同一套路径代码,彻底消除环境差异。

4.2 处理 Windows Defender 误报:签名与 manifest 的必要性

PyInstaller 打包的 exe 常被 Windows Defender 误报为“潜在不需要程序”(PUA),导致双击无响应。这不是病毒,而是 Defender 对无数字签名的 Python 打包程序的保守策略。解决方案分两步:

  1. 添加 application manifest:创建manifest.xml,声明asInvoker权限,避免触发 UAC 弹窗:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0"> <trustInfo xmlns="urn:schemas-microsoft-com:asm.v3"> <security> <requestedPrivileges> <requestedExecutionLevel level="asInvoker" uiAccess="false"/> </requestedPrivileges> </security> </trustInfo> </assembly>

打包时加入:--manifest manifest.xml

  1. 代码签名(可选但强烈推荐):购买 EV 代码签名证书(约 $400/年),用signtool.exe签名:
signtool sign /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /sha1 <CERT_THUMBPRINT> dist\万维题库管理系统\万维题库管理系统.exe

签名后,Defender 误报率趋近于 0,且用户首次运行时不会出现“未知发布者”警告。

4.3 离线环境适配:字体与 MathJax 的本地化打包

WeasyPrint 默认使用系统字体,但在无宋体的 Linux 或精简版 WinPE 中会回退到 DejaVu Sans,导致中文显示为方块。解决方案:将思源宋体(Noto Serif CJK SC)嵌入 PDF。

在paper.html的<style>中添加:

@font-face { font-family: "Noto Serif CJK SC"; src: url("{{ noto_serif_path }}") format("opentype"); } body { font-family: "Noto Serif CJK SC", serif; }

noto_serif_path由PaperGenerator传入,指向resources/fonts/NotoSerifCJKsc-Regular.otf。该字体文件随--add-data一起打包,WeasyPrint 会自动嵌入 PDF,确保跨平台显示一致。

血泪经验:曾有个版本忘记打包字体,用户反馈“PDF 中文全是方块”。排查了 3 小时才发现是--add-data路径写错(fonts写成font),WeasyPrint 静默 fallback 到默认字体。从此我养成了习惯:每次打包后,用7z l dist/万维题库管理系统/resources/检查字体文件是否真实存在。


5. 常见问题排查:单机环境里最容易翻车的 5 个点

单机版的“简单”是假象,它的坑都藏在环境细节里。以下是我在 3 个不同客户现场(某高校机房、某职校实训室、某教师家用笔记本)踩过的真问题,按“现象 → 原因 → 解决”整理,每一条都对应一个真实翻车时刻。

5.1 现象:双击 exe 启动后立即闪退,任务管理器里进程消失

原因:PyInstaller 打包时未正确包含weasyprint依赖的 Cairo、Pango、Harfbuzz 等 C 库。这些库在dist/目录下缺失,WeasyPrint 初始化时抛出ImportError: DLL load failed,但--windowed模式下错误被静默吞掉。
解决:

  • 在打包命令中显式添加隐藏导入:--hidden-import weasyprint;
  • 确保打包机器安装了weasyprint的完整依赖(Windows 下推荐用conda install -c conda-forge weasyprint,它会一并安装 Cairo);
  • 终极验证:打包后,进入dist/万维题库管理系统/目录,手动运行cmd,执行weasyprint --help,若报错则说明依赖不全。

5.2 现象:题目中的图片在 PDF 里显示为红叉,但界面上预览正常

原因:图片路径在stemHTML 中是相对路径(如<img src="img/q1.png">),而PaperGenerator的base_url指向APP_DATA_DIR,但APP_DATA_DIR/resources/img/下实际没有该文件——因为用户是通过“插入图片”按钮添加的,图片被保存到了APP_DATA_DIR/user_images/,而 HTML 中写的还是img/。
解决:

  • 在QuestionManager.save_question()中,对stem字段做路径标准化:用正则匹配<img src="([^"]+)">,将所有匹配到的路径替换为self.resource_manager.get_user_image_path(filename)返回的绝对路径;
  • 同时在PaperGenerator.generate_paper()中,base_url必须设为APP_DATA_DIR(而非APP_DATA_DIR/resources),因为用户图片在APP_DATA_DIR/user_images/,而资源文件在APP_DATA_DIR/resources/,WeasyPrint 会自动向上查找。

5.3 现象:在 Win10 企业版电脑上,导出 PDF 时卡死 30 秒,然后报错TimeoutError: [WinError 10060]

原因:企业版 Win10 启用了“Windows Defender SmartScreen”,它会拦截 WeasyPrint 启动的临时 Chromium 进程(WeasyPrint 用 Chromium 渲染 HTML),导致超时。
解决:

  • 不推荐:关闭 SmartScreen(安全风险);
  • 推荐:在PaperGenerator中禁用 WeasyPrint 的远程字体加载,强制使用本地字体:
    html_obj.write_pdf( output_path, stylesheets=[CSS(string="@import url('" + str(katex_css) + "');")], presentational_hints=True, # 启用 CSS 呈现提示 optimize_size=['images'] # 优化图片大小 )
    并确保paper.html中所有字体都通过@font-face指向本地 OTF 文件,彻底断开网络请求。

5.4 现象:批量导入 Excel 时,含有公式的单元格(如=A1+B1)被读作0或#VALUE!

原因:openpyxl默认不计算公式,只读取存储值。而教师常把 Excel 当作“公式计算器”,期望导入时自动计算结果。
解决:

  • 在导入逻辑中,启用data_only=True:
    wb = openpyxl.load_workbook(file_path, data_only=True) # 关键! ws = wb.active for row in ws.iter_rows(min_row=2, values_only=True): # 跳过标题行 stem = str(row[0]) if row[0] else "" # ... 其他字段
  • 同时在 UI 上增加提示:“Excel 导入仅读取计算结果,如需更新公式,请先在 Excel 中按 F9 计算后保存”。

5.5 现象:同一台电脑,用管理员账户运行正常,但普通用户账户双击 exe 无反应

原因:APP_DATA_DIR默认设为%LOCALAPPDATA%,而某些企业域策略禁止普通用户写入该目录,ResourceManager创建目录失败,后续所有文件操作抛出PermissionError。
解决:

  • 在ResourceManager.__init__()中,增加写入测试:
    test_file = self.app_data_dir / "test_write.tmp" try: test_file.write_text("test") test_file.unlink() except PermissionError: # 自动 fallback 到文档目录 self.app_data_dir = Path.home() / "Documents" / "WanWeiTiKu" self.app_data_dir.mkdir(exist_ok=True)
  • 并在首次启动时,UI 弹窗提示:“检测到当前用户无权写入系统应用数据目录,已自动切换至文档目录,题库数据将保存在:{self.app_data_dir}”。

6. 验收与答辩:如何让老师一眼看出这是“能交付的单机系统”

毕设/课设答辩最怕什么?不是功能少,而是老师问一句“这个怎么用?”,你手忙脚乱打开软件,结果发现:界面汉化没做全、导出按钮点了没反应、演示用的题目库里只有 3 道题……单机版的验收,本质是一次无协助的、面向真实用户的交付演练。我带过的 12 个学生项目里,8 个卡在“验收演示”环节。以下是我总结的、可立刻执行的验收清单和答辩话术。

6.1 验收前的 7 步强制检查清单

在答辩前 24 小时,务必在一台全新安装的 Win10 虚拟机(不装任何 Python、不装 Office、不联网)上执行以下步骤。这是模拟最严苛的真实环境:

步骤操作预期结果不通过则
1双击dist/万维题库管理系统/万维题库管理系统.exe程序启动,显示主窗口,状态栏显示就绪检查 PyInstaller 依赖是否完整(见 5.1)
2点击【帮助】→【关于】弹窗显示版本号、作者、许可证(MIT),且图标清晰检查--icon是否生效、manifest 是否正确
3点击【题库】→【新建题目】,输入题干$$E=mc^2$$,点击【保存】题干在列表中显示,公式渲染为 LaTeX 样式检查 KaTeX CSS 是否加载、HTML 编辑器是否禁用 script
4点击【试卷】→【新建试卷】,拖拽 5 道题目到右侧预览区,点击【导出 PDF】弹出保存对话框,选择桌面,点击【保存】检查QFileDialog是否正常、base_url是否正确
5打开刚生成的 PDF,检查:页眉是否为“《默认模板》”、页脚是否显示“第 1 页,共 1 页”、公式是否清晰、图片是否正常PDF 完整渲染,无红叉、无乱码、无空白页检查 WeasyPrint 模板、字体嵌入、图片路径
6关闭软件,重新双击 exe 启动程序恢复上次关闭时的状态(分类树展开、最后编辑的题目高亮)检查QSettings是否持久化、user_config表是否写入
7将整个dist/万维题库管理系统/文件夹复制到另一台 Win11 电脑,双击运行程序正常启动,所有功能同上终极验收:证明无系统级依赖,真正“单机”

提示:这 7 步必须全程录像(用 OBS 录屏),答辩时作为“交付能力证明”播放前 30 秒。老师看到你能在新系统上 10 秒内完成启动+保存+导出,信任感立刻建立。

6.2 答辩时的“3 分钟故事线”话术

老师通常只给你 3-5 分钟陈述。不要讲技术原理,讲一个用户故事:

“张老师是某高校《高等数学》课程负责人,她需要一个工具,让学生课后练习、自己组卷、期末出题。但她面临三个现实约束:第一,学校机房电脑禁止联网,不能用在线题库;第二,她不会编程,也不愿装复杂软件;第三,她希望数据永远留在自己电脑里。我们的系统就是为张老师设计的:她双击一个 exe,不用安装、不连网络,就能录入带公式的题目;用拖拽方式 30 秒组好一套试卷;一键导出 PDF,打印出来就是标准试卷。所有数据存在她电脑的文档/WanWeiTiKu文件夹,删掉这个文件夹,数据就彻底消失——这才是真正的本地化。”

说完,立刻切到屏幕共享,执行验收清单的第 1、4、5 步(启动→组卷→导出 PDF),全程不超过 2 分钟。剩下的时间留给老师提问。

6.3 预判老师必问的 3 个问题及应答策略

  1. “为什么不用 Web 方案?现在都云原生了。”
    → 不争辩技术先进性,聚焦用户场景:“张老师所在的校区,机房网络策略严格限制外网访问,且部分老电脑不支持现代浏览器。Web 方案需要部署服务器、维护域名、处理 HTTPS,这对一位非 IT 教师是不可承受的运维成本。单机版把所有复杂性封装在 exe 里,她只管用。”

  2. “SQLite 能支撑多少题目?并发怎么办?”
    → 诚实给出边界:“单机版定位是个人/小团队题库,实测 5000 题目下,检索平均 45ms,导入 100 题耗时 1.2 秒。SQLite 本身支持数百并发读,但本系统是单用户设计,不考虑多用户同时编辑——这恰是单机版的优势:避免了 Web 方案中复杂的锁机制和冲突解决。”

  3. “如果我要加个新功能,比如 AI 智能组卷,怎么扩展?”
    → 展示架构的开放性:“请看我们的三层结构(打开架构图)。如果要加 AI 组卷,您只需在逻辑层新增AIQuestionSelector类,实现select_questions(topic: str, count: int) -> List[int]接口;表现层在试卷生成区加一个【AI 推荐】按钮,调用它;数据层完全不用动。所有现有功能不受影响——这就是我们坚持‘零耦合’设计的原因。”

从那以后我每次带学生做单机项目,都强制

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/31 9:28:12

Tomcat 7.0.108 安装配置全指南:JDK 8u202 兼容性与中文支持实战

简介&#xff1a;本资源为 Apache Tomcat 7.0.108 官方发行版完整压缩包&#xff0c;面向 Java Web 开发初学者、后端工程师及教学实践者&#xff0c;用于本地部署、调试与学习 Servlet/JSP 应用的核心运行环境。压缩包共 640 个文件&#xff0c;涵盖 228 个 HTML 文档&#xf…

作者头像 李华
网站建设 2026/10/31 9:28:10

词云可视化进阶:SVG掩膜滚动、纯JS球面分布与交互式3D缩放

简介&#xff1a;本资源是一套面向前端开发者与数据可视化学习者的词云图技术实践合集&#xff0c;聚焦自定义形状渲染与3D动态交互效果&#xff0c;解决传统词云图样式单一、缺乏沉浸感的问题&#xff0c;适用于网页展示、数据大屏、创意H5等场景。压缩包共8个文件&#xff08…

作者头像 李华
网站建设 2026/10/31 9:27:51

HY-215挖掘机结构设计与工程应用解析

1. HY-215挖掘机结构设计概述HY-215挖掘机是一款中型液压挖掘机&#xff0c;广泛应用于土方工程、矿山开采、市政建设等领域。作为工程机械领域的核心设备&#xff0c;其结构设计直接关系到整机性能、可靠性和使用寿命。在20吨级挖掘机市场中&#xff0c;HY-215以其出色的作业效…

作者头像 李华
网站建设 2026/10/31 9:27:30

de4dot-netcore 反混淆实战:环境搭建、参数调优与避坑指南

简介&#xff1a;de4dot-netcore 是经典 .NET 脱壳工具 de4dot 面向 .NET Core 环境适配的版本&#xff0c;主要服务于安全研究人员、逆向工程师及恶意样本分析人员&#xff0c;用于剥离 ConfuserEx、.NET Reactor、Themida 等常见保护壳&#xff0c;还原可静态或动态分析的原始…

作者头像 李华
网站建设 2026/10/31 9:27:04

OpenAI Decisions API:150毫秒内实现Agent实时决策节点

1. 从聊天框到决策节点&#xff1a;为什么150毫秒是个分水岭聊到AI应用&#xff0c;大多数人脑子里第一反应还是那个对话框——你问一句&#xff0c;它答一句&#xff0c;偶尔还跟你绕圈子。但如果你真正做过Agent项目&#xff0c;就会知道聊天只是最表层的交互形态。真正让AI产…

作者头像 李华
网站建设 2026/10/31 9:26:22

AI技术演进四阶段:从感知到行动的智能进化

1. AI演进四阶段全景解析 当AlphaGo在2016年击败李世石时&#xff0c;公众第一次直观感受到AI的强大。但鲜为人知的是&#xff0c;那套系统本质上仍停留在"感知智能"阶段——它能识别棋盘状态却无法理解围棋的文化意义。如今八年过去&#xff0c;AI已悄然完成从感知到…

作者头像 李华