
最近被问得最多的一个毕设题目就是“基于微信小程序的优选驾考SSM”。微信小程序做前端SSMSpring SpringMVC MyBatis做后端搭一套驾考学习、刷题、预约考试的系统。这个组合在近几年的毕业设计里出现频率非常高看起来常规但真正从开题到答辩全程走完还是有不少坑。这篇文我按自己做过和指导过的实际经验把需求拆解、系统架构、核心功能实现、论文写作、排坑实录完整过一遍给正在准备这个题目的同学一条能直接走通的路线也适合想快速上手小程序加SSM开发的读者参考。1. 项目拆解优选驾考系统到底要做什么1.1 从题目反推功能边界拿到题目第一件事不是写代码而是把“优选驾考”这四个字拆开。“优选”不是只放一堆题而是强调精选内容科目一和科目四的精选真题、按错误率收敛的智能推荐、考试预约流程优化。驾考业务拆成用户端和管理端功能边界大概是这样端侧功能模块说明用户端小程序微信登录注册通过 wx.login 获取 openid免输入账号密码用户端科目一/科目四刷题按顺序练习、随机练习、专项练习用户端模拟考试随机组卷、倒计时、自动评分、结果解析用户端错题本自动收录错题、支持重复练习和移除用户端考试预约选择场地/时间段提交预约查看审核结果用户端个人中心头像昵称、学习统计、成绩记录管理端Web或后台接口题库管理题目增删改查、导入导出管理端预约审核管理员审核用户预约状态流转管理端用户管理查看用户列表、禁用异常账号管理端公告发布推送考试通知、政策变更这个边界你先写在文档里后面所有表结构、接口、论文章节都对照它来。很多同学后面越做越乱就是一开始没把功能边界画清楚写着写着什么功能都想加最后系统臃肿、论文也没法圆回来。1.2 为什么选微信小程序 SSM 这个组合微信小程序适合驾考场景的核心原因是“用完即走”。用户考试前刷二十分钟题、查一下预约状态不需要下载安装 App扫码就能用。这个特性在论文的“研究意义”部分很好写低使用门槛、触达成本低、覆盖中老年用户群体。SSM 虽然相比 Spring Boot 显得老但作为毕设它反而有优势。Spring 管对象、SpringMVC 接请求、MyBatis 负责持久层三层结构清清楚楚答辩时老师问你“请求怎么走的”你可以从 Controller 一路讲到 Mapper每一层都有话说。如果用了 Spring Boot 加 Vue 那种搭法很多东西自动配置掉了反而说不清楚底层机制。另外SSM 相关资料极多遇到问题一搜就有答案对没有实战经验的学生非常友好。这个组合还有一个隐藏好处页面逻辑在小程序端业务逻辑在后端天然就是前后端分离的形态论文里能画出清晰的结构图。2. 系统架构与环境准备先搭好地基2.1 一条完整请求是怎么跑的很多同学对“SSM后台”和“小程序前端”之间的关系是模糊的。我习惯用一个打车场景解释小程序是乘客后端接口是出租车司机MySQL 是目的地的地图。乘客要数据先叫车wx.request 发出请求司机接到订单Controller 接收请求规划路线Service 处理业务逻辑最终到达地图点去取货MyBatis 操作数据库再把货带回来返回 JSON。具体到一次刷题请求链路是这样的小程序页面 - wx.request(url) - HTTP请求 - Tomcat - SpringMVC前端控制器 - QuestionController - QuestionService - QuestionMapper - MySQL - 数据逐层返回 - 封装成JSON - 小程序 setData 渲染页面你把这条链路画在论文里基本就是“系统总体架构”那一节的核心图。我建议学生在开题阶段就把这张图手绘一遍后面所有模块都是往里填内容。2.2 开发环境清单环境搭错是常见的起步翻车点。我列一份亲测稳定的版本组合工具版本用途JDK1.8SSM项目运行基础毕设足够Maven3.6.3依赖管理和打包Tomcat8.5.x后端运行容器MySQL5.7数据库IDEA2019及以上后端开发微信开发者工具稳定版小程序开发与调试Navicat任意版本数据库可视化操作Postman任意版本后端接口测试版本不用最新稳定最好。比如 JDK 21 虽然新但可能和旧版 SSM 的某些 jar 包冲突没有必要去踩。Tomcat 也建议 8.5 而不是 10因为 10 之后 Jakarta 命名空间变了老项目跑起来容易报 ClassNotFoundException。2.3 数据库表设计五张核心表够撑起论文数据库设计是论文里“重头戏”也是答辩时老师喜欢深挖的地方。设计核心表时每个字段都要能解释“为什么存在”。下面这五张表基本覆盖全部业务用户表 t_user字段类型说明idint主键自增openidvarchar(64)微信唯一标识建索引nicknamevarchar(50)用户昵称avatarvarchar(255)头像URLphonevarchar(20)手机号预约时用roletinyint0普通用户 1管理员create_timedatetime注册时间openid 要加唯一索引。同一个用户每次 wx.login 返回的 code 不同但后端换取的 openid 是固定的这是冷启动时最容易踩的坑。题库表 t_question字段类型说明idint主键subjectint科目1科目一 3科目四typeint0单选 1判断questionvarchar(500)题干内容option_a / option_b / option_c / option_dvarchar(255)选项answervarchar(5)正确答案analysisvarchar(500)答案解析tagsvarchar(100)标签如扣分、标志科目四和科目一的题型混在一个表里用 subject 区分即可。注意科目四很多选择题是“多选”如果你只想做单选和判断在题目要求里必须写清楚不然表结构和前端都要改。错题表 t_wrong_question字段类型说明idint主键user_idint用户IDquestion_idint题目IDwrong_countint累计错误次数last_wrong_timedatetime最近一次错题时间错题表是“优选”功能的核心支撑。累计错误次数做降序排列就能生成“你最薄弱的十道题”这是论文里非常有亮点的数据推荐逻辑。考试记录表 t_exam_record字段类型说明idint主键user_idint用户IDscoreint考试成绩total_questionint题目总数wrong_countint错题数use_timeint用时秒数create_timedatetime交卷时间预约表 t_appointment字段类型说明idint主键user_idint用户IDsubjectint考试科目exam_datedate预约日期time_slotvarchar(20)时间段statustinyint0待审核 1通过 2拒绝remarkvarchar(255)管理员备注预约表的状态流转待审核 - 通过/拒绝是论文“业务流程设计”章节里好写的内容记得画一张状态图。3. 核心功能实现从0到1能直接落地的方案3.1 小程序页面架构与顶部导航适配小程序端我建议用 tabBar 三页结构首页、题库、我的。首页放公告轮播、考试预约入口、学习统计题库页放科目切换和题目列表个人中心放登录信息、错题本、成绩记录。初始代码结构{ pages: [ pages/index/index, pages/question/question, pages/exam/exam, pages/mine/mine, pages/wrong/wrong ], tabBar: { list: [ { pagePath: pages/index/index, text: 首页 }, { pagePath: pages/question/question, text: 题库 }, { pagePath: pages/mine/mine, text: 我的 } ] }, window: { navigationStyle: custom } }这里有个隐藏问题如果启用自定义导航栏navigationStyle 设置为 custom你就需要自己适配顶部状态栏和胶囊按钮的高度否则页面内容会被微信的胶囊按钮挡住。适配代码我一般写在 page 的 onLoad 里const { statusBarHeight, screenWidth } wx.getWindowInfo(); const menuRect wx.getMenuButtonBoundingClientRect(); this.setData({ statusBarHeight: statusBarHeight, navBarHeight: menuRect.height (menuRect.top - statusBarHeight) * 2 statusBarHeight });原理就是胶囊按钮顶部到状态栏底部有一段间距底部也有对称间距导航栏总高度 状态栏高度 胶囊按钮高度 上下间距之和。如果你不用自定义导航直接保留默认导航栏这段代码可以跳过。但很多同学为了界面美观会自定义这块就成了第一个卡点。3.2 题库列表与“加载更多”的实现题库数据量大一次全加载会让页面卡死必须做分页。小程序里最自然的做法是触底加载更多也就是 onReachBottom 触发了再请求下一页。Page({ data: { questionList: [], pageNum: 1, pageSize: 10, total: 0, loading: false }, onReachBottom() { if (this.data.questionList.length this.data.total || this.data.loading) { return; } this.loadQuestions(); }, loadQuestions() { this.setData({ loading: true }); wx.request({ url: http://192.168.1.100:8080/api/question/list, data: { pageNum: this.data.pageNum, pageSize: this.data.pageSize, subject: this.data.currentSubject }, success: (res) { const data res.data.data; this.setData({ questionList: this.data.questionList.concat(data.list), pageNum: this.data.pageNum 1, total: data.total }); }, complete: () { this.setData({ loading: false }); } }); } });防止重复请求的关键是 loading 标记。很多同学触底加载会出现列表闪烁、重复数据就是没有用 loading 把关。另外后端分页要返回 total 和 pages 字段前端才知道“是否还有下一页”。3.3 刷题与提交答案自动判题和错题记录刷题页设计成“一题一页”题干、选项、提交按钮。用户选择选项后先本地判断对错checkAnswer(e) { const selected e.currentTarget.dataset.value; const question this.data.currentQuestion; if (selected question.answer) { this.setData({ answered: true, correct: true }); } else { this.setData({ answered: true, correct: false }); this.addWrongQuestion(question.id); } }addWrongQuestion 请求后端接口把错题写入数据库。这里要注意不是每次答错都重复新增记录而是如果这条错题已经存在就让 wrong_count 加一。用一条 INSERT ... ON DUPLICATE KEY UPDATE 就能搞定不过需要给 user_id 和 question_id 建联合唯一索引。判断题的选项 A 对应“正确”、B 对应“错误”后端存储答案字段可以直接存 “A” 或 “B”前端不用特意转成布尔值保持统一反而更好扩展。3.4 模拟考试随机抽题和自动评分模拟考试的核心是后端随机组卷。最简单稳定的 SQL 是SELECT * FROM t_question WHERE subject 1 ORDER BY RAND() LIMIT 100;RAND() 在题量几千的表中性能没问题毕设完全够用。但要注意抽取出的试卷要固定下来不能让用户每次刷新题目顺序都变。所以后端在用户进入考试的那一刻把抽取的题目 ID 列表存下来可以存到 session 或一张 exam_question 关联表考试过程中的作答都基于这份固定快照。自动评分逻辑不复杂public ExamResultVO calculateScore(ListAnswerDTO answers) { int score 0; for (AnswerDTO answer : answers) { Question question questionMapper.selectById(answer.getQuestionId()); if (question.getAnswer().equals(answer.getSelected())) { score 1; } } // 科目一每题1分总分100 return ExamResultVO.builder() .score(score) .correctCount(score) .wrongCount(answers.size() - score) .build(); }交卷后要展示三个信息分数、错题解析、本次成绩是否合格驾考科目一 90 分合格科目四 90 分合格。这个合格线写在配置类里不要写死在 SQL 里方便后续调整。3.5 微信登录与授权登录这块是后端最容易出 bug 的地方。小程序端先调 wx.login 拿到临时 code再把 code 传给后端wx.login({ success: res { wx.request({ url: http://192.168.1.100:8080/api/user/login, method: POST, data: { code: res.code }, success: res { wx.setStorageSync(token, res.data.data.token); } }); } });后端拿到 code 后需要向微信接口 https://api.weixin.qq.com/sns/jscode2session 发起请求用 appid 和 secret 换取 openid。这一步用 Spring 的 RestTemplate 就能完成public LoginResult wxLogin(String code) { String url https://api.weixin.qq.com/sns/jscode2session? appid appId secret appSecret js_code code grant_typeauthorization_code; String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); String openid json.getString(openid); // 查库或创建用户生成token }拿到 openid 后给用户生成一个自定义 token 返回给前端后续所有需要登录态的接口都在请求头里带 token。开发时在微信开发者工具勾选“不校验合法域名”可以直接用 http://192.168.x.x:8080 访问本机后端但上传体验版后必须配置 HTTPS 合法域名否则真机无法请求。3.6 SSM后端常用注解和统一接口规范SSM 的 Controller 层注解是论文里必须写清楚的技术点。我整理了一张对照表注解作用使用位置Controller标注这是一个控制器类类上ResponseBody把返回值直接写入HTTP响应体配合Controller返回JSON方法上RequestMapping映射URL路径和请求方式类或方法上RequestParam接收URL参数如?pageNum1方法参数上PathVariable接收路径参数如/question/1中的1方法参数上RequestBody接收请求体中的JSON并绑定到对象方法参数上Autowired注入Service依赖字段上Service标注业务层组件Service实现类上一个标准的分页查询接口长这样Controller RequestMapping(/api/question) public class QuestionController { Autowired private QuestionService questionService; RequestMapping(value /list, method RequestMethod.GET) ResponseBody public Result list(RequestParam Integer pageNum, RequestParam Integer pageSize, RequestParam Integer subject) { PageHelper.startPage(pageNum, pageSize); ListQuestion list questionService.listBySubject(subject); PageInfoQuestion pageInfo new PageInfo(list); return Result.success(pageInfo); } }所有接口返回统一的 Result 结构public class Result { private Integer code; // 200成功500失败 private String msg; private Object data; public static Result success(Object data) { Result r new Result(); r.code 200; r.msg success; r.data data; return r; } }统一返回结构非常重要。否则小程序端每个请求都要写不同的解析逻辑排错时也很痛苦。我见过一个项目里有的接口返回 {code:200}有的直接返回数组还有的返回字符串前端一堆 if else极其混乱。4. 毕业论文怎么写才能有深度4.1 论文框架每一章该写什么毕设论文最忌讳的是“功能说明书”全篇都是截图和代码粘贴。老师要看到的是你“为什么这么设计”的思考过程。一个稳妥的框架是这样的章节核心内容常见错误第一章 绪论研究背景、意义、国内外现状、主要工作背景写成一堆空话套话第二章 关键技术微信小程序、SSM、MySQL 的原理与选型理由变成名词解释没有结合项目第三章 需求分析可行性分析、功能需求、用例图、非功能需求功能需求没有边界第四章 系统设计总体架构图、模块设计、数据库设计、接口设计E-R图和数据表脱节第五章 系统实现分模块展示核心代码和运行截图大量贴代码没有解释第六章 系统测试测试环境、功能测试用例、测试结果只写“测试通过”第七章 总结与展望总结工作、分析不足、展望后续总结等于复制摘要“国内外研究现状”这一节很多同学不知道怎么写。有个取巧但有效的方法去知网或万方搜“在线考试系统”“微信小程序驾考”“移动学习平台”等关键词找几篇近三年的硕士论文看他们怎么归纳已有系统的特点和局限然后列出几个代表系统的对比。你不需要完全创新重点是体现出你调研过同类系统。4.2 把“优选”写实数据统计和智能推荐“优选”两个字如果只在题目里出现论文里找不到落点答辩必被问住。我建议至少做一层数据统计加一层简单推荐错题统计接口可以用一条 SQL 完成SELECT q.id, q.question, q.answer, COUNT(w.id) AS wrong_num FROM t_wrong_question w LEFT JOIN t_question q ON w.question_id q.id WHERE w.user_id #{userId} GROUP BY q.id ORDER BY wrong_num DESC LIMIT 10;小程序端看到一个“薄弱题目 Top10”这就是“优选”的可视化成果。推荐逻辑不用做得太复杂按错误次数降序 按最近错误时间排序已经能体现出“针对用户弱项精准练习”的闭环。论文第 4 章可以画一张推荐流程图用户交卷 - 答案比对 - 错题写入 - 统计排序 - 生成推荐练习每一步都对应代码的哪个方法。4.3 图表、测试数据与答辩准备论文里的图至少要有三张系统总体架构图、功能结构图、数据库 E-R 图。画图工具推荐 ProcessOn 或者 Draw.io线条统一、配色干净不要用 Word 自带的形状硬凑。测试部分要准备一张功能测试用例表用例编号测试功能操作步骤预期结果实际结果结论TC001用户登录微信授权登录自动注册并跳转首页成功通过TC002随机组卷点击“模拟考试”生成100题试卷成功通过TC003提交答案答完所有题后交卷显示分数和解析成功通过TC004错题收藏答错后查看错题本错题自动出现成功通过答辩时老师最常问的几个问题提前准备好答案为什么不用 Spring Boot明确回答学校课程体系以 SSM 为主且 SSM 各层职责清晰便于理解底层原理。微信登录单点问题怎么处理回答用 code 换取 openidopenid 在小程序平台内唯一配合自定义 token 维持会话。错题推荐的算法是什么回答基于错误频次统计的简单推荐属于非个性化推荐后续可引入遗忘曲线优化。5. 常见问题与排坑实录5.1 真机连不上本地后端开发工具里跑得好好的一发到手机就白屏这是最常见的问题。根源通常是代码里写了 http://localhost:8080 或 http://127.0.0.1:8080到了手机localhost 指向手机自己当然连不上。正确做法是改成电脑在局域网里的 IP比如 http://192.168.1.100:8080并且保证手机和电脑连同一个 WiFi。另外微信小程序正式版要求请求域名必须是 HTTPS 且已备案开发调试阶段可以在微信开发者工具右上角“详情 - 本地设置”里勾选“不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书”。5.2 跨域和 Session 失效SSM 后端默认不允许跨域小程序虽然不是浏览器环境但真机调试时仍可能遇到跨域问题。最省事的方案是在后端加一个 CORS 过滤器Component public class CorsFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException { HttpServletResponse response (HttpServletResponse) res; response.setHeader(Access-Control-Allow-Origin, *); response.setHeader(Access-Control-Allow-Methods, GET,POST,PUT,DELETE,OPTIONS); response.setHeader(Access-Control-Allow-Headers, Content-Type,Authorization); chain.doFilter(req, res); } }在 Web.xml 里注册过滤器放到 SpringMVC 的 DispatcherServlet 之前。注意如果后端的 Controller 里用了 session小程序默认是不会自动带上 Cookie 的这也是很多人发现“登录状态一刷新就丢”的原因。解决办法就是别依赖 session用自定义 token 做登录态前端每次 wx.request 在 header 里手动加 token。5.3 加载更多时列表重复或数据错乱加载更多出现重复数据的常见原因就是 pageNum 没有正确累加。还有一个隐蔽问题请求是异步的用户快速触底两次第二次请求还在进行中pageNum 已经加了两次导致跳过一页数据。解决方案就是前面代码里的 loading 标记请求结束之前不允许发起下一次请求。另外每次切换科目时要把 questionList 清空、pageNum 重置为 1不然会混着上一科目的数据。5.4 小程序包体积超限如果用了 uniapp 或者引入了很多静态资源编译时会出现类似 source size 2612kb exceed max limit 2mb 的报错。小程序主包限制 2MB解决办法有三个开启分包把题库、考试等非首页页面放到 subpackages 里压缩图片题库里不要直接存 base64 图片用图床或后端返回图片 URL去掉没用到的组件和插件很多模板自带了一堆 echarts、vant 组件实际用不到。以分包为例app.json 里这样配置{ subpackages: [ { root: pages/exam, pages: [ pages/exam/exam, pages/exam/result ] } ] }注意小程序里访问分包页面的路径要带包名例如 wx.navigateTo({ url: /pages/exam/exam })。分包配置好之后主包体积瞬间能降下来。5.5 常见问题速查表现象可能原因解决方法真机白屏请求地址用了localhost或未勾选域名校验使用局域网IP并勾选不校验合法域名接口返回404Controller或Mapper扫描路径配置错误检查applicationContext.xml和组件扫描包路径登录状态丢失服务端依赖session改用自定义token方案中文乱码接口返回编码不一致设置UTF-8字符集过滤器提交答案没反应请求参数名对不上用Network面板核对请求参数名与后端方法参数数据库连接失败MySQL版本或驱动不匹配MySQL5.7搭配mysql-connector-java 5.1.47订阅消息发送失败模板ID配置错误或用户未授权在后台申请模板并检查小程序订阅消息配置开发调试时优先用微信开发者工具的 Network 面板看请求状态码和返回体。很多前端报错其实是后端已经返回了 500只是前端没有做错误提示逻辑。先把接口用 Postman 全部测通再写页面绑定能省一半排查时间。5.6 体验版分发与反馈收集小程序开发完成后需要把代码上传到微信公众平台生成体验版二维码发给身边的同学和导师试用。在微信开发者工具点击“上传”填好版本号和备注然后在公众平台“版本管理”里把该版本设为体验版就可以生成二维码。体验版不需要审核成员和体验成员都可以直接扫。收集试用反馈的重点是让测试者关注业务流程是否闭环比如“预约考试-收到审核结果-查看记录”这条链路走不走得通。我在实际操作中发现大家反馈最多的问题集中在页面提示不友好比如提交预约后没有任何成功提示用户以为没提交成功。这类问题要在一轮反馈后集中修复。我个人在实际指导项目时最大的体会是这个题目的代码量不大真正拉开差距的是“能不能把系统讲清楚”。把一次请求的完整链路背下来把五张表之间的关联画明白把错题推荐这个“优选”的亮点逻辑说明白答辩基本就稳了。如果后续想扩展可以把科目二、科目三的视频教学和约车进度管理加进来系统就从“刷题工具”升级成“全流程驾考助手”这也正好是“优选驾考”这个题目可以深挖的方向。