资讯详情 SpringBoot+Vue数学题库组卷系统实战:从架构设计到部署上线
📅 2026/10/5 4:24:54
这段时间一直在打磨一套基于SpringBootVue的Web数学题库组卷系统从最初的原型设计到最终能稳定跑在服务器上中间踩了不少坑也沉淀了不少经验。这套系统说白了就是给老师用的——老师可以往题库里录入数学题目、按知识点和难度打标签然后通过预设的组卷策略自动生成一套完整的试卷。对于想拿Spring Boot和Vue做实战项目的朋友来说这个系统的业务逻辑复杂度适中包含前后端分离、权限控制、复杂的查询条件拼接、Word/Pdf文档生成这些典型场景非常适合用来练手和二次开发。这篇文章我会把整个项目的架构拆分、环境准备、核心实现思路、部署细节以及我在开发过程中遇到的坑都记录下来。源码结构和文档体系我会按照一个规范项目的标准来梳理每个关键点尽量讲清楚为什么这么做而不是只给结论。无论是刚学完SSM/Vue基础想找项目练手的在校生还是需要快速搭建教学辅助系统的技术负责人都可以把这篇文章当成一份完整的项目实施参考。1. 组卷系统到底要解决什么业务模块与整体架构拆解开始写代码之前我花了两天时间梳理业务需求。很多新手做项目喜欢上来就建表、写接口结果做到一半发现表结构跟业务对不上返工成本非常高。数学题库组卷系统这类项目业务核心不是增删改查而是规则—题目怎么分类、试卷怎么生成、难度怎么控制这些才是系统的灵魂。1.1 系统模块规划从题库到试卷的完整链路这套系统我最终拆成了七个核心模块每个模块对应独立的功能域边界非常清晰模块名称核心功能技术要点题库管理题目的新增、编辑、批量导入、查询检索知识点树、题型分类、复杂条件组合查询组卷中心手动选题组卷、按策略自动抽题抽题算法、约束条件处理、组卷预览试卷管理试卷的存储、版本管理、导出下载Word/Pdf生成、模板渲染考试管理布置考试、设置考试时间、成绩登记状态机流转、定时任务知识点管理知识点的层级结构维护无限级分类、递归查询用户与权限教师/管理员/学生三种角色JWT认证、路由守卫、接口鉴权系统管理日志审计、数据备份、参数配置AOP切面、定时任务这里我要特别强调一下知识点管理的设计。数学学科的知识点天然是树形结构比如说函数下面有一次函数、二次函数二次函数下面又有图像性质、最值问题数据库里我用了一张自关联的表CREATE TABLE knowledge_point ( id bigint(20) NOT NULL AUTO_INCREMENT, parent_id bigint(20) DEFAULT 0 COMMENT 父级知识点ID0表示根节点, name varchar(100) NOT NULL COMMENT 知识点名称, code varchar(50) DEFAULT NULL COMMENT 知识点编码, level int(4) DEFAULT 1 COMMENT 层级, sort_order int(4) DEFAULT 0 COMMENT 排序号, PRIMARY KEY (id), KEY idx_parent_id (parent_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;可能有人会问为什么不用path字段存全路径比如0-1-3-8我确实看到很多项目这么干查询子节点确实快但随之而来的问题是父子节点移动时要把整条路径都重写而且删除中间节点时很容易产生脏数据。我的处理方式是插入时由Service层计算level查询时递归查子节点数据量在几百个知识点以内这套方案完全够用。这套系统我限制了单节点下的知识点数量不超过200递归查询的性能瓶颈完全可以忽略。1.2 前后端交互链路从Vue路由到Spring Boot接口项目采用了标准的前后端分离架构Vue 3 Element Plus作为前端脚手架Spring Boot 2.7 MyBatis Plus作为后端服务。前端目录结构长这样src/ ├── api/ # 所有接口请求的封装 │ ├── question.js # 题库相关接口 │ ├── paper.js # 试卷相关接口 │ └── auth.js # 登录认证接口 ├── router/ # 路由配置包含动态路由和权限拦截 ├── store/ # Pinia状态管理 ├── views/ │ ├── question/ # 题库管理页面 │ ├── paper/ # 组卷和试卷列表页面 │ ├── exam/ # 考试管理页面 │ └── dashboard/ # 数据统计首页 └── utils/ ├── request.js # 封装axios实例请求/响应拦截器 └── auth.js # token存取和校验工具后端按标准的分层结构组织com.example.exam ├── controller/ # 接口层只做参数接收和结果返回 ├── service/ # 业务逻辑层核心组卷算法在这里 ├── mapper/ # MyBatis Plus的数据访问层 ├── entity/ # 数据库实体类 ├── dto/ # 前端和后端交互的数据传输对象 ├── vo/ # 视图对象比如试卷预览的数据结构 ├── config/ # WebMvc配置、CORS配置、拦截器注册 └── common/ # 统一返回结果、异常处理、工具类一个典型的请求链路是Vue的views/paper/index.vue点击开始组卷→api/paper.js里的autoGenerate(param)方法发起POST请求→request.js拦截器自动附带JWT Token→后端PaperController.autoGenerate()接收参数→PaperService执行组卷算法→返回试卷VO对象→前端渲染试卷预览页面。这套链路在整个项目里被我反复使用核心设计原则是接口只收参数、只返回结果不掺业务逻辑。这样好处很明显后续如果换成其他前端比如小程序接口完全不需要改动。1.3 为什么用Spring Boot 2.7而不是3.x这一点可能有人会觉得抬杠但我真的建议目前大部分毕设、企业内部系统不要盲目上Spring Boot 3.x。主要原因是生态兼容性Spring Boot 3基于Jakarta EE很多老牌的第三方库比如某些代码生成器、直接操作javax.servlet的组件还停留在javax命名空间升级过程容易出幺蛾子。我用的2.7版本正好处于一个成熟稳定期MyBatis Plus、JWT、POI-TL等库都有完美支持性能完全够用。如果项目不涉及响应式编程和GraalVM之类的需求2.7就是最稳的选择。另外JDK我配的是1.8这点也值得新手注意——不是所有环境都能跑JDK 17以上版本用JDK 8能最大程度降低部署门槛。2. 环境准备与项目部署从零到能跑的完整过程这部分我尽量写细一点因为很多朋友卡在部署环节。组卷系统要跑起来不只是把前后端启动就行还涉及数据库初始化、配置调整、前端打包、Nginx反向代理等一系列环节。2.1 本地开发环境清单我自己的开发环境是这样配的供你参考JDK: 1.8推荐用OpenJDK 1.8.0_382不要用Oracle的版本Maven: 3.8.x配置了阿里云镜像不然依赖下载能急死人Node.js: 16.20.xVue 3 Vite需要版本支持IDE: IntelliJ IDEA 2023.x后端 VS Code前端数据库: MySQL 5.78.0也兼容只是注意时区配置差异缓存: 系统我用的是Redis主要用来存Token和组卷时的临时数据Maven镜像配置是个大坑很多新手在settings.xml里没配镜像结果拉一个依赖等半小时。我的settings.xml核心配置是mirror idaliyunmaven/id mirrorOf*/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror2.2 数据库初始化与Full DDL项目里我放了一个db/init.sql脚本包含建库、建表、初始化数据默认管理员账号、示例知识点、示例题目。直接执行即可mysql -uroot -p init.sql关键表一共有12张用户表、角色表、权限表、用户角色关联表、知识点表、题目表、题目选项表、试卷表、试卷题目关联表、考试表、考试班级关联表、操作日志表。这里把最核心的题目表和试卷题目关联表的结构贴出来方便你理解整个系统的数据设计CREATE TABLE question ( id bigint(20) NOT NULL AUTO_INCREMENT, type tinyint(4) NOT NULL COMMENT 题型1单选 2多选 3填空 4解答, difficulty tinyint(4) NOT NULL COMMENT 难度1容易 2中等 3困难, knowledge_point_id bigint(20) NOT NULL COMMENT 所属知识点ID, content text NOT NULL COMMENT 题干支持LaTeX格式, options text COMMENT 选项JSON数组格式, answer text COMMENT 参考答案, analysis text COMMENT 解析, creator_id bigint(20) DEFAULT NULL COMMENT 录入人ID, status tinyint(4) DEFAULT 1 COMMENT 0禁用 1启用, create_time datetime DEFAULT CURRENT_TIMESTAMP, update_time datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_kp_id (knowledge_point_id), KEY idx_type_difficulty (type,difficulty) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;CREATE TABLE paper_question ( id bigint(20) NOT NULL AUTO_INCREMENT, paper_id bigint(20) NOT NULL COMMENT 试卷ID, question_id bigint(20) NOT NULL COMMENT 题目ID, sort_order int(4) NOT NULL COMMENT 题目在试卷中的序号, score int(4) NOT NULL COMMENT 该题分值, PRIMARY KEY (id), KEY idx_paper_id (paper_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;2.3 后端启动与配置项详解后端配置文件是application.yml我重点挑了三个容易出问题的配置来讲解server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/exam_system?useUnicodetruecharacterEncodingutf8mb4useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 3 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl第一serverTimezoneAsia/Shanghai这个参数一定要加不然日期字段会报Cannot convert value ... from type TIMESTAMP to type java.sql.Timestamp这种诡异错误。第二useSSLfalse要写上本地开发环境没有SSL证书不关会警告。第三Redis我用了database: 3避免跟其他项目共用db0造成Key冲突。启动后端非常简单在项目根目录执行mvn spring-boot:run如果你用IDEA直接运行ExamApplication.java主类注意是带SpringBootApplication注解那个。2.4 前端启动、Nginx配置与打包发布前端开发环境的启动也有个小坑Vite默认启动在5173端口而后端接口在8080跨域问题就来了。我的处理是在vite.config.js里配了代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, // 不需要重写路径后端接口统一以 /api 开头 } } }这样前端请求/api/paper/list时Vite会自动转发到http://localhost:8080/api/paper/list跨域问题完美解决。开发阶段不需要在后端开启CORS实际上我后端还是开了CORS兜底防止某些特殊场景出问题。生产部署我就用Nginx来托管前端打包产物配置如下server { listen 80; server_name your-domain.com; location / { root /opt/exam-frontend/dist; index index.html; try_files $uri $uri/ /index.html; # 解决Vue Router history模式刷新404的问题 } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这一行是精华。如果你用history模式网址不带#刷新页面时Nginx会拿着真实的URL路径去找文件找不到就报404加上这一行后会把所有找不到的路径都回退到index.html让Vue Router自己去匹配路由。这一步我敢说90%的前端新手都会踩拿小本本记下来。前端打包命令是npm run build产物在dist/目录下把它整个上传到服务器的/opt/exam-frontend/dist/即可。3. 组卷的核心逻辑抽题算法与试卷生成的技术实现组卷系统最核心的部分不是漂亮的界面而是抽题算法。先规划清楚需求老师要一份一元二次方程知识点的测试卷包含5道单选题、3道填空题、2道解答题总体难度系数控制在0.65左右且同一张试卷里不能出现重复的题目最好每题的知识点分布均衡。这个需求翻译成技术语言就是在多条件的约束下从一个题库数据集合中抽取满足条件的题目子集。我设计了两种模式手动组卷和自动组卷。手动模式简单易懂老师自己从题目列表里勾选然后设置每题分值复杂的是自动模式下面详细讲。3.1 组卷策略参数与抽题流程自动组卷的三个核心参数是题型结构每种题型几道题、难度分布容易/中等/困难各占比、知识点范围限定抽哪些知识点下的题目。举个例子老师设置的是单选题5道容易:中等:困难2:2:1那算法就会先根据知识点范围筛选出所有可用的单选题得到候选集A把候选集A按难度分成三组容易组、中等组、困难组从容易组随机取2题中等组随机取2题困难组随机取1题最后检查取出的5题有没有重复理论上不会因为分组互斥但万一某道题被标记了多个难度属性这种情况我会做一次去重校验打乱顺序后返回结果代码实现大致是这样的逻辑public ListQuestion pickQuestions(PaperGenerateRequest request) { // 1. 根据题型知识点范围查询候选题目 ListQuestion candidates questionMapper.selectByCondition( request.getType(), request.getKnowledgePointIds()); // 2. 按难度分组 MapInteger, ListQuestion groupedByDifficulty candidates.stream() .collect(Collectors.groupingBy(Question::getDifficulty)); // 3. 从每个难度组按指定数量随机抽取 ListQuestion result new ArrayList(); for (Map.EntryInteger, Integer entry : request.getDifficultyCount().entrySet()) { ListQuestion pool groupedByDifficulty.getOrDefault(entry.getKey(), Collections.emptyList()); // 随机洗牌后取前N道 Collections.shuffle(pool); result.addAll(pool.stream() .limit(entry.getValue()) .collect(Collectors.toList())); } // 4. 校验是否抽够指定数量不够则提示老师题库中该难度题目不足 return result; }这个算法看起来很朴素但对老师的使用体验来说完全够用了。真正的优化空间在于如果某次抽题数量不足比如困难题只有4道但需要5道系统应该自动降级用中等题补位而不是直接报错。我在Service层加了兜底逻辑循环检查剩余所需数量从最近的一个难度组补齐同时给前端返回一条提示信息困难题数量不足已用中等题补齐。3.2 设置每题分数与总分自动校验题目抽完之后还要给每一题分配分值。单选题2分、填空题5分、解答题10分这种区分度安排我在前端组件中做成了可编辑表格老师可以逐题调整也可以批量设置同题型统一分值。后端等前端提交整份试卷后要做一次严格的校验public void validatePaperScore(Paper paper) { int totalScore paper.getQuestions().stream() .mapToInt(PaperQuestion::getScore) .sum(); if (totalScore ! paper.getTotalScore()) { throw new BusinessException(试卷总分与题目分值总和不等); } }这里有个我踩过的坑Math对象计算浮点数保持精度问题。如果试卷允许0.5分这样的分值用double累加会出现0.50.51.0但0.10.20.30000000000000004这种经典问题。解决方式是统一用BigDecimal或者在实体类里将分数字段定义为整数×10存储。我最终选了后者score字段存整数前端展示时除以10。好处是数据库和JSON传输都不容易出精度问题坏处是写代码时要注意单位换算。3.3 试卷的Word/Pdf导出POI-TL模板方案选型试卷生成以后老师大概率需要一份Word文档用来排版打印或转发给学生。这一块我试验过三种方案Apache POI直接操作WordXMLiText生成PDF以及POI-TL基于POI的模板引擎。先说结论我最终选的是POI-TL而且强烈推荐。原因如下直接操作POI需要自己处理样式、段落、换页等琐碎细节工作量大且看不出重点iText生成PDF对于数学公式支持不好LaTeX格式的公式在PDF里会渲染成乱码。POI-TL的优势在于先做好一个.docx模板文件里面用{{title}}、{{questions}}这种占位符填充时直接让模板引擎替换成真实数据样式在Word里提前调好代码只需要关心数据填充。测试过的代码片段长这样XWPFDocument document poiTLUtil.compile(templatePath, dataMap);dataMap里放的是试卷标题、题目列表、分值、答案区等。生成的Word文件里每道题自动换行填空题后面留出下划线实际是Word中的连续空格下划线字符解答题下面留出答题空白区域。这一套做下来老师说这次试卷格式终于像模像样了我觉得值了。PDF导出我选择的是通过Word转PDF服务器上装LibreOffice调用soffice --headless --convert-to pdf命令完成转换。Linux服务器上这个方案非常稳定Word转PDF基本能保住所有格式。Windows上装OpenOffice也行只是命令行参数略有不同。这里有个部署细节LibreOffice需要安装中文字体否则PDF里的中文全是豆腐块方块提前在服务器上装fonts-noto-cjk或用系统的/usr/share/fonts/下丢一个中文字体文件。4. 题库设计的关键细节题目模型、知识点树与检索优化如果说组卷算法是系统的灵魂那题库就是血肉。我发现很多开发者在做类似系统时对题目的数据建模考虑得不充分导致后期维护特别痛苦。4.1 题型、难度与知识点的标签体系设计题库三要素我拆为题型、难度、知识点。但这三个维度其实是标签而不是分类因为一道题可能同时属于一元二次方程的解法和根与系数的关系两个知识点。所以严格来说题目和知识点应该是多对多关系。我设计时妥协了一下题目表存一个主知识点目的是方便快速检索真正组卷时如果需要跨知识点抽题提供一个tags字段存JSON数组。这样既有索引查询的效率又保留多标签的灵活性。具体来说ALTER TABLE question ADD COLUMN tags varchar(512) DEFAULT [] COMMENT 扩展标签JSON数组格式;组卷时检索条件不仅看knowledge_point_id还检查tags字段是否包含指定知识点MySQL的JSON_CONTAINS函数支持这种查询。这种设计在题目数量几千条时完全够用如果以后题库量到几万条可能就得上一张question_tag_relation关联表加反查索引了。4.2 题目批量导入与数据清洗题库系统的效率瓶颈在录入环节。一道题一道题手敲不仅慢还容易在HTML格式上出错。所以我做了一个Excel批量导入功能老师按模板填Excel题干、选项A/B/C/D、正确答案、难度、知识点、解析上传后端后用EasyExcel解析然后一条条插入数据库。这里要特别注意选择浮点和数据清洗。Excel里的知识点名称往往是二次函数-图像性质这种带箭头的路径格式导入时要先根据路径找到对应的知识点ID找不到就新建。我在代码里这样处理private Long getOrCreateKnowledgePoint(String path) { if (StringUtils.isBlank(path)) { return null; } String[] names path.split(-); Long parentId 0L; for (String name : names) { KnowledgePoint kp knowledgePointMapper.selectByNameAndParent(name, parentId); if (kp null) { // 新建知识点 kp new KnowledgePoint(); kp.setName(name.trim()); kp.setParentId(parentId); kp.setLevel(names.indexOf(name) 1); knowledgePointMapper.insert(kp); } parentId kp.getId(); } return parentId; }导入过程中我还会做一个冗余校验题干为空跳过、正确答案不在选项里报错、难度超出枚举范围自动修正为默认值。这些规则虽然在代码里只占十几行但实际使用中能省下大量沟通成本——老师上传一份200道题的Excel不可能每一道都规范填写系统能自动纠正的就纠正不能纠正的列个清单给老师反馈。4.3 数学公式在网页上的渲染方案MathJax与LaTeX存储数学题系统绕不开公式渲染。做过数学类Web项目的人都知道如果题干里有平方根、分式、求和符号直接纯文本塞进去会显示成一坨乱码。我的方案是题干存储时用LaTeX格式展示时用MathJax渲染。比如这个一元二次方程求根公式x \frac{-b \pm \sqrt{b^2-4ac}}{2a}在数据库里存的就是上述LaTeX字符串前端在span classmath标签里放入这段字符串MathJax在页面加载后自动把它渲染成漂亮的分式和根号。实际使用时我在编辑器里加了一行按钮老师选中要输入的公式片段点击插入LaTeX弹窗里输入公式源码插入后前端实时预览渲染效果。这一套用户操作成本极低老师反馈良好。后端存储时本来想用markdown-it做转换但考虑到转换容易丢符号就干脆原样存LaTeX渲染交给前端后端不做任何处理省事且没有转换损耗。需要提醒的是MathJax首次加载需要从CDN拉取所需字体和脚本如果服务器在内网无外网环境需要把MathJax完整包下载放到Nginx静态目录然后在前端配置mathjax.config.js里paths指到本地路径。我一开始没意识到这点后来在内网演示时所有公式都是纯代码形式当场尴尬后来赶紧改了配置。4.4 题目的查重策略相似内容校验题库数据多了以后很容易出现重复录入的情况。我在保存题目时加了一道校验对题干做归一化处理去掉所有空格、标点符号、LaTeX关键字然后计算MD5作为指纹字段。新题目插入时先查指纹命中就直接拒绝并提示题库中已存在相同题干的题目。这套方案简单可靠不需要引入SimHash或向量相似度这类高成本方案对一模一样的重复判断题完全够用。真正的语义相近但表述不同的问题比如设a、b为实数求a²b²的最小值和已知a,b∈R求a^2b^2的最小值就只能靠老师人工判断了毕竟现阶段让机器理解数学语义难度还是太大。5. 部署与上线服务器配置、Docker化与运维中遇到的坑项目开发完成并不代表结束上线部署往往才是真正考验技术功底的时候。我把这套系统最终部署在一台2核4G的云服务器上CentOS 7.9数据库和Java进程分开部署。整体架构是Nginx负责前端静态资源和反向代理Spring Boot跑在8080端口MySQL和Redis各自独立运行。5.1 服务器基础环境配置检查项新装机的服务器第一步永远是做安全加固和基础环境安装我整理成了清单防火墙只开放80/443端口和22端口SSH管理用8080端口绝对不对公网开放Nginx编译安装开启gzip压缩前端资源压缩后体积至少减少60%MySQL设置UTF8mb4字符集和lower_case_table_names1Redis设置密码认证requirepass否则会被人拿到公网裸Redis搞攻击JDK用tar.gz方式安装配置好JAVA_HOME环境变量前端Nginx配置里gzip压缩是关键优化项我加了这段gzip on; gzip_min_length 1k; gzip_comp_level 6; gzip_types text/plain text/css application/json application/javascript application/xml text/javascript image/svgxml;压缩之后首屏加载时间从2.8秒降到了1.2秒左右体感提升非常明显。5.2 Docker化部署的优势与Dockerfile编写如果以后系统要迁移到别的机器或者一次性部署多套环境强烈建议Docker化。我的后端Dockerfile长这样FROM openjdk:8-jre-alpine VOLUME /tmp ADD target/exam-system.jar app.jar ENV JAVA_OPTS-Xms256m -Xmx512m -Dfile.encodingUTF-8 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar /app.jar]前端我用Nginx镜像来运行dist产物FROM nginx:1.24-alpine COPY dist/ /usr/share/nginx/html/ COPY nginx.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD [nginx, -g, daemon off;]然后用docker-compose.yml把三个服务编排起来version: 3 services: mysql: image: mysql:5.7 environment: - MYSQL_ROOT_PASSWORDroot123456 volumes: - ./mysql-data:/var/lib/mysql redis: image: redis:6.2 command: redis-server --requirepass redis123 backend: build: ./backend ports: - 8080:8080 depends_on: - mysql - redisDocker化有个隐性坑容器里的MySQL首次启动时会执行初始化权限但如果你把宿主机上旧的mysql-data挂载进去可能出现权限文件不匹配导致连不上。我建议最省事的方式是数据卷目录先留空让MySQL容器第一次启动时自动初始化然后手动执行init.sql脚本。别问为什么我这两套方案都踩过坑。5.3 线上故障排查清单与日志分析上线后最怕半夜发消息说系统挂了。我整理了一份典型的排查路径前端页面打不开先看Nginx进程是否存活systemctl status nginx再看80端口监听netstat -tlnp | grep 80最后看磁盘空间df -h——我遇到过Log文件写满磁盘导致nginx拒服的烂事。接口超时或返回500看后端日志/opt/exam-backend/logs/spring.log重点找ERROR和Exception关键字。如果日志显示数据库连不上用mysqladmin -uroot -p ping测试连通性。Token失效导致频繁重新登录这是我最常用的排查路径90%的情况是Redis服务停了或者Token过期时间配得着太短我设的两小时改成24小时之后基本没人反馈了。文件上传无法使用检查服务器磁盘权限chown -R给上传目录分配好属主否则会出现上传成功但文件不存在的诡异问题。日志这块我用的是Spring Boot默认的logback配置生产环境级别设为INFO只有遇到致命错误才切到DEBUG排查。线上排查现场严禁开DEBUG跑满全日志那是在玩火。我专门写了一个LogbackConfig生产环境按天滚动文件保留30天并且把关键业务动作组卷、导入、导出单独打印到business.log方便定位用户反馈的问题。6. 踩过的坑与进阶优化思路给后来者的建议最后一个部分聊聊一些从实际项目中沉淀下来的细节。有些是小坑有些是设计思路但都是一般文档里不会告诉你的东西。6.1 前后端联调最容易出现的问题清单开发过程中最磨人的不是技术难点而是一些低级但不明显的联调问题。第一个Jackson序列化Long型数据精度丢失。数据库主键是bigint类型Java实体用Long接收传给前端时如果超出JavaScript安全整数范围2^53前端解析后就会精度丢失导致ID1234567890123456789变成了1234567890123456700传回后端查询时永远查不到数据。解决办法是在字段上标注JsonSerialize(using ToStringSerializer.class)。我一开始就注意了这个问题所以在实体类主键上统一加了注解不然排查起来非常揪心。第二个日期格式时区偏差。前后端不在一个时区时日期显示会差8小时。统一方案是后端用yyyy-MM-dd HH:mm:ss字符串格式返回日期前端不做转换直接显示。具体配置在application.yml里spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8第三个Element Plus表格大数据的卡顿优化。题库列表最多的时候有5000多条数据一次性渲染整个表格页面上每次滚动都像幻灯片一样顿卡。解决办法有两个一是前端做分页我采用的是这个每页20条配合后端PageHelper二是大数据渲染改成虚拟滚动。对这套系统来说分页就足够不需要引入额外的复杂机制。第四个页面刷新后Vue Router匹配不到路由。这个问题上面提到过Nginx配置但如果你用的是Hash模式URL带#就完全没这个问题不过URL不好看。我是坚持用History模式配Nginx的try_files体验更自然。6.2 从能用到好用的优化思路项目基本功能跑通后我花了不少时间做体验优化这部分虽然不在原始需求文档里但做完后的效果非常明显。组卷结果的智能微调自动组卷生成的试卷往往会出现某几道题过于相似比如都是解一元二次方程的题目虽然满足了抽题规则但老师看着会觉得不均衡。我在生成试卷后加了一个相似度检测比较题目所属的知识点末级节点如果连续三道以上属于同一个末级知识点就在候选池里替换成其他同级知识点下的题目。代码实现不长但对出卷感受的提升是质的飞跃。数据大屏与学情分析给管理员加了一个Dashboard页面显示题库总量、试卷总量、考试平均分走势、各知识点掌握度雷达图。这些数据全部从业务表里聚合查询而来用SELECT COUNT(*)加几个GROUP BY就够用不值得为它引入OLAP引擎。前端用ECharts做可视化效果很好看也方便老师在组卷时快速判断哪些知识点还需要多准备题目。消息通知与待办组卷任务如果耗时较长比如题库五千道、条件复杂我做了异步处理和消息通知组卷完成后通过站内信提醒教师。实现方式是Spring的Async注解加一个CompletableFuture再配合WebSocket实时推送状态到前端。这个功能一开始没有但后来老师反馈说在低配服务器上组卷会卡页面加了异步后就顺畅多了。6.3 后续可以扩展的方向这套系统目前处于稳定可用状态但如果以后要做二次开发有四个方向我认为很有价值支持更多题型和复合题型比如判断题、证明题、开放性综合题以及一个题干下挂多个子问题的复合题结构。这需要改造question表的模型把题干和子题拆分为主表和子表。组卷策略的算法升级当题库量大到一定程度可以用整数规划或遗传算法求解最优组卷总难度系数最接近目标值、知识点覆盖度最大、曝光度均匀等而不是简单的随机抽样。在线考试与自动判分给系统接入在线考试模块学生在线答题、系统自动判分选择题/填空题可以精确判解答题基于关键词和步骤得分。这一块的复杂度会上一个台阶需要设计答卷表、答题记录、判分规则引擎。AI辅助出题结合大语言模型在已有题目的基础上辅助生成变式题。目前还没有成熟可靠的方案但这确实是教学辅助系统的演进方向。根据我个人实际操作的经验这套系统从开发到上线走下来最大的收获不在于某个具体技术点有多深而在于让我建立了业务先行、技术服务于需求的思路。每个模块我都在问自己这个功能到底有没有人用这样设计老师用起来会不会顺畅与其堆砌一堆华而不实的API不如把几个核心流程打磨到无可挑剔。最后再分享一个小技巧给系统一个重要操作组卷、导出试卷的前后端交互加上日志记录作用不只是排查问题还能帮你分析用户的行为习惯知道老师最常用什么题型组合、什么难度分布这对后续优化真正有用的功能很有帮助。