Spring Boot+MyBatis+MySQL在线教育平台开发实战指南

发布时间:2026/9/12 22:25:28
Spring Boot+MyBatis+MySQL在线教育平台开发实战指南 简介一套基于 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 就能判断用户能否播放视频不需要每次去订单表里过滤 status1 的记录。订单表的数据量增长快选课关系表则增长慢拆开后两者的查询路径都变短了。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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;这段 SQL 里三个细节需要在实际项目中保持一致第一字符集用 utf8mb4 而不是 utf8。课程简介和用户昵称常常包含 emojiMySQL 的 utf8 最多支持三字节存四字节的 emoji 时插入会直接报错。这个问题在 MySQL 安装配置教程里很容易被忽视却是在线教育平台用户反馈里出现频率最高的问题之一。第二主键全部用 BIGINT 自增。MyBatis 在 insert 时配合useGeneratedKeystrue和keyPropertyid插入完成后主键会自动回填到实体类对象的 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户已购课程表;配合 MyBatisMapper 里可以这样写select idcountUserCourse resultTypeint 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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-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?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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 { ListCourse selectOnSaleCourses(Integer status); Course selectByPrimaryKey(Long id); }对应的 mapper/courseMapper.xml 如下?xml version1.0 encodingUTF-8 ? !DOCTYPE mapper PUBLIC -//mybatis.org//DTD Mapper 3.0//EN http://mybatis.org/dtd/mybatis-3-mapper.dtd mapper namespacecom.example.edu.mapper.CourseMapper select idselectOnSaleCourses resultTypeCourse 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在线教育平台的订单创建和最终解锁课程分布在两个 Mappercourse_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 和 passwordrole 由服务端写死成 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 idcourseDetailMap typeCourseVO id propertyid columnc_id / result propertytitle columnc_title / result propertysummary columnc_summary / collection propertysections ofTypeSectionVO id propertyid columns_id / result propertytitle columns_title / collection propertylessons ofTypeLessonVO id propertyid columnl_id / result propertytitle columnl_title / result propertyvideoUrl columnvideo_url / /collection /collection /resultMap select idselectCourseDetail resultMapcourseDetailMap 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 里每层都要写出idMyBatis 靠它来判断哪一行属于同一个对象如果不写 id同一个课时在返回结果里可能被重复拼接。SQL 里注意排序字段聚合多张表时用 s.sort_order、l.sort_order 这样的限定名比用别名更安全。4.3 控制器转给 HTMLThymeleaf 模板里这样遍历嵌套对象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 langzh-cn xmlns:thhttp://www.thymeleaf.org head meta charsetutf-8 title th:text${course.title}课程标题/title link relstylesheet th:href{/css/detail.css} /head body h1 th:text${course.title}/h1 p th:text${course.summary}/p div th:eachsection : ${course.sections} h2 th:text${section.title}/h2 ul li th:eachlesson : ${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 namespacecom.example.edu.mapper.CourseMapper cache evictionLRU flushInterval600000 size512 readOnlytrue/ /mapper参数说明evictionLRU 表示缓存满了淘汰最久未使用的条目flushInterval600000 是 10 分钟的过期时间size512 控制缓存条目上限readOnlytrue 直接返回缓存对象引用性能最高但要求实体类不被外部修改。课程详情和 user_course 鉴权这类 Mapper 开缓存收益最大课程列表这种带排序和搜索条件的 Mapper 不建议开启避免 COUNT 或条件 SQL 之间的缓存互串。5.2 三个高频坑缓存不刷、缓存串数据、读取异常第一个坑课程下架不生效。课程状态从 1 改成 0 之后详情页还能打开。原因是修改课程和读取课程如果落在不同的 Mapper 里update 只会清空当前 Mapper namespace 的缓存。解决办法是把课程、章节、课时读取都收敛到同一个 CourseMapper。第二个坑PageHelper 分页与二级缓存叠加。PageHelper 会先执行 COUNT 再执行分页查询缓存命中的是 COUNT 结果的话翻页会乱。列表查询的 Mapper 不要开缓存这是最快解决办法。第三个坑开启 readOnlyfalse 时实体类没实现 Serializable会抛 SerializationException。如果不想实现序列化接口就老老实实用 readOnlytrue 缓存只读场景。提示验证缓存是否生效最直接的方法是看控制台 MyBatis 的 StdOutImpl 是否打印 SQL。第一次查询打印第二次不带相同参数的查询如果不再打印 SQL说明缓存命中。5.3 用 MySQL workbench 把慢查询抓出来设置 long_query_time1MySQL 会把超过 1 秒的查询记录到慢查询日志SET GLOBAL slow_query_log ON; SET GLOBAL long_query_time 1;日志里出现频率最高的模板是SELECT ... FROM course WHERE title LIKE %关键字%。课程搜索这类模糊查询在小数据量下可以接受等到课程数据量上来之后要么针对搜索字段改用前缀索引要么承载到独立的搜索组件里。对当前这个在线教育平台的单体版本保持 MyBatis 手写 SQL 不变数据库这层把慢查询逐个消灭掉就够了。本文还有配套的精品资源点击获取