简介:这是一份基于微信小程序与Java后端(SSM框架+MySQL)的数学辅导毕业设计完整项目,适合计算机相关专业学生用于毕业设计或课程设计参考。项目按管理员与用户两类角色设计功能模块,管理员可管理用户、学习中心、知识分类、学习周报、口算练习、试题与考试;用户端支持首页、学习中心、考试、个人中心、错题本、收藏管理等操作,覆盖了典型的管理端+小程序端交互场景。资源包共1281个文件,约17.44MB,包含png界面图、js与vue前端逻辑、java后台接口、wxml/wxss小程序页面、sql数据库脚本等,类型齐全,便于按需查阅与二次开发。压缩包内还附有安装运行批处理与说明文档,能够帮助快速搭建环境并理清项目结构。目前已有173人学习使用,对需要快速上手完整小程序+Java后端项目的同学具有较高参考价值。
1. 从一套数学辅导毕业设计说起:小程序 + SSM 的选型逻辑
手头这套“基于微信小程序 + Java 后端的数学辅导毕业设计”,压缩包里有源码、数据库脚本和说明文档。打开目录第一眼看到的是main.css.bak、update-password.vue.bak这类备份文件,以及1-install.bat、2-run.bat、3-build.bat三个批处理,说明作者在开发过程中习惯保留.bak,并且已经替后来者准备了从环境安装到构建的一键脚本。这对课程设计和毕业设计来说非常友好,省去了很多“配环境两小时、跑通五分钟”的尴尬。
它的技术栈是微信开发者工具 + Java + SSM + MySQL + B/S 架构,业务上严格区分管理员端和用户端。管理员端管用户、学习中心、知识分类、学习周报、口算练习、试题、系统、考试;用户端则通过微信小程序完成首页、学习中心、考试、个人中心、错题本、收藏、考试记录等操作。与其说这是一个“毕设项目”,不如说它把在线教育领域最常见的管理端 + 小程序端的双端闭环做了一遍,核心价值在于数据表设计、角色权限划分和考试流程的完整实现。
这套东西适合两类人:一类是做毕业设计或课程设计、需要一个完整项目参考的在校生;另一类是刚接触小程序 + Java 后端协作开发、想看看前后端如何约定接口和数据格式的初级工程师。对于后者,重点不是登录界面的皮相,而是 SSM 如何暴露 REST 接口、小程序端如何用wx.request消费这些接口,以及考试、错题本这些有状态业务如何在 MySQL 里落地。
2. 数据库与模块拆解:管理员、用户两端的表怎么设计
2.1 从需求反推数据表:角色、业务、状态三个维度
拿到摘要里的功能清单,第一件事不是写代码,而是把管理员和用户两类角色的操作映射成数据模型。常见做法是:用户表(管理登录账号)独立,业务表围绕“学习”和“考试”展开,再辅以系统管理所需的配置表。
以这套数学辅导系统为例,核心表至少包括:
user:用户表,字段有id、username、password、role(管理员/普通用户)、name、avatar等。knowledge_category:知识分类表,用于学习中心的知识点分类。learning_center:学习中心内容表,存图文或富文本,外键关联分类。study_weekly:学习周报表,用户每周提交学习情况,管理员可以查看和点评。oral_practice:口算练习表,存储练习题和答案。question:试题表,题型可以是单选、多选、判断,字段包括题干、选项、答案、分值。exam:考试表,定义考试名称、时间、总分。exam_question:考试与试题的关联表,维护某次考试包含哪些题。exam_record:考试记录表,记录用户某次考试的成绩和提交时间。wrong_book:错题本表,用户在考试中答错的题自动归入。
这个表结构设计的核心思想是“考试与试题分离”。exam不直接存题目,而是通过exam_question关联,这样同一道题可以出现在多次考试中,符合题库系统的通用设计。错题本也不需要单独存整道题的副本,只需记录user_id和question_id,展示时再联表查题。
2.2 MySQL 建表示例与字段注释
下面给出exam_record和wrong_book两张表的建表 SQL,这是考试闭环中最容易设计出错的地方。
CREATE TABLE `exam_record` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '记录ID', `user_id` INT NOT NULL COMMENT '用户ID', `exam_id` INT NOT NULL COMMENT '考试ID', `score` DECIMAL(5,2) DEFAULT '0.00' COMMENT '本次得分', `total_score` DECIMAL(5,2) DEFAULT '0.00' COMMENT '试卷总分', `duration_seconds` INT DEFAULT 0 COMMENT '答题耗时(秒)', `submit_time` DATETIME DEFAULT NULL COMMENT '提交时间', PRIMARY KEY (`id`), KEY `idx_user_exam` (`user_id`, `exam_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='考试记录表'; CREATE TABLE `wrong_book` ( `id` INT NOT NULL AUTO_INCREMENT COMMENT '错题ID', `user_id` INT NOT NULL COMMENT '用户ID', `question_id` INT NOT NULL COMMENT '试题ID', `exam_id` INT NOT NULL COMMENT '来源考试ID', `wrong_answer` VARCHAR(255) DEFAULT NULL COMMENT '用户错误答案', `correct_answer` VARCHAR(255) DEFAULT NULL COMMENT '正确答案', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '加入时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_user_question` (`user_id`, `question_id`), KEY `idx_user_exam` (`user_id`, `exam_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='错题本表';说明:exam_record使用DECIMAL(5,2)存储成绩,便于处理 0.5 分这类情况;wrong_book的uk_user_question唯一键避免同一道题重复进入错题本。如果题目本身是判断题,wrong_answer存true或false即可;如果是主观题,这里不适用,需要在question表里加question_type区分。
2.3 数据库初始化与目录里的.sql文件
压缩包内的“数据库”目录通常会包含一个math_tutor.sql或db.sql完整脚本,用 Navicat 或者命令行执行:
mysql -u root -p -e "CREATE DATABASE IF NOT EXISTS math_tutor DEFAULT CHARSET utf8mb4;" mysql -u root -p math_tutor < math_tutor.sql建议在导入之前先用SHOW TABLES;检查是否重复导入。这个项目的表名和字段大多是对应业务的中文拼音缩写,比如user、exam、question,没有使用math_user这类前缀,因此在联表查询时要注意区分字段归属,尽量使用别名。
3. 后端 SSM 接口实现:登录鉴权、学习中心与考试模块
3.1 SSM 分层:Controller、Service、Mapper 的职责切分
这套后端采用 Spring + SpringMVC + MyBatis 的经典组合。项目结构一般按controller、service、mapper分包,mapper接口对应 XML 文件里的 SQL。
先看登录接口,管理员端和用户端共用同一个UserController,通过role字段区分跳转路径。下面是一个典型的登录 Controller 方法:
@RestController @RequestMapping("/user") public class UserController { @Autowired private UserService userService; @PostMapping("/login") public Result login(@RequestBody User user) { // 1. 参数校验 if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) { return Result.error("用户名或密码不能为空"); } // 2. 调用 service 查询 User dbUser = userService.login(user.getUsername(), user.getPassword()); if (dbUser == null) { return Result.error("用户名或密码错误"); } // 3. 判断角色:管理员走 admin 首页,用户走小程序首页 if ("admin".equals(dbUser.getRole())) { return Result.ok().put("token", createToken(dbUser)).put("role", "admin"); } else { return Result.ok().put("token", createToken(dbUser)).put("role", "user"); } } private String createToken(User user) { // 实际项目中推荐用 JWT,这里简单用 UUID 模拟 return UUID.randomUUID().toString().replace("-", ""); } }代码逻辑说明:@PostMapping("/login")接收 JSON 请求体,参数校验放在最前面,避免空指针。createToken这里只是演示,正式项目中应该引入jjwt生成 JWT,并设置过期时间。小程序端拿到 token 后,后续请求需要在 header 中携带token字段,后端用拦截器统一校验。
这里要重点说明一个常见误用:很多课设项目把登录用户信息直接放在 session 里,小程序端调用wx.request时不会自动携带 Cookie,所以必须用 token 机制。SSM 项目中可以写一个HandlerInterceptor,在preHandle里从请求头提取 token,再查 Redis 或数据库确认用户状态。如果项目没引入 Redis,最简单的做法是把已登录 token 存到user表的token字段,前端每次请求带过来,后端比对时间戳是否过期。
3.2 学习中心与知识分类接口
学习中心模块的逻辑很简单:按分类查内容列表,点进去看详情。核心 SQL 在 Mapper XML 里维护,这里给出LearningCenterMapper.xml中的一个条件查询片段:
<select id="selectByKeyword" parameterType="map" resultType="com.demo.entity.LearningCenter"> SELECT lc.*, kc.category_name FROM learning_center lc LEFT JOIN knowledge_category kc ON lc.category_id = kc.id <where> <if test="keyword != null and keyword != ''"> AND (lc.title LIKE CONCAT('%', #{keyword}, '%') OR lc.content LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="categoryId != null"> AND lc.category_id = #{categoryId} </if> </where> ORDER BY lc.create_time DESC </select>参数说明:keyword用于全文模糊匹配,categoryId用于分类筛选。LEFT JOIN知识分类表是为了在列表展示时直接拿到分类名称,避免二次查询。注意这里LIKE模糊查询在数据量大时有性能隐患,但对于毕业设计的数据量完全够用,不要过早用全文索引。
如果前端是小程序原生的swiper轮播图加载学习中心头条,后端还需要提供一个加limit 5的列表接口。常见做法是selectByKeyword方法里加一个limit参数,或者单独写一个selectTop查询。
3.3 考试模块:组卷、提交、自动判分
考试模块是这套系统的重头戏。后端需要提供三个关键接口:
GET /exam/list:获取可参加的考试列表。GET /exam/detail?examId=1:获取考试包含的试题,但选项中的正确答案要置空,不能提前泄露。POST /exam/submit:提交答案,后端逐题判分,记录成绩和错题。
判分逻辑是纯后端计算,不能信任前端传来的分数。下面是一个 submit 方法的简化实现:
@PostMapping("/exam/submit") public Result submit(@RequestBody ExamSubmitDTO dto, @RequestHeader("token") String token) { // 1. 根据 token 获取当前用户 User user = userService.getUserByToken(token); if (user == null) { return Result.error(401, "登录已过期"); } // 2. 查询考试信息及题目列表 Exam exam = examService.getById(dto.getExamId()); List<Question> questions = questionService.listByExamId(dto.getExamId()); // 3. 逐题对比正确答案 int totalScore = 0; List<WrongBookDTO> wrongItems = new ArrayList<>(); for (Question q : questions) { String userAnswer = dto.getAnswers().get(q.getId().toString()); if (q.getAnswer().equalsIgnoreCase(userAnswer)) { totalScore += q.getScore(); } else { wrongItems.add(new WrongBookDTO(q.getId(), userAnswer, q.getAnswer())); } } // 4. 保存考试记录 ExamRecord record = new ExamRecord(); record.setUserId(user.getId()); record.setExamId(dto.getExamId()); record.setScore(totalScore); record.setTotalScore(exam.getTotalScore()); record.setDurationSeconds(dto.getDurationSeconds()); record.setSubmitTime(new Date()); examRecordService.save(record); // 5. 批量写入错题本 if (!wrongItems.isEmpty()) { wrongBookService.batchInsert(user.getId(), dto.getExamId(), wrongItems); } return Result.ok().put("score", totalScore); }逻辑说明:判分时用equalsIgnoreCase兼容大小写,因为判断题答案可能传true或True。dto.getAnswers()是一个Map<String, String>,key 是题目 ID,value 是用户选择的答案。错题本在提交时一次性写入,避免循环调用单条 insert,减少数据库开销。
这里有个细节容易踩坑:questionService.listByExamId查出来的题目列表,如果某些题被删除了,会导致dto.getAnswers()里对应 key 为空,最终被判为错误。更严谨的做法是允许空答案自动视为错误,同时记录用户没有作答。
4. 小程序端对接:首页、学习周报、错题本与口算练习的实现要点
4.1 小程序项目结构与wx.request封装
微信开发者工具导入项目后,标准目录是pages、static、utils、app.js、app.json。这套资源里还能看到IndexMain.vue.bak、IndexHeader.vue.bak这些备份文件,说明作者可能同时维护过一个后台管理的前端页面(可能是 Vue 写的,但最终发布的源码中并没有使用它们)。真正的小程序端入口是app.js里的globalData.apiBaseUrl。
接口请求必须统一封装,不要在每个页面里直接写wx.request。在utils/request.js里做一层封装:
const BASE_URL = 'http://localhost:8080/math'; function request(url, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method: method, data: data, header: { 'Content-Type': 'application/json', 'token': wx.getStorageSync('token') }, success: (res) => { if (res.data.code === 401) { wx.navigateTo({ url: '/pages/login/login' }); reject(res.data); } else { resolve(res.data); } }, fail: (err) => reject(err) }); }); } module.exports = { request, BASE_URL };参数说明:token从本地缓存读取,每次请求自动带上。res.data.code === 401是后端约定的登录失效状态码,小程序端收到后统一跳转登录页。BASE_URL中的localhost只能用于本地模拟器,真机调试时需改为局域网 IP,比如http://192.168.1.100:8080/math。
4.2 首页与学习中心的数据绑定
首页通常由swiper轮播图、知识分类列表和课程推荐组成。在pages/index/index.js的onLoad生命周期中请求/learning/center/list:
onLoad() { this.fetchLearningList(); }, fetchLearningList() { const categoryId = this.data.currentCategoryId || ''; request(`/learning/center/list?categoryId=${categoryId}&pageNum=1&pageSize=10`) .then(res => { this.setData({ learningList: res.data.records }); }); }交互说明:点击左侧分类 Tab 时,更新currentCategoryId,并重新调用fetchLearningList。这里要注意setData的数据量,一次渲染 10 条推荐内容即可,图片较多的列表建议用lazy-load属性。
4.3 学习周报与错题本的联动
学习周报是用户端的高频功能。用户提交周报时,后端需要做归属校验,只允许用户操作自己的数据。前端pages/weekly/weekly.js中提交表单:
submitWeekly() { const content = this.data.editorContent; const plan = this.data.planContent; if (!content.trim()) { wx.showToast({ title: '学习内容不能为空', icon: 'none' }); return; } request('/weekly/save', 'POST', { weeklyContent: content, nextPlan: plan, weekDate: this.data.weekDate }).then(() => { wx.showToast({ title: '提交成功', icon: 'success' }); this.fetchMyWeeklyList(); }); }这里的业务逻辑要点是:每周只能提交一份周报,后端 save 时通过user_id + week_date做唯一约束,如果重复提交就执行UPDATE而不是INSERT。错题本页面通常是从考试结果页跳转过来,直接请求/wrong/list?userId=xxx。但记住:不要在小程序端传userId作为查询参数,应该从 token 中解析用户身份,避免越权。后端写法是:
if (user.getId() != wrongBook.getUserId()) { return Result.error("无权查看他人错题"); }4.4 口算练习与考试倒计时
口算练习通常是随机整数加减乘除,前端可以用Math.random生成题目,后端只需要记录用户的练习结果。但在这套系统中,口算练习表是存在数据库里的,由管理员录入,用户刷题时请求/oral/random?count=10获取随机口算题。
考试页面需要使用wx.createInnerAudioContext或wx.vibrateShort做交卷提醒?不必要。真正需要细心的是倒计时逻辑:
startCountdown() { this.timer = setInterval(() => { const remain = this.data.remainSeconds - 1; if (remain <= 0) { clearInterval(this.timer); this.submitExam(); // 自动交卷 } else { this.setData({ remainSeconds: remain }); } }, 1000); }注意用clearInterval清除定时器,否则页面跳转后定时器会继续执行,导致在非考试页自动交卷。另外,在小程序切后台时setInterval会被暂停,恢复前台后需要重新计算剩余时间,建议用服务器时间做校准。
5. 排错与改造:把毕业设计改成课程设计/项目实战的落地技巧
5.1 三个.bat文件各自的用途与报错排查
压缩包根目录下有三个批处理文件,它们针对 Windows 环境做了操作简化。先看1-install.bat的内容通常是:
@echo off echo 开始安装依赖... mvn clean install -DskipTests pause2-run.bat一般是:
@echo off cd /d %~dp0 mvn spring-boot:run pause3-build.bat则是:
@echo off mvn clean package -Dmaven.test.skip=true copy target\math-0.0.1-SNAPSHOT.jar .\release\ pause说明:这三个脚本把 Maven 的本地安装、运行、打包三个阶段分离。如果执行1-install.bat报错,多半是 JDK 或 Maven 环境变量配置不对,用java -version和mvn -version检查版本;如果 Maven 依赖下载超时,需要在settings.xml中配置阿里云镜像。2-run.bat启动后端口是 8080,如果被占用,在application.properties中修改server.port,同时也要改小程序端的BASE_URL。
5.2 修改小程序启动加载页的快捷办法
热搜里有一条“修改刚进入的加载页面”,很多初次接触小程序的人会在app.json的pages数组里调整顺序,这是标准做法。但要注意:app.json中pages的第一项就是启动页。如果项目首页逻辑依赖管理员登录,可以把pages/index/index放在第一位,然后在index.js的onLoad中根据 token 是否存在决定跳转还是继续渲染。如果只是想要一个启动欢迎页,就设置一个单独的pages/splash/splash,停留两秒后wx.reLaunch到首页。
| 修改点 | 文件位置 | 具体操作 |
|---|---|---|
| 启动页顺序 | app.json | 将目标页面路径移到pages数组第一位 |
| 欢迎页展示 | pages/splash/splash.js | 在onLoad中加setTimeout跳转 |
| 跳过用户重复登录 | utils/request.js | 判断缓存 token,存在则直接进入首页 |
5.3 从毕业设计到项目实战:接口权限和后端安全改造
毕设项目为了演示简单,往往所有接口都是裸奔的。如果想把它写成简历上的“项目实战”经历,至少要补上三处:
第一,密码加密。user表的password字段不要存明文,用BCryptPasswordEncoder加密存储。登录时先查出一行,再调用matches()比对。第二,接口权限区分。管理员接口和用户接口要分开,比如所有/admin/**接口用拦截器校验role == 'admin',非管理员直接返回 403。第三,SQL 注入和 XSS。MyBatis 的${}要避免使用,#{}才有预编译效果。保持当前 XML 里<where>、<if>的写法即可,不要因为图方便把LIKE CONCAT('%', #{keyword}, '%')改成${keyword}。
如果想把考试模块做得更贴近线上,建议在exam表增加start_time和end_time,提交时校验当前时间是否在有效区间内,并在exam_record上增加唯一索引uk_user_exam (user_id, exam_id),防止同一个用户对同一场考试提交两次。这是很多课设系统容易漏掉的健壮性问题,加上后整个项目的完成度会明显提升。
本文还有配套的精品资源,点击获取