☰
在线课堂微信小程序毕设开发:SSM+MySQL闭环实现
2026/10/9 20:51:08 网站建设 项目流程

简介:这是一份面向计算机专业毕业设计场景的在线课堂微信小程序完整项目方案,基于微信小程序+SSM+MySQL架构开发,覆盖管理员、教师、学生三类角色,包含课程信息、作业布置与评分、课堂签到、考试管理、课程资源等核心业务,适合需要快速搭建同类系统或参考设计文档的学生使用。压缩包共1137个文件,约81.95MB,文件类型以java后端代码、vue管理端页面、wxml/wxss小程序页面、js逻辑脚本、png/jpg图片素材为主,另含sql数据库脚本、doc文档、mp4演示视频及项目配置文件,结构便于按源码、数据库、文档分类查阅。已有240人学习下载。资源附带开题报告、毕业论文、答辩PPT与操作演示视频,可帮助理解系统架构、数据库设计以及从需求分析到部署演示的完整流程,对准备毕业设计或课程设计答辩具有直接参考价值。

1. 在线课堂微信小程序:为什么这个题目能兼顾工作量与答辩通过率

把课题换成"在线课堂微信小程序"之后,两三周内把前后端和文档全串起来,是我见过最顺的毕业设计路线之一。这个题不大,做下来会发现它正好把微信小程序前端、SSM 后端、MySQL 数据库捏成一个完整闭环:学生端登录、看课程、下单、播放视频,管理端维护课程和订单,开题、论文、答辩、视频演示又互相印证,每块工作量都答得上来。下面按做这套系统的顺序讲:先确认选型,再定数据库和接口,然后走一遍小程序端从登录到播放的链路,最后写部署避坑与答辩演示技巧。适合正在选题、想真正跑通并有底气答辩的同学。

2. 系统骨架与选型理由:小程序+SSM+MySQL 为什么能扛住答辩

2.1 小程序端承担什么:课程浏览、下单、播放的闭环边界

在线课堂的本质是"内容展示—购买—观看",小程序端把这三步全包了。选微信小程序而不是普通网页,有三个很现实的原因:第一,微信授权登录省掉了注册、找回密码这一整块功能,登录链路短,演示时也更流畅;第二,开发者工具免费,真机调试方便,答辩论证性强;第三,video 组件天然支持 mp4 点播,比在网页里拼播放器省事得多。所以这个题目的客户端形态,小程序是最自然的选择。

但小程序也不是没有边界。主包大小限制 2MB,课程视频绝对不能打进包里,必须放在服务器或对象存储上,小程序里只存一个视频地址;直播功能也不要碰,在线课堂毕设里通常做录播回放,直播牵扯到推流、连麦、弹幕一堆额外功能,扩展开来工作量和风险都不可控。管理端一般单独做一个后台页面,用 JSP 或一个简单的后台管理页挂在同一套 SSM 项目里,和用户端共用 Service 和 Mapper,这部分在答辩里属于加分项,不属于必须项。

我把端侧明确分成三块:小程序用户端、后台管理页、后端服务与数据库。用户端页面控制在 8 个以内:首页、分类页、课程列表、课程详情、下单页、我的课程、学习记录、个人中心,页面越少越容易在答辩前把细节打磨完。后台管理页只做课程上架下架、订单查看、轮播图设置这三个功能,够演示就行。

2.2 SSM 后端:配置越透明,答辩越有话说

后端选 SSM(Spring + SpringMVC + MyBatis)而不是 Spring Boot,不是不知道 Boot 省事,而是毕设场景下有更实际的考量。Spring Boot 的自动配置确实舒服,但正因为太自动,配置变成了黑匣子,评委追问"你项目启动时加载了什么""事务拦截是怎么生效的"这类问题时,很容易卡壳。SSM 的 XML 配置和 bean 装配是显式的,每一条都能逐行解释,这反而成了答辩时的保底弹药。

