微信小程序考试系统与SSM框架:架构、接口与部署全解析
2026/9/16 15:12:40 网站建设 项目流程

简介:一套基于微信小程序与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 后,先到DispatcherServlet,SpringMVC 把 URL 映射到 Controller;Controller 调用 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 // 个人中心 └── components

app.jsonpages数组第一项决定启动页面。一个高频定制需求是改造刚进入时默认的加载页面,把首屏从“主页”切换成“登录页”或“考试列表”。改法很简单:调整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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

几个设计要点:题目表用options_json而不是另建选项表,对单表查询友好,代价是多选题需要前端自行解析字符串;考试记录和答题明细分离,目的是事后能追溯到每一题的作答情况,管理端做错题统计时可以直接按question_id聚合。openid必须加唯一索引,因为同一个人反复进入小程序时,后端要按openid查重而不是无脑插入。

3. 后端接口实现:微信登录、题目下发、交卷判分

SSM 项目的开发顺序一般从登录开始,因为考试系统每个请求都必须带上用户身份。这里的登录不是传统用户名密码,而是微信官方推荐的wx.login获取临时code,后端再用code向微信接口换取openidsession_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_type=authorization_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 id="batchInsertAnswers" parameterType="list"> INSERT INTO answer_detail (record_id, question_id, user_answer, is_correct) VALUES <foreach collection="list" item="item" 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时读取;复杂方案是后端提供“恢复考试”接口,按userIdexamId查未交卷记录。两者结合效果最好。

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.propertiesspring-mvc.xmlmybatis-config.xml。最常见的问题是 MySQL 8.0 的驱动类从com.mysql.jdbc.Driver变成了com.mysql.cj.jdbc.Driver,URL 还要额外带serverTimezone=Asia/Shanghai。如果数据库是 5.7,就保持老驱动:

jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/exam_db?useUnicode=true&characterEncoding=utf8 jdbc.username=root jdbc.password=123456

spring-mvc.xml里要确认<context:component-scan>的包路径覆盖controllerservice.implspring-dao.xml里要确认mapperLocations指向classpath*:mapper/*.xml,否则 MyBatis 找不到 SQL 和接口的绑定关系。

5.2 启动后端和导入小程序的完整步骤

在 IDEA 里打开 Maven 工程,等待依赖下载。推荐用本地 Tomcat 启动而不是tomcat7-maven-plugin,因为后者默认的 Application context 容易和代码里配置的路径不一致。部署时选择war exploded,Application 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-placeholderlocation
401 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 ConcurrentHashMap<Integer, 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 生成 Excel:

public void exportScores(Integer examId, HttpServletResponse response) throws IOException { List<ScoreVO> 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;filename=scores.xlsx"); workbook.write(response.getOutputStream()); }

注意导出接口不要走 JSON 那套返回封装,直接用response输出流,这样前端用wx.downloadFile下载时拿到的是文件而不是 JSON。导出的时间字段要用DateTimeFormatter明确格式化,避免直接输出Date对象导致时区偏移。这个改进做完,管理端功能才算闭环。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询