
先从一次真实的项目经历说起。去年年中公司培训部提了个需求内部员工技能认证考试原先都是发Excel问卷、人工阅卷、再手工录入成绩一场两百人的考试光催卷子、判卷子、汇总成绩就得折腾三四天。他们想要一套能在内网部署的在线考试系统要求很直接——能管理题库、能随机组卷、能自动判分、能看成绩统计最好还能扛得住几百人同时交卷。接手这个项目时我第一反应就是选择SpringBootVueMyBatisMySQL这套组合。原因很简单团队里Java基础扎实前端Vue生态熟MySQL是公司标准数据库MyBatis又足够灵活适合考试系统这种查询条件复杂、报表统计多的业务场景。这套架构在开源社区里方案非常成熟遇到问题能找到大量参考案例对工期紧张的项目来说是稳妥选择。整个项目从需求确认到上线前后用了大约六周时间。这篇文章就把完整的实现过程拆开来讲包括数据库设计、后端核心接口、前端页面结构、以及我在实际开发中踩过的坑。如果你正在准备做类似的在线考试系统或者想了解SpringBootVue项目从零到上线的完整链路这篇文章应该能帮你少走不少弯路。1. 需求拆解与数据库建模考试系统最核心的地基1.1 需求边界划定不要一上来就写代码做任何管理系统第一步不是建工程而是把需求边界画清楚。在线考试系统表面看就是出题-答题-判分但真正深入下去每一项都有大量细节题库层面题目类型有哪些单选、多选、判断题是不是都要支持题目需不需要按科目/难度分组需不需要支持批量导入组卷策略随机抽题还是固定试卷如果随机抽题每个知识点的抽题比例怎么控制考试过程考试中途断网怎么办考试时间到了没交卷怎么办考生能否重复登录判分逻辑多选题的判分规则是什么少选给不给分主观题需不需要人工复核成绩分析除了个人成绩是否需要统计部门通过率、知识点正确率这类报表我和培训部反复确认后把第一期需求收敛为以下核心点单选/多选/判断三种客观题支持按科目和难度分组随机组卷且可设置各题型数量考试中断续考自动判分以及基础的成绩导出功能。第一期先不做主观题、不做复杂的数据分析——先把主链路跑通比什么都重要。1.2 数据库表设计五张核心表撑起整个业务这个系统的数据库设计我花了整整一天时间因为表结构直接决定了后续开发的复杂度。最终的物理模型包含五张核心业务表外加两张配置表用户与角色sys_user用户表字段包括id、username、passwordBCrypt加密、real_name、department_idsys_role/sys_user_role角色表与用户角色关联表用于区分管理员、教师、考生三种角色题库与试卷exam_question题目表。核心字段包括question_type1单选、2多选、3判断、subject_id所属科目、difficulty1-5难度等级、content题干、options选项JSON格式存储、answer正确答案、analysis答案解析exam_paper试卷表。字段包括paper_name、duration考试时长分钟、total_score、pass_score、status草稿/已发布/已结束考试记录exam_record考试记录表。这是整个系统最核心的表记录每位考生的每次考试情况字段包括user_id、paper_id、start_time、submit_time、score、status0未开始、1进行中、2已交卷、3考试超时具体判分记录exam_record_answer答题明细表。每个考生每道题的作答结果用于后续人工复核和试卷回顾字段包括record_id、question_id、user_answer、is_correct、score这里有一个关键设计需要重点说明为什么答案明细要单独建表而不是直接存在考试记录表里因为一次考试会有几十上百道题如果全部塞进一条记录里要么字段多得离谱要么得用大文本字段存储后续想做某道题的正确率统计就得解析大文本性能和可维护性都很差。拆成明细表后统计某一题的正确率就是一条简单的SQL按question_id分组统计答对数量除以总答题数。这个设计在后续做成绩分析时省了大量力气。1.3 MyBatis表关联与VO设计数据库字段到前端对象的映射表结构设计完成后紧接着要解决的就是MyBatis的映射问题。我在这个项目里采用了一套比较固定的分层模式Entity与数据库表结构一一对应的实体类字段名和表字段用驼峰命名自动映射VOView Object面向接口返回的对象比如试卷VO需要包含题型说明、总分、题量等冗余展示字段DTOData Transfer Object接收前端参数的对象比如组卷DTO包含题型数量、科目范围、难度分布等这里最容易踩的坑是直接用Entity返回给前端。实际项目中前端往往需要一些数据库里不存在的冗余字段比如试卷列表需要显示题目数量而这个值存储在关联表中如果直接用Entity返回要么得在Entity里塞无关字段要么得额外写一堆关联查询。我在这个项目里统一坚持Controller返回VO接收DTO的原则虽然多写几个类但后期维护时思路极其清晰。MyBatis的XML映射里多表关联查询是核心难点。就拿考试记录列表来说前端要展示的数据包括考生姓名来自用户表、部门名称来自部门表、试卷名称来自试卷表、得分来自考试记录表。这时就需要一条三表关联的SQL。如果只是简单嵌套查询会产生经典的N1问题。我在实践中优先选择联表查询加ResultMap手动映射避免滥用MyBatis的嵌套select在数据量大时性能差异非常明显。2. SpringBoot后端架构与核心模块实现2.1 工程结构设计与统一响应体后端采用标准的SpringBoot单工程结构按业务模块分包而不是按技术分层分包。这个选择是经过考量的按业务分包controller/service/mapper按模块组织在项目规模扩大时找代码的效率远高于按技术分层组织。最终结构如下com.example.exam ├── config // 配置类CORS、拦截器、全局异常处理 ├── controller // 控制层按业务模块拆分 │ ├── QuestionController │ ├── PaperController │ ├── ExamController │ └── ScoreController ├── service // 业务逻辑层 ├── mapper // MyBatis的Mapper接口 ├── entity // 数据库实体 ├── vo // 视图对象 ├── dto // 数据传输对象 └── utils // 工具类JWT、Excel导出等接口设计上我封装了一个通用的Result响应体包含code、message、data三个字段。code为0表示成功非0表示业务异常。所有接口返回统一结构前端只需封装一个axios拦截器统一处理就能避免大量重复的错误处理代码。由于考试系统面向的是企业内部用户前后端分离部署时必然遇到跨域问题。我推荐在SpringBoot层面统一通过CorsFilter配置跨域规则而不是在每个Controller上写CrossOrigin注解。前者是全局生效、配置清晰后者分散在各处难以维护。2.2 核心接口实现组卷、交卷与判分后端最核心的接口有三个组卷、交卷、判分。其中组卷接口的设计直接关系到考试的科学性判分接口则关系到并发稳定性和数据准确性。组卷接口的设计思路组卷的本质是按规则从题库中随机抽取题目。我实现了一个PaperService.generatePaper()方法核心逻辑如下接收前端传参DTO包含试卷名称、各题型数量、知识点范围、难度分布按题型拆分轮次每种题型分别执行一次查询使用ORDER BY RAND() LIMIT n随机抽取确保抽题范围符合设定的知识点和难度条件组合所有题目计算总分生成试卷记录将题目快照存入exam_paper_question关联表——这里特别关键试卷生成后即使题库中的题目后来被修改已生成的试卷也不能受影响因为考完试要做试卷分析、争议仲裁如果题目内容随时会变这些工作就无从谈起这里还有一个细节值得注意如果题库量大且抽取权限频繁ORDER BY RAND()在百万级数据下会非常慢。我的处理方式是先按条件查出符合条件的题目id列表用Java的Collections.shuffle()做内存随机打乱后再截取所需数量。这样既保证了随机性又避免了大表随机排序的性能问题。交卷判分的并发处理交卷是考试系统并发压力最大的时刻。几百名考生同时交卷如果处理不好很容易出现部分考生成绩丢失或数据库连接池耗尽。我在实现时做了三件事交卷接口异步化——考生点击交卷后后端立即返回交卷成功实际判分和成绩落库通过线程池异步执行。考生的核心诉求是交卷成功这个反馈成绩稍等片刻出结果完全可以接受判分逻辑放在Service层使用事务确保成绩记录和答题明细要么全部写入、要么全部回滚数据库层面使用record_id user_id的唯一索引防止并发重复交卷造成数据重复判分逻辑本身比较简单因为数据库设计时就把题目答案存成了标准答案字段多选的规则通过字符串匹配实现。值得注意的是判分时不要直接在前端提交的答案上做逻辑判断而是以考试记录中的题目快照为准重新查询标准答案有效防止前端篡改提交的答案字段。这类安全问题在开发时容易被忽略但线上系统必须提前考虑。2.3 JWT认证与权限控制三种角色的访问边界这个系统有管理员、教师、考生三种角色权限边界必须清晰管理员管用户、管题库教师管命题、管组卷考生只能答题和查成绩。认证方案我选的是JWTJSON Web Token流程是用户登录成功后后端生成一个包含用户id和角色信息的token返回给前端前端每次请求在Header中带上token后端通过拦截器解析token识别用户身份并校验权限。为什么用JWT而不是传统的Session关键考量是前后端分离——Session天然依赖服务器端存储在集群部署时需要额外的Session共享方案如Redis。而JWT是无状态的token本身携带用户信息后端只需验证签名即可不需要额外的存储开销。当然JWT也有缺点比如无法主动失效我当前的处理方式是给token设置有效期关键操作如修改密码后强制重新登录。权限控制我用的是SpringBoot拦截器配合自定义注解Target(ElementType.METHOD) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String[] value(); }在需要权限控制的接口上标注RequireRole({ADMIN})拦截器解析token拿到角色后做匹配校验。这种方式比Spring Security更轻量也能满足这个项目的需求。如果未来要接入更复杂的权限模型如细粒度的数据权限再演进到Spring Security也不迟。3. Vue前端工程设计与考试流程开发3.1 前端工程结构与路由组织Vue部分我选择的是Vue2 Vue Router Vuex axios这套成熟组合。工程采用标准的vue-cli脚手架初始化按业务模块划分views目录src ├── api // 接口请求封装 ├── assets // 静态资源 ├── components // 通用组件 ├── router // 路由配置 ├── store // Vuex状态管理 └── views // 页面 ├── login ├── admin // 管理员端用户管理、题库管理 ├── teacher // 教师端组卷、试卷管理 └── student // 考生端考试列表、答题页、成绩查询路由设计时我采用了动态路由的思路根据用户登录后的角色动态注册路由表而不是静态写死所有路由。这样考生登录后浏览器里根本不存在管理端路由从入口上就切断了越权访问的可能。路由守卫beforeEach中判断token是否存在与有效这是整个前端安全的第一道关卡。3.2 在线答题页考试体验的关键所在考试页是整个前端开发中耗时最长的部分。用户要看着倒计时答题可能会遇到刷新页面、浏览器崩溃的情况。这个页面的核心要求是答题状态不丢失、时间计算准确、交卷不误触。答题状态保存我采取了两级方案每答一题立即调用后端接口保存作答记录记录当前题号和答案甚至在点击下一题时就把上一题的答案批量提交。考试页面中我用Vuex的store管理当前试卷信息和已作答案同时周期性地将作答进度同步到后端。如果考生刷新页面重进考试页时从后端拉取最新作答进度渲染已保存的答案——这也是实现断点续考的基础。倒计时用specific函数每秒刷新显示剩余时间但准确判定超时一定以服务器时间为准交卷时后端会校验submit_time与start_time差值是否超过考试时长前端倒计时只是展示作用不能作为判定的唯一依据。3.3 管理端页面表格、弹窗与批量导入管理端的核心是题库管理页面。这是一个典型的CRUD页面包含题目列表支持按题型、科目、难度筛选、添加/编辑题目弹窗选项采用动态增减的JSON编辑器、批量导入功能支持Excel模板上传。批量导入是教师用户最关心的功能——手动录入几百道题非常痛苦如果能从Excel批量导入效率提升是质的飞跃。前端我用el-upload组件封装了上传逻辑后端解析Excel时选择了Apache POI库逐行校验题型、选项格式、答案格式校验失败的行返回详细错误信息行号错误原因全部通过后批量插入数据库。这个功能上线后培训部把过去积累的三千多道历史试题一次性导入省了至少一周的人工录入时间。4. 上线前的测试排坑与MyBatis/MySQL联动优化4.1 并发交卷压测与数据库连接池调优系统开发完成后我第一时间用JMeter做了并发测试。测试场景设定为200人同时交卷结果第一次压测就暴露了问题数据库连接池直接耗尽大量请求报错。排查发现SpringBoot默认的HikariCP连接池最大连接数是10而交卷接口涉及多张表的读写操作每个请求占用连接的时间较长并发一上来就扛不住了。我的调整方案是这样的将spring.datasource.hikari.maximum-pool-size调高到50minimum-idle设为10同时把判分逻辑优化为批量写入。但这只是第一步更深层次的问题是事务粒度——交卷线程池中的判分任务如果串行执行队列积压严重。然后我改为每个事务处理一批20条答题明细减少了无效的事务开销。调整后再次压测两百并发交卷的接口响应时间从最开始的3秒多降低到900毫秒左右基本稳定。这个排查过程让我深刻体会到SpringBoot项目上线前连接池参数调优和事务粒度设计是必须做的前置工作而不是等到线上出问题才处理。4.2 MySQL索引优化记录明细表查询慢的根因另一个典型问题是exam_record_answer答题明细表的查询。考试结束后管理员要看所有考生的答题明细列表SQL需要按考试记录id查询并关联题目表获取题干。数据量只有几万条时这个查询就耗费了3秒多显然不合理。通过EXPLAIN分析执行计划我发现record_answer表上的查询没有命中任何索引导致全表扫描。解决方案是给关键外键字段建立联合索引idx_record_question(record_id, question_id)。建立索引后同样的查询瞬间降到几十毫秒。这里想特别提醒的是MyBatis的XML里如果写了复杂的动态SQL一旦涉及多表关联最好先通过EXPLAIN查看索引命中情况再决定是否补充冗余字段。为了查询性能必要时可以在业务表中冗余存储一些展示字段比如题目类型名称以空间换时间这在报表类页面中是常见的优化手段。4.3 前后端联调中的常见合作问题前后端联调阶段也遇到了不少值得记录的坑。最典型的一类是字段类型不一致后端返回的BigDecimal类型前端用parseFloat计算后会损失精度造成成绩显示错误再比如日期字段后端返回的是LocalDateTime序列化后的字符串前端如果用时间戳做倒计时计算又是一次踩坑。我后来统一在全局配置里设定了Jackson的日期序列化格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8前端开发环境里用axios拦截器统一处理时间字段避免每个页面单独处理。联调时的另一个有效实践是后端主动维护一份接口文档我推荐用Apifox每次修改接口后及时同步给前端同事。光靠口头沟通一定会在某个深夜因为参数名对不上而双双加班。前后端协作最尴尬的就是前端说接口返回了但字段不对后端说我明明传了结果发现是命名不一样——bizData和biz_data之争能反复上演。提前约定好命名规范我偏向于前后端统一使用驼峰命名能省掉大量无效沟通。5. 系统演示与部署上线从本机到内网服务器的完整流程5.1 构建打包前端静态资源与后端可执行Jar分离部署系统上线采用前后端分离部署前端打包成静态文件交给Nginx托管后端打包成可执行Jar运行在Java环境中。这种方式的好处是前端静态资源由Nginx直接处理并实现反向代理后端专注提供API接口彼此独立、各自扩容。前端构建时要注意的是Vue项目中的axios请求地址不应该写死成IP或域名而是通过环境变量区分开发环境和生产环境。我在项目根目录配置了.env.development和.env.production两个环境文件在代码中通过process.env.VUE_APP_BASE_API获取请求路径。这样打包时就不会遇到本地能调通、部署到服务器就404的尴尬问题。Nginx反向代理的配置核心在于将/api前缀的请求转发到后端服务server { listen 80; server_name exam.example.com; root /opt/exam-front; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } # 前端路由使用history模式时必须配置try_files location / { try_files $uri $uri/ /index.html; } }配置里最关键的是try_files那条——Vue Router如果用了history模式即去除URL中的#刷新页面时Nginx会按路径找静态文件找不到就会404。加上try_files后Nginx会回退到index.html由前端路由自己解析路径刷新问题就解决了。这个配置几乎每个Vue项目部署时都会遇到提前写上可以少踩一次坑。后端Jar包运行我习惯用Systemd配置一个服务而不是直接nohup java -jar。原因有两个一是服务崩溃后systemd能自动重启保证可用性二是开机自启服务器重启后不用人工干预。配置文件中设置好JAVA_OPTS比如-Xms512m -Xmx1g防止内存分配不均导致的GC问题。5.2 初始化数据与生产环境验证不能只在测试库上自嗨上线前的最后一步是数据初始化和生产环境验证。我用Flyway管理数据库脚本保证在不同环境开发、测试、生产中数据库结构完全一致。项目启动时Flyway会自动执行版本化SQL从建表语句到初始数据默认管理员账号、角色、科目数据都按版本记录下来。这比手动去生产库执行SQL脚本可靠得多——至少不会出现生产库字段比测试库少一列的惨剧。生产环境部署完成后我用编写好的自测用例跑了一遍全流程管理员登录创建科目与用户、教师导入题库并组卷、考生登录参加考试并交卷、查看考试成绩与导出Excel。同时检查了几个容易忽视的细节考试页倒计时是否和服务端时间一致、考生交卷后是否还能重复进入考试页面、成绩列表的数值位数是否保留两位小数。这些细节虽小却直接决定用户对系统专业度的体感——一个排序错乱、一个日期格式不对都足以让使用者对整套系统失去信心。6. 复盘与经验总结这套系统还能怎么进化系统上线稳定运行后我对这个项目做了整体复盘。如果现在重新做一次有几个地方我会做出调整SpringSecurity替换自定义拦截器当前用注解拦截器实现了基础的RBAC基于角色的访问控制足够支撑现状。但后续如果要做细粒度的数据权限比如教师只能维护自己负责科目的题库自定义方案会越写越复杂那时迁移到SpringSecurity会更划算引入缓存层题库的查询频率极高但变化频率很低非常符合Redis缓存的典型场景。当前阶段靠MySQL性能还扛得住数据量进一步增长时优先把热点题目列表和试卷试题快照缓存到Redis判分逻辑可配置化现在多选判分规则写死在代码里后续如果培训班提出少选得部分分这类需求还得发版。更好的做法是把判分规则配置化存入数据库配置表运营人员改配置即可生效主观题支持与人工复核流程这是最明确的下一个迭代方向。在线考试系统如果只支持客观题适用范围会窄很多。加入主观题后需要新增一道人工阅卷的工序——教师端分配待阅卷试题、批注、打分再汇总到考生最终成绩。这套流程的设计我打算采用任务队列模式确保阅卷任务分配均衡、状态流转清晰最后再分享一个自己印象深刻的经验开发这类管理系统时一定要尽量seeing the system as a product——它不是一堆CRUD接口的堆砌而是真实用户每天要用的工具。研发人员很容易沉迷于技术实现本身的优雅却忽略了用户的真实痛点。这个考试系统上线后培训部同事最感谢的功能不是高深的并发设计而是批量导入试题和成绩一键导出Excel。技术永远为业务服务能用、好用、愿意用才是最根本的检验标准。如果这篇文章对正在做或准备做在线教育类系统的朋友有帮助那就值得了。欢迎在评论区交流你的技术选型和踩坑经验一起把这类系统做得更好。