在线考试系统开发实战:Spring Boot+Vue前后端分离完整实现 接到一个在线考试系统的开发任务最终确定用 Java Vue 这套组合来实现交付物里还要包含源码、数据库脚本和完整文档。刚开始我也有点轻视它觉得在线考试无非是题库管理、组卷、答题、判分这几个模块做一个能跑的 Demo 不难。真把项目做完之后我发现自己原来的判断错得离谱考试系统几乎把前后端分离开发里的典型问题都覆盖了一遍权限控制、复杂表单、随机组卷、并发提交、状态同步、跨域部署每一个点都能写不少文章。这篇文章我把整个项目的思考过程、技术选型、数据库设计、核心功能实现和踩坑记录都整理出来希望能给正在做同类项目的同学或者准备给公司做内部考核平台的朋友提供一份可以落地的参考。如果你正准备拿“在线考试系统”做毕业设计、课程设计或者企业内部想要一套独立运行的考试平台这篇文章会从需求到核心实现完整走一遍。文章不省略关键实现也不回避坑整体按照“真实能跑起来”的标准来写你可以直接把思路和代码结构迁移到自己的项目里。1. 在线考试系统的需求拆解与整体设计1.1 核心业务场景分析考试系统最常见的使用场景有三类学校里的期末上机考试和随堂测验、企业或培训机构的知识考核与培训认证、还有各类竞赛和资质认证的线上答题。这些场景表面看起来差不多但需求侧重点差别很大。学校考试看重试题覆盖范围和自动判分的准确性企业内部考核更看重用户管理和成绩汇总导出竞赛类场景则对防作弊和并发提交要求更高。如果一开始不把目标场景确定清楚后面的表结构和权限设计很容易返工。我这次做的系统定位是通用型在线考试平台以学校和企业内部考核为参考基准同时兼顾考生端的高并发访问。系统核心角色分三类管理员负责基础数据维护和全局配置教师或出题人负责题库维护、组卷和试卷发布考生通过账号登录后参加考试、查看成绩。整个业务闭环是管理员创建考试计划教师维护题库并组卷系统统一发布考生在线答题并交卷后台自动判分管理员处理成绩发布和导出。设计这份需求时我建议先画出角色和状态图把“草稿、已发布、进行中、已结束”这几个考试状态定义清楚。很多开发者一上来就写接口结果做到后面发现试卷已经发布了还能修改题目考生交完卷还能重新提交这就是状态边界没划明白。1.2 前后端分离的整体架构这个系统采用前后端分离架构后端基于 Spring Boot 提供 RESTful API前端使用 Vue 开发管理端和考生端两个界面数据库使用 MySQL部署时可以分别打包部署。前端通过 axios 访问后端接口所有请求都携带 JWT 令牌后端通过拦截器统一校验身份和权限。为什么坚持前后端分离而不是像传统项目那样用 JSP 或 Thymeleaf 直接渲染页面最直接的原因是可维护性。考试系统的前端交互非常重答题页要处理倒计时、题目切换、本地暂存答案、切屏警告等状态如果和后端模板耦合在一起前后端同学没法并行开发一个招聘管理页面的小改动都可能重新打包整个应用。分离之后只要接口约定清晰前端可以独立开发联调后端也不需要关心页面渲染细节后续如果要加移动端或者小程序接口还能继续复用。部署上前端构建后的静态文件放在 Nginx 里后端打包成 Jar 包单独运行。浏览器访问 NginxNginx 把 /api 开头的请求反向代理到后端服务。这个方案看起来简单但能撑住几百人同时在线考试。实际项目中不需要一开始就上微服务单体应用加合理缓存已经足够应对大多数考试场景。1.3 功能模块规划功能模块可以按照角色权限拆成三个端。管理端用户管理、角色权限分配、公告管理、考试计划管理、数据统计、系统日志。教师端题库管理、试题批量导入、手工组卷、自动组卷、试卷预览、考试发布。考生端在线登录、查看可参加考试列表、进入考试、答题操作、交卷确认、成绩查询。把这些功能落成表格会更清楚模块功能点主要角色用户管理用户增删改查、重置密码、分配角色管理员题库管理单选、多选、判断、主观题的增删改查教师组卷管理手工选题、按条件自动抽题、分值设置教师考试管理创建考试、指定试卷、配置时间、发布管理员在线考试进入考试、答题、交卷、倒计时、防作弊考生判分统计客观题自动判分、主观题人工判分、成绩导出教师/管理员系统管理操作日志、数据备份、参数配置管理员模块拆完以后接口设计就基本有数了。我的经验是先把核心主流程打通也就是从创建试卷到考生交卷出成绩这条链路再补外围功能。如果一开始就把角色管理、审计日志、数据可视化的优先级排到核心流程前面很容易陷入边界细节里出不来。2. 技术选型与核心原理2.1 后端为什么用 Spring Boot MyBatis Plus后端选用 Spring Boot最重要的原因是生态成熟、上手快。在线考试系统涉及的常见组件比如登录鉴权、文件上传、Excel 导入导出、定时任务、数据库连接池Spring Boot 都有非常成熟的整合方案。开发阶段用内嵌 Tomcat 打一个 Jar 包就能本地启动部署阶段也不需要单独装外置容器这对中小型项目非常友好。数据库访问层我用了 MyBatis Plus。它最大的优势是单表 CRUD 不需要手写 SQL内置的方法直接完成分页查询和逻辑删除业务开发速度会快不少。但这里要提醒一下MyBatis Plus 不是万能的涉及多表关联、复杂统计和动态条件查询时该写 XML 还是要写。比如成绩统计要按班级、按试卷、按时间维度汇总直接靠它的 QueryWrapper 拼条件会非常痛苦写一个清晰的 SQL 加 GROUP BY性能和维护性都会更好。权限部分我用的是 Sa-Token 和 JWT 结合的方式也可以直接用 Spring Security。如果项目比较紧建议先用一种轻量方案把功能跑通再考虑接入重量级安全框架。考试系统的核心不是花哨的权限框架而是严格的接口鉴权和数据隔离只要能保证考生不能访问管理接口、教师不能提交试卷答案后期的权限扩展都是可控的。2.2 前端为什么用 Vue Element UI前端选择 Vue核心原因是它的响应式机制非常适合考试答题页。考生在做题时需要反复切换题目、保存答案、标记不确定题目、实时更新答题卡状态这个页面如果操作 DOM 会写得非常崩溃。Vue 把数据和视图自动绑定之后我只需要维护一个 answerMap 对象题目状态和答题卡进度就能自动刷新。管理端我使用了 Element UI 组件库。它的表格、表单、弹窗、树形控件都很完整题库管理和试卷管理这类后台页面几乎不用自己写基础组件能省大量时间。考生端则更多是自定义页面比如答题卡侧边栏、倒计时条、题目区滚动定位、交卷确认弹窗这些组件化拆分后复用性很好后续如果要增加新题型只需要扩展对应渲染组件。前端构建工具建议直接用 Vite。Vue 3 项目配 Vite 启动速度非常快开发体验比基于 Webpack 的工程好不少。如果项目需要兼容旧浏览器再考虑 Webpack 或者把构建目标降级。2.3 鉴权方案与请求链路在线考试系统必须分清角色权限这是整个系统的安全基础。我采用的方案是登录成功后后端生成 JWT 令牌前端把令牌存在本地存储中每次请求在请求头里加上 token后端拦截器解析令牌并校验用户身份。用户在数据库中的角色权限通过 RBAC 模型管理接口层再根据角色注解或自定义判断来限制访问范围。为什么不用传统 Session因为考试系统经常需要横向扩容比如全校同时考试时部署多个后端实例。Session 默认存在单台服务器内存里扩容后用户可能被路由到另一台实例导致登录状态丢失。JWT 本身无状态令牌中携带用户标识任何实例都能独立校验。当然 JWT 也有无法主动失效的问题所以我在用户表上增加状态字段账号被禁用后每次请求还会查一次用户状态牺牲一点速度换取安全性。请求链路大致是这样的浏览器发起请求Nginx 把 /api 前缀转发到后端后端拦截器先校验 token 是否存在以及是否过期然后从 token 中读取用户 id根据用户 id 查询当前角色和权限最后再进入 Controller 执行业务逻辑。这条链路清晰以后很多跨域和 404 问题都能独立排查。3. 数据库设计与核心表结构3.1 用户、角色与权限表大部分系统的第一步都是用户模块。考试系统一般有管理员、教师、考生三种角色我设计了三张核心表sys_user 保存用户基本信息sys_role 保存角色定义sys_user_role 保存用户和角色的关联关系。如果有更细粒度的功能权限可以再加 sys_menu 和 sys_role_menu但对考试系统来说角色到接口的映射通常已经够用。sys_user 表的基本结构如下CREATE TABLE sys_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, real_name VARCHAR(50) NOT NULL, user_type TINYINT NOT NULL COMMENT 1管理员 2教师 3考生, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, phone VARCHAR(20), create_time DATETIME NOT NULL );密码字段存的是 BCrypt 加密后的哈希值不要明文存储也不要用简单的 MD5。考试系统一旦出现用户数据泄露影响范围会很坏。所有表我都加了 create_time 和 update_time 这类审计字段接操作日志时非常方便。3.2 题库与试卷表题库模块是考试系统的核心资产。我看过不少项目把题目所有信息塞进一张宽表最后维护起来非常难受。按照正常业务习惯我把题库拆成 question 和 question_option 或直接在题目表里用 JSON 字段存选项。单选题、多选题、判断题、填空题、简答题的字段差异比较大用一张表加题型字段再配合统一选项结构兼容性最好。题库表核心字段大概是CREATE TABLE question ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bank_id BIGINT NOT NULL COMMENT 所属题库, question_type TINYINT NOT NULL COMMENT 1单选 2多选 3判断 4填空 5简答, content TEXT NOT NULL COMMENT 题干, options JSON COMMENT 选项内容判断题为true/false, answer TEXT COMMENT 标准答案客观题存答案标识, analysis TEXT COMMENT 答案解析, difficulty TINYINT COMMENT 难度1-5, score DECIMAL(5,2) NOT NULL, create_user_id BIGINT, create_time DATETIME );把选项直接存成 JSON可以让不同类型的题目共用一张表避免每个题型都建一张子表。查询的时候Java 后端把 JSON 解析成 List 或自定义对象前端渲染也会更方便。如果担心 JSON 字段查询效率通常也不需要担心因为题目很少通过选项内容去检索。试卷表又分为 paper试卷主表和 paper_question试卷题目关联表。paper 保存试卷名称、总分、考试时长、组卷方式、说明等信息paper_question 保存每道题在该试卷中的顺序、分值和个人随机顺序。要注意的是组卷完成以后试卷题目和分值应当固定下来教师不能随手修改否则会出现考生刚做完题后台把答案改了的情况。更好的做法是发布时生成一份试卷快照考试过程中所有数据都基于快照。3.3 考试记录与判分数据结构考生考试过程的记录是整个系统里最需要仔细设计的部分。我定义了 exam_record 和 answer_record 两张表。exam_record 表示一次考试会话保存考生、试卷、开始时间、交卷时间、总得分、状态answer_record 保存每一道题的作答内容、实际得分、判分时间。这样设计的好处是每个答案都可以独立回溯也方便主观题人工判分的扩展。exam_record 表核心字段如下CREATE TABLE exam_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_id BIGINT NOT NULL, paper_id BIGINT NOT NULL, user_id BIGINT NOT NULL, start_time DATETIME NOT NULL, submit_time DATETIME, total_score DECIMAL(8,2) DEFAULT 0, status TINYINT NOT NULL COMMENT 0答题中 1已交卷 2考试异常 3已判分, client_ip VARCHAR(64), UNIQUE KEY uk_exam_user (exam_id, user_id) );answer_record 表保存题目快照和考生答案CREATE TABLE answer_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, exam_record_id BIGINT NOT NULL, question_id BIGINT NOT NULL, question_type TINYINT NOT NULL, answer_content TEXT COMMENT 考生答案选项标识或文本, actual_score DECIMAL(5,2) DEFAULT 0, is_correct TINYINT DEFAULT NULL COMMENT 客观题是否正确, create_time DATETIME NOT NULL );这里有一个关键点exam_record 表要建联合唯一约束exam_id, user_id防止同一个考生对同一场考试重复创建记录。交卷接口也要做幂等即使前端因为网络问题重复提交也不会生成多条成绩记录。这个约束在高并发时会很管用。4. 核心功能实现与实操细节4.1 登录鉴权与会话管理用户登录接口本身很简单复杂的是登录之后的身份保持和权限控制。我实现的逻辑是登录接口接收用户名和密码校验通过后加载用户信息和角色生成 JWT 令牌返回给前端。前端把 token 存到 localStorage 或者 Pinia/Vuex 里后面每次请求通过 axios 拦截器统一注入到 Authorization 请求头。后端拦截器负责统一验签和解析用户身份。以下是一个简化的核心逻辑public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; } String token request.getHeader(Authorization); if (StringUtils.hasText(token)) { Long userId JwtUtil.parseToken(token); if (userId ! null) { UserContext.set(userId); return true; } } response.setStatus(401); return false; }需要注意的是拦截器中解析出的 userId 要用 ThreadLocal 或请求上下文保存后面 Controller 和 Service 层才能拿到当前用户信息。请求结束一定要记得清理 ThreadLocal否则线程池复用时会串用户。这个小坑我排查了很久印象非常深刻。登录注册这块还要做登录失败锁定。考试系统最容易遇到的是暴力破解账号所以连续输入错误五次后应当锁定账号十五分钟。密码加密使用 BCrypt每次加密结果不同安全性比传统加盐 MD5 好很多。4.2 自动组卷与试卷分配算法自动组卷是很多同学觉得“玄学”的部分其实核心就是两步先按条件过滤符合条件的题目再按权重随机抽题。比如一份试卷要求单选 20 题多选题 10 题判断题 10 题且难度比例是容易 30%、中等 50%、困难 20%那我在每个题型、难度组合的分组里用洗牌算法随机抽取指定数量题目。伪代码如下for each type in [单选, 多选, 判断]: for each difficulty in [1,2,3]: pool select questions where typetype and difficultydifficulty targetCount totalCount * ratio shuffle(pool) picked pool[0:targetCount] add picked to paper这个实现要注意两点。一是选题之后要去重某些题干可能被录入了多条记录需要按题干文本或唯一编号去重。二是当某个分组的题目数量不足时要有降级策略要么提示教师补充题库要么允许放宽难度条件而不是静默少题。我的做法是计算出不足的分组数量直接在前端提示“某难度单选题目不足请调整条件”让教师重新组卷而不是生成一份不完整的卷子。组卷完成后题目在 paper_question 表中保存固定顺序。但进入考试时前端可以拿到题目列表后对题目顺序做一次洗牌选项顺序也可以打乱。这样相邻考生看到的题目顺序不同能在一定程度上防止互相瞄题。不过要注意打乱顺序必须和 answer_record 保存的“对应题目 id”一致不能只打乱显示顺序而提交答案时对应错题目。4.3 在线答题、倒计时与防作弊在线答题的难点有两个一是倒计时准确性二是答案不丢失。倒计时不能完全依赖前端定时器因为浏览器标签页切换时定时器可能被挂起前端时间也容易被手动修改。我的方案是考试开始时后端在 exam_record 中写入 start_time前端每次进入考试页面都从后端加载剩余秒数。答题过程中前端定时器只负责展示和提醒真正的超时判断在后端。后端判断是否超时的逻辑是交卷接口收到请求时比较当前服务器时间和 exam_record.start_time 加考试时长。如果当前时间已经超过截止时间仍然允许提交但只保存已答题目剩余未答题目记为零分。不要直接拒绝保存因为这会给网络卡顿的考生非常大的压力。答案保存方面我在前端用数据结构 Map 保存所有作答内容每切换一道题就自动保存到本地存储同时开启一个 30 秒一次的自动保存定时器定时调用保存接口。这样即使考生手机断电、浏览器崩溃、意外关闭页面再次进入考试也能恢复到最近一次保存的答案。保存接口也要做幂等核心逻辑是找到当前 exam_record 下对应 question_id 的 answer_record有则更新没有则新增。防作弊部分我做了三件事切屏检测、进入考试时的人脸采集、管理员后台的考试实时状态。切屏检测主要靠浏览器事件当页面失去焦点超过五次系统在 exam_record 上记录异常状态提醒监考人员人工核查。人脸采集这个功能需要摄像头权限纯前端实现有一些隐私风险如果只是校内使用可以做成进入考试前拍照留档不做实时识别。4.4 自动判分与成绩统计客观题判分是简单但容易出错的地方。我的方案是交卷后不立即批量判分而是由后台任务异步处理。交卷接口只负责持久化 answer_record然后发送一个判分消息给核心线程池或者消息队列判分程序遍历考生的答案记录和 question 表的 answer 字段做比对算出总分更新 exam_record。异步的好处在于交卷高峰时前端不用等待所有题目判完才提示交卷成功体验会好很多。主观题判分走人工通道。我在管理端增加一个“待判分”列表教师点开可以看到考生提交的文本答案和题目标准答案输入分数后保存。为了公平我建议一张试卷的同一道主观题固定由同一位教师判分避免评分标准不一致。这里还可以加一层盲评隐藏考生姓名减少人为干扰。成绩统计模块后端用一个比较复杂的动态 SQL 完成。根据考试 id 聚合 exam_record按班级、部门、试卷分组统计参考人数、平均分、最高分、最低分、各分数段人数。最后通过 EasyExcel 导出 Excel管理员直接拿到成绩单。统计接口要控制好查询范围如果数据量很大可以考虑提前把成绩汇总结果缓存到 Redis避免每次刷新都跑全表聚合。5. 常见问题排查与踩坑实录5.1 跨域问题导致登录失效或接口不通前后端分离项目第一次联调时基本上都会遇到跨域问题。表现为前端请求接口报 403 或者 CORS 错误最典型的是预检请求 OPTIONS 没有返回正确响应。浏览器在发送跨域 POST 请求时会先发送一个 OPTIONS 请求询问服务器是否允许跨域如果后端没有明确返回允许的请求头后续业务请求就不会发送。解决办法是在后端加全局 CORS 配置。我这里提供一个比较完整的 Spring Boot 配置方式Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .maxAge(3600); } }如果前后端通过 Nginx 同源部署跨域问题其实可以完全规避。Nginx 把前端静态文件和 /api 接口都挂在同一个域名下浏览器访问时是同源请求后端就不需要额外开启 CORS。这是生产环境更推荐的做法。开发阶段本地联调保留上面的 CORS 配置部署阶段去掉或者放行指定域名即可。5.2 随机组卷出现空卷或重复题随机组卷最容易出现两种问题一个是抽不到题目导致试卷为空另一个是同一道题被重复选中。空卷大概率是筛选条件太严格比如同时要求题型、难度、知识点归属符合条件的题目数量为零。重复题则有可能是题库里存在完全相同的题目或者抽题逻辑没有做去重。我的排查顺序是先看题库数据量再打印筛选条件和查询结果数量。如果某个条件查询结果为零就在前端把空条件分组标红提醒教师补充题目。去重方面我在抽取时用一个 Set 保存已经选中的题目 id每抽一题前先判断是否已选过。重要的一点是不要把去重只放在代码里极端情况下还要在 paper_question 上加paper_id, question_id唯一索引兜底。5.3 交卷高峰丢成绩考试结束前十分钟大量考生同时交卷这是系统压力最大的时刻。如果交卷接口直接同步插入多条 answer_record再同步计算总分很容易出现连接池打满、请求超时、部分数据写入失败的情况。更严重的是同一个考生重复点击交卷按钮导致成绩记录被重复创建最终成绩出现多条脏数据。解决这个问题我用了三个手段。第一exam_record 表加唯一约束从数据库层面保证一场考试一个考生只能有一条记录。第二交卷接口做业务幂等收到重复请求时如果发现 exam_record 已经处于已交卷状态直接返回上一次交卷结果不再重新处理。第三把批量答案保存放到事务里同时限制单次批量大小避免一次事务插入几千条数据。如果并发量再大一些可以在前端交卷后禁用按钮并加上请求锁后端再用 Redis 分布式锁保护同一考生的提交请求。5.4 前端刷新页面出现 404使用 Vue Router 的 history 模式时前端页面刷新会直接向服务器请求对应路径。如果没有匹配的静态文件后端就会返回 404。开发环境没问题因为开发服务器会做 history fallback但生产环境如果直接用 Nginx 托管静态文件就必须配置 fallback。Nginx 配置核心是location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }加上这一句之后所有未匹配到的路径都会回到 index.html前端路由再自行解析。如果是用 hash 模式路径带 #则不会有这个问题但 URL 不够美观。部署考试系统我建议直接用 history 模式配合上面的 try_files体验会好很多。5.5 考试时间与时区坑考试系统对时间非常敏感尤其是开考时间、截止时间和倒计时。最初我直接用 Java 的 LocalDateTime 存储结果前端传过来的时间和服务器时间差了 8 个小时考试明明没开始考生却看到已经可以进入。排查后发现问题在于前后端时间序列化时没有统一时区数据库连接的 timezone 和 JVM 默认时区不一致。我的处理方案是后端统一使用时间戳或 UTC 字符串与前端交互存储到数据库时统一使用服务器本地时区。具体来说Spring Boot 配置文件中设置 Jackson 的日期格式为“yyyy-MM-dd HH:mm:ss”并指定时区数据库连接串加上 serverTimezoneAsia/Shanghai同时约定前端只发送时间戳不发送本地格式化字符串。这样整个链路的时间就完全统一了。倒计时前端显示多少秒后端只存一个截止时间戳保证逻辑清晰。6. 源码、数据库脚本与文档的组织思路6.1 源码目录规划这个项目不采用多人大型微服务架构但源码目录依然需要清晰分层。后端我习惯按功能模块分包而不是按技术层次全部堆在 controller、service、mapper 三个包下面。比如 exam 模块下放 ExamController、ExamService、ExamMapper、ExamRecordController 等这样找代码时不用跨多个包翻来翻去后续做功能裁剪也更方便。后端主要目录大致是src/main/java ├── common // 通用工具、常量、异常处理 ├── config // 配置类、CORS、拦截器、Sa-Token ├── controller // 接口层 ├── service // 业务层 ├── mapper // MyBatis-Plus Mapper ├── entity // 数据实体 ├── security // JWT、权限相关 └── module/exam // 考试模块内部细分前端目录则建议src ├── api // 按模块拆分接口请求 ├── assets // 静态资源 ├── components // 公共组件 ├── router // 路由配置 ├── store // Pinia 状态管理 ├── views │ ├── admin // 管理端页面 │ ├── teacher // 教师端页面 │ └── student // 考生端页面 └── utils // 工具函数、axios封装源码组织得好不好直接决定别人接手时的心情。很多项目功能完全正常但最终交付时因为目录乱、命名不规范让人觉得代码质量很差。我建议每个模块的 Controller 只做参数校验和结果包装业务逻辑尽量下沉到 ServiceController 里不要写超过二十行的复杂逻辑。6.2 数据库脚本与初始化数据处理数据库脚本是交付文档里容易被忽视但极其重要的部分。我的做法是建一个 sql 目录按顺序编号存放脚本01_schema.sql 负责建库建表02_data.sql 负责初始化基础数据管理员账号、角色、系统参数03_update.sql 用于后续表结构变更。脚本文件命名带上日期和描述例如 20250115_add_exam_status.sql比一个巨大的 all.sql 好维护得多。初始化数据里至少要包含一个可用的管理员账号、几个测试题库和一道示例题目这样拿到网站的人导入数据库后直接就能登录跑通流程。不要偷懒只给空表结构至少让用户能立刻看到登录成功后的界面。字符集统一使用 utf8mb4排序规则建议用 utf8mb4_general_ci 或 utf8mb4_unicode_ci否则中文可能出现乱码。如果项目要求更高可以引入 Flyway 做数据库版本管理每次启动自动执行新的迁移脚本。考试系统的表结构稳定后其实不太需要频繁迁移所以我更倾向于普通 SQL 脚本加文档说明简单直接对学习者压力也小。6.3 文档怎么写才能让接手人省心一份好的项目文档不需要写得像教科书但一定要说清楚三件事项目怎么启动、数据库怎么初始化和有哪些核心配置。我在交付文档里固定包含四个部分README 总览、环境要求与启动步骤、数据库初始化说明、接口文档。接口文档可以直接集成 Swagger或者写一份 Markdown 表格列出主要接口、请求参数和返回结构。部署文档我会写清楚 Java 环境版本、Maven 打包命令、前端构建命令、Nginx 配置片段和 MySQL 初始化命令。不要把“过程省略反正能跑就行你自己试试”这种想法带到交付环节。文档最大的价值是减少沟通成本你花半小时写清楚可能帮接手的人省两天时间。在文档里我还加了一个 troubleshooting 章节专门记录常见报错和解决方式。比如“前端请求接口 404 检查 Nginx 代理”“数据库连不上检查连接串时区”“登录后请求 401 检查 token 是否过期”。这些内容过了两个月自己回头也会有需要更不用说后面接手的技术同事了。说实话做完整套在线考试系统之后我有一个特别强烈的体会这类项目的难点从来不在某个单一接口而是把权限、状态、数据一致性这些基础问题想清楚。与其急着堆功能不如先花时间画清楚角色和流程把题库和试卷的表结构设计好再慢慢往里加防作弊、统计报表这些外围能力。如果你也正好卡在这个项目上我建议你先跑通一个最简闭环建一份试卷让考生登录答题交卷后自动出分。这个闭环通了剩下的所有功能都只是下一步迭代。