news 2026/10/22 15:49:03

SQLite题库管理系统单机版:数据库设计与组卷实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SQLite题库管理系统单机版:数据库设计与组卷实践

简介:万维题库管理系统单机版是一款面向教育机构及个人教师的离线试题管理工具,主要用于试题的分类维护、批量导入导出、随机组卷与答案解析等场景,适合无稳定网络或对试题数据隐私性要求较高的用户使用。压缩包共11个文件、约14.21MB,以SETUP.EXE主安装程序为核心,搭配DLL、LIB等安装支持库以及INI、INS等配置文件,另附Readme-说明.htm帮助文档,覆盖从安装引导到参数配置的完整流程。目前已有2255人学习下载。通过安装部署这套系统,用户可在本地独立完成题库建设与试卷生成,按学科或难度分类管理试题,并利用随机组卷功能快速整编测试卷。结合Readme中的说明掌握导入导出格式与组卷规则,可进一步减少人工整理题目的重复劳动,适合需要离线维护题库的教师、培训人员与考试组织者。

1. 万维题库管理系统单机版:为什么本地优先是教研管理的刚需

当一份试卷背后躺着一千多道题目,散落在十几个 Word 文件里,整理靠复制粘贴、组卷靠手工挑题时,你就知道题库管理缺在哪了。万维题库管理系统单机版解决的正是这个场景:把题目按科目、知识点、题型和难度结构化存入本地库,支持批量导入、快速检索、按规则组卷,全程不需要服务器和网络。它的核心资产是一份本地数据库文件,复制一份就是备份,这是单机版最本质的竞争力。对个人教师、培训机构教研组、备赛团队来说,这是投入产出比最高的起步方式。下面这套方案,从数据模型写到组卷逻辑,坑我也一并标出来,照着做可以少走小半年的弯路。

2. 数据模型与选型:三张表怎么把题库收纳干净

2.1 题目为什么必须拆成三张表:category、question、option 的关系

我第一次做题库时试图把一道题塞到一行里:题干、选项A、选项B、选项C、正确答案、解析。结果导出试卷时还能应付,但一旦想改某个选择题的选项B、或者想统计某道题的选项分布,就只能在 Excel 里横向对齐;选项字段一多,表结构动不动要改,后来想扩展多选和判断题时更麻烦。最常见的可靠做法,是把题目相关的信息拆成 category 表和 question 表和 option 表:

  • category:分类树。用 parent_id 支持两级或三级目录,比如“高等数学”是父类,“极限与连续”是子类。表结构只需要 id、name、parent_id 三列。
  • question:题目主表。存储与“题目本身”绑定的信息:属于哪个分类、什么题型、题干文本、参考答案、解析、难度、创建时间。表里不存选项,因为题型不同,选项的数量和形式完全不同。
  • option:选项表。每道题一行,存题号、选项键(A/B/C/D)、选项文本、是否正确。判断题可以把“正确”“错误”当作两个选项塞进去,也可以用 qtype 单独分支处理。

为什么选项单独拆表而不是用 JSON 拼在一个字段里?第一个原因是查询:你经常会按选项文本搜索某道题;第二个原因是更新:修改某个选项不需要重写整个题干字符串;第三个原因是扩展:多选、判断题和填空的答案结构差异很大,拆开之后题型再多也只改 qtype 枚举,不动表结构。

三张表的 DDL 大概是这样的:

CREATE TABLE category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, parent_id INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE question ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, qtype INTEGER NOT NULL, -- 1单选 2多选 3判断 4填空 5问答 content TEXT NOT NULL, -- 题干,纯文本或富文本 answer TEXT DEFAULT '', -- 填空/问答的参考答案,选择题留空 analysis TEXT DEFAULT '', -- 答案解析 difficulty INTEGER NOT NULL DEFAULT 3, -- 1~5,默认中等 created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (category_id) REFERENCES category(id) ); CREATE TABLE option_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, opt_key TEXT NOT NULL, -- A、B、C、D content TEXT NOT NULL, is_correct INTEGER NOT NULL DEFAULT 0, FOREIGN KEY (question_id) REFERENCES question(id) ON DELETE CASCADE );

