Spring Boot+MyBatis+MySQL在线教育平台开发实战指南
2026/9/12 22:25:21 网站建设 项目流程

简介:一套基于 Spring Boot + MyBatis + MySQL + HTML 的在线教育平台系统源码,面向 Java 学习者、毕业设计及初级开发人员。平台分前台与后台:前台支持滚动横幅、课程推荐/最新/免费/实战分类、关键字与类别搜索、课程详情、用户登录及在线支付;后台涵盖管理员登录、课程与类别管理、用户与授课老师管理、订单管理、菜单管理、友情链接、系统属性及轮转图片等业务模块,覆盖电商式课程交易完整流程。资源包共 1404 个文件,约 14.38MB,以 Java 源码、HTML、JavaScript、CSS、XML 配置和 SQL 脚本为主,同时包含图片与音视频素材。文件排布清晰体现 Spring Boot 控制层、Service 业务层、MyBatis Mapper 及前端页面分层,便于导入 IDE 后快速联调运行。已有 377 人下载学习,内含 142 个 Java 类与 104 个 HTML 页面,可直接用于理解课程上架、购物车支付、订单状态流转及后台权限控制等典型场景,也可作为课程设计或二次开发的基础工程。

1. 基于 Spring Boot + MyBatis + MySQL + HTML 的在线教育平台,从哪里开始搭

在线教育平台的业务主链路其实不复杂:注册登录、浏览课程、下单支付、解锁视频,后台负责课程上下架。这个模型读多写少、实体关系清晰,放在 Spring Boot + MyBatis + MySQL + HTML 组合里很合适。Spring Boot 快速组装服务端流程,MyBatis 保留手写 SQL 的掌控力,MySQL 存放用户、课程、订单三类数据,HTML 配合 Thymeleaf 在服务端直接渲染页面,省掉前后端分离的联调和跨域处理。对课程设计、技术练手、中小团队做教育产品 MVP 都够用。下面按我的实际开发顺序走:MySQL 表设计先于代码,然后搭 Spring Boot + MyBatis 骨架,完成业务服务和 HTML 渲染,最后处理缓存与排错。

2. 在线教育平台的 MySQL 数据模型:建表与关系设计先行一步

在写任何 Spring Boot 代码以前,应该先定下数据库的表结构。这个顺序对 MyBatis 格外重要,因为 Mapper XML 里的列名、Service 里的实体对象、HTML 页面要展示的字段,都以 MySQL 表结构为准。表设计没想清楚就开启项目,后面每一次改表都牵连 Mapper、Service、模板三层。

2.1 用户、课程、交易、选课四类表如何划分

在线教育平台的最小数据集可以分成四个域:用户域只放 sys_user,课程域放 course、course_section、course_lesson 三张表,交易域放 course_order,再加一张连接用户和课程的 user_course 表。下表是我在这类项目里常用的划分方式:

业务域表名存储内容关键索引
用户域sys_user用户名、密码哈希、角色、注册时间uk_username
课程域course标题、简介、封面、价格、教师ID、上下架状态idx_teacher, idx_status
课程域course_section章节名、所属课程、排序值idx_course_sort
课程域course_lesson课时名、所属章节、视频URL、排序值idx_section_sort
交易域course_order订单号、用户ID、课程ID、金额、支付状态uk_order_no, idx_user_status
选课关系user_course用户ID、课程ID、解锁时间uk_user_course

把 course_order 和 user_course 分开是这个模型的要点。订单表描述的是支付这件事的完整状态机,包含待支付、已支付、已取消;而 user_course 记录的是“用户已经拥有某门课”这个结果。学习页做权限校验时,直接查 user_course 就能判断用户能否播放视频,不需要每次去订单表里过滤 status=1 的记录。订单表的数据量增长快,选课关系表则增长慢,拆开后两者的查询路径都变短了。

2.2 建表 SQL 与字段设计:utf8mb4、自增主键、状态用数字

下列建表 SQL 可以直接拿去执行,字段注释和索引设计是针对在线教育平台的典型场景准备的:

