ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

基于SpringBoot3+Vue3的智能课程学习系统开发实战

2026/9/5 17:58:02 拓冰建站 浏览量
基于SpringBoot3+Vue3的智能课程学习系统开发实战 智能课程学习系统的开发难点通常不是某个接口写不出来而是课程数据、接口返回结构和 Vue3 页面之间能否形成一条完整链路。一个基于 Java SpringBoot3 Vue.js3 MySQL 的课程学习系统核心就是三件事把课程和分类存进 MySQL用 SpringBoot3 把课程查询、选课、推荐接口暴露出来再让 Vue3 页面把这些接口完整消费掉。下面按一条从数据库到前端页面的主线来做最小闭环包含表结构、后端 Service、Mapper XML、前端 API 封装、联调验证和常见报错最终得到一个可以直接继续扩展的课程学习系统骨架。很多课程学习类项目会把精力放在页面样式上等后端联调时才意识到数据结构没设计好。出现这种问题是因为没有先想清楚“这个系统最少要完成哪些业务动作”。与其急着写代码不如先把一个最小业务闭环定下来后面的实现节奏会顺很多。1. 智能课程学习系统的业务边界和数据库设计1.1 “智能”在这个项目里落地成什么功能课程学习系统的常规功能包括课程展示、课程筛选、课程详情、用户选课和学习记录。这里的“智能”如果一开始就引入推荐算法、行为埋点、标签画像开发周期会不可控。对一个最小闭环来说更务实的做法是把“智能”做成可解释的规则推荐按用户已经选择的课程分类从相同分类里找出还未选择的课程再按课程报名人数倒序返回。这样既能让系统具备“根据历史选择找相似课程”的能力也方便后续替换成真正的算法模型。建议先接受一个判断智能推荐不是这个项目的全部它只是课程业务上的一个功能出口。真正重要的是课程、分类、选课记录三类数据能够稳定地关联起来。数据结构稳定之后无论换成协同过滤还是向量召回都只是替换查询逻辑不需要推翻页面和接口。1.2 三张核心表课程、分类、选课记录课程学习系统的最小数据模型可以拆成三张表。课程表保存课程的基础信息分类表保存课程所属方向选课记录表保存学生与课程的关系。选课记录表是理解这个项目数据流的关键它让系统知道某个学生学过哪些分类从而为推荐提供依据。CREATE DATABASE IF NOT EXISTS edu_smart_course DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE edu_smart_course; CREATE TABLE course_type ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 分类ID, type_name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序值, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINE InnoDB COMMENT 课程分类表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 课程ID, title VARCHAR(120) NOT NULL COMMENT 课程标题, cover VARCHAR(255) DEFAULT COMMENT 封面图, intro TEXT COMMENT 课程简介, type_id BIGINT NOT NULL COMMENT 分类ID关联 course_type.id, teacher VARCHAR(50) DEFAULT COMMENT 授课老师, price DECIMAL(10, 2) DEFAULT 0.00 COMMENT 价格单位元, student_count INT DEFAULT 0 COMMENT 报名人数, status TINYINT DEFAULT 1 COMMENT 状态 1上架 0下架, publish_time DATETIME COMMENT 上架时间, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_type_id (type_id), KEY idx_status (status), KEY idx_publish_time (publish_time) ) ENGINE InnoDB COMMENT 课程表; CREATE TABLE course_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL COMMENT 学生ID关联 student.id, course_id BIGINT NOT NULL COMMENT 课程ID关联 course.id, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_course (student_id, course_id) ) ENGINE InnoDB COMMENT 学生选课记录表;这里有一个常见取舍课程表与分类表之间没有建物理外键选课表与课程表之间也没有建物理外键。很多学习项目喜欢加上物理外键认为这样更安全但当课程删除、分类调整、批量导入数据时物理外键反而会成为变更瓶颈。实际项目中更常见的做法是用普通索引保证查询效率由 Service 层保证数据关系遇到违规操作再通过异常处理兜底。选课记录表的唯一索引uk_student_course是必须的它能从数据库层面防止同一个学生重复选择同一门课程。1.3 前后端分工和接口边界系统的后端职责只负责数据库读写和业务规则前端职责只负责展示课程、传递查询条件、触发选课动作。前端不能依赖数据库字段的直觉去拼装业务数据后端需要把分类名称、选课状态这类已经加工好的数据直接返回。项目最少需要四个接口可以把后续扩展方向也框定在这条主线上。接口请求方式作用/api/course/type/listGET返回全部课程分类/api/course/pageGET分页查询课程支持关键词和分类筛选/api/course/recommendGET根据学生已选分类返回推荐课程/api/course/selectPOST学生选择课程重复选择会被拦截没有登录模块时推荐接口和选课接口可以通过studentId传入演示数据。项目接入登录后再改成从 Token 或 Session 中取出当前用户这是后端接口设计中最常见的扩展点。接下来按这个边界准备工程。2. 环境准备与工程骨架先对齐版本再写功能SpringBoot3 和早期 SpringBoot2 的底层差异较大如果后端工程初始化版本不对代码写好后很可能在启动阶段报错。前端 Vue3 对 Node.js 版本也有要求因此在克隆模板或创建工程前应先用表格把环境版本固定下来。2.1 本机环境版本清单软件建议版本说明JDK17SpringBoot3 依赖 Jakarta EEJDK8 无法编译Spring Boot3.2.x示例配置使用 3.2.5MyBatis-Plusmybatis-plus-spring-boot3-starter 3.5.5注意不能使用面向 SpringBoot2 的旧 starterMySQL8.0字符集使用 utf8mb4Node.js18Vite 5 要求高版本 NodeVue3.4.x使用script setup组合式 APIElement Plus2.7.xVue3 的 UI 组件库安装 MySQL 后需要先把它启动起来再检查3306端口。启动 MySQL 后用命令行或可视化客户端执行上一节的建表 SQL。如果连接时出现Access denied检查用户名和密码是否与后续application.yml配置一致。2.2 创建后端 SpringBoot3 工程后端工程可以使用 Spring Initializr 创建也可以手写 Maven 工程。建议包名结构为com.example.educourse模块内部再按entity、mapper、service、controller、vo、config分包。严格的按业务分包在大型项目中会导致包数量爆炸但当前这个规模下按技术分层阅读顺序更直观。后端pom.xml依赖是启动能否成功的关键需要检查 MyBatis-Plus 依赖名是否带spring-boot3。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.2.5/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version3.5.5/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency /dependenciesSpringBoot3 由jakarta.*包名体系驱动如果误用了早期版本的mybatis-plus-boot-starter运行时会因为无法找到javax.sql.DataSource相关适配或 AOP 类而启动失败。2.3 创建前端 Vue3 工程前端工程使用 Vite 初始化创建之后还需要额外安装 Vue Router、Axios 和 Element Plus。Element Plus 对表单和列表场景支持完整可以节省大量样式代码。npm create vitelatest edu-course-front -- --template vue cd edu-course-front npm install npm install vue-router4 axios element-plus安装完成后核对前端目录确保src/views和src/api目录存在。Vite 工程默认没有 vue-router 的目录结构需要手动添加路由文件和路由出口。2.4 准备后端配置 application.yml在src/main/resources/application.yml中配置数据源、MyBatis-Plus 的 XML 路径和日志。MySQL 连接串里的serverTimezone不能省略否则会导致时间字段偏移。allowPublicKeyRetrievaltrue是 MySQL8 连接时常见参数用于支持使用 caching_sha2_password 认证的账号。server: port: 8080 spring: application: name: edu-course-server datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/edu_smart_course?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: root hikari: maximum-pool-size: 20 minimum-idle: 5 mybatis-plus: mapper-locations: classpath*:mapper/**/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmapper-locations指定 XML 文件位置后业务 SQL 可以集中在 XML 中维护避免把大量动态 SQL 拼进 Java 代码。开发阶段打开StdOutImpl日志能够看到每次请求执行的 SQL 和参数排错效率会高很多生产环境再切换成异步日志或关闭。3. 后端课程模块实体映射、查询 SQL 与业务规则后端代码的清晰度取决于实体、VO、Mapper XML 和 Service 各层边界是否明确。课程表结构并不是天然适合直接返回给前端的比如前端显示需要分类名称但 course 表里只有type_id。因此后端要区分实体和 VO 两个概念。3.1 实体类如何映射数据库表Course 实体使用 MyBatis-Plus 的注解完成表和字段映射。因为表字段是下划线风格Java 属性是驼峰风格开启map-underscore-to-camel-case后大部分字段可以自动映射但主键策略、逻辑删除等仍建议通过注解显式声明。package com.example.educourse.entity; import com.baomidou.mybatisplus.annotation.IdType; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; import java.math.BigDecimal; import java.time.LocalDateTime; Data TableName(course) public class Course { TableId(type IdType.AUTO) private Long id; private String title; private String cover; private String intro; private Long typeId; private String teacher; private BigDecimal price; private Integer studentCount; private Integer status; private LocalDateTime publishTime; private LocalDateTime createTime; private LocalDateTime updateTime; }如果你的数据库字段数量较多可以按这张基础结构扩展比如增加课时数、难度等级、课程时长等字段。但要注意前端卡片每个课程对象只使用哪些字段就把哪些字段放入 VO不需要把数据库所有字段强行暴露给页面。3.2 列表 VO比实体多一个分类名称CourseVO 继承 Course再追加一个typeName属性这样列表接口一次就能给前端返回课程标题、老师和分类名称。继承方式在字段数量可控时清晰在字段差异大时推荐单独声明 CourseVO而不是让 VO 与实体强耦合。package com.example.educourse.vo; import com.example.educourse.entity.Course; import lombok.Data; import lombok.EqualsAndHashCode; Data EqualsAndHashCode(callSuper true) public class CourseVO extends Course { private String typeName; }查询逻辑放在 CourseMapper 中通过 XML SQL 对course和course_type做 LEFT JOIN。前台只查status 1的上架课程下架课程不能不小心出现在入口页。package com.example.educourse.mapper; import com.baomidou.mybatisplus.core.conditions.Wrapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Constants; import com.example.educourse.entity.Course; import com.example.educourse.vo.CourseVO; import org.apache.ibatis.annotations.Param; import java.util.List; public interface CourseMapper extends BaseMapperCourse { IPageCourseVO selectCoursePage(IPageCourseVO page, Param(keyword) String keyword, Param(typeId) Long typeId); }XML 文件放在src/main/resources/mapper/CourseMapper.xml。resultType使用全限定名CourseVO避免依赖包的别名配置。分页查询对象IPage由 MyBatis 框架自动处理开发者只需要关心 SQL 和查询条件。?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.educourse.mapper.CourseMapper select idselectCoursePage resultTypecom.example.educourse.vo.CourseVO SELECT c.id, c.title, c.cover, c.intro, c.type_id, t.type_name, c.teacher, c.price, c.student_count, c.status, c.publish_time, c.create_time, c.update_time FROM course c LEFT JOIN course_type t ON c.type_id t.id WHERE c.status 1 if testkeyword ! null and keyword ! AND (c.title LIKE CONCAT(%, #{keyword}, %) OR c.teacher LIKE CONCAT(%, #{keyword}, %)) /if if testtypeId ! null AND c.type_id #{typeId} /if ORDER BY c.publish_time DESC, c.id DESC /select /mapper这个 XML 的动态 SQL 是最常见的手写方式。if判断可以避免传入空关键词时生成多余的AND。注意不要使用直接字符串拼接的方式生成 SQL这里参数通过#{}预编译绑定可以避免普通 SQL 注入风险。3.3 配置分页插件否则分页不会生效MyBatis-Plus 的分页查询不是天然生效的需要先注册MybatisPlusInterceptor并加入分页插件。如果漏掉这个配置IPage的参数只是被当作普通参数传给 SQL返回对象里 total 永远是 0。package com.example.educourse.config; import com.baomidou.mybatisplus.annotation.DbType; import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor; import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); PaginationInnerInterceptor paginationInterceptor new PaginationInnerInterceptor(DbType.MYSQL); paginationInterceptor.setMaxLimit(200L); interceptor.addInnerInterceptor(paginationInterceptor); return interceptor; } }setMaxLimit(200L)表示单页最大允许 200 条避免有人把 pageSize 设置为极大值后拖垮数据库。分页插件会为selectCoursePage自动生成 COUNT 查询。开发者不需要在 XML 中再写一条 COUNT SQL。3.4 Service 层查询、选课和推荐规则Service 层承担核心业务。分页查询逻辑很简单直接调用 Mapper 方法。选课逻辑则需要利用唯一索引拦截重复选择并用异常处理器把错误信息转成统一返回。推荐逻辑是本项目的“智能”落点。选课记录复用 MyBatis-Plus 的 BaseMapper 即可为了演示不需要在 CourseMapper 中新增其它方法。下面是 CourseService 的核心代码。package com.example.educourse.service; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.example.educourse.entity.CourseSelection; import com.example.educourse.mapper.CourseMapper; import com.example.educourse.mapper.CourseSelectionMapper; import com.example.educourse.mapper.CourseTypeMapper; import com.example.educourse.vo.CourseVO; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import java.util.Collections; import java.util.List; import java.util.Set; import java.util.stream.Collectors; Service public class CourseService { private final CourseMapper courseMapper; private final CourseSelectionMapper selectionMapper; public CourseService(CourseMapper courseMapper, CourseSelectionMapper selectionMapper) { this.courseMapper courseMapper; this.selectionMapper selectionMapper; } public IPageCourseVO pageCourses(long pageNum, long pageSize, String keyword, Long typeId) { PageCourseVO