微信小程序考试系统与SSM框架:架构、接口与部署全解析
发布时间:2026/9/16 15:12:52来源:尧图网络
简介一套基于微信小程序与SSM框架的在线考试系统源码专为Java毕业设计及课程作业场景打造适合计算机科学与技术、软件工程等专业学生参考。系统涵盖用户管理、题库管理、考试流程控制、自动评分等核心模块前端采用微信小程序与Vue页面后端由Spring、SpringMVC、MyBatis支撑前后端配合完成完整在线考试闭环。资源包共1109个文件大小约31.94MB主要包含java后端逻辑、vue前端页面、wxml/wxss小程序界面、js交互脚本、sql数据库初始化脚本以及bat一键安装/运行/打包脚本目录结构清晰便于按模块阅读和二次开发。包内另有README及文档可辅助理解项目部署与设计思路。目前已有90人学习浏览能够作为毕业设计选题、项目答辩展示或SSM框架实战练习的可靠参考资料。通过阅读源码、脚本与配套文档可以快速掌握微信小程序与SSM的整合方式、前后端数据交互流程及考试系统的业务设计要点。1. 微信小程序考试系统 SSM先看透这个组合再动手跑代码一个名为weixin060微信小程序考试系统ssm.zip的压缩包通常是高校软件工程和计算机应用方向最常见的毕业设计交付物。里面装的是三样东西微信小程序前端、基于 SSM 的 Java 后端、以及数据库初始化脚本。这个标题把轻量级前端容器和最经典的 Java Web 组合绑在一起用来做一套多角色考试业务学生端在小程序里登录、答题、查分管理端在 Web 后台维护题库和成绩记录。为什么这个组合值得拆开研究微信小程序天然解决了触达问题学生扫码即用不需要安装 App。SSM 则是 Java 岗位面试里问得最多的框架组合Spring 管对象SpringMVC 管请求路由MyBatis 管 SQL。做这个题目核心不是把考试系统做得多复杂而是把“用户从微信打开小程序到交卷看到成绩”这条链路上的每个环节都走通。下面从项目骨架、后端接口、小程序联调、本地部署到上线优化完整过一遍。2. 拆项目骨架SSM后端三层分包、小程序页面、数据库表拿到 zip 后别急着跑。一般先看目录结构和 SQL 脚本确认项目是不是“能下载就能跑”的完整交付。SSM 后端无论叫什么名字分包规律都是 Controller-Service-Mapper 三层小程序端则是 pages 目录下按功能拆页面再加一个统一请求封装。2.1 SSM 后端的典型分包和请求流转一个标准 SSM 考试系统后端目录结构大致如下exam-server ├── pom.xml ├── src/main/java │ ├── com.example.exam │ │ ├── controller │ │ │ ├── LoginController.java │ │ │ ├── ExamController.java │ │ │ └── ScoreController.java │ │ ├── service │ │ │ ├── ExamService.java │ │ │ └── impl/ExamServiceImpl.java │ │ ├── mapper │ │ │ ├── UserMapper.java │ │ │ ├── QuestionMapper.java │ │ │ └── ExamRecordMapper.java │ │ ├── entity │ │ │ ├── User.java │ │ │ ├── Exam.java │ │ │ └── Question.java │ │ └── common │ │ ├── Result.java │ │ └── JwtUtil.java │ └── resources │ ├── mapper (UserMapper.xml, QuestionMapper.xml) │ ├── spring-dao.xml │ ├── spring-mvc.xml │ ├── mybatis-config.xml │ └── jdbc.properties请求进入 Tomcat 后先到DispatcherServletSpringMVC 把 URL 映射到 ControllerController 调用 Service 处理业务Service 通过 Mapper 接口操作 MyBatis 管理的数据库连接。流程不复杂真正容易翻车的是配置jdbc.properties里的数据库连接参数以及 Spring 扫描 Mapper XML 的路径。这两个地方配错项目一启动就报Invalid bound statement (not found)或者数据源无法初始化。2.2 小程序端目录结构与页面职责小程序端是另一个独立文件夹用微信开发者工具打开。典型结构exam-miniapp ├── app.js ├── app.json ├── utils │ └── request.js ├── pages │ ├── login │ ├── index // 考试列表 │ ├── exam // 答题页 │ ├── result // 成绩页 │ └── mine // 个人中心 └── componentsapp.json里pages数组第一项决定启动页面。一个高频定制需求是改造刚进入时默认的加载页面把首屏从“主页”切换成“登录页”或“考试列表”。改法很简单调整pages数组顺序把目标页面放到第一个。需要注意修改后要在微信开发者工具里点击“编译”重新生成否则真机预览仍会走旧缓存。答题页如果开启下拉刷新需要单独在对应页面的.json里配置enablePullDownRefresh: true并且手动关闭window级的全局刷新样式否则会出现两个圆圈动画。2.3 数据库表设计围绕考试的事后审计来做考试系统的核心不是存题目而是存“谁在什么时间把什么答案提交上来了”。建议至少五张表用户表、试卷表、题目表、考试记录表、答题明细表。下面是一个可用的精简建表脚本CREATE TABLE sys_user ( id int(11) NOT NULL AUTO_INCREMENT, openid varchar(64) DEFAULT NULL, nickname varchar(32) DEFAULT NULL, role tinyint(4) DEFAULT 0 COMMENT 0学生 1教师, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam ( id int(11) NOT NULL AUTO_INCREMENT, title varchar(100) NOT NULL, duration int(11) DEFAULT 30 COMMENT 考试时长单位分钟, start_time datetime DEFAULT NULL, end_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE question ( id int(11) NOT NULL AUTO_INCREMENT, exam_id int(11) NOT NULL, content text, type tinyint(4) DEFAULT 0 COMMENT 0单选 1多选 2判断, options_json varchar(500) DEFAULT NULL, answer varchar(10) DEFAULT NULL, score int(11) DEFAULT 5, PRIMARY KEY (id), KEY idx_exam_id (exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE exam_record ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL, exam_id int(11) NOT NULL, score int(11) DEFAULT NULL, submit_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_exam (user_id,exam_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE answer_detail ( id int(11) NOT NULL AUTO_INCREMENT, record_id int(11) NOT NULL, question_id int(11) NOT NULL, user_answer varchar(255) DEFAULT NULL, is_correct tinyint(4) DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;几个设计要点题目表用options_json而不是另建选项表对单表查询友好代价是多选题需要前端自行解析字符串考试记录和答题明细分离目的是事后能追溯到每一题的作答情况管理端做错题统计时可以直接按question_id聚合。openid必须加唯一索引因为同一个人反复进入小程序时后端要按openid查重而不是无脑插入。3. 后端接口实现微信登录、题目下发、交卷判分SSM 项目的开发顺序一般从登录开始因为考试系统每个请求都必须带上用户身份。这里的登录不是传统用户名密码而是微信官方推荐的wx.login获取临时code后端再用code向微信接口换取openid和session_key。3.1 微信登录接口code 换 openid 的完整写法Controller 层很薄RestController RequestMapping(/api) public class LoginController { Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody LoginRequest req) { // 小程序端 wx.login 拿到的临时凭证 code String openid userService.exchangeOpenid(req.getCode()); User user userService.findOrCreate(openid); String token JwtUtil.createToken(user.getId()); return Result.ok(token); } }关键实现在 Service 里。用RestTemplate调微信接口public String exchangeOpenid(String code) { String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; RestTemplate rest new RestTemplate(); String resp rest.getForObject(url, String.class); JSONObject obj JSON.parseObject(resp); if (obj.getIntValue(errcode) ! 0) { throw new BizException(obj.getString(errmsg)); } return obj.getString(openid); }这里有三个参数必须搞清appid是你的小程序唯一标识secret是对应密钥js_code是前端传来的临时凭证。secret只能放在服务端一旦打进小程序代码任何人都能从包里解出来。换到openid后去sys_user表查是否存在没有就插入一条并初始化角色。登录成功后签发 JWT小程序端存在本地后续请求放到Authorization头由 SpringMVC 拦截器统一校验。3.2 核心考试接口校验时间、返回题目、提交判分考试系统三个核心接口获取考试列表、获取试卷题目、提交答案。获取题目时绝对不能把正确答案返回去否则抓包就能看到所有答案。实际开发中我会单独建一个QuestionVO只返回id, content, options_json, type, scoreanswer字段留在服务端。RestController RequestMapping(/api/exam) public class ExamController { Autowired private ExamService examService; GetMapping(/{examId}/questions) public Result questions(PathVariable Integer examId) { Exam exam examService.getById(examId); if (!examService.isWithinTime(exam)) { return Result.fail(不在考试时间内); } return Result.ok(examService.listQuestionVO(examId)); } PostMapping(/submit) public Result submit(RequestBody SubmitRequest req, RequestAttribute(userId) Integer userId) { examService.submit(userId, req); return Result.ok(); } }submit方法需要放在事务里因为交卷时要做三件事计算总分、写exam_record、写answer_detail。在 Service 实现类上打Transactional是 SSM 时代的标准操作。判分逻辑按题目类型区分单选题直接比对字符串多选题要先把用户提交的选项排序再比对判断题用“1/0”存储避免大小写问题。3.3 MyBatis 的 Mapper 层动态 SQL 处理批量插入交卷时答案可能有几十条循环单条 INSERT 性能差且容易超时。使用 MyBatis 的foreach一次插入insert idbatchInsertAnswers parameterTypelist INSERT INTO answer_detail (record_id, question_id, user_answer, is_correct) VALUES foreach collectionlist itemitem separator, (#{item.recordId}, #{item.questionId}, #{item.userAnswer}, #{item.isCorrect}) /foreach /insert注意批量插入的 SQL 长度受 MySQLmax_allowed_packet限制。一张试卷 50 道题完全不是问题但考试系统如果面向全校几千人同时交卷数据量会快速上升届时可以按 200 条一批循环插入避免单条 SQL 过大。4. 小程序端从加载到交卷请求封装、顶部导航栏适配、倒计时后端接口就绪后小程序端要解决三件事每个请求自动带上身份、考试时间实时准确、交卷前不丢数据。下面按这三条展开。4.1 统一请求封装每个请求带上 token 和后端解耦小程序的wx.request不支持 Promise但可以用一层封装把它变成 Promise页面里配合async/await写起来更顺// utils/request.js const BASE_URL http://localhost:8080/exam-server/api const request (url, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL url, method: method || GET, data: data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) }, success(res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data) } else if (res.statusCode 401) { wx.removeStorageSync(token) wx.navigateTo({ url: /pages/login/login }) } else { wx.showToast({ title: res.data.msg, icon: none }) reject(res.data) } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }) reject(err) } }) }) } module.exports { request }这个封装把三件事收在同一个函数里开发环境地址、Authorization头、后端返回的code判断。考生反馈最多的错误是request:fail通常不是代码问题而是后端没启动、局域网 IP 不对或者手机和电脑不在同一网段。解决顺序是先用 curl 调一次后端接口确认服务通再查小程序端BASE_URL。4.2 考试页面的倒计时和答案暂存答题页加载题目后用setInterval做倒计时data: { remainSeconds: 1800, currentIndex: 0, answerList: [] }, onLoad(options) { this.examId options.examId this.fetchQuestions() }, fetchQuestions() { request(/exam/${this.examId}/questions, GET).then(list { this.setData({ questionList: list }) this.startTimer() }) }, startTimer() { this.timer setInterval(() { const remain this.data.remainSeconds - 1 this.setData({ remainSeconds: remain }) if (remain 0) { clearInterval(this.timer) this.submitExam() } }, 1000) }考生切到后台再回来setInterval会被挂起倒计时会变慢。这里需要做一个补偿onHide时记录时间戳onShow时用当前时间减去onHide时间把差值从remainSeconds中扣除。另外答题页如果使用自定义导航栏顶部导航栏高度在不同机型上差异很大iPhone 刘海屏和 Android 全面屏的胶囊位置不同要用wx.getMenuButtonBoundingClientRect()获取胶囊位置再计算可点击区域的顶部偏移否则答案索引条会被刘海挡住。4.3 单选、多选、交卷前的最后校验type字段决定渲染单选框或复选框单选用radio-group多选用checkbox-group。后端返回的options_json是字符串必须JSON.parse之后再渲染。交卷时把所有答案组装成[{questionId, answer}]数组同时统计未答题数量submitExam() { const { questionList, answerList } this.data const unanswered questionList.filter(q !answerList.find(a a.questionId q.id)) if (unanswered.length 0) { wx.showModal({ title: 提示, content: 还有${unanswered.length}题未作答确定交卷, success: (res) { if (res.confirm) this.doSubmit() } }) } else { this.doSubmit() } }项目里如果需要修改刚进入的加载页面另一个常见场景就是恢复未完成的考试。考生意外退出答题页后再次进入应自动断点续答。简单方案是把answerList写入wx.setStorageSynconLoad时读取复杂方案是后端提供“恢复考试”接口按userId和examId查未交卷记录。两者结合效果最好。5. 本地跑通全流程JDK、Tomcat、MySQL、微信开发者工具从 zip 到能演示最耗时的是环境。SSM 项目对版本组合很敏感推荐一套稳定组合JDK 1.8、Maven 3.6.3、Tomcat 8.5、MySQL 5.7。下表是各组件版本和注意事项组件建议版本说明JDK1.8高版本 JDK 会遇到反射和 JSP 编译兼容问题Spring5.1.xSSM 主流版本配合 JDK8 稳定MyBatis3.4.x3.5 以上需要替换mybatis-spring包名MyBatis-Spring2.0.x与 MyBatis 3.4/3.5 匹配Tomcat8.5支持 Servlet 3.1和 SpringMVC 配合良好MySQL5.78.0 会导致驱动类名和 ServerTimezone 都变化5.1 修改配置文件的三个位置后端跑起来之前检查jdbc.properties、spring-mvc.xml、mybatis-config.xml。最常见的问题是 MySQL 8.0 的驱动类从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.DriverURL 还要额外带serverTimezoneAsia/Shanghai。如果数据库是 5.7就保持老驱动jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/exam_db?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456spring-mvc.xml里要确认context:component-scan的包路径覆盖controller和service.implspring-dao.xml里要确认mapperLocations指向classpath*:mapper/*.xml否则 MyBatis 找不到 SQL 和接口的绑定关系。5.2 启动后端和导入小程序的完整步骤在 IDEA 里打开 Maven 工程等待依赖下载。推荐用本地 Tomcat 启动而不是tomcat7-maven-plugin因为后者默认的 Application context 容易和代码里配置的路径不一致。部署时选择war explodedApplication context 设置成/exam-server。后端启动后先用 Postman 调一次登录接口确认返回 token再用手机或电脑访问后端 IP最后才打开微信开发者工具。小程序端导入项目时选择前端目录填入 AppID。没有 AppID 可以选测试号但测试号无法调用微信登录只能在前端 mock 一个 token。本地调试时微信开发者工具右上角“详情 → 本地设置”勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”否则wx.request直接报url not in domain list。5.3 常见启动报错和排查顺序报错现象大概率原因解决办法Invalid bound statementMapper XML 未被扫描检查mapperLocations路径和包名NoSuchBeanDefinitionExceptionService 实现类没加Service检查spring-dao.xml的包扫描范围Failed to configure a DataSourcejdbc.properties未加载检查context:property-placeholder的location401 Unauthorized拦截器把登录接口拦截了在拦截器排除路径中放行/api/login小程序请求request:fail域名未配置或未开启调试本地开发勾选不校验合法域名排查这类 SSM 项目的顺序永远是先看 Tomcat 日志再用 Postman 测接口最后调小程序页面。不要在后端都没通的情况下反复改小程序代码那样只会增加噪音。6. 上线前值得做的4个改进事务、缓存、拦截校验、成绩导出如果项目要交到导师手里或真正上线下面四个小改动性价比最高。它们不改变现有业务但能让代码质量明显上一个台阶。6.1 给交卷方法补上事务和重试交卷接口涉及多表写入我见过很多毕业设计在ExamServiceImpl里忘了加Transactional导致answer_detail插入失败时exam_record已经落库成绩异常。正确做法Transactional(rollbackFor Exception.class) public void submit(Integer userId, SubmitRequest req) { // 计算总分 // 插入 exam_record // 批量插入 answer_detail }rollbackFor Exception.class让运行时异常默认回滚主要为了把数据库操作包成一个原子事务。如果交卷接口压力大可以在Transactional外层再套一个synchronized或者数据库乐观锁防止同一考生重复交卷。6.2 给考试列表接口加本地缓存考试系统的高频查询是首页考试列表和公告这类数据变化频率低。用ConcurrentHashMap做最简单的进程内缓存private final ConcurrentHashMapInteger, Exam examCache new ConcurrentHashMap(); public Exam getById(Integer examId) { return examCache.computeIfAbsent(examId, this::queryFromDb); }缓存要设置过期策略比如 30 分钟清一次。在管理端维护题库时主动移除缓存否则教师改了考试时间学生端看不到。更复杂的 Redis 方案在这种项目里没有必要单机部署、学生量百到千级本地缓存足够。6.3 请求参数校验逐年变严考试系统最容易出安全问题的接口是提交答案。后端要校验questionId是否属于当前试卷answer是否合法字符exam是否在考试时间内。用 Hibernate Validator 做基础校验public class SubmitAnswerItem { NotNull private Integer questionId; NotBlank Size(max 255) private String answer; }然后在 SpringMVC 配置里启用校验Controller 参数上标Valid。缺陷是它只能做格式校验判断题目归属仍然要在 Service 里写 SQL。防作弊的笨办法是记录考生每次进入试卷的时间超过两次标记异常同时前端禁掉截图但防不了截屏只能后端控制交卷时间窗口。6.4 成绩导出 Excel用 poi 工具类收尾管理端导出成绩是高频需求。用 Apache POI 生成 Excelpublic void exportScores(Integer examId, HttpServletResponse response) throws IOException { ListScoreVO scores scoreMapper.selectByExamId(examId); XSSFWorkbook workbook new XSSFWorkbook(); Sheet sheet workbook.createSheet(成绩); Row header sheet.createRow(0); header.createCell(0).setCellValue(学号); header.createCell(1).setCellValue(姓名); header.createCell(2).setCellValue(分数); for (int i 0; i scores.size(); i) { Row row sheet.createRow(i 1); row.createCell(0).setCellValue(scores.get(i).getStudentNo()); row.createCell(1).setCellValue(scores.get(i).getStudentName()); row.createCell(2).setCellValue(scores.get(i).getScore()); } response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment;filenamescores.xlsx); workbook.write(response.getOutputStream()); }注意导出接口不要走 JSON 那套返回封装直接用response输出流这样前端用wx.downloadFile下载时拿到的是文件而不是 JSON。导出的时间字段要用DateTimeFormatter明确格式化避免直接输出Date对象导致时区偏移。这个改进做完管理端功能才算闭环。本文还有配套的精品资源点击获取
网站建设高端定制企业官网