这里选了最简单的 INTEGER PRIMARY KEY 写法,自增行为由行号保证,单机版不需要纠结主键回补的问题。三个地方要留意:分类表 parent_id 不要设外键约束,根分类的父节点是 0;option_item 表的正确答案字段不要建唯一索引,因为多选的正确答案不止一个;难度字段要定死取值 1~5,否则导入脏数据后组卷条件会失效。

索引是单机版容易被忽视的部分。分类一般几十条,没必要加索引;但 question 表超过两千行后,按分类、题型、难度做的组合查询会明显变慢。在建表脚本里顺手给 category_id、qtype、difficulty 各建一个索引,多数组卷查询都能走索引。这类索引有利无弊,因为单机版写入量很小,索引带来的额外写入成本几乎可以忽略。

2.2 选型对比:SQLite 与 JSON 文件、本地数据库服务的取舍

很多人的第一版题库是直接拿 JSON 文件存的。几百条题时没问题,但超过两三千条之后,每次修改都要把整个文件读进来改完再写回去。先不说性能,程序的崩溃点正好落在写回那一步,数据损坏概率会直线上升。JSON 文件没有事务、没有行级锁,也无法真正支持多人同时打开编辑。Excel 同理,当编辑工具用可以,当“数据库”用一定会遇到并发覆盖的问题。

SQLite 是单机版最稳妥的存储层。它是嵌入式数据库,不需要单独安装服务程序,连接就是打开一个本地文件;支持 ACID 事务和行级锁;还支持标准 SQL,配合内置的 sqlite3 驱动,连第三方依赖都不用装。题库系统读写量都不大,单文件四处拷贝也方便,SQLite 几乎是为单机题库量身定做的。

相比在本地装 MySQL 或 PostgreSQL,SQLite 省掉的东西非常多:不用起服务、不用配账号和端口、不用处理连接池、不用考虑防火墙。对“一台电脑、一个人用”的单机场景,本地数据库服务是典型的大炮打蚊子,只会增加部署成本和排障成本。我一般建议的选型顺序是:纯文件(JSON)只用来做导入导出,绝不作为存储主库;单机场景直接用 SQLite;真正出现多人同时协作写题库的需求,再部署数据库服务。

2.3 单机版的边界:什么规模下必须换架构

SQLite 不是没有天花板。它适合“单写多读”,单机版通常是一个人维护题库、最多几个老师同时查题目,读写冲突几乎不存在。但出现下面三种信号时,就该考虑换客户端服务器架构了:第一个是同时写入人数超过三五个,SQLite 的写锁会让界面操作频繁卡顿;第二个是题库需要跨校区、跨机构实时同步,单机文件复制做不到;第三个是数据量突破百万级,虽然 SQLite 在一台机器上也能撑,但备份、迁移、权限控制会开始别扭。

换架构的迁移路径也不复杂,因为数据模型不变,只是把 sqlite3 连接换成数据库服务连接,查询语句基本不用动。这种“先用 SQLite、后换库”的路径,前提是业务逻辑层不要写死 SQLite 特有的函数。我在组卷模块里只用了 WHERE、ORDER BY、LIMIT 这类标准语法,后来迁移时几乎没有改过业务代码。

3. 核心功能落地:初始化脚本、增删改查和 Excel 批量导入

3.1 数据库初始化:首次运行要做的事

初始化逻辑只有一条规则:检查数据库文件是否存在,不存在就建表、建索引、预置默认分类;存在就跳过。这里容易翻车的是用__file__定位数据库路径——当程序被打包成 exe 后,__file__指向临时解包目录,数据会写到临时目录里,重开程序就找不到了。我一般把数据库放在和代码同级的 data 目录,打包后用配置文件或环境变量指定实际数据目录。

