ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+MyBatis+MySQL在线教育管理系统源码实战解析

SpringBoot+Vue+MyBatis+MySQL在线教育管理系统源码实战解析 前阵子帮一个培训机构做内部系统选型把市面上开源的在线教育项目翻了个遍发现一个通病很多号称“企业级”的源码打开之后就是几张表加几个CRUD接口课程、订单、权限做到一半就断掉了根本没法定制二次开发。正好手头有一套自己维护了挺久的SpringBoot Vue MyBatis MySQL后台管理系统功能完整、代码也整理过索性把这套《企业级在线教育系统管理系统源码》重新梳理了一遍把从技术选型到表设计、从接口实现到部署排坑的完整过程写出来。这篇内容不是简单放个下载链接就完事而是把整个系统拆开来讲为什么要选这套组合、课程和订单这两条主链路怎么串、数据库哪些表是核心、前后端联调时最容易踩什么坑、拿到源码后怎么快速跑起来并扩展。适合正在做毕业设计、想找一套能真正二次开发的Java项目、或者培训机构想搭自有管理平台的读者参考。1. 为什么做一套可复用的在线教育管理后台1.1 一个培训机构后台的真实需求清单我先还原一下我当时收到需求时的场景机构要有自己的课程体系讲师能维护课程资料运营能管上下架学员能看到课程并付费购买管理员要能查看订单和基础经营数据。听起来不复杂可真落到系统上至少涉及讲师信息、课程分类、课程章节、学员账号、订单支付、后台权限、轮播图、公告通知、数据统计这些模块。这套系统里的功能模块我按实际业务权重排了个优先级模块核心功能对应主要表讲师管理讲师信息、头像、简介、教学风格lecturer课程管理分类、课程上下架、章节资源course / course_section学员管理学员账号、购买记录、课程开通状态sys_user / user_course订单模块下单、支付回调、订单查询orders / pay_record权限管理用户角色、菜单权限、操作权限sys_user_role / sys_menu运营配置轮播图、公告、推荐位banner / notice数据统计课程销量、营收、学员增长趋势orders 聚合查询这里面最难的不是某张表怎么写而是模块之间的数据关系。讲师发布的课程要能关联到分类学员买了课程要能给权限订单要能追溯支付流水前后端接口不能各写各的。所以我在设计时先把这些业务实体理清楚再开始动代码。1.2 做系统前先定边界哪些功能必须做哪些先不做很多人在做管理系统时会犯一个毛病功能越想越多最后做到一半发现原来连数据库表都设计错了。我给自己定了一个原则第一版只做管理后台不做学员端的前台展示页面不做直播不做社区问答不做视频转码。为什么这样切因为管理后台是业务数据的核心课程、订单、权限这些能力都建立在管理后台之上。前台页面只是把数据用不同方式展示出来直播属于独立能力视频转码更应该交给云服务而不是自研。这套源码把管理后台做扎实学员端、小程序端、前台门户都可以通过同一套后端接口延展后面想接哪端都行。2. 技术选型与项目结构解析2.1 后端用 SpringBoot省去大量配置把精力留给业务SpringBoot 到今天已经不算新东西但我仍然认为它是这类管理系统的首选。它内嵌 Tomcat不用单独部署容器配置基于约定一个注解就能启动服务。更重要的是 Starter 机制引入 web、validation、mybatis、mysql 依赖都是开箱即用不需要像老 Spring 项目那样写一堆 XML 配置。这套系统的后端使用经典分层结构Controller 只做参数接收和结果返回Service 处理业务逻辑Mapper 负责数据库操作。实体类、DTO、VO 分开避免直接把数据库实体暴露给前端。后端目录结构edu-admin-api ├── src/main/java │ ├── common # 统一返回、异常处理、常量 │ ├── config # 配置类 │ ├── controller # 接口层 │ ├── service # 业务逻辑层 │ ├── mapper # MyBatis接口 │ ├── entity # 数据实体 │ └── vo/dto # 视图对象与传输对象 └── src/main/resources ├── mapper # MyBatis XML文件 └── application.yml2.2 前端用 Vue Element开发效率优先生态足够成熟管理后台这类系统核心是表单、表格、弹窗、权限菜单本身没有太复杂的交互。Vue 的组件化开发配合 Element 组件库能把这部分效率拉到很高。路由用 Vue Router 管理页面按模块划分接口请求统一放在 api 目录里封装。前端目录结构edu-admin-web ├── src │ ├── api # 接口封装 │ ├── router # 路由配置 │ ├── views # 页面组件 │ ├── components # 通用组件 │ ├── utils # 工具类 │ └── layout # 后台布局 ├── package.json └── vite.config.js前端使用 Vite 作为构建工具开发环境自带代理转发联调时不用折腾跨域。2.3 MyBatis 存在的意义复杂查询要自己掌控 SQL有人问过为什么不用 JPA我的答案是业务系统里复杂查询太多MyBatis 能直接手写 SQL可控性更强也方便 DBA 做 SQL 审核和优化。课程分页、订单统计、多表关联查询这些用动态 SQL 写起来非常顺手。MyBatis 的 XML 文件维护在 resources/mapper 目录下和 Mapper 接口一一对应。需要注意开启驼峰命名映射否则数据库的 create_time 字段没法自动映射成 createTime。2.4 整体技术组合为什么这么搭SpringBoot 负责提供稳定的后端基础MyBatis 负责把 SQL 层面做得灵活可控Vue 负责界面快速实现MySQL 负责持久化业务数据。这套组合最大的优势是周边生态成熟、参考资料多、招人容易。这里说的“招人容易”对培训机构来说特别现实。企业找人做二开市场上能很快找到熟悉这套技术栈的人不会被单一技术绑定。3. 业务闭环课程、订单、权限是怎么串起来的3.1 讲师维护课程运营负责上下架业务链路的第一步是讲师和课程。讲师表记录讲师的基础信息课程表记录标题、封面、价格、分类、上下架状态课程章节表再挂到课程下面。运营人员看到的是可管理的后台学员看到的是最终可购买的课程列表。课程有草稿、待审核、已上架、已下架四种状态。讲师创建课程后默认是草稿运营审核通过后上架学员端才可见。这套状态设计在企业系统里非常关键避免讲师随手改一个价格就对外发布。新增课程流程创建课程基本信息 - 完善章节与资源 - 提交审核 - 审核通过上架 - 学员端可见。3.2 学员下单到开课一条完整状态流在线教育最核心的一条链路是学员浏览课程 - 点击购买 - 生成订单 - 调用支付平台 - 支付回调确认 - 开通课程权限。订单状态我用四个值来表示待支付、已支付、已关闭、已退款。这里有几个关键点值得展开订单创建是在学员点击购买时生成的此时必须先创建订单再调用支付不能先把支付做完了再补订单。支付回调是所有环节里最需要小心的因为回调可能重复到达系统要保证幂等。我的做法是在订单表上建了 unique key以订单号作为业务唯一标识回调过来先查订单状态如果已经是已支付就直接返回成功不重复处理。支付成功后要给学员开通课程权限。这里我用 user_course 表记录学员和课程的关联关系包含开通时间、来源订单。后续学员端查“我已经购买的课程”直接查这张表就行不用再去关联订单表算状态。3.3 后台权限基于 RBAC 的细化控制管理系统必须解决“谁能干什么”的问题。我用的经典 RBAC 模型五张表用户表、角色表、菜单表、用户角色关系表、角色菜单关系表。超级管理员拥有全部权限普通运营只能管理课程和内容财务角色只能看订单和统计讲师只能维护自己的课程。前端根据登录用户的菜单权限动态生成路由后端接口再做一次权限校验两层都控制避免有人绕过前端直接调接口。权限这块不能省否则后续系统一旦开放多角色协同会变成谁都能改数据的状态。3.4 统计报表的简单实现思路统计模块在初期不需要引入复杂的报表引擎我的做法是直接用 SQL 聚合。课程销量按订单表 group by 课程 ID营收统计按支付成功的时间和金额 group by 天学员增长趋势按注册时间 group by 天。数据量上来之后再加索引或者把统计结果放到缓存里初期完全够用。4. 数据库建模核心表结构与取舍记录4.1 用户、角色、权限表设计用户表包含账号、密码、昵称、头像、手机号、用户类型、状态这些字段密码存的是 BCrypt 加密串不存明文。用户类型用来区分管理员和学员在某些场景下还需要区分讲师通过 type 字段加角色表配合使用。CREATE TABLE sys_user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL COMMENT 登录账号, password varchar(100) NOT NULL COMMENT BCrypt加密密码, nickname varchar(50) DEFAULT NULL COMMENT 昵称, avatar varchar(500) DEFAULT NULL COMMENT 头像URL, user_type tinyint(4) DEFAULT 0 COMMENT 用户类型 0管理员 1学员 2讲师, status tinyint(4) DEFAULT 1 COMMENT 状态 1正常 0禁用, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表;权限相关表不再展开完整 DDL核心关系是用户挂角色角色挂菜单。菜单表里有一个关键字段 perm存按钮级别的权限标识比如course:add、order:view后端接口校验时用这些标识做细粒度控制。4.2 课程相关表课程-章节-资源课程表、章节表、资源表是教育系统的内容骨架。课程表我已经在前面给过结构这里补章节表的设计思路。章节表挂在课程表下用 course_id 关联包含章节标题、排序、视频地址、课件地址、时长、是否免费试看。课程可能同一级章节也可能有二级章节我通过 parent_id 自关联实现层级。课程表两个查询场景要重点建索引一个按分类和状态查课程列表一个按讲师 ID 查我发布的课程。所以我在 course 表上建了idx_category_status和idx_lecturer两个索引。4.3 订单与支付流水状态和幂等怎么保证订单表重点关注业务唯一性和状态查询效率。order_no 是业务订单号生成规则我用的是时间戳加随机数数据库层加了唯一索引兜底。用户维度和状态维度建联合索引这样“查某个用户有哪些待支付订单”这类高频操作能走索引。CREATE TABLE orders ( id bigint(20) NOT NULL AUTO_INCREMENT, order_no varchar(32) NOT NULL COMMENT 订单号, user_id bigint(20) NOT NULL COMMENT 下单用户, course_id bigint(20) NOT NULL COMMENT 课程, pay_amount decimal(10,2) NOT NULL COMMENT 支付金额, pay_status tinyint(4) NOT NULL DEFAULT 0 COMMENT 0待支付 1已支付 2已关闭 3已退款, pay_time datetime DEFAULT NULL COMMENT 支付时间, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user_status (user_id, pay_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单表;支付流水表记录每次支付回调的第三方交易号、支付平台、回调状态、回调时间。订单表和支付流水表是一对多关系一次订单可能因为回调异常产生多条支付记录但订单的最终状态只有一个。系统里所有关键写操作都放在事务里尤其是支付回调到开通课程权限这个动作要么同时成功要么整体回滚。4.4 表设计的几个通用约定所有业务表都带create_time、update_time、del_flag这三个字段。del_flag 默认是 0删除走逻辑删除不物理删数据。这样做的好处是数据可追溯误删可以恢复后续做数据分析时也不会缺历史数据。所有表引擎用 InnoDB字符集用 utf8mb4能完整支持中文和 emoji排序规则选 utf8mb4_general_ci。5. 前后端对接中的关键实现5.1 统一返回结构与异常处理前后端分离最大的痛点是各写各的返回格式前端拿到的数据结构五花八门。这套系统里所有接口统一返回 Result 对象包含 code、message、data 三个字段。前端拿到结果后先判断 code 是不是 200再做后续处理。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT error(int code, String message) { ResultT result new Result(); result.code code; result.message message; return result; } }业务异常统一抛自定义异常由全局异常处理器捕获并转成规范结构返回。这样代码里不需要到处写 try catch业务逻辑干净很多。Controller 层的接口示例RestController RequestMapping(/admin/course) public class CourseController { Resource private CourseService courseService; GetMapping(/page) public ResultPageResultCourseVO page(CourseQuery query, RequestParam(defaultValue 1) int page, RequestParam(defaultValue 10) int size) { return Result.success(courseService.pageCourse(query, page, size)); } PostMapping public ResultVoid save(RequestBody Valid CourseDTO dto) { courseService.saveCourse(dto); return Result.success(null); } }5.2 MyBatis 动态 SQL 与分页课程列表查询需要支持标题模糊搜索、状态筛选、分类筛选这种场景非常适合 MyBatis 的动态 SQL。条件只有非空时才拼到 where 里查询更灵活。select idselectCoursePage resultTypecom.example.vo.CourseVO SELECT c.id, c.title, c.cover_url, c.price, c.status, c.create_time, l.name AS lecturer_name FROM course c LEFT JOIN lecturer l ON c.lecturer_id l.id where if testquery.title ! null and query.title ! AND c.title LIKE CONCAT(%, #{query.title}, %) /if if testquery.status ! null AND c.status #{query.status} /if if testquery.categoryId ! null AND c.category_id #{query.categoryId} /if /where ORDER BY c.create_time DESC /select分页这块我直接用 PageHelper它能在不改 SQL 的情况下自动拼接 limit 语句并返回总记录数。如果不想引入额外依赖也可以手写 LIMIT #{offset}, #{size}我这里选择前者省事。5.3 Vue 端 token、路由守卫与接口封装前端登录成功后拿到 token存到 localStorage。axios 请求拦截器在每次请求时自动带上 Authorization 头响应拦截器判断返回值中的 code如果遇到 401 则跳转到登录页。这样业务代码里只需要关心数据不需要每个页面都处理登录失效。import axios from axios const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res.data }, error { ElMessage.error(网络异常请稍后重试) return Promise.reject(error) } )路由守卫做登录拦截没有 token任何页面都进不去强制跳到登录页已登录用户访问登录页自动回到首页。这块逻辑不多但对系统体验影响很大。5.4 文件上传从本地目录到 MinIO 的演进思路讲师头像、课程封面、章节课件都要传文件。第一版我在本地磁盘指定一个 upload 目录用绝对路径映射成 URL 返回给前端。本地方式够用但项目部署到多台机器时会遇到文件不同步的问题。我的建议是二开时把存储层抽出来接口保持一致性内部实现可以切换到 MinIO 或云对象存储。MinIO 部署简单、兼容 S3 接口局域网私有化部署场景下很合适。从主题词里看到有人关注 minio 集成到 springboot这个方向下篇我可以单独写先在系统里预留了存储策略接口。6. 从源码到跑起来部署步骤与排坑记录6.1 本地启动全过程拿到源码后按下面步骤走前提是环境已经装好 JDK 8/11、Maven 3.6、Node 16、MySQL 5.7 或 8.0。创建数据库 edu_online执行 doc/sql 目录下的初始化脚本导入基础表结构和初始数据。修改后端 application.yml 里的数据库连接信息改成你自己的账号密码。如果需要本地文件上传配置 upload.path 为本地绝对路径比如D:/edu-upload。后端启动有两种方式开发环境直接在 IDEA 里运行 Application 主类生产环境用mvn clean package -DskipTests打包然后java -jar执行 jar 包。前端进入 edu-admin-web 目录先执行npm install安装依赖再执行npm run dev启动开发服务器。默认后端端口是 8080前端开发服务器端口是 5173。前端配置了代理/api开头的请求会自动转发到后端 8080不需要额外处理跨域。6.2 最容易踩的五个坑第一个坑是 MySQL 连接失败。MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver不是老版本的com.mysql.jdbc.Driver。连接 URL 里一定要带serverTimezoneAsia/Shanghai否则报时区错误。很多新手在这一步卡很久。第二个坑是 MyBatis 映射不上。数据库字段是下划线风格Java 属性是驼峰风格如果没在 application.yml 里开启 map-underscore-to-camel-case所有字段都为 null。这个问题检查起来隐蔽我基本每次搭项目都会确认一下这个配置。第三个坑是依赖下载慢。Maven 和 npm 默认都走国外源建议改成国内镜像。Maven 的 settings.xml 配置阿里云镜像npm 用npm config set registry切换源。否则一上来下载依赖就要等很久。第四个坑是文件上传的目录不存在。上传接口报错说文件路径找不到解决方法是启动时自动检测目录不存在就创建。我封装了一个 FileStorageService初始化时做目录检查代码更健壮。第五个坑是前端代理配错。如果直接用 axios 请求http://localhost:8080会出现跨域正确做法是通过 Vite 代理把/api转发到后端。代理配好之后前端代码里尽量不写完整域名方便后续换环境。6.3 二次开发方向建议这套系统的定位是业务骨架真正落地时一定会加功能。我建议优先扩展这三个方向。第一个是视频播放。课程章节里的视频地址目前是普通 URL二开时可以接阿里云 VOD、腾讯云点播或者自己部署一套视频播放服务配合防盗链签名。热词里有人提到 vue 播放 m3u8这个方向可以直接在现有课程详情页上加一个播放器组件。第二个是小程序端。后端接口如果已经按 RESTful 风格提供小程序端只要封装 request 工具登录改成微信授权登录课程列表、订单、我的课程这些页面都可以复用。第三个是营销功能。优惠券、拼团、分销、限时秒杀这些功能都需要在订单模块基础上扩展。订单表里加几个扩展字段比如优惠券 ID、活动 ID、分销人 ID再配合结算逻辑就能串起来。系统最关键的点不在某个页面多好看而是数据关系设计得够不够清楚。课程、订单、用户、权限这四条线理清了加功能只是时间问题。这套系统我在培训机构的环境里实际跑过几个月课程量、订单量上来之后整体比较稳定没有出现明显的性能瓶颈。拿到源码后建议别急着加新功能先把课程发布、下单、支付回调、开通课程权限这条主链路完整跑通再考虑直播、分销、小程序这些扩展业务系统最怕的就是主链路没吃透就堆功能。后续如果时间允许我会把小程序端对接和视频播放地址的权限签名单独写一篇。部署或者二次开发过程中遇到问题可以在评论区留言我看到了会尽量回复。
返回列表