常见做法是把项目搭成单模块 Maven 工程,分 controller、service、mapper、entity、common 五个包。Controller 只做参数接收和结果封装,Service 层放业务规则,Mapper 层只写 SQL,三层分开后,后面写论文的"系统设计"一章几乎可以直接抄自己的代码结构,这是选 SSM 的第二个好处——文档和代码互相印证,工作量不会浪费。

开发效率上的损失我会用一个统一返回体和一个全局异常处理器来补:所有接口返回 {code, msg, data} 三字段结构,业务异常通过 BizException 抛出并由全局处理器统一转成 JSON,前端小程序只管按 code 判断,不用每个接口单独处理错误。环境方面,我一般固定用 JDK 8 配 Tomcat 8.5,这对老框架最稳,数据库驱动和连接池用常规的 MySQL Connector 加 Druid(也可以换成 HikariCP),版本上别追新。

2.3 MySQL 表规划与接口约定:先把数据字典定下来

在线课堂这类系统,表数量控制在 8 张左右最合适,太少撑不起论文里的 ER 图,太多又给自己找麻烦。我常用的核心表是这样的:

表名作用关键字段
admin后台管理员登录id, username, password
user小程序用户id, openid, nickname, avatar, create_time
course_category课程分类id, name, sort
course课程主表id, category_id, title, cover, intro, price, video_url, status
course_order订单表id, order_no, user_id, course_id, amount, status, pay_time
favorite收藏表id, user_id, course_id, create_time
study_record学习记录id, user_id, course_id, last_time, create_time
banner首页轮播图id, image_url, course_id, sort

金额一律用 int 存"分",不要用 double,避免浮点误差,价格展示时再除以 100;所有时间字段用 datetime;状态字段用 tinyint:课程 0 下架 1 上架,订单 0 待支付 1 已支付 2 已取消。接口风格统一为 /api/course/list、/api/order/create 这样的路径,分页参数固定叫 pageNum 和 pageSize,pageSize 服务端封顶 50,避免前端传一个 9999 把接口拖垮。

这里有个容易被忽略的约定:小程序端字段用驼峰,Java 实体也用驼峰,MySQL 表字段用下划线,中间由 MyBatis 的 mapUnderscoreToCamelCase 配置自动转换。这个开关记得在 mybatis-config.xml 里打开,否则 json 序列化时会出现字段兜底为 null 的玄学问题。数据字典的作用不只是开发,它还是论文里"数据库设计"一章的原材料。我习惯在项目目录里放一个 database.md,把建表语句、字段说明、状态枚举都放进去,开题报告和论文的相关章节直接从里面摘,比对着数据库临时回忆字段靠谱得多。

2.4 单模块工程目录与启动顺序:先让项目能跑再写代码

项目骨架决定了后面两个月开发是否顺,我建议一开始就按下面的结构建好:

ProjectRoot/ ├── pom.xml ├── src/main/java/com/example/onclass/ │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ ├── common │ └── config ├── src/main/resources/ │ ├── mapper/*.xml │ ├── mybatis-config.xml │ ├── spring-mvc.xml │ └── jdbc.properties └── src/main/webapp/

先跑通再写代码,顺序是:启动 MySQL 并导入数据库脚本,改 jdbc.properties 里的账号密码,Maven 编译,部署到 Tomcat,最后在微信开发者工具里导入小程序端,把登录接口调通。这里有一个习惯值得养成:每写一个接口,先在浏览器或 Apifox 里验证返回 JSON,再让小程序接。这样出问题时能快速定位是后端问题还是前端问题,能省掉大量前后端互相甩锅的时间。

3. 把数据库和接口先钉死:课程、订单、学习记录的设计与实现

3.1 三张关键表:字段、索引与状态设计

这三张表是整个系统的地基。课程表负责"卖什么",订单表负责"卖出去"的记录,学习记录表负责"买了之后看没看",先建这三张,其他表围绕它们补。

CREATE TABLE `course` ( `id` int NOT NULL AUTO_INCREMENT, `category_id` int NOT NULL DEFAULT 0 COMMENT '课程分类', `title` varchar(100) NOT NULL COMMENT '课程标题', `cover` varchar(255) DEFAULT '' COMMENT '封面图', `intro` text COMMENT '课程简介', `price` int NOT NULL DEFAULT 0 COMMENT '价格,单位分', `video_url` varchar(255) DEFAULT '' COMMENT '视频地址', `student_count` int NOT NULL DEFAULT 0 COMMENT '学习人数', `status` tinyint NOT NULL DEFAULT 1 COMMENT '0下架 1上架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_category` (`category_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';

课程表把录播视频地址直接放在主表里,是毕设常用的简化做法。如果要扩展成多章节课程,可以再加一张 course_section 表,通过 course_id 关联视频和章节名,这是一个很自然的后续扩展点。索引上加 category_id 和 status,是因为列表页最常见的查询条件是"按分类看上架课程",复合查询走这两个索引就够,不需要更多冗余索引。

CREATE TABLE `course_order` ( `id` int NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单号', `user_id` int NOT NULL COMMENT '下单用户', `course_id` int NOT NULL COMMENT '课程', `amount` int NOT NULL COMMENT '实付金额(分)', `status` tinyint NOT NULL DEFAULT 0 COMMENT '0待支付 1已支付 2已取消', `pay_time` datetime DEFAULT NULL COMMENT '支付时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`), KEY `idx_user` (`user_id`), KEY `idx_course` (`course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

订单号的唯一索引是必须的,订单号用时间戳加随机数生成,也可以带用户 id 后四位,确保并发下不重复。user_id 和 course_id 各建普通索引即可。状态字段涉及后续接口判断,在代码里统一用常量类或枚举值,不要散落在业务代码里直接写数字。

CREATE TABLE `study_record` ( `id` int NOT NULL AUTO_INCREMENT, `user_id` int NOT NULL, `course_id` int NOT NULL, `total_watch` int NOT NULL DEFAULT 0 COMMENT '累计观看秒数', `last_watch_time` datetime DEFAULT NULL COMMENT '最近观看时间', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_user_course` (`user_id`, `course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学习记录表';

学习记录表用 (user_id, course_id) 做唯一索引,意味着同一用户同一课程只保留一条累计记录,每次打开视频时做累加更新,而不是每次播放都插一条新记录。这样"我的课程"和"继续学习"这类页面查询都非常简单,只需要一张表按 user_id 倒序取。

3.2 课程列表与详情接口:分页参数与返回字段裁剪

课程接口要同时服务列表页和详情页。列表页只需要标题、封面、价格、学习人数,没必要把 intro 和 video_url 这种大字段带出去;详情页才需要完整信息。所以列表接口返回的是裁剪后的 CourseVO,详情接口返回完整字段。

// CourseController.java @RestController @RequestMapping("/api/course") public class CourseController { @Resource private CourseService courseService; @GetMapping("/list") public Result list(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, @RequestParam(required = false) Integer categoryId) { PageHelper.startPage(pageNum, Math.min(pageSize, 50)); List<CourseVO> list = courseService.listOnSale(categoryId); return Result.success(new PageInfo<>(list)); } @GetMapping("/detail") public Result detail(@RequestParam Integer courseId, @RequestParam(required = false) Long userId) { return Result.success(courseService.detail(courseId, userId)); } }

PageHelper 是 MyBatis 最常用的分页插件,startPage 放在查询语句执行前一行即可,它会拦截下一条 SQL 自动拼 limit。pageNum 从 1 开始,pageSize 默认 10,服务端用 Math.min(pageSize, 50) 做上限保护。detail 接口里的 userId 是可选参数,方便详情页同时返回"当前用户是否已购买、是否已收藏"两个布尔字段,这样小程序端不用额外再调两个接口。

对应的 Service 里有一个细节:列表页的 SQL 只查 status=1 的课程,这个过滤条件写在 service 层组装,而不是让前端传 status 参数。前端的 status 属于不可信参数,服务端必须自己定死"仅上架课程可在列表出现",这是接口安全和业务规则的分界。

3.3 下单接口与事务控制:幂等和状态机

下单是系统里唯一需要事务和状态机的场景。下面的代码是 Service 层的核心逻辑,我通常不建议把这段逻辑放到 Controller 里,因为事务注解和业务校验必须在一个事务边界内才可靠。

// OrderService.java @Transactional(rollbackFor = Exception.class) public Long createOrder(OrderCreateDTO dto) { Course course = courseMapper.selectById(dto.getCourseId()); if (course == null || course.getStatus() != 1) { throw new BizException("课程不存在或已下架"); } // 幂等:同一用户对同一门课只允许存在一笔有效订单 int count = orderMapper.countByUserAndCourse(dto.getUserId(), dto.getCourseId()); if (count > 0) { throw new BizException("请勿重复下单"); } CourseOrder order = new CourseOrder(); order.setOrderNo(OrderNoGenerator.create()); order.setUserId(dto.getUserId()); order.setCourseId(course.getId()); order.setAmount(course.getPrice()); // 金额以课程表为准,不信任前端传值 order.setStatus(0); // 0 待支付 orderMapper.insert(order); return order.getId(); }

@Transactional 保证"校验课程、查重、插入订单"要么全成功要么全失败,不会出现订单插进去了但课程被下架这种半截状态。金额取课程表的 price 而不是前端传来的 amount,前端传任何价格都不可信。幂等判断用 countByUserAndCourse,配合 (user_id, course_id) 的普通索引即可,毕设场景下已经够用。

下单之后是模拟支付。真实微信支付需要商户号、证书和回调地址,学生项目和毕设场景拿不到这些资质,所以常规做法是提供一个 /api/pay/simulate 接口,接收 orderId,校验订单属于当前用户且状态为 0,然后把状态改成 1 并写入 pay_time。答辩时被问到支付,直接说明这是模拟支付,重点讲订单的状态机:0 待支付、1 已支付、2 已取消。支付成功的后续动作也在这一个事务里完成:把课程的 student_count 加一,同时插入一条 study_record,保证"购买成功"和"可以开始学习"是一致的。

4. 小程序端从登录到播放:核心链路的三段代码

4.1 登录态与 token:wx.login 到后端 code2Session

小程序端的登录和传统密码登录不一样,它不需要用户输入任何账号密码。流程是:小程序调 wx.login() 拿到一个临时的 code,把 code 传给后端;后端拿这个 code 加上自己的 appid 和 secret,去微信的 jscode2session 接口换 openid 和 session_key;openid 就是用户的唯一标识,后端拿它查 user 表,查到就返回 token,查不到就先自动注册再返回 token。

// utils/request.js const BASE_URL = 'http://127.0.0.1:8080/api'; // 本地联调地址,真机调试需替换 function request(path, method = 'GET', data = {}) { return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + path, method, data, header: { 'Content-Type': 'application/json', 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.data.code === 200) { resolve(res.data.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 };

所有请求都走这个封装,好处是 header 里自动带上 token,接口返回统一结构 code/msg/data,小程序端不用在每一个页面里重复写错误弹窗。BASE_URL 在本地联调时可以直接填后端 IP,但注意这是开发期做法,真机预览时 127.0.0.1 指向的是手机自己,必须换成电脑的局域网 IP,或者在工具里勾选不校验域名(后面避坑章节会细说)。

登录页面拿到 code 后调用后端接口:

// pages/login/login.js const { request } = require('../../utils/request'); Page({ async onLoad() { const { code } = await wx.login(); // 1. 微信临时凭证 const res = await request('/auth/login', 'POST', { code }); wx.setStorageSync('token', res.token); // 2. 后端签发的自定义 token wx.setStorageSync('userInfo', res.userInfo); wx.switchTab({ url: '/pages/index/index' }); } });

后端 LoginController 拿到 code 后,用 HttpClient 调用微信接口换 openid(或者在后端引入封装好的工具类)。本地如果配了测试号,这一步填真实的 appid 和 secret 就能通;如果暂时不想暴露 secret,可以先写一个 MockAuthService 返回写死的 openid,把登录链路跑通后再替换成真实实现。答辩时如实说即可,重点是这整个链路你在开发期是理解并走通了的。

4.2 课程列表与详情:wx.request 封装与数据绑定

列表页的关键代码集中在加载函数和触底分页。下面的代码省略了模板部分,只保留跟数据流相关的部分,思路是:onLoad 请求第一页,onReachBottom 请求下一页,setData 做增量追加。

// pages/course/list.js const { request } = require('../../utils/request'); Page({ data: { list: [], pageNum: 1, pageSize: 10, hasMore: true }, onLoad() { this.loadCourses(); }, async loadCourses() { if (!this.data.hasMore) return; const data = await request('/course/list?pageNum=' + this.data.pageNum + '&pageSize=' + this.data.pageSize); this.setData({ list: this.data.list.concat(data.list), hasMore: data.list.length === this.data.pageSize, pageNum: this.data.pageNum + 1 }); }, onReachBottom() { this.loadCourses(); } });

hasMore 的判断依据是"本次返回的条数是否等于 pageSize",等于说明可能还有下一页,小于说明到底了。这个判断在数据和 pageSize 稳定的前提下是成立的,但如果服务端分页逻辑有 bug,会出现反复请求同一页的现象,所以调试时先在控制台打印一次返回数据的条数。pageNum 在触底时才加一,避免请求跳过页。

详情页除了要渲染课程信息,还要处理"已购买/未购买"两种状态。页面加载时请求 /course/detail 并带上 courseId 和 userId,返回的 bought 字段直接决定页面展示"开始学习"按钮还是"立即购买"按钮。收藏按钮也用同一份数据初始化,点击时调 /favorite/toggle 并切换按钮状态,不要用本地变量暂存状态,否则页面刷新后状态会丢。

4.3 下单、模拟支付与 video 播放:一个完整闭环

详情页点击"立即购买"后,顺序调用下单接口和模拟支付接口,这是用户端最核心的一条链路:

// pages/course/detail.js async buyCourse() { const orderRes = await request('/order/create', 'POST', { courseId: this.data.course.id }); // 毕设环境不接真实支付,模拟支付直接把订单状态置为已支付 await request('/pay/simulate', 'POST', { orderId: orderRes.id }); await request('/study/start', 'POST', { courseId: this.data.course.id }); wx.showToast({ title: '购买成功', icon: 'success' }); this.setData({ bought: true }); }

三个接口的顺序不能乱:先创建订单,再模拟支付,最后初始化学习记录。study/start 的作用是在学习记录表里插入一条初始记录,这样"我的课程"页面在购买成功后立刻能看到这门课,同时视频播放页可以从记录里读取上次看到的位置。如果中途某个接口失败,用 try/catch 包住并在 catch 里提示用户,不要出现下单成功但页面没有变化的半截状态。

视频播放是购买完成的最终落点。小程序用 video 组件播放 mp4 文件,模板大致如下:

<video wx:if="{{bought}}" src="{{course.videoUrl}}" controls object-fit="contain" autoplay="false" show-center-play-btn="{{true}}"></video>

bought 为 true 时才渲染 video,这样未购买用户拿不到视频地址的下载入口。src 一定是完整的 https 地址,开发时可以临时用 http 并勾选不校验域名,但安卓真机上 http 的 mp4 经常会被拦截,最终演示前把视频放到 https 可访问的地址上最稳妥。video 组件本身支持 controls、object-fit 这些常用属性,不需要引入任何第三方播放器,这也是选微信小程序做在线课堂的另一个省事点。

5. 部署与避坑:本地跑通到答辩演示的 5 个常见坑

5.1 网络请求不通:开发者工具正常但真机白屏

现象:开发者工具里数据和页面都正常,一用手机预览,页面空白,控制台一堆 failed,报的却是同一个请求。

原因:开发者工具默认勾选了"不校验合法域名",所以 localhost 和 http 请求在工具里畅通无阻;真机环境则严格要求 https 且域名要在小程序后台的 request 合法域名列表里。很多同学第一次真机预览都会在这里卡一下。

解决:临时调试时,在开发者工具的详情—本地设置里勾选"不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书",同时把 BASE_URL 从 localhost 改成电脑的局域网 IP,保证手机和电脑在同一网络。正式答辩演示前,如果条件允许,把后端部署到一台有 https 域名的服务器上,并在小程序后台配置 request 合法域名,这样最稳,也最像真实的线上工程。

5.2 Tomcat 启动失败:端口占用与 JDK 版本不匹配

现象:Tomcat 启动到一半报"Port 8080 was already in use",或者项目编译通过但启动时抛 java.lang.NoClassDefFoundError 一类莫名错误。

原因:8080 端口被其他进程占了;另一个隐藏问题是 JDK 版本太新,和一些老 SSM 依赖在反射、模块访问上的兼容性不好,常见于用 JDK 11 及以上跑 JDK 8 时代的项目。

解决:Windows 上执行下面的命令找到占用进程再结束它,macOS 上用 lsof -i:8080。

# Windows netstat -ano | findstr 8080 taskkill /PID 进程号 /F # macOS / Linux lsof -i:8080

如果端口没被占而项目启动类异常,直接把环境固定为 JDK 8 + Tomcat 8.5 的组合,这是我在多台机器上验证过最不容易出岔子的搭配。改完 Tomcat 端口后,记得小程序端的 BASE_URL 也要同步改,否则会出现"服务端能访问,小程序一直转圈"的尴尬局面。

5.3 中文乱码:数据库、连接串、页面三级字符集不一致

现象:课程标题里中文全变成问号,英文和数字正常,数据库里直接查也是乱码。

原因:建库时没指定字符集,默认成了 latin1;或者 JDBC 连接串没带字符集参数;或者页面响应头没声明 utf-8。三级里只要有一级不对,中文就在某个环节被转坏。

解决:建库用 DEFAULT CHARSET utf8mb4,确认库和表字符集可以用下面的 SQL:

SHOW CREATE TABLE course;

如果看到 charset=latin1,就是建表时用的默认字符集不对,建表语句要显式加DEFAULT CHARSET=utf8mb4。JDBC 连接串后面拼useUnicode=true&characterEncoding=utf8,后端接口统一在响应的 Content-Type 里带 charset=utf-8。已经乱掉的数据不要试图在库里修复,删掉重新插入,否则可能在缝隙里残留错码,排查起来折腾一晚上。

5.4 Maven 依赖下载慢或失败:仓库镜像的配置问题

现象:build 时卡在 downloading,等了很久最后报 Could not resolve dependencies,或者个别 jar 一直下不下来。

原因:默认的中央仓库在国外,网络不稳定时下载会失败。有些同学在 settings.xml 里写了多个镜像,结果镜像之间互相覆盖,反而更慢。

解决:只保留一个镜像配置,配到 Maven 安装目录下 conf/settings.xml 的 mirror 节点里,mirrorOf 写 central,表示只对中央仓库生效,其他仓库不受影响。参考配置:

<!-- 只保留一个 mirror,mirrorOf 写 central,避免多个镜像互相覆盖 --> <mirror> <id>mirror</id> <mirrorOf>central</mirrorOf> <url>https://你的镜像站地址/repository/public</url> </mirror>

改完 settings.xml 记得在 IDE 里检查 Maven 配置指向的是同一份 settings,否则改了等于没改。另外,项目一直卡在某个 jar 下载时,先把本地仓库里对应的 .lastUpdated 文件删掉再重试,避免 Maven 以为是新下载。

注意:如果本地仓库里已经有一份损坏的 jar,即使镜像配置正确,Maven 也可能优先使用旧文件。遇到诡异报错时,把本地仓库对应目录整个删掉再重新下载,是最快的出路。

5.5 视频播放只有声音没有画面:MP4 编码与 URL 协议问题

现象:video 组件能播放声音,但画面黑屏,进度条正常走动。

原因:最常见是编码问题,小程序对视频编码兼容性最好的是 H.264 视频编码 + AAC 音频编码,如果素材是用其他编码格式(比如部分软件默认输出的 HEVC/H.265)压制,播放时就会出现只出声不出画。另一个原因是地址用的 http,在部分安卓机型上会被拦截。

解决:用 ffmpeg 或格式转换工具把视频统一转成 H.264+AAC 的 MP4,ffmpeg 命令行参考:

ffmpeg -i input.mp4 -c:v libx264 -c:a aac -movflags +faststart output.mp4

转完再看一眼编码信息确认。-movflags +faststart让视频元数据挪到文件头部,mp4 在网页和播放器里可以边下边播,体验会好很多。地址统一用 https。视频转码这件事看起来是小事,但几乎每年都有人踩,而且现象极其迷惑,属于那种排查半天发现是编码格式的经典翻车现场。

6. 答辩演示与验证:让评委十分钟看懂你的在线课堂

6.1 演示顺序:按业务链路讲故事,不按功能清单报菜名

答辩演示最容易犯的错是打开小程序后一个页面一个页面点过去,评委听完不知道业务是怎么闭环的。我建议按"用户的一天"来讲:打开小程序,微信授权登录,看到首页轮播和课程推荐,点进一门课看详情,下单,模拟支付,立刻能看视频。这条链路正好把登录、接口、事务、状态机、数据表全串起来,每个环节都能引出你设计上的取舍。演示前把这条链路完整走三遍,确认断网、清缓存、切换账号都不会出岔子,最好准备一台备用手机,避免现场真机预览出问题。

6.2 画一张能讲清楚"我做了什么"的架构图

不用画复杂的分层架构图,画一张"一次购买请求"的时序图就够了:用户在小程序点购买,小程序发起 POST /api/order/create,后端 Controller 接收参数,Service 校验课程和重复订单,Mapper 执行 insert,MySQL 返回主键,再调 /api/pay/simulate 改状态,最后返回页面刷新。每个箭头旁边标上接口名和表名,评委一眼就能看到前后端、数据库三层是通的。这张图画好了,比念十页论文都有说服力。

6.3 三个必答问题:SSM 选型、模拟支付、表设计逻辑

评委大概率会问三个方向:为什么不用更轻的框架,支付为什么是模拟的,表结构为什么这么设计。回答思路分别是:SSM 的配置显式、分层清晰,能把框架脉络讲清楚,适合体现对 Java Web 体系的理解;模拟支付是因为真实支付需要商户资质,毕设重点在订单状态流转和事务一致性上,并主动说清楚如果接真实支付,回调接口应该怎么设计;表设计上,课程表直接带 video_url 是为了控制毕设范围,扩展章节时可以加 section 表,学习记录表用唯一索引支撑"继续学习"功能。

我自己做这个题时的教训是:答辩被追问到模拟支付的退单场景时,我一时答不上来,后来把订单状态机完整画出来,讲清楚退款是一个从已支付流转到已取消的过程,评委反而认可了思路。诚实说明边界、把方案讲完整,比强行说"都做完了"效果好得多。这篇关于在线课堂微信小程序的拆解就写到这里,希望帮到你。

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

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

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

立即咨询