import sqlite3 import os BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DATA_DIR = os.path.join(BASE_DIR, "data") DB_PATH = os.path.join(DATA_DIR, "question_bank.db") SCHEMA = """ CREATE TABLE IF NOT EXISTS category ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, parent_id INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS question ( id INTEGER PRIMARY KEY AUTOINCREMENT, category_id INTEGER NOT NULL, qtype INTEGER NOT NULL, content TEXT NOT NULL, answer TEXT DEFAULT '', analysis TEXT DEFAULT '', difficulty INTEGER NOT NULL DEFAULT 3, created_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) ); CREATE TABLE IF NOT EXISTS option_item ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, opt_key TEXT NOT NULL, content TEXT NOT NULL, is_correct INTEGER NOT NULL DEFAULT 0 ); CREATE INDEX IF NOT EXISTS idx_question_category ON question(category_id); CREATE INDEX IF NOT EXISTS idx_question_qtype ON question(qtype); CREATE INDEX IF NOT EXISTS idx_question_difficulty ON question(difficulty); """ def init_db(): os.makedirs(DATA_DIR, exist_ok=True) conn = sqlite3.connect(DB_PATH) conn.executescript(SCHEMA) conn.commit() conn.close()

逻辑说明:SCHEMA 字符串里建了三张表和三个索引,IF NOT EXISTS保证重复执行安全。DATA_DIR用exist_ok=True自动创建,避免首次运行因目录不存在报错。这段代码在启动函数里先执行一次,之后连接数据库就不需要重复检查表结构了。

参数说明:如果想把数据库放到用户目录,把DATA_DIR改成os.path.expanduser("~/.local/share/qbank")或%APPDATA%下的对应路径。Windows 上尤其建议这样,避免安装目录有写权限问题。另外初始化完顺手打印一个日志,方便判断是不是走了初始化分支。

3.2 题目增删改查:四个函数覆盖日常操作

有了基础连接之后,增删改查的封装是题库管理最常用的部分。这里我用了“无状态连接”的写法:每个函数内部自己开关连接。单机版每次操作只有毫秒级开销,不需要连接池,代码反而干净。

def connect(): conn = sqlite3.connect(DB_PATH) conn.execute("PRAGMA foreign_keys = ON") return conn def add_question(cat_id, qtype, content, options=None, answer="", analysis="", difficulty=3): conn = connect() cur = conn.cursor() cur.execute( "INSERT INTO question(category_id, qtype, content, answer, analysis, difficulty) " "VALUES(?,?,?,?,?,?)", (cat_id, qtype, content, answer, analysis, difficulty), ) qid = cur.lastrowid if options: for key, text, correct in options: cur.execute( "INSERT INTO option_item(question_id, opt_key, content, is_correct) " "VALUES(?,?,?,?)", (qid, key, text, 1 if correct else 0), ) conn.commit() conn.close() return qid def update_question(qid, content=None, analysis=None, difficulty=None): fields, args = [], [] if content is not None: fields.append("content = ?") args.append(content) if analysis is not None: fields.append("analysis = ?") args.append(analysis) if difficulty is not None: fields.append("difficulty = ?") args.append(difficulty) if not fields: return args.append(qid) conn = connect() conn.execute( f"UPDATE question SET {', '.join(fields)} WHERE id = ?", args ) conn.commit() conn.close() def delete_question(qid): conn = connect() conn.execute("DELETE FROM question WHERE id = ?", (qid,)) conn.commit() conn.close() def fetch_questions(category_id=None, qtype=None, difficulty=None, keyword=""): conn = connect() conn.row_factory = sqlite3.Row sql = "SELECT * FROM question WHERE 1=1" args = [] if category_id: sql += " AND category_id = ?" args.append(category_id) if qtype: sql += " AND qtype = ?" args.append(qtype) if difficulty: sql += " AND difficulty = ?" args.append(difficulty) if keyword: sql += " AND content LIKE ?" args.append(f"%{keyword}%") rows = conn.execute(sql, args).fetchall() conn.close() return [dict(row) for row in rows]

