资讯动态

Spring Boot 3 + Vue3 + MySQL 课程网站全栈开发实战

发布时间:2026/9/5 18:02:11 来源:尧图企业网站定制
选择“课程网站”作为毕业设计或简历项目时很多同学最头疼的不是某个知识点学不会而是“前端、后端、数据库要如何组合成一个能跑起来的系统”。网上搜到的资料往往是单个接口、单个页面真正要把 Spring Boot、Vue3、MySQL 串成一条完整业务链路还是需要自己踩不少坑。这篇教程围绕“课程网站系统”这个典型全栈项目从数据库设计开始到 Spring Boot 3 后端接口再到 Vue3 前端页面完整走一遍课程分类、课程列表、课程新增、编辑、上下架、删除的全流程。项目既适合 JavaWeb 课程设计也适合作为前后端分离项目的入门练手。如果你是跟着教程从头敲一遍最终会得到一套可本地运行的前后端分离项目后端提供 REST 风格接口前端通过 Axios 调用接口展示课程数据并完成管理操作。整个过程会涉及 Spring Data JPA 数据访问、统一响应体设计、跨域配置、Vue Router 路由、Element Plus 表单校验等常用开发技能。1. 系统定位与技术选型1.1 课程网站需要哪些核心功能课程网站系统的本质是“课程信息的管理与展示”。用户侧需要看到的是一张课程列表可以按分类筛选、按名称搜索点进课程详情后能看到讲师、简介、封面图和价格等信息管理侧则需要维护课程分类并对课程进行上架、下架、资料修改等操作。这看起来简单但落到系统设计上至少要拆分出如下功能点模块功能说明课程分类分类列表查询、分类维护课程展示公开课程列表、分类筛选、关键词搜索、课程详情课程管理新增课程、编辑课程、上下架、删除课程数据统计课程学习人数、课程数量统计可作为后续扩展用户体系用户注册登录、角色权限分离可作为后续扩展本文把重点放在主链路分类维护、课程展示、课程 CRUD。这样代码量可控同时覆盖了“前台查询 后台管理”的完整业务闭环。登录鉴权部分会在最后的生产化改进中说明。1.2 为什么选择 Spring Boot 3 Vue3 MySQL这套组合在目前的 JavaWeb 课程设计和实际项目中使用率都很高主要有三个原因。第一Spring Boot 3 已经全面进入 Jakarta EE 命名空间底层依赖 Spring Framework 6。相比早期的 Spring Boot 2.x它强制要求 JDK 17 及以上语法和生态都已经偏向现代 Java。对新手来说使用 Spring Boot 3 可以避免一上来就学习过时的 Servlet API 写法。第二Vue3 是目前前端 Vue 生态的主力版本。配合 Vite 构建工具开发时热更新非常快。Vue 3 的组合式 API 也让逻辑复用变得更清晰组件代码的可维护性比 Vue 2 时代高很多。第三MySQL 是中小型业务系统最常用的关系型数据库。课程网站的数据模型并不复杂用 MySQL 管理课程分类、课程、用户、学习记录等表都是很成熟的做法。而且网上关于 MySQL 安装、索引优化、事务使用的资料非常多遇到问题容易排查。1.3 前后端分离架构与工程目录规划前后端分离的意思是后端只提供 JSON 接口不关心页面长什么样前端只负责渲染页面不直接操作数据库。两者通过 HTTP 接口通信。后端项目的包结构建议如下course-website-backend ├── pom.xml └── src/main ├── java/com/example/course │ ├── CourseApplication.java │ ├── common │ │ ├── PageResult.java │ │ └── Result.java │ ├── config │ │ └── CorsConfig.java │ ├── controller │ ├── dto │ ├── entity │ ├── repository │ ├── service │ └── vo └── resources └── application.yml前端项目结构会在后面的章节展开。这里先明确包结构的意义entity 负责映射数据库表repository 负责数据访问service 负责业务逻辑controller 负责接收请求和返回结果dto 负责接收前端参数vo 负责向后端返回页面展示数据。结构清晰之后排查问题会变得非常轻松。2. 环境准备与数据库设计2.1 开发环境版本说明开始编码前先确认本机环境。版本需要根据实际项目调整本教程以如下环境为基线组件版本说明JDKJDK 17 及以上MavenMaven 3.8 及以上Spring Boot3.3.x 系列MySQL8.xNode.jsNode 18 及以上前端构建工具Vite 5.xIDEIntelliJ IDEA 或 VSCode如果你使用的是 JDK 8无法直接运行 Spring Boot 3 项目必须先升级 JDK。MySQL 安装完成后需要保证本机 3306 端口可以正常访问。Node.js 可以通过node -v命令检查版本。2.2 数据库表结构设计课程网站的核心是课程表。围绕课程还需要一张课程分类表。分类和课程是一对多关系一个分类下可以有多门课程一门课程只能属于一个分类。课程表设计时除了标题、封面、讲师、简介这些展示字段还需要关注状态字段。这里用status表示课程上下架1 代表上架0 代表下架。前台只能查询到上架课程后台可以看到全部课程。如果后续要支持用户登录和学习记录可以再增加用户表和课程学习记录表。本文先把基础数据表建好用户相关表放在扩展部分说明。2.3 初始化 SQL 脚本新建数据库并执行以下 SQLCREATE DATABASE course_website DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE course_website; CREATE TABLE course_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 分类名称, sort INT DEFAULT 0 COMMENT 排序数值越小越靠前, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 课程分类表; CREATE TABLE course ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL COMMENT 分类ID, title VARCHAR(100) NOT NULL COMMENT 课程标题, cover_url VARCHAR(500) COMMENT 封面图地址, teacher VARCHAR(50) COMMENT 讲师姓名, summary VARCHAR(500) COMMENT 课程简介, price DECIMAL(10, 2) DEFAULT 0.00 COMMENT 课程价格0表示免费, study_count INT DEFAULT 0 COMMENT 学习人数, status TINYINT DEFAULT 1 COMMENT 状态1上架0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, KEY idx_category_id (category_id), CONSTRAINT fk_course_category FOREIGN KEY (category_id) REFERENCES course_category (id) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4 COMMENT 课程表;插入基础测试数据INSERT INTO course_category (name, sort) VALUES (Java, 1); INSERT INTO course_category (name, sort) VALUES (前端开发, 2); INSERT INTO course_category (name, sort) VALUES (数据库, 3); INSERT INTO course (category_id, title, cover_url, teacher, summary, price, study_count, status) VALUES (1, Spring Boot 3 实战教程, https://example.com/course-1.png, 张老师, 从零开始掌握Spring Boot 3核心用法, 129.00, 1200, 1), (2, Vue3 全家桶入门, https://example.com/course-2.png, 李老师, 系统学习Vue3组合式API与Vite, 99.00, 800, 1), (3, MySQL 性能优化基础, https://example.com/course-3.png, 王老师, 索引、SQL优化与慢查询分析, 199.00, 300, 0);这段 SQL 中封面图片地址是示例值。实际项目中封面图通常会上传到 OSS 对象存储或者使用本地静态资源目录保存。2.4 项目初始化方式后端项目可以直接通过 IDEA 的 Spring Initializr 创建选择 Java 17、Spring Web、Spring Data JPA、MySQL Driver、Validation、Lombok 这几个依赖。也可以手动创建 Maven 项目后添加 pom.xml 依赖。前端项目使用 Vite 脚手架创建npm create vitelatest course-website-frontend -- --template vue cd course-website-frontend npm install npm install element-plus axios vue-routernpm create vite创建项目后默认的模板是 Vue3 JavaScript。如果你更习惯 TypeScript也可以把--template vue换成--template vue-ts本教程为了减少额外配置使用 JavaScript 版本。3. 后端工程搭建与核心代码实现3.1 Maven 依赖配置创建后端项目后pom.xml 中的核心依赖如下parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version3.3.5/version relativePath/ /parent properties java.version17/java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-jpa/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies版本号以spring-boot-starter-parent为准Spring Boot 会统一管理其他依赖版本所以 mysql 驱动、Lombok 不需要手动写版本号。如果你的 IDEA 在刷新依赖时下载较慢可以配置 Maven 国内镜像源。3.2 application.yml 配置在src/main/resources/application.yml中写入server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_website?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none show-sql: true open-in-view: false jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai配置说明ddl-auto: none表示不让 Hibernate 自动修改表结构。既然已经手动执行了 SQL 初始化表就不需要 JPA 自动建表避免字段类型和设计不一致。show-sql: true方便在控制台查看 JPA 生成的 SQL排查问题时很有帮助。open-in-view: false是为了避免视图层打开数据库连接。这个配置在 Spring Boot 2.x 后默认是 true但生产环境并不推荐所以这里先改成 false。allowPublicKeyRetrievaltrue是 MySQL 8 连接时常见的参数防止出现公钥检索错误。如果你本机 MySQL 用户密码不是123456请务必改成自己的实际密码。3.3 统一响应体与分页结果后端接口返回给前端的 JSON 如果一个接口一种格式前端封装 Axios 会很麻烦。这里统一封装一个ResultT对象所有接口都返回code、message、data三个字段。// 文件路径src/main/java/com/example/course/common/Result.java Data public class ResultT { private int code; private String message; private T data; public static T ResultT ok(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT fail(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }分页接口需要额外的PageResultT对象// 文件路径src/main/java/com/example/course/common/PageResult.java Data public class PageResultT { private ListT list; private long total; public static T PageResultT of(ListT list, long total) { PageResultT result new PageResult(); result.setList(list); result.setTotal(total); return result; } }使用 Lombok 的Data可以自动生成 getter/setter减少样板代码。3.4 实体类与 Repository课程表对应的实体类如下// 文件路径src/main/java/com/example/course/entity/Course.java Data Entity Table(name course) public class Course { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name category_id) private Long categoryId; Column(name title) private String title; Column(name cover_url) private String coverUrl; Column(name teacher) private String teacher; Column(name summary) private String summary; Column(name price) private BigDecimal price; Column(name study_count) private Integer studyCount; Column(name status) private Integer status; Column(name create_time, updatable false) private LocalDateTime createTime; Column(name update_time) private LocalDateTime updateTime; PrePersist public void prePersist() { LocalDateTime now LocalDateTime.now(); createTime now; updateTime now; } PreUpdate public void preUpdate() { updateTime LocalDateTime.now(); } }实体字段和数据库列一一对应。注意prePersist会在插入前自动填充时间preUpdate会在更新前自动刷新更新时间不需要在 Service 里手动 set 时间字段。分类实体类似可以直接复用同样结构// 文件路径src/main/java/com/example/course/entity/CourseCategory.java Data Entity Table(name course_category) public class CourseCategory { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private Integer sort; Column(name create_time, updatable false) private LocalDateTime createTime; }Repository 层接口继承 JpaRepository// 文件路径src/main/java/com/example/course/repository/CourseRepository.java public interface CourseRepository extends JpaRepositoryCourse, Long { PageCourse findByTitleContainingAndStatus(String title, Integer status, Pageable pageable); PageCourse findByCategoryIdAndTitleContainingAndStatus(Long categoryId, String title, Integer status, Pageable pageable); PageCourse findByTitleContaining(String title, Pageable pageable); }Spring Data JPA 会根据方法名自动解析 SQL。findByTitleContaining会生成WHERE title LIKE ?的查询前端不传关键词时传入空字符串即可查询全部。3.5 DTO、VO 与 Service 业务层由于接口不能直接把实体类返回给前端这里先定义新增和编辑时接收参数的 DTO// 文件路径src/main/java/com/example/course/dto/CourseSaveDTO.java Data public class CourseSaveDTO { NotNull(message 分类不能为空) private Long categoryId; NotBlank(message 课程标题不能为空) private String title; private String coverUrl; private String teacher; private String summary; private BigDecimal price; private Integer status; }课程分页查询返回的 VO 需要补充分类名称方便前台直接显示// 文件路径src/main/java/com/example/course/vo/CourseVO.java Data public class CourseVO { private Long id; private Long categoryId; private String categoryName; private String title; private String coverUrl; private String teacher; private String summary; private BigDecimal price; private Integer studyCount; private Integer status; private LocalDateTime createTime; }CourseService 承担核心业务逻辑// 文件路径src/main/java/com/example/course/service/CourseService.java Service RequiredArgsConstructor public class CourseService { private final CourseRepository courseRepository; private final CourseCategoryRepository categoryRepository; Transactional(readOnly true) public PageResultCourseVO pagePublic(String keyword, Long categoryId, int pageNum, int pageSize) { Pageable pageable PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Direction.DESC, createTime)); PageCourse coursePage; if (categoryId ! null) { coursePage courseRepository.findByCategoryIdAndTitleContainingAndStatus( categoryId, keyword, 1, pageable); } else { coursePage courseRepository.findByTitleContainingAndStatus(keyword, 1, pageable); } return toPageResult(coursePage); } Transactional(readOnly true) public PageResultCourseVO pageAdmin(String keyword, int pageNum, int pageSize) { Pageable pageable PageRequest.of(pageNum - 1, pageSize, Sort.by(Sort.Direction.DESC, createTime)); PageCourse coursePage courseRepository.findByTitleContaining(keyword, pageable); return toPageResult(coursePage); } Transactional(readOnly true) public CourseVO detail(Long id) { Course course courseRepository.findById(id) .orElseThrow(() - new RuntimeException(课程不存在)); CourseVO vo convert(course); String categoryName categoryRepository.findById(course.getCategoryId()) .map(CourseCategory::getName) .orElse(未分类); vo.setCategoryName(categoryName); return vo; } public Long createCourse(CourseSaveDTO dto) { Course course new Course(); applyDto(course, dto); course.setStudyCount(0); if (course.getStatus() null) { course.setStatus(1); } return courseRepository.save(course).getId(); } public void updateCourse(Long id, CourseSaveDTO dto) { Course course courseRepository.findById(id) .orElseThrow(() - new RuntimeException(课程不存在)); applyDto(course, dto); courseRepository.save(course); } public void deleteCourse(Long id) { courseRepository.deleteById(id); } private void applyDto(Course course, CourseSaveDTO dto) { course.setCategoryId(dto.getCategoryId()); course.setTitle(dto.getTitle()); course.setCoverUrl(dto.getCoverUrl()); course.setTeacher(dto.getTeacher()); course.setSummary(dto.getSummary()); course.setPrice(dto.getPrice()); course.setStatus(dto.getStatus()); } private PageResultCourseVO toPageResult(PageCourse coursePage) { ListCourse courses coursePage.getContent(); MapLong, String categoryMap loadCategoryMap(courses); ListCourseVO voList courses.stream().map(course - { CourseVO vo convert(course);

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价