ARTICLE DETAIL

资讯详情

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

SSM+MySQL在线课堂微信小程序毕业设计:从架构到避坑全链路

SSM+MySQL在线课堂微信小程序毕业设计:从架构到避坑全链路 简介这份资源是面向高校计算机相关专业毕业设计场景的在线课堂微信小程序完整项目包适合正在准备毕设、需要一套可运行赛题方案的学生与指导教师。项目采用Java SSM框架与MySQL数据库开发前端为微信小程序使用微信开发者工具调试系统划分管理员、教师、学生三类角色覆盖班级、课程、作业、选课、评分、签到、资源、成绩、评价、试题、试卷与考试等管理模块界面清晰、操作简单可作为在线课堂管理规范化与系统化的参考实现。压缩包共1366个文件约62.08MB包含112个java后端源码、126个vue前端组件、154个js脚本、66个wxml与62个wxss小程序页面样式、2个sql数据库脚本以及png、svg、jpg等界面素材和mp4演示视频另附开题报告、论文答辩与毕业论文文档。目前已有20人学习下载读者可据此获得完整源码、数据库、论文材料与演示视频快速理解SSM与小程序协作的开发流程与排错思路。1. 在线课堂微信小程序一套 SSMMySQL 的毕业设计到底要交付什么打开任务书那一刻多数人第一反应是在线课堂四个字太虚——直播录播还是点播真正动手才知道毕业设计级别的在线课堂微信小程序核心不是把腾讯课堂复刻一遍而是把课程展示、章节学习、视频播放、订单支付、学习记录这条主链路跑通后端用 SSMSpring SpringMVC MyBatis扛住接口MySQL 存数据小程序端负责交互。它解决的是学生能选课、能看视频、能查进度管理员能上架课程、管订单、看统计这一整套闭环。适合谁适合计算机毕业设计选题卡在想做点有业务感的项目、又不想碰直播推流这种深水区的同学也适合想拿一套完整 SSM 分层架构练手的 Java 新手。下面按先立架构、再落代码、最后避坑的顺序把这条链路拆开讲透。2. 技术选型与库表设计为什么是 SSM 而不是 SpringBoot2.1 选型理由答辩老师想听的不是最新是讲得清很多同学一上来就想换 SpringBoot觉得 SSM 老。但毕业设计的评分逻辑里架构清晰度 技术新颖度。SSM 的三层结构Controller → Service → Mapper边界分明答辩时你能指着代码说清请求从哪进、业务在哪层、SQL 在哪写这比 SpringBoot 自动装配一堆 starter 却讲不明白强得多。而且 SSM 的 XML 配置虽然啰嗦但每一行都能对应到一个知识点DispatcherServlet 怎么分发、事务怎么配、MyBatis 的 mapper 怎么扫描。这些恰恰是答辩老师爱追问的地方。MySQL 这边选 5.7 还是 8.0 都行但要注意驱动包版本匹配MySQL 8.0 用com.mysql.cj.jdbc.Driver连接串要带时区和 SSL 参数否则启动就报错。这是新手第一个翻车点后面避坑章细说。2.2 核心库表六张表撑起整个业务在线课堂的数据模型不用设计得太花抓住用户、课程、章节、订单、学习记录、管理员六个实体就够。下面是我一般会用的建表结构字段名和类型都经过实际跑通验证。-- 用户表小程序端登录用户 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid唯一标识, nickname VARCHAR(64) DEFAULT NULL, avatar VARCHAR(255) DEFAULT NULL, phone VARCHAR(20) DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 课程表课程主信息 CREATE TABLE t_course ( id INT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, cover VARCHAR(255) COMMENT 封面图URL, price DECIMAL(10,2) DEFAULT 0.00, teacher VARCHAR(64), intro TEXT, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 章节表课程下的视频章节 CREATE TABLE t_chapter ( id INT PRIMARY KEY AUTO_INCREMENT, course_id INT NOT NULL, title VARCHAR(128), video_url VARCHAR(255), duration INT COMMENT 时长秒, sort INT DEFAULT 0, KEY idx_course (course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 订单表 CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, course_id INT NOT NULL, amount DECIMAL(10,2), status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 学习记录表 CREATE TABLE t_study_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, course_id INT NOT NULL, chapter_id INT NOT NULL, progress INT DEFAULT 0 COMMENT 播放进度秒, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_user_chapter (user_id,chapter_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 管理员表 CREATE TABLE t_admin ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL, password VARCHAR(64) NOT NULL COMMENT MD5存储, UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上有几个点值得说t_study_record用user_id chapter_id做唯一键这样续播功能可以直接用INSERT ... ON DUPLICATE KEY UPDATE一条 SQL 搞定不用先查再判断。t_order的order_no单独建唯一索引防止重复下单。字符集统一utf8mb4否则课程标题里带个 emoji 就存不进去。2.3 SSM 分层骨架包结构先定死包结构定不好写到一半就会乱。我一般按这个来com.xxx.onlineclass ├── controller // 接口层只做参数校验和返回 ├── service │ └── impl // 业务逻辑事务加在这层 ├── mapper // MyBatis 接口 ├── entity // 实体类和表一一对应 ├── dto // 前端传参对象 ├── vo // 返回给前端的对象 └── util // 工具类如微信登录、订单号生成Controller 里不写业务Service 里不写 SQLMapper 里不写判断——这条纪律守住代码就不会烂。事务注解Transactional加在 Service 实现类的方法上别加到 Controller否则事务边界会出问题。3. 后端接口实现从微信登录到视频播放的完整链路3.1 微信登录code 换 openid 的正确姿势小程序端调wx.login()拿到临时 code传给后端后端拿 code appid secret 去微信接口换 openid。这一步是整条链路的地基写错了后面全崩。// WxLoginUtil.java public class WxLoginUtil { private static final String APPID 你的appid; private static final String SECRET 你的secret; private static final String URL https://api.weixin.qq.com/sns/jscode2session; public static String getOpenid(String code) throws Exception { // 拼接请求参数grant_type固定为authorization_code String params appid APPID secret SECRET js_code code grant_typeauthorization_code; // 用HttpURLConnection发GET请求避免引入额外依赖 URL url new URL(URL ? params); HttpURLConnection conn (HttpURLConnection) url.openConnection(); conn.setRequestMethod(GET); BufferedReader reader new BufferedReader( new InputStreamReader(conn.getInputStream(), UTF-8)); StringBuilder sb new StringBuilder(); String line; while ((line reader.readLine()) ! null) { sb.append(line); } reader.close(); // 返回JSON用fastjson解析出openid JSONObject json JSON.parseObject(sb.toString()); if (json.containsKey(openid)) { return json.getString(openid); } throw new RuntimeException(微信登录失败 json.getString(errmsg)); } }逻辑说明这段代码做了三件事——拼参数、发请求、解析返回。参数里grant_type固定值不能改js_code就是前端传来的 code且一个 code 只能用一次用完即失效。返回的 JSON 里如果带errcode说明失败常见的是 40029code 无效和 45011频率限制。参数说明APPID和SECRET必须放配置文件别硬编码进代码提交到仓库。SECRET泄露等于别人能冒充你的小程序。生产环境建议加一层缓存同一个 openid 短时间内重复登录直接返回减少对微信接口的调用。拿到 openid 后去t_user表查有就返回用户信息没有就插入一条新记录这就是静默注册。返回给前端的应该是一个自定义的 token比如 JWT 或者简单的 UUID 存 Redis后续接口靠这个 token 识别用户而不是每次都传 openid。3.2 课程列表与详情分页查询和关联查询课程列表接口要支持分页别一次性SELECT *全查出来。MyBatis 配合 PageHelper 是最省事的做法。!-- CourseMapper.xml -- select idselectCoursePage resultTypecom.xxx.entity.Course SELECT id, title, cover, price, teacher, intro, status FROM t_course WHERE status 1 if testkeyword ! null and keyword ! AND title LIKE CONCAT(%, #{keyword}, %) /if ORDER BY create_time DESC /select// CourseServiceImpl.java Override public PageInfoCourse getCourseList(int pageNum, int pageSize, String keyword) { // 开启分页PageHelper会自动拦截下一条SQL加LIMIT PageHelper.startPage(pageNum, pageSize); ListCourse list courseMapper.selectCoursePage(keyword); return new PageInfo(list); }逻辑说明PageHelper.startPage()必须紧挨着查询语句中间不能插入其他数据库操作否则分页会作用到错误的 SQL 上。这是 PageHelper 最经典的坑。返回的PageInfo里带了总条数、总页数、当前页数据前端直接拿来渲染列表和分页器。参数说明pageNum从 1 开始pageSize建议不超过 20小程序端一屏放不下太多。keyword做模糊搜索时注意CONCAT的写法MySQL 里字符串拼接用CONCAT不是。课程详情接口需要关联查章节列表用一次查询搞定别在循环里查数据库N1 问题resultMap idCourseDetailMap typecom.xxx.vo.CourseDetailVO id columnid propertyid/ result columntitle propertytitle/ result columnprice propertyprice/ collection propertychapters ofTypecom.xxx.entity.Chapter id columncid propertyid/ result columnc_title propertytitle/ result columnvideo_url propertyvideoUrl/ result columnduration propertyduration/ /collection /resultMap select idselectCourseDetail resultMapCourseDetailMap SELECT c.id, c.title, c.price, ch.id AS cid, ch.title AS c_title, ch.video_url, ch.duration FROM t_course c LEFT JOIN t_chapter ch ON ch.course_id c.id WHERE c.id #{courseId} ORDER BY ch.sort ASC /select逻辑说明用LEFT JOIN一次查出课程和它所有章节MyBatis 的collection标签自动把结果映射成嵌套对象。ORDER BY ch.sort保证章节按顺序排列。注意别名c_title是为了避免和课程标题的title冲突。参数说明courseId是路径参数Controller 里用PathVariable接收。如果课程没有章节chapters会是空列表而不是 null前端判断length即可。3.3 视频播放与学习记录续播功能怎么做视频文件本身不建议存 MySQL存文件服务器或者对象存储数据库只存 URL。小程序端用video组件播放src绑定后端返回的videoUrl。续播的核心是用户进入章节时先查t_study_record有没有记录有就seek到上次的进度。播放过程中定时上报进度。// StudyRecordServiceImpl.java Override Transactional public void saveProgress(Integer userId, Integer courseId, Integer chapterId, Integer progress) { StudyRecord record new StudyRecord(); record.setUserId(userId); record.setCourseId(courseId); record.setChapterId(chapterId); record.setProgress(progress); // 利用唯一键存在则更新进度不存在则插入 studyRecordMapper.insertOrUpdate(record); }insert idinsertOrUpdate INSERT INTO t_study_record (user_id, course_id, chapter_id, progress) VALUES (#{userId}, #{courseId}, #{chapterId}, #{progress}) ON DUPLICATE KEY UPDATE progress #{progress}, update_time NOW() /insert逻辑说明ON DUPLICATE KEY UPDATE依赖唯一键uk_user_chapter一条 SQL 完成有则更新、无则插入比先SELECT再判断高效得多也避免了并发下的重复插入。参数说明progress单位是秒前端从video的timeupdate事件里拿currentTime取整上报。上报频率别太高建议每 10 秒或暂停时上报一次否则数据库写入压力大。小程序端seek用videoContext.seek(progress)注意要在bindloadedmetadata之后再调否则不生效。4. 小程序端对接请求封装与页面交互4.1 请求封装统一处理 token 和错误小程序端别在每个页面里裸写wx.request封装一层统一加 token、统一处理错误码。// utils/request.js const BASE_URL https://你的域名/api; function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { content-type: application/json, token: wx.getStorageSync(token) || }, success(res) { // 后端统一返回 {code, msg, data} if (res.data.code 200) { resolve(res.data.data); } else if (res.data.code 401) { // token失效重新登录 wx.removeStorageSync(token); wx.showToast({ title: 请重新登录, icon: none }); reject(res.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明用 Promise 包一层页面里就能用async/await写代码清爽。header里带 token后端拦截器统一校验。code 401单独处理清掉本地 token 让用户重新登录。参数说明BASE_URL必须是 HTTPS小程序正式环境不允许 HTTP。开发阶段可以在开发者工具里勾选不校验合法域名但上线前必须配好服务器域名。4.2 课程详情页视频播放与进度上报// pages/course/detail.js const { request } require(../../utils/request); Page({ data: { course: {}, chapters: [], currentChapter: null, lastProgress: 0 }, onLoad(options) { this.courseId options.id; this.loadDetail(); }, async loadDetail() { const data await request({ url: /course/detail/ this.courseId }); this.setData({ course: data, chapters: data.chapters }); }, // 点击章节播放 async playChapter(e) { const chapter e.currentTarget.dataset.chapter; // 查询上次播放进度 const record await request({ url: /study/progress, data: { chapterId: chapter.id } }); this.setData({ currentChapter: chapter, lastProgress: record ? record.progress : 0 }); }, // 视频加载完成后跳转到上次进度 onVideoLoaded() { if (this.data.lastProgress 0) { const ctx wx.createVideoContext(courseVideo); ctx.seek(this.data.lastProgress); } }, // 播放进度变化节流上报 onTimeUpdate(e) { const current Math.floor(e.detail.currentTime); // 每10秒上报一次 if (current - this.lastReport 10) { this.lastReport current; request({ url: /study/save, method: POST, data: { courseId: this.courseId, chapterId: this.data.currentChapter.id, progress: current } }); } } });逻辑说明playChapter先查进度再设置当前章节onVideoLoaded在视频元数据加载后seek到上次位置。onTimeUpdate里做了节流每 10 秒才上报一次避免频繁请求。参数说明wx.createVideoContext(courseVideo)里的 id 要和video组件的id属性一致。seek的单位是秒。lastReport是页面级变量不用放data里因为它不需要驱动视图更新。5. 避坑与排查那些让答辩当场卡壳的问题5.1 现象启动报错 Cannot load driver class: com.mysql.jdbc.Driver原因MySQL 8.0 的驱动类名变了旧版是com.mysql.jdbc.Driver8.0 是com.mysql.cj.jdbc.Driver。同时连接串需要加时区和 SSL 参数。解决把jdbc.properties里的 driver 改成com.mysql.cj.jdbc.DriverURL 改成jdbc.urljdbc:mysql://localhost:3306/online_class?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueserverTimezone不设会报时区错误useSSLfalse避免 SSL 连接警告allowPublicKeyRetrievaltrue解决 8.0 的认证插件问题。5.2 现象PageHelper 分页失效返回了全部数据原因PageHelper.startPage()后面没有紧跟着查询语句中间插了别的数据库操作导致分页参数被消耗到了错误的 SQL 上。解决确保startPage和查询之间没有其他 Mapper 调用。如果确实需要先做别的查询把startPage挪到目标查询的前一行。另外检查 PageHelper 版本和 MyBatis 版本是否兼容版本不匹配也会静默失效。5.3 现象小程序请求后端报 不在以下 request 合法域名列表中原因小程序正式环境要求所有请求域名在后台配置白名单且必须是 HTTPS。解决开发阶段在微信开发者工具详情 → 本地设置里勾选不校验合法域名。上线前登录小程序管理后台在开发 → 开发设置 → 服务器域名里添加你的后端域名。注意域名不能带端口号必须是 443 端口。5.4 现象视频播放到一半卡住或者 seek 不生效原因seek调用时机太早视频元数据还没加载完或者视频文件本身不支持 Range 请求导致无法拖动进度。解决seek必须放在bindloadedmetadata事件之后。视频服务器要支持 HTTP Range 请求Nginx 默认支持但如果用 Java 直接读文件流返回需要手动处理Range头。最简单的办法是把视频放 Nginx 静态目录或者对象存储它们天然支持 Range。5.5 现象订单重复创建用户点两次生成两条订单原因前端没有防重复点击后端没有做幂等控制。解决前端按钮点击后立即置灰请求返回再恢复。后端在创建订单前先查该用户该课程是否已有未支付或已支付订单有就直接返回已有订单。更严谨的做法是用order_no的唯一索引兜底插入冲突时捕获异常返回已有订单。6. 答辩演示的加分技巧用数据说话而不是念 PPT答辩现场老师最烦的是对着 PPT 念本系统采用了 SSM 框架实现了课程管理功能。你要做的是打开浏览器和小程序现场跑一遍完整链路边操作边讲技术点。演示顺序我一般这样安排先打开小程序展示课程列表顺口说一句这里用了 PageHelper 分页每页 10 条点进课程详情章节用 LEFT JOIN 一次查出避免 N1播放视频并拖动进度条续播功能靠唯一键 ON DUPLICATE KEY UPDATE 实现然后切到后台管理页面新增一门课程回到小程序刷新看到新课程前后端分离接口用 token 鉴权。如果老师追问你这个和网上开源的有什么区别别慌挑一个具体点讲深。比如你可以说开源项目大多用 SpringBoot我用 SSM 是因为想把 DispatcherServlet 的请求分发流程讲清楚您看我的 web.xml 里拦截器、编码过滤器、静态资源处理都是手动配的。这一句话就把为什么不用新技术变成了我理解底层原理。还有一个血泪经验答辩前一定要在答辩用的电脑上完整跑一遍别只在自己电脑上测过。环境变量、MySQL 密码、小程序 appid 都可能不一样。我见过太多人现场启动报错满头大汗调了十分钟印象分直接扣光。提前把数据库脚本、配置文件、依赖包都打包好到现场导入就能跑。最后一个习惯代码里关键位置写注释尤其是微信登录、订单创建、进度上报这三处。答辩老师翻代码时看到注释会觉得你思路清晰。注释不用多一行说清为什么这么写就够了。希望帮到你。本文还有配套的精品资源点击获取
返回列表