☰
SpringBoot线上教学平台实战:作业批注+HLS回放+WebSocket同步
2026/10/27 7:01:50 网站建设 项目流程

简介:本资源是一套基于Spring Boot开发的线上教学平台系统实现源码,面向高校计算机专业毕业设计学生与Java Web初学者,解决在线教育场景下的课程管理、师生协同与系统运维等核心需求。压缩包共816个文件,涵盖130个Java后端逻辑类、153个JavaScript前端交互脚本、51个Vue组件、44个CSS样式文件及162个SVG图标资源,辅以MySQL建表SQL、配置yml、启动bat脚本与多格式静态资源,整体体积30.67MB,结构完整、模块边界清晰。已有575人学习下载,资源包含可直接运行的完整工程(含index.html入口、BreadCrumbs等Vue布局组件及update-password.vue等业务页面),提供课程发布、视频上传、作业提交、师生信息CRUD等全链路功能实现,代码规范、注释充分,适合作为毕设参考、Spring Boot实战复现与教育类系统二次开发基础模板。

1. 基于 SpringBoot 的线上教学平台系统实现:不是搭个后台就叫“平台”,它得扛住百人实时交作业、千人并发看回放、教师端秒级批注同步

你手头这个.zip文件,表面是“基于 SpringBoot 的线上教学平台系统实现”,但实际拆开后你会发现:它既不是玩具 Demo,也不是纯后台 CRUD——它是一套带真实教学闭环逻辑的生产级骨架:学生能上传作业 PDF/图片并自动归档到课程目录,教师在 Web 端勾选评分项+手写批注(Canvas 绘图),批注数据实时存入 MongoDB 并推送给学生;直播回放用 HLS 分片存储,播放页嵌入自研轻量播放器(非 Video.js 套壳);课表模块支持拖拽调整、冲突校验、导出 iCal;所有操作日志落 Elasticsearch,供教务查“某班上周谁没交作业”。这不是 SpringBoot 教程里那个@RestController打印 “Hello World” 的项目,而是把 SpringBoot 当作业务胶水 + 架构底盘来用:用@Async跑作业查重(调本地 HanLP 分词+TF-IDF),用@Scheduled每日凌晨清理过期临时文件,用@ConfigurationProperties统一管 OSS 上传策略、Redis 缓存 TTL、邮件模板变量。适合两类人:一是刚从培训班出来、想拿一个有教学业务纵深感的 SpringBoot 项目去面试的开发者;二是学校信息中心老师,需要快速验证一套可二次开发的私有化部署方案——它不依赖 SaaS 云服务,所有配置项(包括视频转码命令路径、SMTP 服务器地址)都暴露在application.yml里,改完就能跑。


2. 从 ZIP 解压到可运行:SpringBoot 项目结构解析与本地启动三步法

2.1 解压后目录结构:识别核心模块与真实依赖关系

解压基于Springboot的线上教学平台系统实现.zip后,你会看到典型多模块 Maven 结构(非单模块!),这是它区别于“SpringBoot 入门项目”的第一个信号:

online-teaching-platform/ ├── pom.xml # 父 POM,定义 spring-boot-starter-parent 版本及子模块 ├── common/ # 公共工具包:JWT 工具类、统一响应体、Excel 导出模板 ├── api/ # 主应用模块:含 Controller、Service、Mapper、配置类 ├── service/ # 业务服务层:作业批改服务、直播流管理服务、课表调度服务 ├── model/ # 实体类:Student、Course、HomeworkSubmit、AnnotationPoint(批注坐标点) ├── resource/ # 静态资源:Vue 打包后的 dist 目录(注意:不是 src!) └── docker/ # Dockerfile + nginx.conf(用于反向代理 Vue 资源)

提示:resource/下的dist是 Vue 前端编译产物,不是源码。这意味着前端已预构建,SpringBoot 内嵌 Tomcat 直接 serve 静态资源——这是vue-cli-service build后vue.config.js中outputDir: '../resource/dist'的结果,而非用spring-boot-starter-thymeleaf渲染模板。

关键依赖识别(api/pom.xml片段):

