ARTICLE DETAIL

资讯详情

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

微信小程序在线课堂SSM源码解析:从接口设计到部署全流程

微信小程序在线课堂SSM源码解析:从接口设计到部署全流程 简介基于SSM框架的微信小程序在线课堂项目源码面向课程设计、毕业设计及Java全栈学习者。小程序端涵盖课程分类浏览与关键词搜索、视频直播和点播、学习进度记录、互动讨论、作业提交与批改、个性化推荐及学习数据分析等功能后端借助SSM完成业务逻辑与用户管理可帮助开发者理解前后端协作的在线教育系统实现。压缩包共1244个文件约52.31MB包括Java服务端代码、Vue后台页面、微信小程序页面文件、SQL脚本及配置文件并附带一键安装运行脚本。已有151人学习下载。项目提供完整源代码、后台管理端和数据库脚本覆盖在线课堂核心业务闭环可作为课程设计答辩与项目实战范本。1. 这一版在线课堂赢在把 SSM 的底子用在了正确的地方接手这套「微信小程序在线课堂 SSM」源码时我的第一反应不是去跑通它而是先看它的工程结构。压缩包里那一串main.css.bak、update-password.vue.bak、IndexAsideStatic.vue.bak以及三个.bat脚本直接暴露了这是一个经历过迭代、从后台管理端到小程序端完整配套的课设级项目。很多人一听到 SSM 就觉得老但在这个项目里Spring 管理业务 Bean、Spring MVC 负责接口路由、MyBatis 做数据持久化恰好覆盖了在线课堂最核心的诉求课程列表要分页、学习进度要落库、作业提交要事务。这套组合在中小并发下远比微服务架构省心也更容易在一周内改完论文里的功能点。它适合谁适合正在做微信小程序毕业设计、需要一套前后端完整可运行代码的人也适合想搞懂「小程序 Java 后端」真实交互方式的开发。我会从路由、请求封装、缓存和附件落地这几个角度把它拆开讲透。2. SSM 后端的接口设计从 Controller 到 MyBatis 映射的完整链路微信小程序端的所有页面都依赖后端接口返回 JSON 数据而 SSM 项目里这一层的核心就是写好 Controller、Service、Mapper 三层。很多课设代码的问题在于把业务逻辑全堆在 Controller 里导致接口又长又难维护。这套项目的做法更接近实际工程我们来拆开看它是怎么组织的。2.1 统一返回结构与异常处理小程序端不做重复判断小程序端每次请求都关心三件事是否成功、返回什么数据、失败是什么原因。如果每个接口返回格式不统一前端就要写一堆if (res.data.code 200)之类的判断。这个项目在com.onlineclass.common包下定义了一个Result类所有接口统一返回{ code, msg, data }结构。public class ResultT { private Integer code; private String msg; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMsg(success); result.setData(data); return result; } public static T ResultT error(Integer code, String msg) { ResultT result new Result(); result.setCode(code); result.setMsg(msg); return result; } }这段代码的逻辑很直接success方法把业务数据data塞进统一外壳error方法只返回错误码和提示。前端拿到这个结构后只需要在封装的request方法里判断一次code 200就可以往下走出错时直接把msg弹出提示框。参数校验失败、业务异常、未知异常在ControllerAdvice全局异常处理器里分别返回 400、500 等状态码避免异常堆栈直接暴露给小程序端。2.2 课程列表接口分页参数与搜索关键词的处理方式课程浏览与搜索是这个系统最核心的接口。文章开头的功能描述里提到了分类浏览和关键词搜索这两个需求在数据库层面其实可以合并成一个带条件的动态查询。在 Mapper 层使用if标签拼接 SQL比硬编码多个接口要优雅得多。RestController RequestMapping(/api/course) public class CourseController { Autowired private CourseService courseService; GetMapping(/list) public ResultPageResultCourse list( RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String keyword, RequestParam(required false) Integer categoryId) { PageResultCourse page courseService.queryPage(pageNum, pageSize, keyword, categoryId); return Result.success(page); } }对应到 MyBatis 的 XML 映射文件里核心的 SQL 是这样写的select idqueryPage resultTypecom.onlineclass.entity.Course SELECT c.id, c.title, c.cover_url, c.teacher, c.price, c.category_id FROM course c where if testkeyword ! null and keyword ! AND (c.title LIKE CONCAT(%, #{keyword}, %) OR c.teacher LIKE CONCAT(%, #{keyword}, %)) /if if testcategoryId ! null AND c.category_id #{categoryId} /if /where ORDER BY c.create_time DESC /select这里有几个关键点值得注意。where标签会自动去掉多余的AND前缀避免 SQL 语法错误LIKE查询使用CONCAT拼接百分号而不是在 Java 层拼好再传入这是为了防止 SQL 注入的关键习惯pageNum和pageSize通过 PageHelper 插件自动拼接LIMIT语句不需要手写分页 SQL。如果keyword为空字符串if判断会直接跳过相当于退化为纯分类筛选或全量列表。2.3 数据库表设计一张表满足课设要求的设计思路后端项目的数据库设计决定了功能点的扩展成本。这套在线课堂的物理模型没有做成过度范式化的十几张表而是控制在 8 张核心表以内这符合课程设计的篇幅要求同时也能覆盖摘要里提到的所有功能点。见下表表名核心字段对应功能userid, openid, nickname, avatar小程序登录用户courseid, title, cover_url, teacher, price课程展示与搜索categoryid, name, sort课程分类course_videoid, course_id, video_url, duration章节视频与点播learning_recordid, user_id, video_id, progress学习进度记录discussionid, course_id, user_id, content, create_time互动讨论区homeworkid, course_id, title, deadline作业发布homework_submitid, homework_id, user_id, file_url, status作业提交与批改homework_submit表与user表是典型的关联关系批改状态用status字段区分0 待批改、1 已通过、2 被打回。学习进度的progress字段存的是播放百分比小程序端每 10 秒上报一次后端只更新不插入通过video_id user_id做唯一约束避免重复记录。提示如果每一步的学习行为都做成一条插入记录后期分析学习行为很简单但数据量会迅速膨胀。这套项目用「只更新一行进度」的做法减轻了数据库压力也简化了表结构。3. 小程序端的请求封装与登录态管理从wx.login到请求拦截器小程序端很多人一上来就写wx.request每个页面复制粘贴后来改接口地址时改到怀疑人生。这套项目的小程序端把请求统一封装在utils/request.js里并要求所有页面都调用这个封装方法工程结构上明显比直接拼接 URL 规范得多。3.1 request.js 封装与 token 注入const BASE_URL http://localhost:8080/online-class; function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { wx.navigateTo({ url: /pages/login/login }); } else { wx.showToast({ title: res.data.msg, icon: none }); } }, fail(err) { wx.showToast({ title: 网络异常请检查后端服务, icon: none }); reject(err); } }); }); } module.exports { get: (path, data) request(path, GET, data), post: (path, data) request(path, POST, data) };这段request.js的核心逻辑是把所有与后端通信的细节集中管理。Authorization头从本地缓存读取 token每次请求自动携带code 200时直接返回data调用方不需要再解一层壳遇到 401 跳转登录页让用户重新走登录流程。这样设计之后每个页面的代码就会很薄比如课程列表页只需要调用const { get } require(../../utils/request.js); Page({ data: { courseList: [], pageNum: 1 }, onLoad() { this.loadCourses(); }, loadCourses() { get(/api/course/list, { pageNum: this.data.pageNum, pageSize: 10 }) .then(list this.setData({ courseList: list.records })) } })3.2 登录态如何和 SSM 后端对齐小程序的登录机制和网页不同。网页靠 session cookie小程序要先用wx.login拿到临时凭证code再把code发给后端。后端拿着code去微信接口换openid查库之后签发自定义 token。这个项目在LoginController里做的事情本质就是一次code换openid、再换 token 的过程。PostMapping(/wx/login) public ResultMapString, Object wxLogin(RequestBody MapString, String params) { String code params.get(code); String url String.format( https://api.weixin.qq.com/sns/jscode2session?appid%ssecret%sjs_code%sgrant_typeauthorization_code, appid, secret, code); RestTemplate restTemplate new RestTemplate(); String response restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(response); String openid json.getString(openid); User user userMapper.selectByOpenid(openid); if (user null) { user new User(openid); userMapper.insert(user); } String token UUID.randomUUID().toString().replace(-, ); jedisUtil.set(token, user.getId().toString(), 7 * 24 * 3600); MapString, Object result new HashMap(); result.put(token, token); result.put(userInfo, user); return Result.success(result); }这段代码把微信登录的完整链路串起来了。向微信接口发请求用的是RestTemplate拿到openid后先查库查不到就插入新用户这属于「首次登录自动注册」的通用做法。token 用 UUID 生成存到 Redis 并带上 7 天过期时间Redis 的键就是 token值就是用户 ID。后续每个需要登录的接口都从请求头里取 token再查 Redis 得到用户 ID实现了无状态鉴权。Redis 的引入让 SSM 项目在登录态管理上没有直接查数据库性能上比纯表查询好很多。3.3 解决「修改刚进入的加载页面」和「顶部导航栏高度」的适配问题热词里提到的「修改刚进入的加载页面」在小程序里指app.json中window节点的navigationBarTitleText以及在pages/index/index的onLoad里做的数据预加载。如果你的版本在小程序启动时闪了一下白屏或者加载顺序不对常见调整集中在以下三处{ window: { navigationBarTitleText: 在线课堂, navigationBarBackgroundColor: #4A90D9, navigationBarTextStyle: white } }「顶部导航栏高度」问题在 Android 和 iOS 上表现不同Android 状态栏高度通常是 24pxiPhone X 及以上是 44px。小程序自定义导航栏时常用的适配方式是获取系统信息后动态计算占位高度const systemInfo wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); Page({ data: { navHeight: (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height } })这里的计算逻辑是胶囊按钮顶部到状态栏底部的距离加上胶囊按钮自身高度就是导航栏总高度。拿到navHeight后动态设置占位 view 的样式就能适配所有机型的顶部布局。这是小程序开发里一个非常高频的适配问题尤其是使用自定义导航栏把navigationStyle设为custom时几乎必须写这一段。4. 视频点播、讨论区与作业提交三个典型业务场景的实现与坑点前两章解决了「怎么连起来」的问题这章要回答「业务怎么做」。视频点播、讨论区、作业提交这三个模块其实现思路分别代表了文件播放、内容管理、上传下载三类基础能力在同类课设或小型项目中非常典型。4.1 视频点播与学习进度记录视频点播在小程序端用video组件配合src属性即可播放。但要注意这个项目没有做视频转码或加密直接存放 mp4 文件这在课设中是够用的。真正有技术含量的是学习进度的记录方式。video idcourseVideo src{{videoUrl}} bindtimeupdateonTimeUpdate bindendedonVideoEnded initial-time{{lastProgress}}/videobindtimeupdate事件在播放过程中会高频触发如果每触发一次就上报后端会产生大量无效请求。项目里的做法是前端做节流每 10 秒记录一次当前播放时间只有在进度变化超过 5% 时才调用上报接口。onTimeUpdate(e) { const current e.detail.currentTime; const duration e.detail.duration; const progress Math.floor(current / duration * 100); if (Math.abs(progress - this.lastReportedProgress) 5) { post(/api/learning/record, { courseId: this.data.courseId, videoId: this.data.videoId, progress: progress }); this.lastReportedProgress progress; } }后端收到进度后执行INSERT INTO learning_record ... ON DUPLICATE KEY UPDATE progress VALUES(progress)这一条 SQL 同时完成了第一次记录和后续进度更新省去了一次查询判断的交互。你可能会问为什么用 MySQL 而不用 Redis 存进度——因为 Redis 里的数据最终也要定期刷回数据库对于课设项目来说直接更新 MySQL 反而更简单也方便论文里写数据库设计章节。4.2 讨论区与 SQL 注入防御讨论区是典型的用户生成内容UGC场景也是网课平台里最活跃的部分。只要实现了发帖和回复就足以撑起一个功能点。这套项目的discussion表设计了course_id字段把讨论和课程绑定content字段存文本内容create_time由数据库自动填充。CREATE TABLE discussion ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, user_id INT NOT NULL, content VARCHAR(500) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_course (course_id) );关键在content字段的写入链路。如果用字符串拼接 SQL 的方式写入用户输入一段; DROP TABLE user; --就可以直接毁掉整个系统。这个项目的 Mapper 层使用#{}占位符MyBatis 会将其转换为预编译的?从根本上杜绝了注入问题。如果你在自己的项目里用的是 JDBC 的Statement请换成PreparedStatement这是底线要求。4.3 作业提交与微信小程序文件下载作业提交模块的前端界面通常是表单 文件选择后端接收文件并保存。文件保存路径在 Windows 和 Linux 上有一处容易踩坑的点路径分隔符。压缩包里出现了main.css.bak这类备份文件说明项目是在 Windows 下开发调试的如果直接写成D:/upload/这种绝对路径部署到 Linux 服务器就会报路径不存在。常见做法是把上传路径配置到配置文件里并用File.separator拼接。后端接收文件的接口长这样PostMapping(/homework/submit) public ResultString submitHomework( RequestParam(file) MultipartFile file, RequestParam(homeworkId) Integer homeworkId, RequestParam(userId) Integer userId) throws IOException { String originalFilename file.getOriginalFilename(); String suffix originalFilename.substring(originalFilename.lastIndexOf(.)); String newFileName UUID.randomUUID().toString().replace(-, ) suffix; String uploadDir uploadConfig.getPath(); File dir new File(uploadDir); if (!dir.exists()) { dir.mkdirs(); } file.transferTo(new File(uploadDir File.separator newFileName)); homeworkSubmitMapper.insert(homeworkId, userId, newFileName, 0); return Result.success(newFileName); }这段代码做了三件事生成不重复的文件名、确保目录存在、保存文件并写入提交记录。file.transferTo是 Spring 封装好的保存方法底层依赖 Servlet 容器的临时文件机制。文件名用 UUID 重命名小心不要直接用用户上传的原始文件名否则多个用户提交同名homework.docx时会互相覆盖。这个步骤在压缩包里以本地 Windows 路径保存部署时一般要把upload.path改成/data/online-class/upload/之类的 Linux 路径。小程序端下载作业附件时通常先通过后端接口拿到文件 URL再用wx.downloadFile下载到本地临时路径最后用wx.openDocument打开预览。如果遇到 iOS 上预览失败大概率是文件后缀与 MIME 类型不匹配需要注意在服务端设置Content-Type响应头。提示这个项目的文件存取路径如果被小程序端直接展示为http://localhost:8080/upload/xxx.pdf在真机预览时一定会加载失败。模拟器上 localhost 指向电脑真机上 localhost 指向手机自身。建议把 base URL 改成局域网 IP 或已备案的线上域名。5. 从1-install.bat到部署离线环境下的环境还原与启动排错接下来看工程附带的那三个.bat。1-install.bat、2-run.bat、3-build.bat是整个项目在 Windows 环境下的手工部署链路。在线安装依赖可能因为网络原因失败而.bat脚本处理的是离线还原场景。5.1 三个.bat脚本分别做了什么# 1-install.bat安装依赖和初始化数据库 echo off echo [1/3] Installing Maven dependencies... call mvn clean install -Dmaven.test.skiptrue echo [2/3] Initializing database... mysql -uroot -p123456 sql/init.sql echo [3/3] Installation completed. pause# 2-run.bat启动后端服务 echo off echo Starting OnlineClass Server... call mvn spring-boot:run pause# 3-build.bat生产环境打包 echo off echo Packaging project... call mvn clean package -Dmaven.test.skiptrue -Pprod echo Package generated at target/online-class.war pause三个脚本对应阶段不同1-install做了 Maven 依赖安装和数据库初始化2-run用开发模式直接跑起服务3-build构建生产包。你在实际使用时如果1-install.bat执行后控制台显示BUILD FAILURE大多数是 Maven 仓库里缺少依赖且网络不通这时需要配置阿里云镜像仓库也就是在settings.xml里加入镜像地址后重试。数据库初始化时如果提示Access denied多半是脚本里的账号密码和你的本地 MySQL 不一致把-p123456改成自己的密码即可。5.2 微信开发者工具里的调试与真机预览后端跑起来后小程序端要确认三个配置项否则无法完成一次完整联调。在request.js里把BASE_URL改成你的后端实际地址。如果使用微信开发者工具且勾选了「不校验合法域名」http://localhost:8080可以正常工作真机预览时这个地址不可达必须改为电脑在局域网中的 IP例如http://192.168.1.100:8080同时后端防火墙放行 8080 端口。小程序真机预览还要求 HTTPS 和备案域名开发阶段可以在开发者工具的「详情 → 本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。5.3 抓到小程序请求快速还原出后端接口文档如果你拿到的源码没有完整的接口文档但项目可以正常运行那可以从小程序端反推后端接口的签名。先用 Charles 或微信开发者工具自带的 Network 面板抓包看request.js封装的请求路径和参数名再对照后端Controller的RequestMapping生成接口列表。抓包时确保请求发到本机不要开启代理混淆。比如课程列表接口被封装为/api/course/list参数是pageNum、pageSize、keyword、categoryId那你基本可以确定CourseController里对应方法的形参名就是这四个。整理成一张二三十行的接口表改写成论文里的「系统接口设计」章节就能直接用了。5.4 处理.bak文件与恢复备份逻辑压缩包中大量.bak后缀文件其实是上一版代码的备份它们与被替换的源文件并存。为了不干扰编译第一件事是识别哪些.bak需要恢复、哪些可以直接删。通常.vue.bak或.css.bak的前一版内容如果包含update-password、IndexAsideStatic这类独立功能命名就说明它们是旧版前端页面的备份恢复后可以查看页面结构的演变过程。删除时用del /s *.bak文件路径下全部清掉但建议先看一眼内容再删曾经有同学把唯一的可用版本留在.bak后缀下源文件反而被覆盖最后只能从.bak里找回来。6. 把「课设级别」改造成「简历可用」安全、缓存与拆分思路最后这一步是针对希望把这个项目写进简历的读者。课设项目通常默认在内网运行暴露到公网后会出现三类问题爆破登录接口、没有权限控制、文件上传路径可预测。既然源码已经到手动手改这三点即可有效提升项目质量。6.1 给接口加上RequireLogin拦截逻辑很多课设接口不在后端做登录校验直接裸奔任何人拿到接口地址就能查全部数据。引入拦截器之后未登录请求直接返回 401小程序端也会被统一踢回登录页。public class LoginInterceptor implements HandlerInterceptor { Autowired private JedisUtil jedisUtil; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (token null || !jedisUtil.exists(token)) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\not login\}); return false; } return true; } }注册到 Spring MVC 时用addPathPatterns(/api/**)拦截所有业务接口再用excludePathPatterns(/api/wx/login)放行登录接口。这个改造在代码层面只有十几行但面试时可以很自然地说出「我为所有接口加了登录拦截器使用 Redis 做了 token 鉴权」。6.2 用 Spring Cache 给课程列表降热点课程列表通常是整个系统里访问频率最高的接口商品、视频、文章类应用全都适用。给CourseService加一个查询缓存Redis 有值则直接返回无值则查库后写入可以显著降低数据库压力。Service public class CourseServiceImpl implements CourseService { Autowired private StringRedisTemplate redisTemplate; public PageResultCourse queryPage(int pageNum, int pageSize, String keyword, Integer categoryId) { String cacheKey course:page: pageNum : pageSize : keyword : categoryId; String cached redisTemplate.opsForValue().get(cacheKey); if (cached ! null) { return JSON.parseObject(cached, PageResult.class); } PageResultCourse result courseMapper.queryPage(pageNum, pageSize, keyword, categoryId); redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(result), 10, TimeUnit.MINUTES); return result; } }缓存 key 直接拼接了分页参数和查询条件意味着不同筛选条件各自有独立缓存逻辑虽然粗暴但在数据量不大的场景下完全够用。加缓存后会出现旧数据问题——课程更新后缓存不刷新。一个简单的兜底方案是把课程更新的接口里同时删除对应缓存 key改成redisTemplate.delete(course:page:*)用通配符删除所有课程列表缓存或者干脆把过期时间设成 5 到 10 分钟让缓存自然失效。6.3 文件访问鉴权避免上传目录变公共网盘压缩包里附带了一堆.bak文件这个细节很能说明问题项目默认没有区分「公开资源」和「私有资源」。课程封面和视频预览图是公开资源但作业附件必须鉴权后才有权访问。最简单的做法是把作业附件移到upload/private/子目录然后用一个FileController统一提供带 token 校验的文件下载接口。这样目录结构清晰了也能收掉一个明显的信息泄露面。改造完成后再重新跑一遍3-build.bat用mvn clean package打出一份新的 war 包你的本地环境就完整恢复并升级了。之后再跑小程序端按「登录 → 浏览课程 → 打开视频 → 提交作业 → 查看讨论」的顺序完整过一遍这套源码就在你手里真正跑通了。本文还有配套的精品资源点击获取
返回列表