ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

微信小程序考试系统+SSM:从数据表设计到事务提交的完整实践

微信小程序考试系统+SSM:从数据表设计到事务提交的完整实践 简介基于Spring、SpringMVC和MyBatis框架的微信小程序考试系统是一套完整的Java毕业设计项目面向计算机科学与技术、软件工程等专业学生可用于毕业设计、课程作业或项目实训。资源包共包含1109个文件涵盖Java后端源码、Vue管理端页面、微信小程序前端代码、SQL数据库脚本、JSON及XML配置、图片样式等压缩包大小约31.94MB目录结构清晰便于逐模块查阅。目前已有90人学习下载可作为课程设计或毕业设计的重要参考。系统实现了用户管理、题库管理、在线考试、自动评分等核心功能前后端紧密配合有助于深入理解SSM框架与微信小程序开发的技术整合。资源附带启动与构建脚本便于本地部署调试适合在此基础上进行二次开发与功能扩展也适合用于学习前后端分离项目的基本架构与实现思路。1. 微信小程序考试系统SSM这个项目真正难的不是写页面微信小程序考试系统SSM 这个标题最容易让新人误判的是以为小程序页面写完项目就结束了。一个考试系统由三个角色组成管理员维护题库教师设置试卷学生在微信小程序里答题和查分。真正决定项目能不能过审和做深的是三层SSM 的接口设计、MySQL 的题目表结构、以及答题提交时的事务一致性。把这套跑通后续换成 uniapp 微信小程序重新实现前端或者把 SSM 换成 Spring Boot都只是工作量问题。这套项目方案适合正在做微信小程序毕业设计的读者也适合企业内部培训考试项目参考。下面的内容按同类项目最通用的实现展开把核心链路和容易翻车的点讲清楚。2. SSM 考试数据模型表怎么拆题库和试卷才能灵活组卷2.1 从一次考试流程反推需要的业务表用户进入考试系统后先做身份认证然后在小程序端看到一张已经组好的试卷答题结束提交后端计算客观题分数并保存明细。如果希望教师能随机组卷、按难度抽题就不能把题目直接写在试卷表里也不能把答案字段堆在一个大表里。我一般按以下职责拆分表名核心字段作用t_userid, username, password, role, nickname, avatar用户与角色student / teacher / admint_questionid, type, content, option_json, answer, score, difficulty题目、标准答案与难度标记t_examid, title, start_time, end_time, duration, total_score一场考试的元信息t_exam_questionexam_id, question_id, score试卷与题的关联允许同一题在不同考试中分数不同t_exam_recordid, exam_id, user_id, submit_time, score, status一次考试的整体记录t_answer_detailrecord_id, question_id, user_answer, is_correct每个题的对错明细这个拆法参考了同类 SSM 项目里的常见做法。t_exam_question 是典型的多对多中间表没有它就无法做到每一场考试抽不同题、每题设置不同分值。t_answer_detail 保留用户原始答案便于以后判分规则变化后重新计分。比如某道多选题标准答案从 A,C 改成 A,B 时可以根据原始选项重算成绩而不必让考生重新考试。2.2 MySQL 建表脚本与字段类型选择以下是核心建表脚本去掉了外键约束原因后面会提CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, role VARCHAR(10) DEFAULT student, nickname VARCHAR(50) DEFAULT , avatar VARCHAR(255) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_question ( id INT PRIMARY KEY AUTO_INCREMENT, subject_id INT NOT NULL, type TINYINT NOT NULL COMMENT 1单选 2多选 3判断, content TEXT NOT NULL, option_json TEXT COMMENT 选项数组转JSON, answer TEXT NOT NULL COMMENT 单选题填A多选题填A,B, score INT NOT NULL DEFAULT 2, difficulty TINYINT DEFAULT 1 COMMENT 1易 2中 3难 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_exam_question ( exam_id INT NOT NULL, question_id INT NOT NULL, score INT NOT NULL DEFAULT 2, PRIMARY KEY (exam_id, question_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段类型说明题目选项用 option_json 而不是拆成 option_a、option_b 等列是因为单选、多选、判断的选项数量不一致json 可以灵活扩展解析时用 fastjson 或 Jackson。answer 字段对单选题存“A”多选题存“A,B”判断存“T/F”统一用字符串提交后按同样的规则比较。这里去掉外键是因为考试系统的试卷快照不要求数据库强一致删除题目时如果已经被考试引用应该在业务层做状态校验而不是直接让数据库拒绝删。utf8mb4 是为了存用户昵称里的生僻字和表情符号很多小程序项目实例在导出 SQL 时默认用 utf8等到用户上传头像昵称带 Emoji 就报 Incorrect string value这点要提前规避。2.3 为什么用 SSM 而不是把业务逻辑塞进小程序SSM 框架在这个标题里指的是 Spring Spring MVC MyBatis。Spring 管事务Spring MVC 暴露 REST 接口MyBatis 负责 SQL 映射。很多做微信小程序毕业设计的同学会把判分逻辑写在 JavaScript 里这在小规模演示没问题但一旦题目需要后台批改、成绩汇总导出小程序端就无法复用。SSM 的好处在于第一MyBatis 可以写动态 SQL 用于随机抽题第二Spring 的Transactional能保证提交答案时成绩记录和答题明细同时成功或同时回滚第三后端接口可以被 H5 管理后台或其他客户端复用。常见误区是项目里整包上传的 SSM 代码中接口返回类型不统一导致小程序端到处判断 res.data.data.data。因此后面所有接口都约定使用统一响应体 Result包含 code、message、data 三个字段小程序端才能在一个封装函数里统一处理。3. SSM 后端实现登录鉴权、自动组卷和成绩落库3.1 Maven 项目结构与核心依赖先看依赖。基于 SSM 的考试系统pom.xml 中通常包括 spring-webmvc、mybatis、mybatis-spring、mysql-connector-java、druid、fastjson。注意 Spring 版本和 JDK 版本要匹配例如 JDK 1.8 配 Spring 5.3.x 是常见组合。dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.3.20/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version /dependency项目结构常见的做法是在com.example.exam下拆controller、service、mapper、entity并用spring-mvc.xml、spring-mybatis.xml两个配置文件。很多本地运行的 SSM 项目代码会直接使用 Druid 连接池在jdbc.properties里写jdbc.urljdbc:mysql://localhost:3306/exam?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai。这里的serverTimezone参数特别重要缺了会导致日期字段偏差 8 小时或者直接报 SQLException而characterEncodingutf8mb4的作用在上一章已经说过是为了存储 Emoji 和生僻昵称。3.2 小程序 token 登录要绕过 Cookie 就得自己发令牌微信小程序里登录时基于 SSM 的考试系统常用方案是账号密码登录后签发一个 token小程序端每次请求在 header 里带上Authorization: token。因为小程序的网络请求不自动携带 CookieSSM 的 HttpSession 在这里不可靠。如果做微信授权登录头像昵称获取要用button open-typechooseAvatar配合input typenickname不能直接wx.getUserProfile拉取头像昵称这是新版本微信的隐私限制。我一般用 UUID 作为 token存到 Redis 或数据库的 t_user_token 表中这里采用内存 Map 做演示RestController RequestMapping(/api/user) public class UserController { PostMapping(/login) public Result login(RequestBody LoginParam param) { User user userService.findByUsernameAndPassword(param.getUsername(), param.getPassword()); if (user null) { return Result.error(用户名或密码错误); } String token UUID.randomUUID().toString().replace(-, ); TokenCache.put(token, user.getId()); return Result.ok(token); } }逻辑说明Result是统一响应体至少包含code、message、data三个字段。登录成功后生成 32 位 token与用户 id 建立关联。这里用内存 Map 只适合单机演示正式部署应使用 Redis并设置过期时间比如 2 小时。参数上注意密码不要明文存要用 BCrypt 或至少加盐 MD5小程序端登录输入框要设置passwordtrue避免键盘自动大写带来的输入错误。如果是在本地联调后端返回的 token 会显示在微信开发者工具的 Network 面板里直接复制到 Storage 中调试其他接口即可。3.3 MyBatis 随机组卷与考点覆盖自动组卷的 SQL 是考试系统的核心。一种常见做法是按题型、难度分组每类题目用ORDER BY RAND() LIMIT n抽取然后批量插入 t_exam_question。这个方案在题量小时足够超过十万题时性能会下降可以采用按 id 随机偏移的方式这里先给基本 SQLselect idselectRandomQuestions resultMapQuestionResultMap SELECT * FROM t_question WHERE type #{type} AND difficulty #{difficulty} ORDER BY RAND() LIMIT #{limit} /select逻辑说明type和difficulty是查询参数SQL 先按条件过滤再随机排序取前 N 条。组卷后应该计算总分是否等于考试设置的总分否则学生看到的是配分不对的试卷。为了控制考点覆盖可以在 t_question 表增加knowledge_point字段组卷时先按知识点分组再随机否则容易出现整张试卷都落在同一章节的情况。抽取完成之后将 exam_id 与 question_id 批量插入 t_exam_question这段逻辑要放在一个事务里保证组卷要么完整写入要么不落任何脏数据。3.4 提交答案必须是一个事务成绩计算与明细写入学生在小程序端点击交卷后后端同时要执行三步保存考试记录、保存每条答题明细、计算总分。这三步中任意一步失败都要回滚。下面给出 Service 层示例Transactional(rollbackFor Exception.class) public SubmitResult submit(SubmitParam param) { ExamRecord record new ExamRecord(); record.setExamId(param.getExamId()); record.setUserId(param.getUserId()); record.setSubmitTime(new Date()); int total 0; ListAnswerDetail details new ArrayList(); for (AnswerItem item : param.getAnswers()) { Question q questionMapper.selectById(item.getQuestionId()); boolean correct q.getAnswer().equalsIgnoreCase(item.getUserAnswer()); total correct ? item.getScore() : 0; AnswerDetail detail new AnswerDetail(); detail.setQuestionId(q.getId()); detail.setUserAnswer(item.getUserAnswer()); detail.setIsCorrect(correct); details.add(detail); } record.setScore(total); examRecordMapper.insert(record); answerDetailMapper.batchInsert(record.getId(), details); return new SubmitResult(record.getId(), total); }逻辑说明Transactional确保insert和batchInsert在同一事务中如果中途查询题目失败已经执行的 insert 会回滚。比较答案时单选题直接忽略大小写比较多选题需要先排序再比较因为用户答案可能是 A,C 与 C,A排序后统一。注意这里item.getScore()应该是试卷里该题的分值不要用 t_question.score因为不同考试中同一题可能配不同分。事务提交后返回 recordId 和总成绩小程序端再跳转到结果页。以下异常场景最容易暴露事务配置问题异常场景预期表现观察点题目在考试前被删除回滚接口返回“试卷已变更”查询 question 时返回 null答案明细超过题目数量回滚拒绝提交入参校验 List 长度数据库写入超时回滚连接池排队Druid 监控中活跃连接数升高4. 微信小程序端考试倒计时、答题卡和导航栏适配4.1 用 Promise 封装 wx.request 并统一处理 token微信小程序自身的wx.request是回调风格多个接口调用会形成嵌套。常见的做法是封装一个 request.jsconst request (url, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: https://api.example.com url, method, data, header: { Authorization: wx.getStorageSync(token) || }, success(res) { if (res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.message, icon: none }); reject(res.data); } }, fail: reject }); }); };逻辑说明所有接口的基地址和请求头集中在这里维护登录后把 token 存到wx.setStorageSync(token, token)。当后端返回的 code 不为 0 时统一弹 toast避免每个页面重复处理错误。开发调试时微信开发者工具要勾选“不校验合法域名”真机预览则必须把域名配置到小程序后台的 request 合法域名并且使用 HTTPS。关于抓包在电脑端抓小程序流量时Charles 和 Burp Suite 通常需要配合系统网络设置才能生效微信开发者工具的请求直接走本地链路在 Network 面板里看更直观所以本地联调优先看 Network 面板不用急着上抓包工具。4.2 单选与多选题目渲染、取值和状态保存考试页面最常用的组件是radio-group和checkbox-group但很多人踩坑在于绑定值类型。后端返回的 option_json 解析后选项值可能是字符串或数字建议统一转成字符串再渲染。以下是一个简易的 wxmlblock wx:for{{questions}} wx:keyid view classquestion-card text{{index 1}}. {{item.content}}/text radio-group>onRadioChange(e) { const index e.currentTarget.dataset.index; const selected e.detail.value; this.setData({ [questions[${index}].selected]: selected }); wx.setStorageSync(exam_ this.examId, this.data.questions); }逻辑说明单选组件切换时e.detail.value是所选 radio 的 value直接赋给当前题目。多选需要先取得原数组再判断是否已存在不能直接覆盖。将未提交的答案缓存到本地可以在小程序被微信杀进程后恢复避免用户答题一半丢失。注意setData的路径更新写法用数组下标动态拼接性能远好于整体赋值大对象。如果题目数量超过 30建议再做分页加载一次 setData 几万字符在低端安卓机上会有明显卡顿。4.3 考试倒计时的生命周期管理考试系统离不开倒计时常见实现是在onLoad里setInterval每秒减 1在onUnload清除。问题是用户切到后台超过一定时间setInterval 会被微信挂起回到页面时时间和真实时间差很多。更可靠的做法是记录结束时间戳用当前时间与结束时间求差onShow() { const remain Math.floor((this.endTime - Date.now()) / 1000); this.setData({ remain }); this.timer setInterval(() { this.setData({ remain: this.data.remain - 1 }); if (this.data.remain 0) { clearInterval(this.timer); this.submitExam(); } }, 1000); }这里用结束时间戳而不是每秒减固定值能避免小程序在后台恢复后时间偏差。倒计时结束后立即调提交接口同时要加一个标志位防止onShow重复创建定时器。还有个细节考试页面默认会上拉滚动和下拉刷新答题过程中用户误触下拉会造成页面抖动应在 page.json 里设置enablePullDownRefresh: false同时把backgroundTextStyle保持默认不要在手势操作上做多余动画。4.4 顶部导航栏高度和加载页按照胶囊位置动态适配微信小程序不同机型的状态栏高度不同顶部导航栏高度不能写死。常见做法是使用wx.getMenuButtonBoundingClientRect()获取右上角胶囊的位置结合wx.getSystemInfoSync()计算自定义导航栏的高度。如果你用默认导航栏所谓“修改刚进入的加载页面”通常指的是替换项目启动后看到的第一个页面或者是在onLoad里控制wx.showLoading和wx.hideLoading。对于考试系统加载页不适合放太多动画建议在请求试卷接口时显示全屏 loading请求完成立刻隐藏。机型状态栏高度导航栏估算高度iPhone 1447px92pxAndroid 常见机型24px64px实际开发中不要直接用表格数据因为同一机型的微信版本也会影响胶囊高度。我一般写一个 getNavBarHeight 工具函数在页面onLoad里计算后 setData动态绑定自定义导航栏的样式。这是基于 SSM 的考试系统前端最容易被忽略的适配点很多小程序项目实例里都是简单写死 64px结果测试机显示异常。自定义导航栏时还要注意右侧胶囊是悬浮的不要把标题文字和按钮挤到胶囊下面。5. 上线前必做的验证接口压测、成绩导出和防重复提交5.1 用 Jmeter 压测考试提交接口先跑满 50 并发考试系统最容易出问题的不是登录而是交卷瞬间。一个班几十人同时交卷如果 SSM 后端没有做连接池调优Druid 默认最大连接数可能直接被占满。我一般用 Jmeter 添加线程组并发 50循环 10 次采样/api/exam/submit接口观察失败率和响应时间。测试前先清空 t_exam_record 中的测试数据避免主键冲突。如果出现 DataAccessException优先检查 mapper.xml 里 insert 语句是否缺少useGeneratedKeystrue keyPropertyid否则回填 recordId 时会拿不到自增主键。5.2 用 EasyExcel 导出考试记录别一张张查成绩SSM 项目导出 Excel 常用 POI但 POI 的 API 繁琐。建议用 EasyExcel定义导出模型public class RecordExcel { ExcelProperty(用户名) private String username; ExcelProperty(成绩) private Integer score; }在 Controller 中下载时设置响应头Content-Disposition: attachment; filenameexam_result.xlsx。注意 Excel 列名与数据库字段对应长数字如用户 id 会变成科学计数法需要把 id 字段转成 String 再导出。导出接口要加权限校验只允许教师或管理员访问否则学生可以批量下载全班成绩。压测时这个导出接口也值得单独跑一次因为全量查询在百万记录下可能撑爆内存可以用 Jmeter 看线程数也可以直接观察 JVM 堆内存变化。5.3 客户端防重复提交与 SQL 注入检查最后一个必须验证的是防重复提交。小程序端点击交卷后按钮要立即置灰同时后端也要做幂等给 t_exam_record 增加request_id字段提交时先按exam_id user_id request_id查记录存在则直接返回原结果。这样即使弱网重试也不会生成两条成绩。在 SSM 中还要检查 mapper 里的${}占位符凡是动态排序、字段名处用了${}的地方必须用白名单校验值参数一律用#{}。上线前可以用 sqlmap 扫一次登录、组卷接口发现报错回显就处理。做到这里这个微信小程序考试系统SSM 的骨架才算真正能进考场。本文还有配套的精品资源点击获取
返回列表