逻辑说明:connect()里加PRAGMA foreign_keys = ON很关键。SQLite 默认不启用外键约束,不加这一行,ON DELETE CASCADE就是摆设,删除题目时选项表会留下孤儿数据。add_question里先插主表拿到lastrowid,再循环插入选项,两个操作在同一个事务里提交,保证选项不会只插一半。update_question用动态拼接字段的方式,只更新传入的列,避免误清空其它字段。

参数说明:options参数约定为三元组列表,格式是("A", "单选文本", True),其中第三个值表示是否正确。fetch_questions的difficulty传 0 时不会参与过滤,因为默认值取不到;如果按难度精确筛选,要保证前端传来的值是 1~5 区间内的整数。

3.3 Excel 批量导入:格式约定与解析代码

批量导入是题库系统能真正用起来的跳板。手工录入几百道题,录入错误率高还慢,一般首次建库都是用现成的 Word 或 Excel 题目表批量导入。我在项目里约定的 Excel 列格式如下:分类、题型、题干、选项A、选项B、选项C、选项D、答案、解析、难度。题型用中文写“单选”“多选”“判断”“填空”“问答”,答案列里单选写“A”,多选写“ABD”,判断写“正确/错误”,填空和问答写参考答案。

import pandas as pd QTYPE_MAP = {"单选": 1, "多选": 2, "判断": 3, "填空": 4, "问答": 5} def get_or_create_category(conn, name): cur = conn.cursor() cur.execute("SELECT id FROM category WHERE name = ?", (name,)) row = cur.fetchone() if row: return row[0] cur.execute("INSERT INTO category(name, parent_id) VALUES(?, 0)", (name,)) return cur.lastrowid def import_excel(filepath): df = pd.read_excel(filepath, dtype=str).fillna("") required = ["分类", "题型", "题干", "选项A", "选项B", "选项C", "选项D", "答案", "解析", "难度"] for col in required: if col not in df.columns: raise ValueError(f"缺少列:{col},模板必须包含:{required}") conn = connect() imported = 0 try: for idx, row in df.iterrows(): qtype = QTYPE_MAP.get(str(row["题型"]).strip(), 0) if qtype == 0: raise ValueError(f"第{idx + 2}行题型无法识别:{row['题型']}") cat_id = get_or_create_category(conn, str(row["分类"]).strip()) difficulty = int(row["难度"]) if str(row["难度"]).strip() else 3 if difficulty not in range(1, 6): difficulty = 3 options = None answer = "" if qtype in (1, 2): options = [] valid_answer = str(row["答案"]).upper().strip() for key in ["A", "B", "C", "D"]: text = str(row[f"选项{key}"]).strip() if text: options.append((key, text, key in valid_answer)) elif qtype == 3: options = [] answer_val = str(row["答案"]).strip() options.append(("A", "正确", answer_val == "正确")) options.append(("B", "错误", answer_val == "错误")) else: answer = str(row["答案"]).strip() add_question(cat_id, qtype, str(row["题干"]).strip(), options=options, answer=answer, analysis=str(row["解析"]).strip(), difficulty=difficulty) imported += 1 conn.commit() except Exception as e: conn.rollback() raise ValueError(f"导入失败,已回滚:{e}") finally: conn.close() return imported

逻辑说明:导入函数先把表头校验放在最前面,列名不对直接抛错,防止用户拿错模板硬跑。get_or_create_category按分类名查找,不存在就插入,这样几万道题里重复出现的分类名只会建一条记录。单选和多选走同一个解析分支:先确定“答案”字符串里包含哪个选项键,决定is_correct。判断题把“正确”“错误”做成固定选项,省掉单独维护判断题选项表的逻辑。

参数说明:pandas.read_excel必须加dtype=str,否则 Excel 会把“题干”里的数字串改成科学计数法。fillna("")把合并单元格和空行产生的 NaN 变成空字符串,避免后续类型报错。事务提交放在循环外面,所以一次导入几十万条也不会卡在频繁提交上;任一行出错就全体回滚,出现重复导入时清理比较省心。

