Java SSM+Vue在线考试系统:组卷答题、自动判分与毕业设计实战

发布时间:2026/11/2 11:22:44
Java SSM+Vue在线考试系统:组卷答题、自动判分与毕业设计实战 简介这是一套基于 Java 的在线考试系统完整开发资料采用 SSM 框架与 Vue 前端实现包含源码、数据库脚本及配套论文适合计算机专业学生用于毕业设计、课程设计或中小型考试系统二次开发。压缩包共 534 个文件约 15.54MB以 95 个 Java 源文件、95 个 class 编译文件、43 个 Vue 组件、34 个 XML 配置及 SQL 数据库脚本为主另有 js、css、图片和项目构建脚本结构覆盖后端控制层、前端页面与部署配置。系统分为前台考生与管理员两类角色支持首页、个人中心、考试成绩、公告通知、试题、试卷和考试管理等模块可帮助读者理解角色权限、试卷生成、考试记录与成绩统计等业务逻辑。目前已有 159 人学习配套论文与数据库便于撰写文档和本地运行调试适合需要完整项目案例与排错参考的读者。1. 从一个真实选题场景说起这套 Java SSM Vue 的在线考试系统能干什么做毕业设计最难受的不是不会写代码而是开题两周了还在纠结做什么。前阵子一位学弟找我需求很具体要有增删改查但不能只是增删改查要有角色区分但别一上来就微服务。最后选定了在线考试系统这条线——教师出题组卷、学生限时答题、交卷自动判分、后台按班级看成绩。这份带源码、数据库脚本和论文文档的 SSM Vue 在线考试系统正好把这条链路走通了后端用 Java 的 Spring SpringMVC MyBatis前端 Vue 配组件库数据落到 MySQL工程结构是典型的前后端分离。它适合两类人赶课程设计/毕业设计、需要一套能跑又能改的完整骨架的以及刚学完 SSM、想找个中等体量项目练手的。下面按「它是什么、怎么跑起来、核心链路怎么写、怎么改成自己的课题」一层层拆。2. 技术选型与分层架构SSM 三件套加 Vue 前后端分离是怎么搭起来的2.1 后端分层为什么这套骨架用 SSM 而不是 Spring Boot先说选型。这套项目属于 Spring Boot 铺开之前的经典写法Spring 负责 Bean 管理和事务SpringMVC 负责 URL 到方法的映射MyBatis 负责 SQL 与对象之间的转换。对毕业设计场景来说这种「配置显式暴露」的结构反而有好处答辩老师问「事务在哪配的」「Mapper 是怎么被扫描到的」你能直接翻出那个 xml 指给他看而不是被自动配置遮在黑匣子里。代价是配置文件多第一次跑容易在路径和扫描包上翻车这个后面第 5 章会具体说。分层是标准的四层Controller 接参数做校验Service 承担业务和事务边界DAOMapper只碰 SQL实体层再拆成 DO对应表和 VO对应页面。一个请求进来Controller 先做参数合法性判断转成 DTO 交给 ServiceService 里开事务、调多个 Mapper最后统一包装成Result返回。这套统一返回体是整个项目的地基先看它// com.exam.common.Result public class ResultT implements Serializable { private Integer code; // 200 成功401 未登录403 无权限500 业务异常 private String msg; // 给前端直接弹提示的文案 private T data; // 业务数据列表接口会再包一层 PageResult public static T ResultT ok(T data) { ResultT r new Result(); r.code 200; r.msg success; r.data data; return r; } public static T ResultT fail(Integer code, String msg) { ResultT r new Result(); r.code code; r.msg msg; return r; // data 留空前端只看 code 和 msg } }code是前后端约定好的状态位前端拦截器只认它不去解析 HTTP 状态码这样 Tomcat 抛 500 时前端也能拿到可读文案。msg一定要写成用户能看懂的话别把NullPointerException直接塞进去——这是很多模板项目的通病答辩演示时弹一串堆栈很难看。data泛型化是为了让同一套包装既能返回单个对象也能返回分页结果。配套的还有两样东西一个是全局异常处理器ControllerAdvice把 Service 抛的自定义BizException统一转成Result.fail另一个是 Mapper 扫描配置常见做法是在 Spring 的 xml 里写MapperScannerConfigurer把basePackage指到com.exam.dao漏了这一行启动就会报「No qualifying bean of type XxxMapper」。2.2 前端结构Vue 单页应用与 axios 请求封装前端是 Vue 单页应用路由用 vue-router状态用 vuex 存登录用户和角色UI 用组件库的表格、表单、弹窗把管理端铺出来。学生端和教师端在同一个工程里通过路由的meta.role区分不搞两套代码。真正决定这个项目好不好改的是请求封装这一层。几乎所有页面都会 import 同一个 axios 实例登录态、错误提示、401 跳转都收在这一个文件里改一处全站生效// src/utils/request.js import axios from axios import { Message } from element-ui import router from /router const service axios.create({ baseURL: /api, // 开发环境由 devServer 代理转发生产环境交给 nginx timeout: 15000 // 交卷、导出这类接口留足时间别用默认值 }) // 请求拦截统一带上登录票据 service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }, error Promise.reject(error)) // 响应拦截把 Result 拆开页面里只拿 data service.interceptors.response.use(res { const body res.data if (body.code 401) { localStorage.clear() router.replace(/login) return Promise.reject(new Error(登录已失效)) } if (body.code ! 200) { Message.error(body.msg || 请求失败) return Promise.reject(new Error(body.msg)) } return body.data // 注意这里返回的不是 res页面里直接 .then(data ...) }, error { Message.error(网络异常请稍后重试) return Promise.reject(error) }) export default servicebaseURL写成/api是个关键决定开发阶段由vue.config.js的 proxy 转发到后端 Tomcat生产阶段由 nginx 把/api反代过去前端代码里永远不出现http://localhost:8080换服务器时不用改一行业务代码。timeout给了 15 秒因为交卷接口要写多张表默认的几秒钟容易在慢机器上误报超时学生一刷新就可能重复提交。响应拦截器里return body.data这一步要留意它意味着所有页面拿到的已经是剥了壳的业务数据新写的接口如果没按Result包页面里就会拿到undefined这是二次开发时最常见的自坑点。2.3 数据库设计考试系统绕不开的七张表表设计决定后面代码好不好写。这套项目的核心表大致是这么分的表名作用关键字段sys_user用户管理员/教师/学生username、password、role、class_idcourse课程course_name、teacher_idquestion题库type、content、options、answer、score、difficultypaper试卷total_score、pass_score、duration、start_time、end_timepaper_question试卷与题目的关联paper_id、question_id、question_score、sortexam_record一次考试记录paper_id、user_id、status、score、submit_timeexam_answer逐题作答明细record_id、question_id、user_answer、is_right、get_score其中两个设计点值得单独说。第一题目选项options用文本存 JSON 字符串形如[{key:A,value:...}]而不是拆成选项表——考试题的选项没有独立查询需求拆表只增加 join 成本。第二exam_record.status是整套系统的幂等开关0 待考/进行中、1 已交卷待批、2 已出分。后面讲交卷时会看到所有防止重复提交的逻辑都压在这一个字段上。建表脚本里几个类型的字段别随手改CREATE TABLE exam_answer ( id bigint(20) NOT NULL AUTO_INCREMENT, record_id bigint(20) NOT NULL COMMENT 考试记录id, question_id bigint(20) NOT NULL COMMENT 题目id, user_answer varchar(2000) DEFAULT NULL COMMENT 学生作答多选按字母升序存如ABD, is_right tinyint(1) DEFAULT NULL COMMENT 1对 0错 NULL待人工批阅, get_score decimal(5,1) DEFAULT 0.0 COMMENT 本题得分, PRIMARY KEY (id), UNIQUE KEY uk_record_question (record_id,question_id), KEY idx_record (record_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT答题明细;user_answer给了 2000 长度是因为简答题的文字答案也塞在这个字段里用varchar(255)会被截断学生写了半屏答案只存进前 255 个字成绩自然对不上。is_right允许为 NULL用来区分「判错了」和「还没判」——这个区分在主观题场景下必须保留。uk_record_question唯一索引是最后一道防线即使前端重复提交重复插入也会被数据库挡下来。3. 目录结构与启动链路从导入 SQL 到登录成功的四步3.1 工程目录一览模块怎么分、文件放哪儿拿到压缩包先别急着点运行花五分钟把目录扫一遍后面找配置文件能省一半时间。项目是典型的 Maven 多模块或者单模块分包的布局大致长这样exam-system/ ├── exam-web/ # 后端主工程打成 war 部署到 Tomcat │ ├── src/main/java/com/exam/ │ │ ├── controller/ # 登录、题库、试卷、考试、成绩、统计 │ │ ├── service/ # 接口 impl 实现事务在这里加 │ │ ├── dao/ # Mapper 接口与 xml 一一对应 │ │ ├── entity/ # DO 实体和表字段同构 │ │ ├── vo/ # 页面模型PaperVO、QuestionVO、ScoreVO │ │ ├── common/ # Result、常量、工具类、自定义异常 │ │ └── interceptor/ # 登录与角色拦截器 │ ├── src/main/resources/ │ │ ├── spring/ # spring-core.xml、spring-mybatis.xml、spring-mvc.xml │ │ ├── mapper/ # 所有 MyBatis 的 xml与 dao 包同名同结构 │ │ ├── jdbc.properties # 数据库连接信息第一个要改的文件 │ │ └── log4j.properties │ └── src/main/webapp/WEB-INF/web.xml ├── exam-vue/ # 前端工程 │ ├── src/api/ # 按模块拆的接口函数 │ ├── src/views/ # admin / teacher / student 三套页面 │ ├── src/router/index.js # 路由表 角色守卫 │ ├── src/utils/request.js # 上一节的 axios 封装 │ └── vue.config.js # 开发代理 ├── sql/exam.sql # 建库建表 初始数据 └── doc/ # 论文文档与部署说明判断一个 SSM 项目规不规范有个土办法看mapper目录下的 xml 文件名是否和dao包下的接口名完全对应。对不上就得靠namespace硬撑改起来极容易漏。这套项目是对齐的QuestionMapper.java对应QuestionMapper.xml找 SQL 时按名字翻就行。3.2 后端配置改三处就能脱离原作者的机器后端跑不起来九成卡在配置。第一处是jdbc.properties把用户名密码换成自己本机的jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/exam_system?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码这里有两个坑要提前说。serverTimezone必须写MySQL 8 的驱动漏了它连接能建上但所有时间字段会错 8 小时或直接抛异常考试开始时间变成未来时间学生会看到「考试未开始」。useSSLfalse在本地关掉避免自签证书告警刷屏日志。第二处是spring-mvc.xml里的静态资源与注解驱动第三处是web.xml里的 Spring 容器加载和字符编码过滤器。字符编码过滤器要放在所有 filter 最前面否则中文题目在 POST 提交时容易变成问号——这个 bug 在题库导入环节特别隐蔽等到考试时才发现题干乱码。部署方式上这类工程通常是打 war 包丢进 Tomcat或者在 IDE 里配一个本地 Tomcat 实例。常见做法是 IDEA 里Run → Edit Configurations → Tomcat Server → LocalDeployment 页签添加exam-web:war explodedApplication context 设成/exam。如果上下文路径和前端代理里的不一致登录接口会稳定 404这一条后面还会提。3.3 前端启动npm 装依赖与代理转发前端三步走装依赖、改代理、起服务cd exam-vue npm install --registryhttps://registry.npmmirror.com # 国内源装得快 npm run servevue.config.js里要确认代理指向后端真实的端口和上下文路径module.exports { devServer: { port: 9528, proxy: { /api: { target: http://localhost:8080/exam, // 与 Tomcat 的 context 保持一致 changeOrigin: true, pathRewrite: { ^/api: } // 去掉 /api 前缀再转发 } } } }pathRewrite是这一段的重点。前端请求/api/exam/list转给后端时变成/exam/exam/listtarget 已经带了/exam。如果后端 Controller 上的RequestMapping写的是/exam而你 target 又没带 context就会拼出/exam/exam/list这种重复路径。遇到 404 先别怀疑代码把浏览器 Network 里的完整请求 URL 抄下来对着 Tomcat 的访问日志比一遍十秒能定位。3.4 初始化数据导入 SQL 与第一个账号建库语句和初始数据都在sql/exam.sql里用命令行或者客户端工具导入即可。顺序上先建库、再建表、最后插初始数据脚本如果没写USE语句导入前要手动选中目标库否则表会建到默认库里后端连上去一片空。初始账号通常在脚本末尾的INSERT里一般会给出管理员、教师、学生各一个。第一次登录成功后第一件事是把密码改掉再演示——答辩现场被人用默认密码登进管理员后台这种事不算罕见。另外学生账号一般绑定了class_id如果这个字段是 NULL按班级查成绩的页面就会查不到数据看着像功能坏了其实是数据没初始化好。4. 核心链路拆解一场考试从组卷到出分的完整实现4.1 组卷固定挑题与随机抽题的两种写法组卷是把question表里的题按规则塞进paper_question。项目里一般两种模式并存教师手动勾选指定题目或者按题型、难度、数量随机抽。手动模式简单前端传一个questionIds数组后端批量插入关联表随机模式要写动态 SQL。先看手动组卷的落库逻辑// PaperServiceImpl.createPaper Transactional(rollbackFor Exception.class) public Long createPaper(PaperDTO dto) { Paper paper new Paper(); BeanUtils.copyProperties(dto, paper); paper.setCreateBy(UserContext.getUserId()); // 从登录态取别信前端传的 paper.setStatus(0); // 0草稿 1已发布 paperMapper.insert(paper); // 主键回填到 paper.id ListPaperQuestion list new ArrayList(); for (Long qid : dto.getQuestionIds()) { Question q questionMapper.selectById(qid); PaperQuestion pq new PaperQuestion(); pq.setPaperId(paper.getId()); pq.setQuestionId(qid); pq.setQuestionScore(q.getScore()); // 默认用题库分值允许前端覆盖 list.add(pq); } paperQuestionMapper.batchInsert(list); // 一条 insert ... values (...),(...) 搞定 paper.setTotalScore(list.stream().mapToDouble(PaperQuestion::getQuestionScore).sum()); paperMapper.updateTotalScore(paper.getId(), paper.getTotalScore()); return paper.getId(); }Transactional(rollbackFor Exception.class)里必须写rollbackForSpring 默认只对RuntimeException回滚一旦 batchInsert 抛的是受检异常主表已经插进去了明细全丢试卷就是一张空卷。createBy从服务端的登录上下文取不用前端传的值否则学生改个请求体就能造出别人名下的试卷。总分不是前端算完传上来的而是后端拿明细求和再回写防止前端传个 60 分但题目加起来 100 分。随机抽题的 SQL 常见写法是ORDER BY RAND() LIMIT #{num}题量几百条时能用几千条以上就会明显拖慢稳妥的做法是先按题型查出 id 列表在 Java 里Collections.shuffle取前 N 个再按 id 批量查详情两段 SQL 换掉一次全表排序。4.2 开考试卷下发与学生答题卡生成学生点「开始考试」后端要做三件事校验考试时间窗、防止重复生成答题记录、下发不含答案的试卷。这里最容易翻车的是「下发的内容」。很多模板项目图省事直接把question实体整个塞给前端answer字段跟着一起下发了——打开浏览器 F12 就能看到标准答案这个漏洞在答辩时被问一句就很难看。正确做法是转 VO 并清空答案public PaperVO startExam(Long paperId, Long userId) { Paper paper paperMapper.selectById(paperId); Date now new Date(); if (now.before(paper.getStartTime()) || now.after(paper.getEndTime())) { throw new BizException(不在考试时间范围内); } // 同一学生同一试卷只允许一条记录已存在就直接复用断线重连场景 ExamRecord record examRecordMapper.selectByPaperAndUser(paperId, userId); if (record null) { record new ExamRecord(); record.setPaperId(paperId); record.setUserId(userId); record.setStartTime(now); record.setStatus(0); examRecordMapper.insert(record); } else if (record.getStatus() ! 0) { throw new BizException(该场考试已交卷不能重复作答); } ListQuestionVO questions questionMapper.selectByPaperId(paperId); questions.forEach(q - q.setAnswer(null)); // 关键答案绝不下发 PaperVO vo new PaperVO(); vo.setPaper(paper); vo.setQuestions(questions); vo.setRecordId(record.getId()); vo.setRemainSeconds((paper.getEndTime().getTime() - now.getTime()) / 1000); return vo; }remainSeconds由服务端算前端拿它做倒计时而不是信任浏览器本地时间。学生改系统时间想多考十分钟改的只是显示交卷时服务端照样按end_time判超时。record.getStatus() ! 0的判断是断线重连的分水岭允许刷新页面继续答但不允许交完卷再进来。注意selectByPaperAndUser上的唯一约束(paper_id, user_id)不能省两个浏览器同时点开始靠代码判断有并发窗口靠数据库才拦得住。4.3 交卷判分客观题自动批、主观题挂起交卷是整个系统里逻辑最密的一段。前端把答题卡数组传上来后端要逐题判分、写明细、汇总总分、更新状态还要处理超时和重复提交。核心代码如下Transactional(rollbackFor Exception.class) public ScoreVO submit(Long recordId, ListAnswerDTO answers) { ExamRecord record examRecordMapper.selectById(recordId); // 幂等只有 status0 的记录能交卷重复点击第二次会更新 0 行 int rows examRecordMapper.updateStatus(recordId, 0, 1, new Date()); if (rows 0) { throw new BizException(该试卷已提交请勿重复操作); } BigDecimal total BigDecimal.ZERO; boolean hasSubjective false; for (AnswerDTO a : answers) { Question q questionMapper.selectById(a.getQuestionId()); ExamAnswer ea new ExamAnswer(); ea.setRecordId(recordId); ea.setQuestionId(q.getId()); ea.setUserAnswer(normalize(a.getUserAnswer())); // 多选排序、去空格、转大写 if (q.getType() 3) { // 3简答/主观题 ea.setIsRight(null); // 挂起等教师批阅 ea.setGetScore(BigDecimal.ZERO); hasSubjective true; } else { boolean right ea.getUserAnswer() ! null ea.getUserAnswer().equals(normalize(q.getAnswer())); ea.setIsRight(right ? 1 : 0); ea.setGetScore(right ? q.getScore() : BigDecimal.ZERO); total total.add(ea.getGetScore()); } examAnswerMapper.insert(ea); } // 无主观题直接出分有主观题置为待批状态 int finalStatus hasSubjective ? 1 : 2; examRecordMapper.updateScore(recordId, total, finalStatus); // 返回本次客观题得分前端提示「客观题已判 xx 分主观题待老师批阅」 return new ScoreVO(total, hasSubjective); }updateStatus(recordId, 0, 1, ...)这一句是带条件的更新SQL 是update exam_record set status1, submit_time? where id? and status0。它返回影响行数0 就代表这条记录已经被别的请求改过了直接抛异常。这比先select查状态再update可靠得多——后者在双击提交或网络重试时必然出问题我见过一个模板项目因为漏了这层同一份卷子写进去三份成绩最后按平均分算的。normalize方法做三件事去首尾空格、转大写、多选题按字母排序。多选题的答案如果前端传DBA而标准答案是ABD字符串直接比较就是错的必须先排序再比。这一步看着琐碎实际是多选题判分出错的头号原因。主观题用is_right null和get_score 0挂起等教师批阅接口再回填并重算总分、把状态推到 2别在交卷时直接算 0 分出分学生会以为系统把大题判错了。4.4 登录态与角色权限拦截器加注解三个角色共用一套接口权限必须在服务端拦。这套项目用的方案很朴素登录成功后把用户信息写进 Session 或者生成一个票据返回给前端后续请求由拦截器统一校验。朴素但够用也容易讲清楚。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest req, HttpServletResponse resp, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) return true; // 放行静态资源 HandlerMethod hm (HandlerMethod) handler; RequireRole rr hm.getMethodAnnotation(RequireRole.class); if (rr null) return true; // 没标注就是公开接口 UserVO user (UserVO) req.getSession().getAttribute(loginUser); if (user null) { writeJson(resp, Result.fail(401, 请先登录)); return false; } if (!Arrays.asList(rr.value()).contains(user.getRole())) { writeJson(resp, Result.fail(403, 无权限访问)); return false; } return true; } }配合RequireRole({1,2})这样的方法注解教师才能调用的组卷接口、管理员才能调的账号管理接口就都能标注上。writeJson要把Result序列化回响应流并设置application/json;charsetutf-8否则前端拿到一段 HTML 报错页拦截器提示的「无权限」根本显示不出来。前端路由守卫只是体验层真正拦得住人的是这一层别只做前端。5. 常见问题排查跑不起来时先查这五处5.1 启动阶段404 和连不上数据库现象一前端登录页能打开点登录报 404。原因通常是代理的target和 Tomcat 的实际访问路径对不上或者pathRewrite把前缀削多了削少了。解决方式先在浏览器直接访问http://localhost:8080/exam/login确认后端单独能通再对比vue.config.js里的 target 与 IDEA 里 Deployment 配的 Application context两边必须一模一样。改完vue.config.js必须重启npm run serve热更新不会重新加载代理配置这一点坑过很多人。现象二启动报Could not get JDBC Connection。原因要么是 MySQL 服务没起要么是jdbc.properties里的库名和你导入的库名不一致脚本里叫exam_system你建成了exam要么是驱动版本与 MySQL 大版本不匹配。解决先用命令行mysql -uroot -p登进去show databases;确认库存在再核对 URL 里的库名和端口MySQL 8 用com.mysql.cj.jdbc.DriverMySQL 5 用com.mysql.jdbc.Driver写错会报ClassNotFoundException日志里搜驱动类名最快。5.2 运行阶段跨域、时间与中文现象三控制台报Access-Control-Allow-Origin接口请求被浏览器拦下。原因是你绕过了代理直接在 axios 里写了完整域名或者把前端打包后的静态文件丢到了另一个端口。解决开发阶段老老实实走 proxy如果确实需要直连就在spring-mvc.xml里配CorsInterceptor或在 Controller 上加CrossOrigin别用前端 mock 糊过去——上线时这层配置会一起消失。现象四考试时间显示成 8 小时前或 8 小时后。原因是 JDBC URL 少了serverTimezone或者数据库字段用了datetime而 Java 侧是Date两头时区理解不一致。解决URL 补serverTimezoneAsia/Shanghai实体上时间字段统一用java.util.Date配JsonFormat(patternyyyy-MM-dd HH:mm:ss, timezoneGMT8)让前后端看到的字符串完全一致。这个 bug 在演示时特别致命学生会看到「考试未开始」而实际时间已经到了。现象五中文题目提交后变成问号。原因是web.xml里没配CharacterEncodingFilter或者配了但顺序排在业务 filter 之后。解决把编码过滤器放到最前面同时确认数据库和表的字符集是utf8mb4。如果历史数据已经存坏了改配置也救不回来只能重新导入题库所以建库那一步就别偷懒。5.3 业务阶段分数与重复提交现象六多选题答案明明对了却判 0 分。原因就是前面提到的字符串顺序问题ABD和DBA直接比较不相等。解决判分前统一做「去空格 转大写 按字母排序」并且把标准化逻辑抽成一个方法交卷写入和学生端回显都调它避免两处写法不一致。上线前拿多选题单独造三条数据全对、漏选、多选各测一遍五分钟能省掉一整轮返工。6. 二次开发把这套通用系统改成自己课题的三个切入点改动之前先想清楚一件事答辩看的是你有没有理解链路不是功能堆了多少。所以我的建议是从「一条链路改透」开始而不是四面开花。第一个切入点是判分策略。现在客观题一题一分、答对给满、答错给零你可以把它抽成策略类单选零一判分、多选支持「漏选给半分、错选给零分」、判断题按题型走同一接口。改法是在Question上加一个scoreRule字段交卷时按规则查实现类ScoreStrategy接口两个方法judge(userAnswer, rightAnswer)和calcScore(question, isRight)。这个改动代码量不大但能讲清楚「为什么用策略模式替换 if-else」比加十个增删改查页面有含金量。验证方式是把同一份答题卡分别按两套规则算一遍对比明细表里的get_score是否逐条符合预期。第二个切入点是统计维度。现在的成绩大概率只按考试记录列表展示你可以加「班级平均分、题目正确率、分数段分布」三个查询。题目正确率直接用exam_answer按question_id分组算sum(is_right)/count(*)注意把is_right is null的主观题排除掉否则正确率会被拉低。分数段分布用case when分段统计SQL 一条搞定。前端用图表库把这三张图画出来论文里的「系统测试与效果展示」章节立马有内容。第三个切入点是并发与体验。可以给学生端加自动保存草稿答题时每 30 秒把已选答案写到本地或者后端临时表刷新页面能恢复。也可以在交卷接口上加一层基于record_id的本地锁双保险。改完之后怎么验证我自己的习惯是准备三份数据一份干净的满分卷、一份故意错几题的卷、一份含主观题待批的卷每次改动完把这三份各跑一遍对一遍exam_answer明细和exam_record总分。有一次我图快只测了满分卷结果漏选加分的分支写反了直到演示前一天才被问出来连夜改到两点。从那以后每次动判分逻辑我都强制走一遍这三份数据再提交不管改的是一行还是一百行。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询