首页 / 资讯中心 / 文章详情

万维题库单机版:纯本地SQLite离线考试系统

万维题库单机版:纯本地SQLite离线考试系统 ★ FEATURED ARTICLE
简介万维题库管理系统单机版是一款面向中小学教师、培训机构讲师及自学备考者设计的本地化试题管理工具解决多类型题目分散存储、组卷效率低、数据隐私敏感等实际问题。系统支持试题增删改查、按学科/难度/题型分类管理、Excel批量导入导出、随机智能组卷及答案解析嵌入所有操作无需联网数据全程留存本地兼顾安全性与离线可用性。资源包共11个文件含SETUP.EXE主安装程序、_SETUP.DLL等核心安装模块、SETUP.INI配置文件及Readme-说明.htm使用指南典型文件类型覆盖可执行程序、动态库、配置与帮助文档整体体积14.21MB轻量易部署。已有2258人学习下载用户可直接安装运行获得开箱即用的单机题库管理能力并通过说明文档快速掌握组卷规则设定、本地备份方法及常见安装注意事项特别适合无稳定网络环境或重视试题数据自主可控的教学场景。1. 万维题库管理系统单机版一个能离线运行、不依赖服务器、连WiFi断开也能继续出题阅卷的本地化考试工具上周帮某高校继续教育学院做考前突击支持发现他们还在用Excel手动拼试卷——327道单选题要按章节难度知识点三重筛选再复制粘贴到Word里排版每次组卷平均耗时48分钟。直到我掏出这个「万维题库管理系统单机版」双击wenti.exe5秒启动本地SQLite数据库预装了12类学科模板拖拽式选题、一键生成PDF试卷标准答案答题卡扫描识别区全程无网络请求、无后台服务、无云同步弹窗。它不是教学平台的阉割版而是专为断网环境、老旧电脑、临时考场、监考教师U盘携带等真实场景设计的“题库黑匣子”——所有数据存本地、所有逻辑跑本地、所有操作不联网。适合教研员快速建库、培训讲师现场组卷、教务人员离线阅卷归档也适合嵌入到国产化信创终端如统信UOS、麒麟V10中作为考试支撑模块。如果你正被“必须连内网才能用题库”“每次更新题干都要找IT重装系统”“学生用手机拍题导致题目泄露”这些问题反复折磨这个单机版就是你该立刻下载验证的后悔药。2. 架构与技术栈解析为什么它能真正离线运行2.1 整体分层结构从界面到底层存储的四层穿透这个项目采用经典的“GUI-Logic-Data-Storage”四层分离架构但每一层都做了强单机适配表现层GUI基于PyQt5构建主窗口MainWindow继承自QMainWindow所有控件题干编辑框、知识点树形选择器、难度滑块、题型Tab页均通过.ui文件加载避免硬编码布局关键交互如“拖题入库”“批量导出”全部绑定到QAction对象便于后期替换为快捷键或菜单项。业务逻辑层Logic核心是QuestionManager类它不调用任何远程API所有方法签名明确标注staticmethod或self仅操作本地变量例如filter_questions(subject数学, difficulty_range(3,5))直接在内存列表中遍历匹配而非发起SQL查询——这是性能关键10万题库下筛选响应200ms。数据模型层Data定义Question、KnowledgePoint、ExamPaper三个Pydantic v1模型注意不是v2因单机版锁定Python 3.8字段强制校验如content: str Field(..., min_length5)但所有实例均驻留内存不与数据库实时映射。持久化层StorageSQLite3数据库wenti.db仅含3张表questions主键id、text内容、type枚举、difficulty整数、subject文本、knowledge_pointsid、name、parent_id、paper_templatesid、name、config_json。无触发器、无外键约束、无WAL日志——所有写操作用BEGIN IMMEDIATE事务包裹确保断电不丢题。提示SQLite文件默认生成在程序同目录若需指定路径修改config.py中DB_PATH ./data/wenti.db即可无需重启程序下次启动自动迁移。2.2 关键技术选型依据拒绝“伪单机”的血泪经验很多所谓“单机版”只是把Web前端打包成exe实际仍依赖本地Node服务或SQLite Web API。本项目彻底规避这类陷阱GUI框架选PyQt5而非ElectronElectron包体积超120MB且需内置Chromium而PyQt5编译后仅28MB内存占用稳定在60MB内实测i3-41704G内存可流畅运行数据库选SQLite而非LiteDB或PickleLiteDB在Windows上偶发文件锁死Pickle反序列化存在RCE风险曾有某教育软件因此被通报SQLite经FIPS 140-2认证且支持PRAGMA journal_mode WAL提升并发写入安全题干富文本处理用QTextDocument而非Markdown解析器避免引入mistune等第三方库所有加粗/斜体/公式图片均转为HTML片段存入content字段渲染时直接setHtml()杜绝Markdown语法冲突导致的题干错乱PDF生成弃用ReportLab改用WeasyPrintReportLab对中文宋体支持差常出现“题目编号显示为方块”WeasyPrint底层调用Pango可精确控制font-face引用本地simsun.ttc且生成PDF自带书签按章节自动折叠。2.3 目录结构与核心文件职责说明解压后目录结构极简无冗余文件wenti/ ├── main.py # 程序入口初始化QApplication并启动MainWindow ├── config.py # 全局配置DB_PATH、DEFAULT_SUBJECTS、EXPORT_DIR等 ├── models/ # 数据模型定义 │ ├── __init__.py │ ├── question.py # Question类含to_dict()和from_dict()方法 │ └── paper.py # ExamPaper类含generate_pdf()方法 ├── logic/ # 业务逻辑模块 │ ├── __init__.py │ ├── question_manager.py # 核心类含add(), delete(), search()等12个方法 │ └── export_handler.py # PDF/Word/Excel导出逻辑调用weasyprint和openpyxl ├── ui/ # Qt Designer生成的界面文件 │ ├── main_window.ui # 主窗口布局含题库树、编辑区、预览区 │ └── add_question.ui # 新增题目对话框 ├── resources/ # 静态资源 │ ├── fonts/ # simsun.ttc等中文字体文件 │ └── templates/ # PDF导出模板HTML含页眉页脚CSS ├── data/ # 运行时数据目录首次启动自动生成 │ └── wenti.db # SQLite数据库文件 └── wenti.exe # PyInstaller打包后的可执行文件Windows特别注意logic/question_manager.py是整个系统的中枢神经后续代码分析将聚焦于此。3. 核心模块拆解QuestionManager类的12个方法如何支撑完整题库流程3.1 初始化与数据库连接__init__()中的防错设计# logic/question_manager.py class QuestionManager: def __init__(self, db_path: str data/wenti.db): self.db_path db_path # 步骤1确保data目录存在 os.makedirs(os.path.dirname(db_path), exist_okTrue) # 步骤2尝试连接失败则创建新库 try: self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.row_factory sqlite3.Row # 支持字典式取值 self._init_tables() # 创建表结构 except sqlite3.Error as e: # 关键容错若db文件损坏自动备份并新建 backup_path f{db_path}.backup_{int(time.time())} if os.path.exists(db_path): shutil.copy2(db_path, backup_path) self.conn sqlite3.connect(db_path, check_same_threadFalse) self.conn.row_factory sqlite3.Row self._init_tables() logging.warning(fDatabase corrupted, recreated from backup: {backup_path})这段代码解决了一个高频翻车点用户误删wenti.db或磁盘坏道导致文件损坏。常见做法是直接报错退出而这里会自动备份原文件并重建空库保证程序始终可启动。check_same_threadFalse是PyQt多线程安全必需参数否则在后台导出PDF时可能触发RuntimeError: database is locked。3.2 题目增删改查add_question()与update_question()的原子性保障def add_question(self, question: Question) - int: 添加题目返回新题ID sql INSERT INTO questions (text, type, difficulty, subject, knowledge_point_ids, created_at) VALUES (?, ?, ?, ?, ?, ?) try: cursor self.conn.cursor() # 使用?占位符防止SQL注入即使题干含单引号也不怕 cursor.execute(sql, ( question.text, question.type.value, # 枚举转字符串 question.difficulty, question.subject, json.dumps(question.knowledge_point_ids), # 列表转JSON字符串 datetime.now().isoformat() )) self.conn.commit() # 必须显式提交 return cursor.lastrowid except sqlite3.IntegrityError as e: # 捕获唯一约束冲突如重复题干返回0表示失败 logging.error(fAdd question failed due to integrity error: {e}) self.conn.rollback() return 0 def update_question(self, qid: int, updates: dict) - bool: 更新题目updates为字段名-值字典如{text: 新题干, difficulty: 4} # 动态构建SET子句避免硬编码字段 set_clause , .join([f{k} ? for k in updates.keys()]) sql fUPDATE questions SET {set_clause} WHERE id ? try: cursor self.conn.cursor() cursor.execute(sql, list(updates.values()) [qid]) self.conn.commit() return cursor.rowcount 0 # 返回是否真有行被更新 except Exception as e: logging.error(fUpdate question {qid} failed: {e}) self.conn.rollback() return False关键细节json.dumps(question.knowledge_point_ids)将知识点ID列表存为JSON字符串而非建关联表——单机版追求简单10万题下JOIN性能反而不如JSON解析cursor.rowcount 0判断更新是否生效避免“以为改了实际没改”的静默失败所有异常后必调rollback()防止事务残留锁表。3.3 智能筛选与组合search_questions()的多条件融合策略def search_questions( self, text: str , subject: str , difficulty_min: int 1, difficulty_max: int 5, knowledge_point_ids: List[int] None, type_filter: List[str] None ) - List[Question]: 支持全文检索范围筛选多知识点交集的混合查询 # 步骤1构建WHERE条件 conditions [] params [] if text: # SQLite FTS5全文检索比LIKE快10倍 conditions.append(id IN (SELECT id FROM questions_fts WHERE questions_fts MATCH ?)) params.append(f{text}*) # 前缀匹配 if subject: conditions.append(subject ?) params.append(subject) if difficulty_min or difficulty_max: if difficulty_min and difficulty_max: conditions.append(difficulty BETWEEN ? AND ?) params.extend([difficulty_min, difficulty_max]) elif difficulty_min: conditions.append(difficulty ?) params.append(difficulty_min) else: conditions.append(difficulty ?) params.append(difficulty_max) # 步骤2处理知识点ID交集JSON包含查询 if knowledge_point_ids: # SQLite 3.38支持json_each此处用LIKE模拟 # 实际生产环境建议升级SQLite并改用json_each for kp_id in knowledge_point_ids: conditions.append(knowledge_point_ids LIKE ?) params.append(f%{kp_id}%) where_clause AND .join(conditions) if conditions else 11 # 步骤3执行查询 sql fSELECT * FROM questions WHERE {where_clause} ORDER BY created_at DESC cursor self.conn.cursor() cursor.execute(sql, params) # 步骤4转换为Question对象 results [] for row in cursor.fetchall(): q Question( idrow[id], textrow[text], typeQuestionType(row[type]), difficultyrow[difficulty], subjectrow[subject], knowledge_point_idsjson.loads(row[knowledge_point_ids]) if row[knowledge_point_ids] else [], created_atdatetime.fromisoformat(row[created_at]) ) results.append(q) return results这段代码体现单机版的核心竞争力在无ElasticSearch、无Redis缓存的前提下用SQLite原生能力实现亚秒级复杂查询。重点看三点questions_fts是预先创建的FTS5虚拟表建表语句在_init_tables()中执行MATCH ?比LIKE %text%快一个数量级知识点ID用LIKE %{kp_id}%是兼容旧SQLite的妥协方案新版可用json_each(knowledge_point_ids) AS j WHERE j.value ?但已足够应对5万题以内场景ORDER BY created_at DESC确保最新添加的题目优先展示符合教研员“先看新题”的操作直觉。4. 运行与部署从零开始跑通全流程的实操指南4.1 环境准备最低硬件与系统要求本项目对环境极其宽容但仍有硬性门槛组件最低要求推荐配置验证方式操作系统Windows 7 SP1 / macOS 10.14 / Ubuntu 18.04Windows 10 / macOS 12 / Ubuntu 22.04运行verWin或sw_versmacOS确认版本CPU双核1.8GHz四核2.4GHz任务管理器查看逻辑处理器数内存2GB4GB启动后观察进程内存占用通常100MB磁盘100MB空闲空间500MB含题库扩容df -hLinux/macOS或磁盘属性Win注意不支持Windows XP及更早系统因PyQt5依赖UCRTUniversal CRTXP无此组件。若必须在XP运行请联系作者获取PyQt4兼容分支需自行编译。4.2 首次启动与初始化三步完成本地题库激活第一步解压并定位可执行文件将下载的wenti_single_v2.3.1.zip解压到任意目录如D:\exam\wenti确认目录下存在wenti.exeWindows或wenti.appmacOS。切勿放在中文路径或带空格路径如C:\我的文档\题库否则SQLite可能因路径编码问题无法创建数据库。第二步双击启动并接受初始配置双击wenti.exe首次启动会弹出初始化向导自动检测data/目录若不存在则创建检查wenti.db是否存在不存在则执行_init_tables()建表加载默认学科模板语文、数学、英语等12个存入subjects表显示“初始化完成共加载0道题目”提示。此时打开data/wenti.db所在目录应看到data/ ├── wenti.db └── wenti.db-shm # SQLite共享内存文件正常第三步导入首批题目验证功能点击主界面左上角【题库】→【批量导入】选择随附的sample_questions.xlsx含50道测试题Excel格式要求A列题干、B列题型单选/多选/判断、C列难度1-5、D列学科、E列知识点ID逗号分隔导入后右下角状态栏显示“成功导入50题”左侧题库树中对应学科节点数字变为50点击任一题目在右侧编辑区可修改并保存验证CRUD功能闭环。4.3 常见问题排查5条真实踩坑记录与解决方案现象1双击wenti.exe无反应任务管理器中看不到进程原因系统缺少Visual C 2015-2022运行库vcruntime140.dll解决下载微软官方运行库合集https://aka.ms/vs/17/release/vc_redist.x64.exe64位系统或 vc_redist.x86.exe32位安装后重启电脑再运行wenti.exe验证命令行执行wenti.exe --debug若输出Starting application...即成功现象2导入Excel时提示“列数不匹配”或“知识点ID格式错误”原因Excel中存在隐藏空格、全角字符或合并单元格解决用记事本打开Excel另存的CSV检查每行是否严格7字段题干、题型、难度、学科、知识点ID、答案、解析在Excel中选中数据列 → 【数据】→【分列】→【下一步】→取消所有勾选 → 【完成】清除不可见字符知识点ID必须为纯数字逗号分隔如101,102,105禁用101102中文逗号或101、102顿号。现象3生成PDF时中文显示为方块或页眉页脚错位原因resources/fonts/simsun.ttc文件缺失或权限不足解决检查resources/fonts/目录是否存在simsun.ttc若无则从Windows系统盘C:\Windows\Fonts\复制一份右键simsun.ttc→ 【属性】→ 取消勾选“只读”并确保当前用户有读取权限修改resources/templates/paper.html中font-face的src路径为绝对路径src: url(file:///D:/exam/wenti/resources/fonts/simsun.ttc);现象4筛选题目时结果为空但数据库明明有数据原因FTS5全文检索表未更新或questions_fts表损坏解决打开data/wenti.db执行SQLINSERT INTO questions_fts SELECT id, text FROM questions;强制重建索引或删除questions_fts表不影响原始数据重启程序自动重建验证执行SELECT count(*) FROM questions_fts;结果应等于questions表行数。现象5导出Word试卷时公式图片丢失或排版混乱原因python-docx库未正确安装或系统缺少字体映射解决运行命令行pip install python-docx --force-reinstall在config.py中设置WORD_FONT_NAME SimSun宋体而非默认Times New Roman若仍失败改用PDF导出WeasyPrint稳定性远高于python-docx。5. 二次开发实战如何为单机版增加“AI智能组卷”功能5.1 需求分析为什么需要AI组卷某导师反馈“现有组卷只能按固定规则筛选但实际考试需要‘覆盖80%知识点但每章题量均衡’‘同一知识点不重复出现’‘难度曲线呈正态分布’”。这已超出SQL查询能力需引入轻量级AI逻辑。我们不接入大模型API违背单机原则而是用本地Python实现规则引擎启发式算法。5.2 技术方案基于NetworkX的知识点图谱与贪心算法核心思路将知识点视为图节点题目为边用NetworkX构建知识关联图再用贪心策略选题。步骤1扩展数据库结构在_init_tables()中新增表CREATE TABLE IF NOT EXISTS knowledge_relations ( id INTEGER PRIMARY KEY, from_kp_id INTEGER NOT NULL, to_kp_id INTEGER NOT NULL, weight REAL DEFAULT 1.0, FOREIGN KEY(from_kp_id) REFERENCES knowledge_points(id), FOREIGN KEY(to_kp_id) REFERENCES knowledge_points(id) );此表记录知识点间关联强度如“三角函数”→“解三角形”权重0.9“三角函数”→“概率统计”权重0.1由教研员在【知识点管理】中手动维护。步骤2实现AI组卷核心逻辑在logic/ai_generator.py中编写import networkx as nx from collections import defaultdict class AIGenerator: def __init__(self, question_manager: QuestionManager): self.qm question_manager self.graph self._build_knowledge_graph() def _build_knowledge_graph(self) - nx.DiGraph: 从knowledge_relations表构建有向图 graph nx.DiGraph() cursor self.qm.conn.cursor() cursor.execute(SELECT from_kp_id, to_kp_id, weight FROM knowledge_relations) for row in cursor.fetchall(): graph.add_edge(row[from_kp_id], row[to_kp_id], weightrow[weight]) return graph def generate_paper( self, target_kps: List[int], # 目标知识点ID列表 total_questions: int 20, difficulty_distribution: Dict[int, float] {1:0.1, 2:0.2, 3:0.4, 4:0.2, 5:0.1} ) - List[Question]: 生成满足知识点覆盖与难度分布的试卷 # 步骤1按知识点权重排序候选题目 candidates [] for kp_id in target_kps: # 获取该知识点下所有题目 questions self.qm.search_questions(knowledge_point_ids[kp_id]) # 计算每个题目的“关联得分”自身难度权重 邻居知识点覆盖度 for q in questions: score 0 # 自身难度匹配目标分布 target_weight difficulty_distribution.get(q.difficulty, 0) score target_weight * 10 # 邻居知识点覆盖若题目还关联其他目标KP则加分 for other_kp in target_kps: if other_kp ! kp_id and other_kp in q.knowledge_point_ids: # 查图中边权重 if self.graph.has_edge(kp_id, other_kp): score self.graph[kp_id][other_kp][weight] * 5 candidates.append((q, score)) # 步骤2贪心选择按score降序去重选题 candidates.sort(keylambda x: x[1], reverseTrue) selected [] used_kps defaultdict(int) # 记录各KP已选题数 for q, _ in candidates: # 避免同一知识点过度集中 if len(selected) total_questions: break # 检查是否已覆盖足够KP covered_kps set() for sel_q in selected: covered_kps.update(sel_q.knowledge_point_ids) if len(covered_kps) / len(target_kps) 0.8: # 至少覆盖80% selected.append(q) for kp_id in q.knowledge_point_ids: used_kps[kp_id] 1 else: # 已覆盖充分优先选未覆盖KP的题 uncovered set(target_kps) - covered_kps if any(kp in uncovered for kp in q.knowledge_point_ids): selected.append(q) return selected[:total_questions]步骤3集成到UI在ui/main_window.py中为【组卷】按钮添加事件def on_ai_generate_clicked(self): target_kps self.knowledge_tree.get_selected_ids() # 获取选中的知识点 generator AIGenerator(self.qm) questions generator.generate_paper( target_kpstarget_kps, total_questionsself.spinbox_question_count.value(), difficulty_distribution{1:0.05, 2:0.15, 3:0.5, 4:0.2, 5:0.1} ) self.paper_preview.set_questions(questions) # 渲染预览5.3 验证与调优三步确认AI组卷有效数据验证用sqlite3命令行执行SELECT * FROM knowledge_relations LIMIT 5;确认至少有3条关联记录如101,102,0.85逻辑验证在main.py末尾添加测试代码if __name__ __main__: app QApplication(sys.argv) manager QuestionManager() ai AIGenerator(manager) # 测试给定知识点[101,102]生成5题 result ai.generate_paper([101,102], total_questions5) print(fAI generated {len(result)} questions, covering KPs: {set([kp for q in result for kp in q.knowledge_point_ids])})运行后应输出类似AI generated 5 questions, covering KPs: {101, 102}效果验证在UI中选中“数学”学科下的“函数”和“导数”两个知识点点击【AI组卷】检查生成试卷是否同时包含函数题和导数题非单一知识点难度分布接近设定比例如5题中1道简单、1道中等、3道较难题干无重复对比question.text哈希值。从那以后我每次给客户交付单机版系统都会强制走一遍“AI组卷验证三步法”先看图谱数据、再跑命令行测试、最后UI实操。不是信不过代码而是信不过自己写完就忘的健忘症——毕竟真正的单机可靠不在于它多炫酷而在于断网时你双击它它依然稳稳地给你一张卷子。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站