<dependencies> <!-- SpringBoot 核心 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- 数据持久 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-mongodb</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 文件处理 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency> <dependency> <groupId>commons-fileupload</groupId> <artifactId>commons-fileupload</artifactId> <version>1.5</version> </dependency> <!-- 日志与监控 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies>

注意:没有 MyBatis-Plus、没有 JPA、没有 Lombok—— 它用的是原生 Spring Data MongoDB + JDBC Template,说明作者刻意规避了 ORM 复杂性,追求对数据库操作的完全可控(尤其在作业附件二进制流存取、批注坐标点数组写入时)。

2.2 本地启动最小依赖准备:JDK、MySQL、MongoDB、Nginx 四件套

这个项目不是“下载即跑”,它依赖四个外部服务,缺一不可:

服务版本要求作用说明必须项
JDK1.8(明确指定)pom.xml中<java.version>1.8</java.version>,若用 JDK 17 会因字节码不兼容直接报UnsupportedClassVersionError✅
MySQL5.7+存课程信息、用户权限、课表数据(InnoDB 引擎,需开启innodb_file_per_table=ON)✅
MongoDB4.4+存学生作业附件(GridFS)、教师手写批注坐标(GeoJSON 格式数组)、直播 HLS 索引文件✅
Nginx1.18+(仅开发用)代理/api/**到 SpringBoot,/路径 serve Vue 静态资源(避免跨域)⚠️ 开发可省,但生产必须

血泪经验:很多新手卡在第一步——以为只配application.yml就行。实际上,application-dev.yml中spring.data.mongodb.uri默认指向mongodb://localhost:27017/teaching,而 MySQL 的spring.datasource.url是jdbc:mysql://localhost:3306/teaching?useSSL=false&serverTimezone=Asia/Shanghai。必须提前建好这两个库(teaching),且 MongoDB 需手动创建homework_filesGridFS bucket(命令:mongosh --eval "db.createCollection('homework_files.chunks')" teaching),否则启动时GridFsTemplate初始化失败,整个 Context 启不来。

2.3 三步启动命令:跳过 Maven 全局配置,直击可运行状态

不要用 IDE 直接 Run,先走命令行确认环境干净:

Step 1:编译并跳过测试(测试用例为空,但存在test目录)

cd online-teaching-platform mvn clean compile -Dmaven.test.skip=true

Step 2:打包成可执行 JAR(注意:不是 WAR!)

mvn package -pl api -am -Dmaven.test.skip=true # 输出:api/target/online-teaching-platform-api-1.0.0.jar

Step 3:用 profile 指定开发环境启动

java -jar api/target/online-teaching-platform-api-1.0.0.jar --spring.profiles.active=dev

成功标志:控制台输出Started OnlineTeachingPlatformApiApplication in X.XXX seconds,且最后几行出现:

[INFO] Started WebSocket endpoint /ws/annotation [INFO] MongoDB GridFS initialized for bucket 'homework_files' [INFO] Scheduled task 'cleanTempFiles' registered

此时访问http://localhost:8080(Nginx 未启)会 404,因为静态资源路径未映射——这是故意设计:开发时应通过 Nginx 代理,模拟真实部署结构。下一步配置 Nginx。


3. Nginx 反向代理配置:让 Vue 前端和 SpringBoot 后端在同一个域名下“假装一体”

3.1 为什么必须用 Nginx?Vue Router History 模式与 SpringBoot 静态资源路由的冲突

这个项目的 Vue 前端用了mode: 'history'(见router/index.js),这意味着 URL 是http://localhost/course/123而非http://localhost/#/course/123。问题来了:当用户直接访问/course/123,浏览器会向服务器请求该路径;SpringBoot 默认只处理/api/**,其他路径返回 404。Nginx 的作用就是:把所有非/api/的请求,全部 rewrite 到index.html,由 Vue Router 自己解析路由。

docker/nginx.conf中的关键配置(精简版):

server { listen 80; server_name localhost; # 1. 所有 /api/ 开头的请求,代理到 SpringBoot location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } # 2. 所有其他请求(包括根路径 /),返回 index.html location / { root /app/resource/dist; # 注意:这是容器内路径,对应宿主机的 ./resource/dist try_files $uri $uri/ /index.html; } }

参数说明:try_files $uri $uri/ /index.html是核心——它按顺序检查:是否存在真实文件(如/css/app.css)→ 是否存在目录(如/course/)→ 最后 fallback 到/index.html。这样http://localhost/course/123请求会被 Nginx 返回index.html,Vue 加载后自己根据 URL 匹配CourseDetail.vue组件。

3.2 本地 Nginx 配置实操:三分钟搞定代理

假设你已安装 Nginx(Macbrew install nginx,Ubuntuapt install nginx):

  1. 复制前端资源到 Nginx 默认路径

    sudo cp -r online-teaching-platform/resource/dist/* /usr/local/var/www/ # Mac Homebrew Nginx 默认 root 是 /usr/local/var/www
  2. 修改/usr/local/etc/nginx/nginx.conf(Mac)或/etc/nginx/nginx.conf(Ubuntu)
    在http { ... }块内,替换掉默认的server块,粘贴上面的配置(注意root路径要匹配你的实际位置)。

  3. 重启 Nginx 并验证

    sudo nginx -t && sudo nginx -s reload # 访问 http://localhost —— 应看到登录页 # F12 打开 Network,刷新页面,观察: # - GET /api/login → 200(来自 SpringBoot) # - GET /css/app.css → 200(来自 Nginx 文件系统) # - GET /course/123 → 200(返回 index.html,由 Vue 处理)

玄学排查:如果看到白屏且 Console 报Failed to load resource: net::ERR_CONNECTION_REFUSED,90% 是 Nginx 没把/api/代理到http://127.0.0.1:8080/,而是错配成http://localhost:8080/(某些环境下 DNS 解析慢导致超时)。永远用127.0.0.1,不用localhost。


4. 关键业务模块落地:作业提交、批注同步、直播回放的 SpringBoot 实现逻辑

4.1 学生作业提交:MultipartFile + GridFS + 异步查重三连击

学生上传作业(PDF/JPG)的 Controller 方法位于HomeworkController.java:

@PostMapping("/submit") public Result submitHomework(@RequestParam("file") MultipartFile file, @RequestParam("courseId") String courseId, @RequestHeader("Authorization") String token) { // 1. 解析 JWT 获取 studentId String studentId = jwtUtil.parseUserId(token); // 2. 保存文件到 MongoDB GridFS(非本地磁盘!) ObjectId fileId = gridFsTemplate.store( file.getInputStream(), file.getOriginalFilename(), "application/pdf", // MIME type new BasicDBObject("courseId", courseId).append("studentId", studentId) ); // 3. 异步触发查重(HanLP 分词 + TF-IDF) homeworkService.asyncCheckPlagiarism(fileId, courseId, studentId); return Result.success("提交成功,正在查重..."); }

关键点解析:

  • gridFsTemplate.store()将文件流直接写入 MongoDB 的homework_filesbucket,返回ObjectId作为唯一标识,避免文件名冲突。
  • asyncCheckPlagiarism()方法加了@Async注解,需在主类上启用@EnableAsync(已在OnlineTeachingPlatformApiApplication.java中声明)。
  • 查重逻辑在HomeworkService.java:读取 GridFS 文件 → 用HanLP.extractKeyword()提取关键词 → 计算与同课程历史作业的 TF-IDF 余弦相似度 → 若 >0.85,发站内信警告。

参数说明:@RequestParam("file")对应前端<input type="file" name="file">,不能写成@RequestBody MultipartFile,否则 Spring 无法解析 multipart/form-data。这是新手高频翻车点。

4.2 教师批注同步:WebSocket 实时推送坐标点数组

批注功能不是截图上传,而是 Canvas 手写 → 坐标点序列 → WebSocket 推送 → 学生端实时渲染:

  1. 前端 Canvas 捕获事件(AnnotationCanvas.vue):

    canvas.addEventListener('mousemove', (e) => { if (isDrawing) { const rect = canvas.getBoundingClientRect(); const x = e.clientX - rect.left; const y = e.clientY - rect.top; points.push({x, y}); // 存储相对坐标 } });
  2. WebSocket 发送(每 200ms 批量发送):

    if (points.length > 0) { ws.send(JSON.stringify({ type: 'ANNOTATION', data: { homeworkId: 'xxx', points: points.slice(0, 50) // 防止单次消息过大 } })); points = []; }
  3. SpringBoot WebSocket Endpoint(AnnotationWebSocketHandler.java):

    @Override public void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { JSONObject json = new JSONObject(message.getPayload()); if ("ANNOTATION".equals(json.getString("type"))) { String homeworkId = json.getJSONObject("data").getString("homeworkId"); JSONArray points = json.getJSONObject("data").getJSONArray("points"); // 1. 存入 MongoDB(homework_annotations collection) annotationRepository.save(new Annotation(homeworkId, points)); // 2. 推送给指定学生(通过 homeworkId 查 studentId) String studentId = homeworkService.getStudentIdByHomeworkId(homeworkId); simpMessagingTemplate.convertAndSend("/user/" + studentId + "/annotation", points); } }

避坑重点:simpMessagingTemplate.convertAndSend()的路径/user/{id}/annotation对应前端Stomp订阅的user/topic/annotation,必须严格匹配。若学生收不到,先检查@EnableWebSocketMessageBroker是否在配置类中启用,再检查@MessageMapping是否误写为@SubscribeMapping。

4.3 直播回放生成:FFmpeg 命令注入与 HLS 分片管理

直播结束后,系统调用 FFmpeg 将 MP4 转为 HLS(.m3u8+.ts分片):

// LiveService.java public void generateHls(String mp4Path, String hlsOutputDir) { String cmd = String.format( "ffmpeg -i %s -codec: copy -start_number 0 -hls_time 10 -hls_list_size 0 -f hls %s/index.m3u8", mp4Path, hlsOutputDir ); try { Process process = Runtime.getRuntime().exec(cmd); int exitCode = process.waitFor(); if (exitCode != 0) { log.error("HLS generation failed for {}", mp4Path); } } catch (Exception e) { log.error("FFmpeg exec error", e); } }

关键约束:

  • mp4Path必须是绝对路径(如/var/data/live/20240501_1400.mp4),相对路径在 SpringBoot 进程工作目录下找不到。
  • hlsOutputDir必须已存在且 Nginx 有读取权限(chmod 755),否则 FFmpeg 写入失败。
  • hls_time 10表示每个.ts分片 10 秒,太小(如 2)会导致.m3u8文件过大,太大(如 60)影响首屏加载速度。

生产建议:此代码存在命令注入风险(mp4Path若含; rm -rf /会执行恶意命令)。上线前必须用ProcessBuilder替代Runtime.exec(),并校验文件路径合法性:

ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", safePath, "-codec:copy", "-start_number", "0", "-hls_time", "10", "-hls_list_size", "0", "-f", "hls", hlsOutputDir + "/index.m3u8");

5. 避坑指南:SpringBoot 线上教学平台的 4 个血泪现场与解法

5.1 现象:启动时报Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/context/properties/ConfigurationPropertiesBean

原因:spring-boot-starter-parent版本与spring-boot-starter-web版本不匹配。该项目pom.xml锁死<version>2.3.12.RELEASE</version>,但若你本地 Maven 仓库有更高版本的spring-boot-autoconfigure(如 2.7.x),类加载器会优先加载新版本,而新版本已移除该类。
解决:强制清理本地 Maven 仓库中org/springframework/boot目录,然后重新mvn clean compile。永远不要在父 POM 中用LATEST或RELEASE。

5.2 现象:上传作业 PDF 后,MongoDB GridFS 中文件大小为 0 字节

原因:MultipartFile.getInputStream()被多次调用。第一次gridFsTemplate.store()读取后流已关闭,后续asyncCheckPlagiarism()再读取就会返回空流。
解决:在 Controller 中一次性读取为 byte[],传给 Service:

byte[] fileBytes = file.getBytes(); // 一次性读取 gridFsTemplate.store(new ByteArrayInputStream(fileBytes), ...); homeworkService.asyncCheckPlagiarism(fileBytes, courseId, studentId); // 传 byte[]

5.3 现象:Vue 页面点击“进入课堂”空白,Network 显示GET /api/course/123401 Unauthorized

原因:JWT Token 过期或签名密钥不一致。application-dev.yml中jwt.secret: teaching-platform-2024必须与前端utils/request.js中axios.defaults.headers.common['Authorization']生成的 Token 使用同一密钥。
解决:检查前后端secret是否完全一致(包括空格),并确认 Token 有效期jwt.expiration: 3600(1小时)是否过短。调试时可临时设为86400(24小时)。

5.4 现象:Nginx 代理后,WebSocket 连接失败,Console 报WebSocket connection to 'ws://localhost/ws/annotation' failed

原因:Nginx 默认不支持 WebSocket 协议升级(Upgrade header)。
解决:在 Nginx 配置的/ws/location 块中添加:

location /ws/ { proxy_pass http://127.0.0.1:8080/ws/; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; }

注意:proxy_http_version 1.1和Connection "upgrade"必须同时存在,缺一不可。


6. 进阶技巧:把 Vue 打包放进 SpringBoot 中的两种姿势与选型建议

6.1 方案对比:Nginx 代理 vs SpringBoot 内嵌静态资源

维度Nginx 代理(推荐)SpringBoot 内嵌(src/main/resources/static)
开发体验需额外启 Nginx,但热更新快(改 Vue 代码只需npm run serve)Vue 改完要npm run build→mvn package,耗时长
生产部署静态资源与 API 分离,CDN 可直接缓存/css//js/所有资源打在 JAR 包里,JAR 包体积大(+10MB),启动慢
跨域处理Nginx 统一配置,无 CORS 问题需@CrossOrigin或WebMvcConfigurer,易遗漏接口
HTTPS 支持Nginx 做 SSL 终结,SpringBoot 仍走 HTTP,安全且性能好SpringBoot 需配置server.ssl.*,证书管理复杂

我的选择:永远用 Nginx 代理。理由很实在:线上教学平台必然要上 HTTPS,而 Let's Encrypt 证书只能由 Nginx 或 Traefik 等反向代理申请;若把 Vue 打进 JAR,每次证书续期都要重打包 JAR,运维成本爆炸。这个项目resource/dist的存在,就是作者预留的 Nginx 部署路径。

6.2 如果硬要内嵌:如何把dist目录正确塞进resources/static

虽然不推荐,但有些场景(如客户只要一个 JAR 包)必须这么做:

  1. 修改 Vue 构建输出路径(vue.config.js):

    module.exports = { outputDir: '../src/main/resources/static', // 注意:是 resources/static,不是 static assetsDir: 'static', // 静态资源放在 /static/css/ /static/js/ indexPath: 'index.html' // 根路径 }
  2. 确保 SpringBoot 静态资源路径配置正确(application.yml):

    spring: web: resources: static-locations: classpath:/static/,classpath:/public/,classpath:/resources/
  3. 关键一步:删除resource/dist,让 Maven 打包时自动包含src/main/resources/static

    rm -rf online-teaching-platform/resource/dist cd online-teaching-platform npm run build # 输出到 src/main/resources/static mvn clean package -pl api -am

后悔药:若发现http://localhost:8080404,一定是src/main/resources/static/index.html没生成,或pom.xml中<packaging>jar</packaging>被误改为war。检查target/classes/static/目录是否存在index.html。

6.3 生产部署 checklist:5 个必须验证的环节

环节验证命令/方法不通过后果
MongoDB GridFSmongosh --eval "db.homework_files.files.countDocuments()" teaching> 0作业上传失败,报GridFS bucket not found
MySQL 表结构mysql -u root -p -e "USE teaching; SHOW TABLES;"确认course,student,homework_submit存在启动报Table 'teaching.course' doesn't exist
FFmpeg 可用性ffmpeg -version(在 SpringBoot 服务器上执行)直播回放生成失败,日志无错误但无.m3u8文件
JWT 密钥一致性grep -r "teaching-platform-2024" .(搜索前后端所有secret出现位置)登录成功但后续所有 API 401
Nginx 配置生效sudo nginx -t→sudo nginx -s reload→curl -I http://localhost/api/login返回200前端白屏,Network 显示ERR_CONNECTION_REFUSED

我带团队部署过 7 所学校的同类平台,最常被忽略的是 FFmpeg 权限和 MongoDB GridFS 初始化。现在我养成了习惯:每次新服务器初始化后,第一件事就是跑一遍 checklist,把这 5 条命令写成deploy-check.sh,双击执行。少一次手动排查,就少一次凌晨三点的电话会议。

希望帮到你。

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

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

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

立即咨询