4. 抽题组卷与单机部署:规则怎么设、参数怎么配

4.1 按规则抽题组卷:题型、难度和分类的组合过滤

组卷是题库系统的核心输出。常见需求是“高等数学下单选 10 道,难度 3,问答题 5 道,难度 2~3”。这里有一个容易踩的误区:直接在数据库里分多次随机取题,整卷没有统一随机种子,导出同一份卷子两次,题目顺序都不同。我一般把组卷规则做成一组结构化参数,并用一个显式的随机种子来控制整个抽题过程。

import random def generate_paper(rules, seed=20240601): # rules 示例: # [{"category_id": 3, "qtype": 1, "difficulty": 3, "count": 10}, # {"category_id": 3, "qtype": 5, "difficulty": 2, "count": 5}] rng = random.Random(seed) conn = connect() picked = [] for rule in rules: sql = "SELECT id, content, answer, difficulty FROM question WHERE 1=1" args = [] if rule.get("category_id"): sql += " AND category_id = ?" args.append(rule["category_id"]) if rule.get("qtype"): sql += " AND qtype = ?" args.append(rule["qtype"]) if rule.get("difficulty"): sql += " AND difficulty = ?" args.append(rule["difficulty"]) rows = conn.execute(sql, args).fetchall() count = max(0, min(rule.get("count", 0), len(rows))) picked.extend(rng.sample(rows, count)) conn.close() # 全卷去重:防止不同规则之间抽中同一道题 seen = set() unique = [] for q in picked: if q[0] not in seen: seen.add(q[0]) unique.append(q) return unique

逻辑说明:每条 rule 是一次独立的条件查询,category_id、qtype、difficulty只要传了值就参与过滤。rng.sample(rows, count)从满足条件的候选中不放回地随机抽,保证同一规则内不会重复。全卷去重是容易被忽略的第二道防线,比如两个规则都只筛“难度 3”而未指定分类,同一道题很可能被抽到两遍。

参数说明:difficulty只支持精确匹配,如果需要“2 到 3 之间”,应把 rule 改为传difficulty_min和difficulty_max,在 SQL 里用BETWEEN ? AND ?。另外,rng.sample返回的顺序是随机的,如果试卷需要按题型分区块排列,在返回前要再做一次“按 qtype 分类再拼接”的处理。

4.2 单机版运行参数:数据目录、日志与启动配置

单机版部署看似简单,最常见的翻车点都在路径上。程序装在 C 盘,数据写在安装目录里,用户手动清理或系统升级后数据就丢了。我一般把单机版的目录分成两层:程序目录只放可执行文件和默认配置,数据目录单独指定,首次启动时做一次检查。