-- 用户表:学生、教师、管理员统一存放,角色靠字符串区分 CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL COMMENT '登录名', password VARCHAR(60) NOT NULL COMMENT 'BCrypt 哈希后的密码', role VARCHAR(20) NOT NULL DEFAULT 'student' COMMENT 'student/teacher/admin', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; -- 课程表:课程基本信息与售卖状态 CREATE TABLE course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL COMMENT '课程标题', summary VARCHAR(500) COMMENT '课程简介', cover_url VARCHAR(500) COMMENT '封面图地址', price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '课程价格', teacher_id BIGINT NOT NULL COMMENT '教师ID,关联 sys_user.id', status TINYINT NOT NULL DEFAULT 0 COMMENT '0下架,1上架', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_teacher (teacher_id), KEY idx_status (status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表'; -- 订单表:记录一次完整的购买流程 CREATE TABLE course_order ( id BIGINT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL COMMENT '业务订单号', user_id BIGINT NOT NULL COMMENT '下单用户ID', course_id BIGINT NOT NULL COMMENT '购买课程ID', amount DECIMAL(10,2) NOT NULL COMMENT '成交金额', status TINYINT NOT NULL DEFAULT 0 COMMENT '0待支付,1已支付,2已取消', paid_at DATETIME NULL COMMENT '支付时间', created_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';

这段 SQL 里三个细节需要在实际项目中保持一致:

第一,字符集用 utf8mb4 而不是 utf8。课程简介和用户昵称常常包含 emoji,MySQL 的 utf8 最多支持三字节,存四字节的 emoji 时插入会直接报错。这个问题在 MySQL 安装配置教程里很容易被忽视,却是在线教育平台用户反馈里出现频率最高的问题之一。

第二,主键全部用 BIGINT 自增。MyBatis 在 insert 时配合useGeneratedKeys="true"keyProperty="id",插入完成后主键会自动回填到实体类对象的 id 字段,服务层立刻就能知道新生成的 ID,不用再做一次查询。

第三,状态字段用 TINYINT 数字而不是 VARCHAR。MyBatis 在做WHERE status = #{status}时,只要传入参数类型是 Integer,就不会出现字符串比较与空格问题。

注意:业务上不要给表添加物理外键。课程和订单的关系由 mapper 里的 JOIN 维护,物理外键在删除课程或用户时会带来额外的数据库锁开销。

2.3 用 user_course 中间表解决购买鉴权:一条 SQL 判断用户有没有课

学习页最常用的判断是“用户能否播放这个课时”。把购买结果抽成一张关系表,鉴权查询就非常简单:

CREATE TABLE user_course ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, course_id BIGINT NOT NULL, acquired_at DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_course (user_id, course_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户已购课程表';

配合 MyBatis,Mapper 里可以这样写:

<select id="countUserCourse" resultType="int"> SELECT COUNT(1) FROM user_course WHERE user_id = #{userId} AND course_id = #{courseId} </select>

由于 uk_user_course 是联合唯一索引,COUNT(1) 在索引上直接完成,不需要回表。application 的 service 层只要判断返回值大于 0 就允许播放。这个设计避免了对 course_order 的重复全表扫描,同时联合唯一索引还能兜底并发下单时的重复插入。

3. 搭建 Spring Boot + MyBatis 项目骨架:依赖、配置与 Mapper 映射

数据库结构定了,下一步就是项目工程化。Spring Boot 版本和 MyBatis starter 的版本兼容性是个老话题,这里给出的依赖组合在 Spring Boot 2.7.x 下工作正常;如果创建项目时 Spring Boot 版本太高,比如 3.x,需要同步确认 mybatis-spring-boot-starter 的版本是否适配 jakarta 命名空间。

3.1 Maven 依赖与 application.yml 配置:MyBatis 的 SQL 打印和驼峰映射

推荐用 start.spring.io 创建项目,web 模块选 Spring Web,同时手工加入 MyBatis Framework、MySQL Driver、Thymeleaf 依赖。最后一个就是标题里 HTML 部分的承载组件。下面这份 pom.xml 片段可以直接放进 Spring Boot 2.7 项目里:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency>

四个依赖各有用处:spring-boot-starter-web 是控制器和内置 Tomcat 的来源;mybatis-spring-boot-starter 是 MyBatis 对 Spring Boot 的官方集成包,org.mybatis 包下的 mapper 扫描和 SqlSessionFactory 自动装配都由它完成;mysql-connector-j 是 MySQL 8.x 的驱动,Spring Boot 的 dependencyManagement 会自动管理版本;thymeleaf starter 让模板可以直接返回 HTML 视图名。还需要确认 spring-boot-starter-jdbc 通过 mybatis starter 传递进来,通常不需要显式声明。

application.yml 这样写:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/edu_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: root thymeleaf: cache: false encoding: UTF-8 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.edu.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

核心参数在这张表里对应得很清楚:

配置项示例值作用
spring.datasource.urljdbc:mysql://localhost:3306/edu_platform库连接串,带上时区参数
mybatis.mapper-locationsclasspath:mapper/*.xml扫描 XML 映射文件的位置
mybatis.type-aliases-packagecom.example.edu.entity简化 XML 中 resultType 的包前缀
mybatis.configuration.log-implStdOutImpl控制台打印带参数 SQL

参数说明集中在 mybatis 域下:mapper-locations 告诉 Spring Boot 去哪里找 XML 文件,常见的包路径是把 mapper XML 放在 src/main/resources/mapper 里;type-aliases-package 简化 XML 里的 resultType 写法,让 MyBatis 知道 Course 这个别名对应哪个包下的类;log-impl 使用 StdOutImpl 时,每一条执行 SQL 都会带参数打印到控制台,这个方法同时解决了 Spring Boot 项目里 MyBatis 配置打印的问题。数据源 URL 里的 serverTimezone 必须填写,MySQL 8 驱动对时区敏感,缺了它启动阶段会报 Server returns invalid timezone 错误。

3.2 实体类、Mapper 接口、XML 三件套:课程查询的最小可运行代码

实体类按数据库表字段逐个写,启用驼峰映射后,数据库列 course_id 会自动对应实体里的 courseId 字段。

public class Course { private Long id; private String title; private String summary; private String coverUrl; private BigDecimal price; private Long teacherId; private Integer status; private Date createdAt; // 省略 getter/setter }

Mapper 接口用 @Mapper 注解标记,Spring Boot 启动时会被 MyBatis 自动扫描:

@Mapper public interface CourseMapper { List<Course> selectOnSaleCourses(Integer status); Course selectByPrimaryKey(Long id); }

对应的 mapper/courseMapper.xml 如下:

<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.example.edu.mapper.CourseMapper"> <select id="selectOnSaleCourses" resultType="Course"> SELECT id, title, summary, cover_url, price, teacher_id, status, created_at FROM course WHERE status = #{status} ORDER BY created_at DESC </select> </mapper>

如果启动报错提示 Invalid bound statement (not found),排错顺序是先确认 mapper XML 文件确实在 resources/mapper 目录下且文件名与接口对应,再确认 namespace 是接口全限定名、select 的 id 与接口方法名完全一致。“绑定失败”十个有八个是这两处拼写不一致导致的。

3.3 事务边界:下单解锁必须同一个 @Transactional

在线教育平台的订单创建和最终解锁课程分布在两个 Mapper:course_order 插入记录,user_course 插入关系。这两个写操作要么同时成功、要么同时失败,最稳妥的做法是在 service 层把两个调用放进同一个事务方法:

@Service public class PurchaseService { @Autowired private CourseOrderMapper orderMapper; @Autowired private UserCourseMapper userCourseMapper; @Transactional(rollbackFor = Exception.class) public void createOrder(Long userId, Long courseId) { CourseOrder order = new CourseOrder(); order.setOrderNo("EDU" + System.currentTimeMillis()); order.setUserId(userId); order.setCourseId(courseId); order.setAmount(new BigDecimal("298.00")); order.setStatus(0); orderMapper.insert(order); } @Transactional(rollbackFor = Exception.class) public void activateCourse(Long userId, Long courseId) { UserCourse userCourse = new UserCourse(); userCourse.setUserId(userId); userCourse.setCourseId(courseId); userCourseMapper.insert(userCourse); } }

两个 @Transactional 共存时,要注意所有事务方法必须通过 Spring 代理的入口进入,不能在同一个类里用 this.activateCourse() 调用。否则 this 调用绕过了代理,事务注解不生效,这一步在线教育平台项目里最容易踩坑。

4. 在线教育平台核心业务:注册登录、课程详情与 HTML 页面渲染

骨架搭好,剩下的工作是把用户、课程、订单穿成一条可运行的业务链路。HTML 在这里作为模板层承担最终展示。

4.1 注册登录:BCrypt 存密码,前端页面只传 username 和 password

用户注册如果是自己手写 Controller,不引入 Spring Security 时也能做安全存储。直接用 spring-security-crypto 里的 BCryptPasswordEncoder,登录时用 matches 方法做校验。角色还需要约束:

角色码默认权限
student浏览上架课程、播放已购课时
teacher管理自己名下的课程
admin全部管理能力

注册接口只接收 username 和 password,role 由服务端写死成 student,避免用户提交一个 role 字段把自己注册成管理员。注意 spring-security-crypto 需要单独添加依赖进入 pom。

@Service public class UserService { private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); @Autowired private UserMapper userMapper; public boolean register(String username, String password) { if (userMapper.findByUsername(username) != null) { return false; } User user = new User(); user.setUsername(username); user.setPassword(encoder.encode(password)); user.setRole("student"); return userMapper.insert(user) > 0; } public User login(String username, String rawPassword) { User user = userMapper.findByUsername(username); if (user != null && encoder.matches(rawPassword, user.getPassword())) { return user; } return null; } }

密码列设计为 VARCHAR(60),因为 BCrypt 哈希固定 60 个字符。登录成功后把 User 对象拷贝为没有 password 字段的视图对象再放进 session,这个细节常被忽略,一旦写日志或直接序列化,密码哈希就泄露了。

4.2 课程详情页:用 resultMap 嵌套 collection 一次取回章节和课时

用户点进课程详情,页面通常要同时显示课程信息、章节列表以及每个章节下的课时列表。MyBatis 的做法是使用 resultMap 里的 collection 嵌套映射,一次 SQL 把三层数据都取回来:

<resultMap id="courseDetailMap" type="CourseVO"> <id property="id" column="c_id" /> <result property="title" column="c_title" /> <result property="summary" column="c_summary" /> <collection property="sections" ofType="SectionVO"> <id property="id" column="s_id" /> <result property="title" column="s_title" /> <collection property="lessons" ofType="LessonVO"> <id property="id" column="l_id" /> <result property="title" column="l_title" /> <result property="videoUrl" column="video_url" /> </collection> </collection> </resultMap> <select id="selectCourseDetail" resultMap="courseDetailMap"> SELECT c.id AS c_id, c.title AS c_title, c.summary AS c_summary, s.id AS s_id, s.title AS s_title, l.id AS l_id, l.title AS l_title, l.video_url AS video_url FROM course c LEFT JOIN course_section s ON s.course_id = c.id LEFT JOIN course_lesson l ON l.section_id = s.id WHERE c.id = #{courseId} ORDER BY s.sort_order, l.sort_order </select>

这段代码有两个关键点:collection 里每层都要写出<id>,MyBatis 靠它来判断哪一行属于同一个对象;如果不写 id,同一个课时在返回结果里可能被重复拼接。SQL 里注意排序字段,聚合多张表时用 s.sort_order、l.sort_order 这样的限定名比用别名更安全。

4.3 控制器转给 HTML:Thymeleaf 模板里这样遍历嵌套对象

Service 层拿到 CourseVO 后,直接放到 Model 里返回视图名:

@Controller public class CourseController { @Autowired private CourseService courseService; @GetMapping("/course/{id}") public String courseDetail(@PathVariable("id") Long id, Model model) { CourseVO course = courseService.getCourseDetail(id); model.addAttribute("course", course); return "course/detail"; } }

对应的 src/main/resources/templates/course/detail.html 长这样:

<!DOCTYPE html> <html lang="zh-cn" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="utf-8"> <title th:text="${course.title}">课程标题</title> <link rel="stylesheet" th:href="@{/css/detail.css}"> </head> <body> <h1 th:text="${course.title}"></h1> <p th:text="${course.summary}"></p> <div th:each="section : ${course.sections}"> <h2 th:text="${section.title}"></h2> <ul> <li th:each="lesson : ${section.lessons}"> <a th:href="@{/learn/{lessonId}(lessonId=${lesson.id})}" th:text="${lesson.title}">课时名</a> </li> </ul> </div> </body> </html>

这里最容易卡住的地方是 HTML 文件无法直接预览,因为 th:text 是服务端语法,直接双击看 HTML 只会看到“课程标题”等默认文字。调试时在 IDEA 里配一个 Thymeleaf 的模板解析器,或者直接启动 Spring Boot 后访问 /course/1。页面里 th:href 生成的是学习页地址,用户点击会跳到前面提到的鉴权方法,先查 user_course 表,有权限才允许播放。

4.4 支付回调后的解锁流程

支付回调通常是外部系统异步触发的,Controller 接收回调请求后,把订单状态改为已支付,再往 user_course 插入一条解锁记录。这一步同样放在事务里处理。并发场景下,用户连续点击两次“立即支付”可能触发两个回调,user_course 的联合唯一索引会确保第二次插入直接报错,异常被 service 层捕获后不会产生重复解锁记录。

5. MyBatis 缓存与在线教育平台读请求的 3 个排错点

在线教育平台的读压力都集中在课程列表和详情页。课程数据的特点是读多写少、价格和简介变更不频繁,这类数据适合用 MyBatis 的二级缓存把频繁的 MySQL 查询挡住。

5.1 二级缓存开在哪个 Mapper 上:课程详情比列表更值得缓存

一级缓存默认开着,但作用域是 SqlSession。Spring Boot 的 Service 每次方法调用都会新建 SqlSession,一级缓存基本形同虚设;二级缓存默认关闭,需要手工在每个 Mapper XML 里声明:

<mapper namespace="com.example.edu.mapper.CourseMapper"> <cache eviction="LRU" flushInterval="600000" size="512" readOnly="true"/> </mapper>

参数说明:eviction=LRU 表示缓存满了淘汰最久未使用的条目;flushInterval=600000 是 10 分钟的过期时间;size=512 控制缓存条目上限;readOnly=true 直接返回缓存对象引用,性能最高,但要求实体类不被外部修改。课程详情和 user_course 鉴权这类 Mapper 开缓存收益最大,课程列表这种带排序和搜索条件的 Mapper 不建议开启,避免 COUNT 或条件 SQL 之间的缓存互串。

5.2 三个高频坑:缓存不刷、缓存串数据、读取异常

第一个坑,课程下架不生效。课程状态从 1 改成 0 之后,详情页还能打开。原因是修改课程和读取课程如果落在不同的 Mapper 里,update 只会清空当前 Mapper namespace 的缓存。解决办法是把课程、章节、课时读取都收敛到同一个 CourseMapper。

第二个坑,PageHelper 分页与二级缓存叠加。PageHelper 会先执行 COUNT 再执行分页查询,缓存命中的是 COUNT 结果的话翻页会乱。列表查询的 Mapper 不要开缓存,这是最快解决办法。

第三个坑,开启 readOnly=false 时实体类没实现 Serializable,会抛 SerializationException。如果不想实现序列化接口,就老老实实用 readOnly=true 缓存只读场景。

提示:验证缓存是否生效,最直接的方法是看控制台 MyBatis 的 StdOutImpl 是否打印 SQL。第一次查询打印,第二次不带相同参数的查询如果不再打印 SQL,说明缓存命中。

5.3 用 MySQL workbench 把慢查询抓出来

设置 long_query_time=1,MySQL 会把超过 1 秒的查询记录到慢查询日志:

SET GLOBAL slow_query_log = ON; SET GLOBAL long_query_time = 1;

日志里出现频率最高的模板是SELECT ... FROM course WHERE title LIKE '%关键字%'。课程搜索这类模糊查询在小数据量下可以接受,等到课程数据量上来之后,要么针对搜索字段改用前缀索引,要么承载到独立的搜索组件里。对当前这个在线教育平台的单体版本,保持 MyBatis 手写 SQL 不变,数据库这层把慢查询逐个消灭掉就够了。

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

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

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

立即咨询