ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

驾考科目一科目四题库设计:SQL表结构与JSON接口支撑四种车型

驾考科目一科目四题库设计:SQL表结构与JSON接口支撑四种车型 简介这份资源面向准备驾照理论考试的学员及驾培类应用开发者提供科目一与科目四的完整题库数据覆盖小车、客车、货车、摩托车四类车型可直接用于刷题软件、小程序或后台系统的数据搭建。包内共约2000个文件以1995个webp图片素材为主用于题目配图与选项图示另含2个sql与2个json文件分别承载题目、章节等结构化数据方便直接导入数据库或前端调用压缩包整体约103MB。题库规模较为完整客车科目一2154题、科目四2126题货车科目一2162题、科目四1206题小车科目一1600题、科目四1300题摩托车科目一446题、科目四383题基本覆盖各类车型的常考知识点。目前已有1014人学习下载适合需要快速获取成套题库数据、减少人工整理成本的开发者与备考用户参考使用。1. 驾照考试科目一科目四题库从 SQL 表结构到 JSON 接口一套数据怎么撑起四种车型做过驾考类应用的人都知道真正卡脖子的从来不是界面而是题库数据本身。科目一和科目四加起来上千道题还要区分小车、客车、货车、摩托车四种准驾车型每道题带图片素材、选项、答案、解析、章节分类稍不留神就会出现「同一道题在不同车型下答案不一致」这种玄学问题。我见过太多团队一开始用 Excel 维护做到三百道题就开始崩改一个选项要翻五个表。这套题库的核心价值在于把题目、选项、答案、图片、车型标签拆成关系型结构同时导出一份 JSON 给前端直接消费。SQL 负责存储和查询JSON 负责接口传输和离线缓存图片素材单独走静态资源目录。适合正在做驾考 App、小程序、H5 刷题页的开发者也适合需要批量导入题库做数据分析的场景。下面把我实际落地时踩过的结构和参数讲清楚。2. 题库表结构怎么设计五张表撑住四种车型和图片素材2.1 题目主表、选项表、车型关联表的分工很多人第一反应是把选项直接塞进题目表的一个字段里用逗号分隔。这么做查询快但一旦要做「选项乱序」「多选答案校验」「选项图片」就彻底翻车。我的做法是拆成五张表question题目主表、question_option选项表、question_vehicle题目与车型多对多、question_category章节分类、question_image图片素材索引。题目主表只存题干、答案、解析、题型、难度这些不随车型变化的核心字段。车型关联表用question_id vehicle_type做联合主键vehicle_type用枚举值区分1 小车、2 客车、3 货车、4 摩托车。这样同一道题如果四种车型都考就插四条关联记录查询时按车型过滤即可不用冗余题干。-- 题目主表只存与车型无关的核心字段 CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_type TINYINT NOT NULL COMMENT 1单选 2多选 3判断, stem TEXT NOT NULL COMMENT 题干文本, answer VARCHAR(16) NOT NULL COMMENT 正确答案多选用逗号分隔如 A,C, explanation TEXT COMMENT 解析, difficulty TINYINT DEFAULT 1 COMMENT 1易 2中 3难, category_id INT NOT NULL COMMENT 章节分类ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_type (question_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 选项表一道题多个选项支持选项带图 CREATE TABLE question_option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, option_key CHAR(1) NOT NULL COMMENT A/B/C/D, option_text VARCHAR(512) NOT NULL, option_img VARCHAR(255) DEFAULT NULL COMMENT 选项图片相对路径, UNIQUE KEY uk_q_key (question_id, option_key), INDEX idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 车型关联表题目与准驾车型多对多 CREATE TABLE question_vehicle ( question_id BIGINT NOT NULL, vehicle_type TINYINT NOT NULL COMMENT 1小车 2客车 3货车 4摩托车, PRIMARY KEY (question_id, vehicle_type), INDEX idx_vehicle (vehicle_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个参数要特别注意answer字段长度给到 16 而不是 1因为多选题答案可能是A,B,C这种形式。如果只给 CHAR(1)插入多选答案时会被截断而且 MySQL 在非严格模式下不报错直接静默丢数据这个坑我踩过一次排查了两小时才发现是字段长度问题。2.2 图片素材的存储路径与命名规范图片素材是这套题库里最容易乱的部分。题目配图、选项配图、解析配图三种如果命名没有规范后期根本对不上。我一般用「题型前缀 题目ID 用途后缀」的命名方式比如q1024_stem.jpg表示第 1024 题的题干图q1024_optA.png表示 A 选项图。图片本身不存进数据库只存相对路径。数据库里question_image表记录question_id、image_type1题干 2选项 3解析、image_path、width、height。前端拿到 JSON 后自己拼接 CDN 前缀。这样做的好处是换存储桶时只改配置不用动数据。CREATE TABLE question_image ( id BIGINT PRIMARY KEY AUTO_INCREMENT, question_id BIGINT NOT NULL, image_type TINYINT NOT NULL COMMENT 1题干 2选项 3解析, image_path VARCHAR(255) NOT NULL COMMENT 相对路径如 /img/q1024_stem.jpg, width INT DEFAULT 0, height INT DEFAULT 0, INDEX idx_question (question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;图片目录建议按车型再分一层因为客车和货车的很多题目配图是专属的。目录结构用img/{vehicle_type}/{question_id}_stem.jpg这样打包给前端做离线缓存时可以按车型整包下载摩托车用户不用下货车的图省流量也省安装包体积。2.3 四种车型的题目差异怎么用关联表表达小车、客车、货车、摩托车的题库有大量重叠但差异点很关键。比如摩托车没有「高速公路」相关题目客车和货车多了「载客载货规定」类题目。如果给每种车型单独建一套题库表维护成本翻四倍改一道题的解析要改四个地方。用关联表的做法是题干和选项只存一份车型差异通过question_vehicle控制可见性。查询小车题库时WHERE vehicle_type 1查客车时WHERE vehicle_type 2。如果某道题只属于货车就只插一条vehicle_type 3的记录。-- 查询小车科目一某章节下的所有题目含选项数量 SELECT q.id, q.stem, q.question_type, COUNT(o.id) AS option_count FROM question q JOIN question_vehicle qv ON q.id qv.question_id LEFT JOIN question_option o ON q.id o.question_id WHERE qv.vehicle_type 1 AND q.category_id 10 GROUP BY q.id ORDER BY q.id;这个查询在题目量到五千以上时question_vehicle的idx_vehicle索引就非常关键。我实测过没加索引时全表扫描要 1.2 秒加了之后降到 30 毫秒以内。另外GROUP BY q.id在 MySQL 5.7 以上默认开启ONLY_FULL_GROUP_BYSELECT里的非聚合字段必须都在GROUP BY里否则报错这个在迁移环境时经常翻车。3. 从 SQL 导出 JSON接口字段设计和批量导出脚本3.1 JSON 结构怎么对齐前端刷题页的消费习惯前端刷题页最怕的是拿到数据还要自己拼装。我的 JSON 结构直接按「一道题一个对象选项是数组图片是对象」来设计前端拿到就能渲染。字段名用驼峰和前端变量命名习惯一致省得来回转换。{ id: 1024, type: 1, stem: 驾驶机动车在高速公路上行驶遇能见度小于50米时以下做法正确的是, options: [ {key: A, text: 以每小时20公里以下的速度行驶, img: null}, {key: B, text: 以每小时60公里以上的速度尽快驶离, img: null}, {key: C, text: 开启雾灯、近光灯、示廓灯、前后位灯和危险报警闪光灯, img: null}, {key: D, text: 在应急车道上停车等待, img: null} ], answer: C, explanation: 能见度小于50米时车速不得超过20公里/小时并从最近的出口尽快驶离高速公路。, images: [], categoryId: 10, difficulty: 2, vehicles: [1, 2, 3] }注意vehicles字段是个数组表示这道题属于哪些车型。前端如果做「当前车型过滤」直接判断vehicles.includes(currentVehicle)就行不用再发一次请求。images数组里放题干和解析的配图选项图放在每个 option 的img字段里这样结构最扁平。3.2 用 Python 脚本把 SQL 结果集转成 JSON 文件导出脚本我一般用 Python 写因为处理 JSON 和文件分片最顺手。核心逻辑是先查题目主表再批量查选项和图片用字典在内存里组装最后按车型分文件输出。import json import pymysql from collections import defaultdict # 连接参数按实际环境改注意 charset 用 utf8mb4 conn pymysql.connect( host127.0.0.1, port3306, userroot, passwordyour_password, databasedriving_exam, charsetutf8mb4 ) cursor conn.cursor(pymysql.cursors.DictCursor) # 1. 查所有题目主表 cursor.execute(SELECT id, question_type, stem, answer, explanation, category_id, difficulty FROM question) questions {row[id]: row for row in cursor.fetchall()} # 2. 批量查选项按 question_id 分组 cursor.execute(SELECT question_id, option_key, option_text, option_img FROM question_option ORDER BY question_id, option_key) options_map defaultdict(list) for row in cursor.fetchall(): options_map[row[question_id]].append({ key: row[option_key], text: row[option_text], img: row[option_img] }) # 3. 批量查车型关联 cursor.execute(SELECT question_id, vehicle_type FROM question_vehicle) vehicle_map defaultdict(list) for row in cursor.fetchall(): vehicle_map[row[question_id]].append(row[vehicle_type]) # 4. 组装并写文件 result [] for qid, q in questions.items(): result.append({ id: qid, type: q[question_type], stem: q[stem], options: options_map.get(qid, []), answer: q[answer], explanation: q[explanation], images: [], # 图片单独查这里先留空 categoryId: q[category_id], difficulty: q[difficulty], vehicles: vehicle_map.get(qid, []) }) with open(question_bank.json, w, encodingutf-8) as f: json.dump(result, f, ensure_asciiFalse, indent2) print(f导出完成共 {len(result)} 道题)这段脚本的关键参数是charsetutf8mb4因为题干里可能有生僻字或特殊符号用 utf8 会丢字符。ensure_asciiFalse保证中文直接输出而不是转成\uXXXX文件体积能小三分之一。indent2是为了方便人工检查如果只给程序消费可以去掉体积再小一截。3.3 按车型拆分 JSON 和图片目录的打包策略全量 JSON 文件如果超过 5MB小程序端加载会明显卡顿。我的做法是按车型拆成四个文件questions_1.json到questions_4.json每个文件只包含该车型的题目。前端首次进入时只加载当前车型切换车型时再懒加载另一个文件。图片目录同样按车型分img/1/、img/2/、img/3/、img/4/。打包时用脚本把每个车型的图片单独压缩成 zip前端按需下载解压到本地缓存。这样摩托车用户不会被迫下载货车的几百张图安装包体积能控制在合理范围。# 按车型拆分图片目录并打包 for v in 1 2 3 4; do mkdir -p dist/img_$v # 从数据库导出该车型的图片路径列表再复制文件 python export_image_list.py --vehicle $v /tmp/img_list_$v.txt while read path; do cp source$path dist/img_$v/ done /tmp/img_list_$v.txt cd dist zip -r img_$v.zip img_$v cd .. done这个脚本里export_image_list.py需要自己实现逻辑就是查question_image表关联question_vehicle输出该车型下所有图片的相对路径。zip -r的压缩级别默认是 6如果图片已经是 JPEG 格式再压缩收益不大可以加-0只打包不压缩速度更快。4. 避坑与排查题库数据落地时最容易翻车的五个点4.1 多选题答案字段被截断导致判分错误现象用户选了 A、B、C 三个选项系统判错但后台看答案确实是 A,B,C。原因answer字段定义成了CHAR(1)插入A,B,C时被截断成A前端拿到的正确答案只有 A。解决把字段改成VARCHAR(16)并且在插入前用脚本校验所有多选题的答案长度超过 1 个字符的必须走多选逻辑。4.2 图片路径大小写不一致导致部分图裂现象本地开发时图片正常显示部署到服务器后部分图 404。原因本地是 Windows 文件系统不区分大小写服务器是 Linux 区分大小写数据库里存的是Q1024_Stem.jpg实际文件是q1024_stem.jpg。解决统一命名规范为全小写导出脚本里加一步image_path.lower()并且在 CI 里加检查发现大写路径直接报错。4.3 车型关联漏插导致题目在某些车型下消失现象小车题库有 1200 道题客车题库只有 800 道但实际客车应该只比小车少 100 道左右。原因批量导入时只插了vehicle_type 1的关联忘了给客车和货车插。解决写一个校验脚本统计每个车型的题目数如果某车型题目数低于预期阈值就告警。另外在导入模板里把车型列做成必填空值直接拒绝导入。4.4 JSON 导出时中文转义导致文件体积翻倍现象导出的 JSON 文件 8MB但实际内容只有 4MB 左右。原因json.dump默认ensure_asciiTrue所有中文都转成了\uXXXX形式每个汉字占 6 个字节。解决加ensure_asciiFalse并且文件用utf-8编码写入。如果前端是浏览器环境注意响应头要带Content-Type: application/json; charsetutf-8否则可能乱码。4.5 章节分类 ID 硬编码导致换教材后全乱现象教材改版后章节顺序调整所有题目的category_id对不上前端章节列表全错。原因category_id直接用了教材的章节号没有中间层。解决建一张category表用自增 ID 做主键教材章节号作为external_code字段存。换教材时只改external_code的映射关系题目关联的category_id不动。5. 进阶技巧用 SQL 视图和 JSON 校验把题库维护成本降下来题库维护到后期最烦的是「改一道题要动三张表」。我的做法是建一个视图把题目、选项、车型、图片全部拼成一行维护人员直接查视图就能看到完整题目不用写多表 JOIN。CREATE VIEW v_question_full AS SELECT q.id, q.stem, q.answer, q.explanation, q.question_type, q.category_id, GROUP_CONCAT(DISTINCT o.option_key ORDER BY o.option_key) AS option_keys, GROUP_CONCAT(DISTINCT qv.vehicle_type ORDER BY qv.vehicle_type) AS vehicle_types, COUNT(DISTINCT qi.id) AS image_count FROM question q LEFT JOIN question_option o ON q.id o.question_id LEFT JOIN question_vehicle qv ON q.id qv.question_id LEFT JOIN question_image qi ON q.id qi.question_id GROUP BY q.id;这个视图的GROUP_CONCAT默认长度限制是 1024 字节如果选项文本很长可能被截断。执行SET SESSION group_concat_max_len 8192;可以临时调大或者直接在配置文件里改。查视图时WHERE vehicle_types LIKE %1%能快速筛出小车题目但注意LIKE会导致索引失效数据量大时还是走关联表查询更稳。另一个技巧是给 JSON 导出加校验。每次导出后跑一个校验脚本检查四件事题目总数是否在预期范围、每道题是否有至少两个选项、多选题答案是否都在选项 key 里、图片路径对应的文件是否存在。这四步能拦住 90% 的数据问题。import json, os with open(question_bank.json, encodingutf-8) as f: data json.load(f) errors [] for q in data: if len(q[options]) 2: errors.append(f题目 {q[id]} 选项少于2个) if q[type] 2: # 多选 keys {o[key] for o in q[options]} for ans in q[answer].split(,): if ans not in keys: errors.append(f题目 {q[id]} 答案 {ans} 不在选项中) for img in q[images]: if not os.path.exists(. img): errors.append(f题目 {q[id]} 图片 {img} 不存在) if errors: print(f发现 {len(errors)} 个问题) for e in errors[:20]: print( -, e) else: print(校验通过)这个脚本我一般挂在导出流程的最后一步校验不通过就不让发布。血泪经验是题库数据的问题越早发现越好等用户做到那道题才报错后悔药都没得吃。我现在的习惯是每次改完题库先跑一遍校验再导 JSON再本地起个页面随机抽 50 道题点一遍。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进