import configparser import logging import os CONFIG_PATH = os.path.join(os.path.expanduser("~"), ".qbank_config.ini") def load_config(): config = configparser.ConfigParser() if os.path.exists(CONFIG_PATH): config.read(CONFIG_PATH, encoding="utf-8") else: default_data_dir = os.path.join(os.path.expanduser("~"), "qbank_data") config["DEFAULT"] = {"data_dir": default_data_dir, "json_export_dir": default_data_dir} with open(CONFIG_PATH, "w", encoding="utf-8") as f: config.write(f) return config def setup_logging(data_dir): os.makedirs(data_dir, exist_ok=True) logging.basicConfig( filename=os.path.join(data_dir, "app.log"), level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s", )

逻辑说明:配置文件放在用户主目录下,不跟随安装目录移动,因此重装程序后配置还在。首次运行会生成默认配置文件,并在用户目录创建qbank_data文件夹,数据库和导出文件都落在这里,避免写入权限问题。

参数说明:如果应用打包成单文件 exe,__file__指向的是临时解包目录,这时加载配置和数据目录必须走os.path.expanduser或环境变量,不能用相对路径。日志级别建议先用 INFO,等出现查不到的问题再调成 DEBUG,因为单机版没有控制台输出,错误信息如果只打内存,用户无从反馈。

5. 单机版避坑指南:五条真实踩坑记录

5.1 题目图片和公式:数据库膨胀到失控

现象:导入一批带截图的题目后,数据库文件从几 MB 变成了几百 MB,打开和备份都越来越慢。

原因:把图片直接转成 BLOB 存进了 SQLite。SQLite 单文件能撑住几十 MB,但几百 MB 的数据库每次提交都要落盘,性能急剧下降,而且备份文件也变得巨大。

解决:图片走“外置文件 + 相对路径”方案。把图片复制到数据目录下的 media 子文件夹,数据库里只存相对路径,显示时再拼绝对路径。迁移时只要把整个数据目录打包拷走,路径用相对方式就不会断。

import shutil import uuid def save_media(src_path, dest_dir): os.makedirs(dest_dir, exist_ok=True) ext = os.path.splitext(src_path)[1] new_name = f"{uuid.uuid4().hex}{ext}" dest = os.path.join(dest_dir, new_name) shutil.copy(src_path, dest) return dest

5.2 重题检测的玄学:同一道题换个换行就判不出

现象:题库里有两道内容几乎一样的题,A 题题干是“以下哪个选项正确?”,B 题多了一个换行或全角空格,人工一眼能看出来是重复,程序却当作两道题都收了。

原因:直接拿原始字符串做等值比较。题干里的换行、全角/半角符号、多余空格都会导致字符串不相等。

解决:在导入前做规范化比较。把字符串里的空白字符全部压缩成单个空格,把全角标点转半角,再去掉首尾空白。做这一步后,大量重复题目能被直接拦住。真正语义层面的重题检测,单机版里不要追求完美,能做到“明显重复识别 + 人工确认”就够用了。

import re def normalize_question_text(text): text = text.replace("\u3000", " ").replace("\n", " ").replace("\r", " ") text = re.sub(r"\s+", " ", text) text = text.strip() return text

5.3 组卷翻车:随机抽题为什么同一道题出两次

现象:组卷导出后发现同一道题出现在两个不同的题型区块里,被当成两道题计分。

原因:规则是逐条执行的,每条规则独立随机抽取,没有跨规则去重。比如“单选 10 道、难度 3”和“问答 5 道、难度 3”里都有可能命中同一道题。

解决:在generate_paper函数返回前加全卷去重步骤,用题目 id 做唯一键。遇到重复时保留第一次抽到的位置,后续重复题目丢弃。如果去重后发现某一区间的题目数量不足,要在 UI 上明确提示“规则命中不足”,而不是直接出卷。

5.4 备份后悔药:WAL 模式下直接复制数据库文件丢数据

现象:把question_bank.db复制到 U 盘作为备份,换电脑打开后提示数据库损坏或缺少最后几分钟的修改。

原因:SQLite 默认在 WAL 模式下工作时,最新数据可能还在question_bank.db-wal文件里,直接复制主库文件等于复制了一个旧版本。

解决:不要用文件系统拷贝做热备份,用 SQLite 的备份接口,它在事务层能拿到一致快照。

def backup_db(target_path): src = sqlite3.connect(DB_PATH) dst = sqlite3.connect(target_path) with dst: src.backup(dst) dst.close() src.close()

5.5 Excel 导入翻车:编码、合并单元格和空行

现象:导入一个老师发的题目表,程序报“第 3 行题型无法识别”,打开 Excel 看第 3 行明明写的“单选”。

原因:多半是 Excel 的合并单元格导致后续行的“分类”或“题型”列是空值;另一个常见原因是某个单元格的值被 Excel 格式化成数字或科学计数法,读出来变成1.3E+15这种字符串。

解决:读取时用dtype=str强制字符串类型,并在解析前对分类列做前向填充,因为合并单元格通常只在首行有值:

df = pd.read_excel(filepath, dtype=str).fillna("") df["分类"] = df["分类"].ffill()

空行问题同样常见:Excel 表格末尾有一行空白,iterrows()会读到一个全空行,把题型解析成 0 直接报错。解决方案是导入前先过滤全部为空的行。做完这三步,能消掉 90% 的导入报错。

6. 让单机版进入正循环:错题本、导出与验证

6.1 错题本:给题库加一个有状态的答题记录

题库系统只有“存题”是不够的,真正有价值的是把“做题-错题-薄弱点”这条闭环补上。我常用的做法是新增一张answer_record表,记录每次答题的题目 id、是否正确、答题时间、所属学生或练习批次。有了这张表,就能统计每道题的答错率,按“错题率 30% 以上的题”抽取复习卷。

CREATE TABLE answer_record ( id INTEGER PRIMARY KEY AUTOINCREMENT, question_id INTEGER NOT NULL, is_correct INTEGER NOT NULL, student_tag TEXT DEFAULT '', answered_at TEXT NOT NULL DEFAULT (datetime('now', 'localtime')) );

6.2 导出试卷:组卷结果落到 Word 的思路

组卷结果最终要给老师印成纸质卷或导成 Word。我一般用 python-docx 生成试卷,标题区写“学校、科目、考试时间”,接着按题型分区渲染每组题目。自定义题号前缀、选项字体和答题留白,这些用 Word 模板占位符会更省事。导出后一定先做一次“答案比对”:从导出的题目 id 反查库里数据,确认每道题的题号顺序没乱,答案标记位置正确。

6.3 验证题库是否健康的三个信号

单机版没有网络服务可监控,维护健康只能靠定期自查。第一个信号是“导入数量对得上”:用count(*)检查每个分类下的题目数,和导入文件的行数对比,误差应该为零。第二个信号是“组卷结果无重复、无缺题”:跑一个覆盖全规则的生成任务,检查返回数量是否等于各 rule 请求数量之和。第三个信号是“备份文件能恢复”:每个月的例行维护日,把备份库恢复到临时目录,执行一次全量查询确认表结构完好、行数一致。这三个信号全部通过,基本上可以判定当前题库没出大问题。

做单机版项目这几年,我最深刻的教训是把“备份”和“数据一致性”当成功能来写,而不是事后补救。题库的价值全在数据里,界面难看一点可以忍,数据丢一次就全完了。希望这篇笔记帮你把这套系统的骨架搭稳,少踩几个我已经踩过的坑。

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

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

集合合并算法详解:从反复扫描到并查集的高效实现

1. 从一个集合合并题说起:这到底在考什么第一次看到{aaa,bbb,ccc},{bbb,ddd},{eee,fff},{ggg},{ddd,hhh}这串东西,很多人第一反应是“不就是把有交集的集合粘在一起吗”。真动手写代码才发现,坑比想象中多:{bbb,ddd}和{ddd,hhh}要…

作者头像 李华
网站建设 2026/10/13 12:03:29

Oracle EBS R12表结构核心解析:多组织架构与常用表关联实战指南

简介:这是一份面向Oracle EBS开发与实施人员的Oracle ERP R12表结构参考包,旨在梳理各核心业务模块的数据表与字段脉络,减少在庞大标准库中盲目摸索的时间。压缩包共112个文件,主要包含58个PDF文档和54个HTML页面:HTML…

作者头像 李华
网站建设 2026/10/14 12:06:35

WinDbg 蓝屏调试实战:从 dump 抓取到驱动定位的完整链路

简介:这份资源是面向Windows开发与系统排障人员的Windbg调试工具详解资料包,适合具备一定C/C基础、需要处理蓝屏崩溃、驱动错误或内存异常的中高级开发者。包内共307个文件,以82个dll动态库、42个h头文件、39个exe可执行程序为主,…

作者头像 李华
网站建设 2026/10/13 13:54:50

AI全面编程时代,工程师怎么写代码?TaoToken统一Key接入实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华