网上在线学习平台Java源码:Spring Boot+MySQL部署实战与避坑指南
发布时间:2026/10/30 0:25:43 锦皓数字建站

简介基于Java的网上在线学习平台设计实现项目适合毕业设计、课程设计及初阶Java Web开发者参考系统实现了学生注册与实名认证、教师作业发布与批改、学习资源管理、个人信息管理、资源上传下载等完整功能覆盖学生、教师两类核心用户角色可支撑在线教学场景的基本业务闭环。压缩包内共2000个文件包括182个Java源码、149个JSP页面、311个JS脚本、240个CSS样式以及大量PNG/JPG/GIF图片素材和SQL数据库脚本总体积69.85MB另附说明文档与部署文档便于理解项目结构并快速启动。文件覆盖后端逻辑到前端展示的完整层次其中SQL脚本可直接导入MySQL数据库图片与样式资源为界面还原提供了充足素材。项目基于JDK1.8与MySQL5.7兼容Eclipse和IntelliJ IDEA开发演示版本可直观体验前后台完整操作。已有56人浏览学习是掌握在线学习系统LMS从数据库设计到代码实现全过程的实用参考。1. 网上在线学习平台的 Java 源码包跑通它之前先明白这几件事昨天还有做毕设的同学问我手里这套网上在线学习平台的 Java 源代码带前后端页面、MySQL 脚本和说明文档能不能直接照着跑。每年这时候类似的提问我都要回几轮因为这类 zip 包表面看是一个完整项目实际卡人的地方从来不在业务逻辑而在把 MySQL 装对、把库表初始化对、把前后端联调通。所谓“完整前后端 mysql 说明文档 LW”拆开看就是一套 Spring Boot 后端配一套 HTML/JS 页面再用 MySQL 存课程、用户、学习记录这些数据LW 和说明文档用来支撑答辩和二次开发。适合谁正在做毕设的学生、刚入职想找完整案例练手的初级开发以及小团队想快速搭一个教学系统做原型的人。这篇文章按我自己的落地顺序从架构拆解讲到避坑再讲到能带着改上线的部分。2. 先把“完整前后端”拆清楚Java 后端、页面与 MySQL 表结构怎么分工很多同学拿到源码第一反应是找 Vue 或 React 工程结果翻遍目录没找到package.json就开始怀疑发错了包。这里要先纠正一个预期这类教学项目的“完整前后端”通常不是前后端分离而是后端渲染或静态页面混部。理解这一点后面所有配置和部署逻辑才顺。2.1 不是前后端分离先分清静态页面和后端接口的边界常见做法是把页面文件放在src/main/resources/static目录下Java 代码负责提供/api/**接口前端页面通过 Ajax 或 Fetch 调这些接口拿数据。整个工程打成一个 jar 包java -jar起来后浏览器直接访问端口就能看到登录页和课程列表页不需要额外启动前端服务。这里有一个很实用的判断方法解压后看目录里有没有src/main/resources/static/index.html或templates目录。有就是混部模式只有src/main/java和src/main/resources里的application.yml没有任何页面文件才是纯后端。混部模式的好处是部署成本低适合毕设答辩和内部演示环境坏处是如果以后要拆小程序或 App需要把接口层单独抽出来重做一套。对学习平台这个场景混部模式完全够用。2.2 用户、课程、选课记录MySQL 核心表的建表思路数据库设计是这类项目的灵魂比页面样式重要得多。我一般会先建一张用户表一张课程表一张学习记录表。课程表里核心字段是课程名称、封面图路径、讲师、价格、上下架状态学习记录表要记录学员 ID、课程 ID、最后观看位置、总时长、更新时间。下面是一份可以直接用的初始化 SQL 片段CREATE DATABASE IF NOT EXISTS online_learning DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE online_learning; CREATE TABLE user ( id INT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT MD5或BCrypt密文, role TINYINT NOT NULL DEFAULT 0 COMMENT 0-学员 1-教师 2-管理员, created_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB; CREATE TABLE study_progress ( id INT NOT NULL AUTO_INCREMENT, user_id INT NOT NULL, course_id INT NOT NULL, position_second INT NOT NULL DEFAULT 0 COMMENT 观看进度单位秒, duration_second INT NOT NULL DEFAULT 0 COMMENT 视频总时长, updated_at DATETIME NOT NULL, PRIMARY KEY (id), UNIQUE KEY uk_user_course (user_id, course_id) ) ENGINEInnoDB;逻辑说明用户表用唯一索引锁住用户名防止重复注册学习记录表把(user_id, course_id)做成联合唯一键这个唯一键是后面做“断点续看”功能的基础也是防止重复插入学习记录的兜底手段。字符集用 utf8mb4 而不是 utf8是因为课程简介和教师名字里可能出现 emoji 或特殊符号utf8 存 emoji 会直接报错。这里我刻意没建物理外键只用普通索引做逻辑关联。教学项目禁止用物理外键因为代答辩时往表里插测试数据有外键约束只会多出一堆顺序上的麻烦再说 MyBatis 里业务逻辑已经控制了关联关系外键的唯一作用就是给自己添堵。AUTO_INCREMENT 从 1000 开始是我个人习惯方便手工插入一批测试账号时一眼区分。2.3 从登录到课程列表一次请求在后端里走了哪几站弄清楚请求链路是读懂这套源码最快的路径。以课程列表为例浏览器访问index.html页面里的fetch(/api/course/list)会把请求发到 Spring Boot 的CourseControllerController 调CourseMapper的方法Mapper 再拼 SQL 查 MySQL查询结果依次往回传最后在页面渲染成课程卡片。代码一般是这个结构RestController RequestMapping(/api/course) public class CourseController { private final CourseMapper courseMapper; public CourseController(CourseMapper courseMapper) { this.courseMapper courseMapper; } GetMapping(/list) public Result list() { return Result.ok(courseMapper.listOnlineCourses()); } }Mapper public interface CourseMapper { Select(SELECT * FROM course WHERE status 1 ORDER BY created_at DESC) ListCourse listOnlineCourses(); }这里有两个细节值得关注。第一Controller 用构造器注入CourseMapper而不是直接Autowired字段注入这是新版 Spring 更推荐的写法好处是单元测试时容易换 mock 对象也防止出现循环依赖。第二Select注解直接写在 Mapper 接口上属于轻量写法如果源码里用的是 XML mapper不要慌两者只是配置位置不同SQL 逻辑完全可以对照着看。登录链路也类似区别在于登录成功后后端会在 Session 里写入用户 ID后面访问/api/**时由拦截器检查 Session 是否存在。教学项目用 Session 足够生产环境再考虑换 JWT这里不展开。3. 在本地把 MySQL 和 Spring Boot 跑通安装、初始化与启动命令讲完结构接下来是动手环节。这一章的目标只有一个让项目在你自己电脑上跑起来。我会按“装 MySQL → 导入 SQL → 改配置 → 启动”四步走每一步都不绕。3.1 MySQL 5.7/8.0 的安装与最稳的启动方式MySQL 版本选择是个现实问题。我看到网上大量安装教程还在讲 5.7.44但新下的源码很多已经按 MySQL 8.0 的驱动写法来配了。我的建议很简单源码 SQL 脚本里如果没写特别老的语法直接用 MySQL 8.0 的 MSI 安装包一路 Next 就能装好如果你在 Linux 服务器上部署用系统自带的包管理器装 8.0 也一样。Windows 装完后服务名一般叫mysql或MySQL80启动命令是# 启动 MySQL 服务Windows net start mysql # 如果服务名是 MySQL80 net start MySQL80 # 如果启动失败先看错误日志 tail -n 50 C:/ProgramData/MySQL/MySQL Server 8.0/Data/hostname.err参数说明net start后面跟的服务名要和“服务管理器”里显示的名称完全一致大小写不敏感但空格和多出来的数字错了就会报“服务名无效”。MySQL 8.0 安装时如果没有自动初始化数据目录会报“服务无法启动”你需要在安装目录下手动执行mysqld --initialize-insecure这条命令会生成一个 root 空密码的实例等服务起来后再自己ALTER USER改密码。用 Docker 装 MySQL 是另一种常见做法但新手经常在“容器起来了但连不上”这件事上翻车原因多半是端口映射漏了-p 3306:3306或者挂载目录权限不对导致容器反复重启。我的建议是本地调试直接用本机安装版别在这个环节同时引入 Docker 和容器网络两层不确定性。3.2 执行 SQL 脚本导入数据最稳的三行命令源码包里的 SQL 文件一般是online_learning.sql或db/init.sql里面通常包含建库、建表和 INSERT 测试数据。导入前先确认 MySQL 服务已经起来然后打开命令行执行# 进入 MySQL 客户端 mysql -uroot -p # 在客户端里执行注意路径不要带中文 source D:/online_learning.sql; # 查看导入结果 USE online_learning; SHOW TABLES;逻辑说明source是 MySQL 客户端内置命令和mysql file.sql等效但source的好处是执行过程中如果有语法错误会继续往下跑不会像重定向输入那样整体中断。导入完成后重点检查三张表course表有没有课程数据user表有没有测试账号study_progress表是否为空表。如果中文乱码多半是客户端连接字符集不是 utf8mb4在登录命令后面加--default-character-setutf8mb4再重新导入。这一步别跳过测试数据后面联调登录和课程列表都要靠它们撑场面。3.3 改连接配置并启动项目application.yml 里那三处必改项Spring Boot 的数据库连接全在application.yml或application.properties里。你不需要读懂每一行只需要盯住三个地方URL、用户名、密码。改错任何一个启动时都会报连接失败但报错信息不一样可以靠这个快速定位。spring: datasource: url: jdbc:mysql://localhost:3306/online_learning?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver server: port: 8080参数说明useSSLfalse是新手最容易漏的一项MySQL 8.0 默认开 SSL 校验不关掉会报SSL connection error本地开发完全没有加密必要直接关。serverTimezoneAsia/Shanghai不写的话8.0 驱动会把数据库时区当成 UTC查出来的时间比本地时间少 8 小时课程创建时间全错位。driver-class-name在 8.0 驱动下必须写com.mysql.cj.jdbc.Driver写老的com.mysql.jdbc.Driver会直接启动失败。改完后在项目根目录执行mvn spring-boot:run看到Started字样就说明起来了浏览器访问http://localhost:8080能出现登录页就说明前后端资源加载正常。如果mvn命令不存在先检查 JDK 版本这类教学项目多数要求 JDK 8 或 11用 17 以上跑老项目会遇到一堆依赖版本不兼容的怪问题。4. 常见问题排查从 MySQL 启动失败到排序错乱的 5 个案例跑通只是开始联调和测试阶段才会集中踩坑。下面这几条是我在帮人看这类项目时反复见到的每一条都按“现象 → 原因 → 解决”的顺序写可直接对着排查。4.1 net start mysql 提示服务无法启动现象执行net start mysql时提示“服务无法启动”但服务列表里确实有这个服务。打开 Windows 事件查看器能看到 MySQL 相关的报错记录。原因最常见的有三种。第一种是 MySQL 数据目录没有初始化安装过程被中断或手动拷贝过数据文件第二种是my.ini配置文件里的basedir或datadir路径写错MySQL 找不到目录自然起不来第三种是端口被占用常有旧版 MySQL 或 Navicat 自带的 MariaDB 抢占了 3306。解决先看错误日志日志路径在my.ini里datadir指定的目录下文件名类似主机名.err。日志里写Cant start server: Bind on TCP/IP port就是端口冲突用netstat -ano | findstr 3306找到占用进程并结束它。日志里写Cant find error-message file则是路径配置问题逐字符核对basedir和datadir。什么都没写就退出保险做法是备份datadir下所有文件然后删掉目录重跑mysqld --initialize-insecure虽然会丢数据但能让服务先起来。4.2 MySQL 8.0 的 SSL 连接错误现象Spring Boot 启动时报错关键字是SSL connection error或Public Key Retrieval is not allowed。原因MySQL 8.0 默认启用 caching_sha2_password 认证且服务端默认要求 SSL。Java 驱动和它握手的时如果连接串没关 SSL就会卡在密钥交换环节。Public Key Retrieval is not allowed是 8.0 驱动在非 SSL 连接下尝试获取 RSA 公钥时的额外限制。解决连接串里加useSSLfalseallowPublicKeyRetrievaltrue。这两个参数配合使用前者关掉 SSL 加密后者允许客户端获取公钥完成认证。改完后重启项目如果还报错检查驱动包版本是不是mysql-connector-java8.0.x老的 5.1.x 驱动连 8.0 服务端会有兼容问题需要升到 8.x。4.3 课程按价格或时间排序结果不对MySQL 排序与 OR 查询的坑现象课程列表页按“最新”排序但新上架的课程排在老课程后面或者在筛选条件里写WHERE teacher_id 1 OR course_type 必修时结果出现重复行而且排序是乱的。原因第一类问题的根子是 MySQL 把created_at当成了字符串排序。VARCHAR 类型存的日期会按字典序排2024-09-02和2024-09-01比较没问题但一旦混入了2024-9-02这种格式字典序就会出错。第二类问题出在 OR 查询本身MySQL 的 OR 不会自动去重teacher_id 1 OR course_type 必修如果同一门课同时满足两个条件就会出现两行。解决第一类用ORDER BY created_at DESC前提是字段类型必须为 DATETIME如果库里已经存成 VARCHAR先ALTER TABLE course MODIFY created_at DATETIME NOT NULL再排序就稳了。第二类把 OR 改成 UNION或者SELECT DISTINCT。曾经有同学问“MySQL 的 OR 能去重吗”答案是不能去重要靠 DISTINCT 或改写语句。这个坑在面试里也常被问记住结论OR 只负责条件不管去重。4.4 按钮重复提交导致选课记录重复插入现象用户快速点了两次“选课”按钮study_progress表里同一个用户对同一门课出现两条记录更严重的是订单表里出现重复订单金额对不上。原因前端按钮没有防抖后端接口也没有做幂等校验。用户手速快两次 HTTP 请求都到了后端后端每次都执行了 INSERT。解决前端先把按钮置灰这是成本最低的防线function submitEnroll() { const btn document.getElementById(enrollBtn); btn.disabled true; // 立即禁用防止二次点击 fetch(/api/course/enroll, { method: POST }) .then(res res.json()) .then(data { // 业务处理 }) .finally(() { btn.disabled false; // 请求结束后恢复允许重新操作 }); }后端再补一道拦截查询study_progress表里(user_id, course_id)是否已存在存在就返回“已选课”。我在 2.2 里特别强调的联合唯一键uk_user_course在这里就是最后一道保底就算前端后端都没拦住数据库也会把第二条 INSERT 直接拒绝而不是产生脏数据。4.5 导入 SQL 脚本中文乱码现象source导入后前端页面课程名称显示成???或一堆乱码但英文正常。原因SQL 文件本身是 UTF-8 编码但 MySQL 客户端连接时用的默认字符集不是 UTF-8导致数据写入表里时已经被转成了错误编码。这一步数据已经脏了改连接参数只是让显示正常救不回已写入的数据。解决导入前先确认 SQL 文件编码。用 Notepad 或 VS Code 打开文件右下角显示 UTF-8 就对了显示 GB2312 就先另存为 UTF-8。然后在导入命令里强制指定字符集mysql -uroot -p --default-character-setutf8mb4 online_learning.sql如果是已经导入过的脏数据最干净的做法是DROP DATABASE online_learning;再重新建库导入。在导入这种事上不要指望 ALTER TABLE 改 charset 能把已损坏的数据恢复那些???是老早之前就被丢弃的字节改编码救不回来只有重导一条路。5. 从“能跑”到“能答辩/能上线”学习进度、选课与 MySQL 调优项目跑通之后接下来就该考虑功能补全和性能问题了。这一章不讲炫技只讲三个最常被问到的改造点。5.1 给学员端加“我的课程表”一次典型的后端改动原项目里“课程列表”通常是公开的点进去就能看视频。但一个像样的学习平台必须有“我的课程”概念也就是学员选课后才能在“我的课程表”里看到这门课。这个改动需要动三处数据库加选课表、Mapper 加关联查询、页面加一个 Tab。选课表结构不用重新设计直接复用study_progress表因为选课的本质就是“建立用户和课程的关联”。查询某学员的课程表时用 JOIN 把课程信息带出来SELECT c.id, c.title, c.cover_url, c.teacher_name FROM study_progress sp JOIN course c ON sp.course_id c.id WHERE sp.user_id #{userId} ORDER BY sp.updated_at DESC;逻辑说明这个 JOIN 查询的核心是sp.user_id做过滤条件然后用sp.updated_at排序实现“最近在学的课程排前面”。#{userId}是 MyBatis 的占位符写法它会自动做参数转义比拼字符串安全。加完这步后前端“我的课程表”只需要调/api/course/myList接口传当前登录用户 ID 即可。如果我拿到手源码的登录逻辑里有把用户 ID 存 Session接口里就用session.getAttribute(userId)拿不要相信前端传过来的用户 ID这个习惯能从根上防越权访问别人课程表。5.2 用事务和 upsert 保存学习进度避免先查再改学习进度保存是个高频写操作如果写成“先 SELECT 查进度再 UPDATE 更新”高并发下会产生竞态条件而且每次做两次 SQL 查询。更严重的是两个请求同时查到旧值后提交的那个会把先提交的覆盖掉这也就是常说的“更新丢失”。解决这个问题MySQL 有更直接的办法INSERT ... ON DUPLICATE KEY UPDATE也就是 upsert 语法。INSERT INTO study_progress (user_id, course_id, position_second, duration_second, updated_at) VALUES (#{userId}, #{courseId}, #{positionSecond}, #{durationSecond}, NOW()) ON DUPLICATE KEY UPDATE position_second VALUES(position_second), duration_second VALUES(duration_second), updated_at NOW();参数说明ON DUPLICATE KEY UPDATE触发条件是表里有唯一键冲突。我们在 2.2 里定义的uk_user_course (user_id, course_id)在这里起了决定性作用——同一用户同一课程只会保留一行第二次上报走的是 UPDATE 分支而不是新增。VALUES()函数取值来自本次 INSERT 语句这样就把“查一次再改一次”缩成了“一条语句搞定”省了往返也天然处理了并发冲突。这个方法同样适用选课、收藏、点赞这类场景比逻辑代码里if (exists) update else insert简洁得多。至于 MySQL 事务Spring Boot 里只要在 Service 方法上加Transactional注解多条写操作就会自动包进同一个事务里。需要注意的是不要在这个场景里引入存储过程来做判断业务逻辑放在服务层比放在数据库里更容易调试和迁移。5.3 上线前必看的三个 MySQL 参数索引、连接池、慢查询本地跑通和部署上线是两回事。上线前我会固定做三件事检查全表扫描、改连接池参数、开慢查询日志。索引的问题最隐蔽。原项目如果比较小课程表可能连索引都没建数据量到几千条时查询还能忍到几万条时课程列表接口会明显变慢。我一般直接看 Mapper 里 WHERE 和 ORDER BY 涉及的字段为它们补上索引ALTER TABLE course ADD INDEX idx_status_created (status, created_at); ALTER TABLE study_progress ADD INDEX idx_user_course (user_id, course_id);逻辑说明第一条索引同时覆盖“查询上架课程”和“按创建时间排序”两个场景因为索引最左前缀原则status 和 created_at 的顺序不能反第二条索引其实已经被uk_user_course覆盖了这里写它只是为了强调唯一索引也同时是普通索引别建重复索引浪费磁盘。连接池参数里最值得调的是maximum-pool-size教学项目默认 10本地够用部署到服务器后按CPU核心数乘以2再加磁盘数这个经验公式估四核机器我一般设 20 左右。再配合慢查询日志把超过 1 秒的 SQL 捞出来看基本能把 90% 的性能问题定位到位。6. 进阶技巧学习进度断点续看前后端各要改什么很多在线学习平台只做到了“学完打勾”但用户真正的痛点是上一节课看到 23 分钟下次打开得从头拖进度条。断点续看不难难在做得不烦人——既不能每秒钟都往数据库写一条记录也不能完全靠用户手动点“保存进度”。我这里给的是我认为最稳的增量上报方案。前端思路是视频播放过程中每 5 秒采集一次当前播放位置把它保存在一个全局变量里同时向后端上报一次进度。注意上报内容不是“累计看了多久”而是“这次请求时的播放位置”。后端拿到后用 5.2 的 upsert 语句更新study_progress表。用户再次进入课程时前端调进度查询接口拿到position_second值赋给播放器的currentTime就完成了续看。核心代码如下let reportTimer null; let lastPosition 0; video.addEventListener(timeupdate, () { lastPosition Math.floor(video.currentTime); // 只存不报节流 }); video.addEventListener(play, () { reportTimer setInterval(() { fetch(/api/progress/save, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ courseId: currentCourseId, positionSecond: lastPosition }) }).catch(() {}); // 上报失败不打断播放 }, 5000); }); video.addEventListener(pause, () { clearInterval(reportTimer); // 暂停即停避免无效上报 fetch(/api/progress/save, { // 暂停瞬间再补一次 method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ courseId: currentCourseId, positionSecond: lastPosition }) }).catch(() {}); });这段代码里有几个细节是我自己踩过坑后加上的。timeupdate在播放时触发的频率很高每秒钟能触发四到五次不能在里面直接发网络请求不然数据库会被写穿。用setInterval固定 5 秒上报一次是播放器类项目常用的节流手段既能让服务端拿到足够新的进度又把写库频率控制在每分钟 12 次左右。catch(() {})里故意把错误吞掉是保证网络抖动时不会弹错误提示打断用户看视频进度丢 5 秒的数据完全能接受。暂停时补报一次是防止用户看完就关页面、最后一次进度还停留在 5 秒前。课程继续播放时从study_progress表把position_second读出来传给播放器设置currentTime断点续看就闭环了。我第一次做这个功能时图省事用setInterval每秒上报结果结课高峰时 MySQL 的负载直接翻了一倍被运维同事狠批了一顿。后来才换成 5 秒节流加暂停补报的方案写库量降了五倍用户体验反而没差别。“宁可少报不可报死”这个教训我记到现在。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。