
1. 项目整体架构解析与技术选型逻辑1.1 在线教育平台的核心业务场景与技术难题做在线教育平台管理系统这件事最早是因为一个做职业技能培训的朋友找到我。他们的线下招生一年不如一年想搞一套线上课程点播系统让学员能在手机、电脑上随时看课。需求看起来很清晰但真正落地的时候你会发现一套“企业级”系统要扛住的可不只是几个页面。一个完整的在线教育平台核心业务链路大概是这样用户注册登录-浏览课程/搜索课程-试听/查看详情-下单购买-在线播放视频-记录学习进度-评价打分-讲师端上传课程-运营端做数据统计。这套链路里藏着几个真问题用户认证和安全控制。课程内容是付费资产不能让没购买的人直接拿播放地址白嫖。视频播放体验。在线教育最核心的是视频但普通的MP4直出在浏览器兼容性、防盗链、大并发下都扛不住。目前主流的方案是转码成m3u8切片走HLS流媒体协议。订单和支付的一致性问题。用户下单、支付回调、订单状态更新这一套如果没处理好就会出现“钱扣了但课程没开通”这种致命事故。运营侧的数据统计和内容管理。课程分类、讲师管理、销售数据、学习时长统计都需要一个清晰的后台支撑。再看技术侧基于SpringBootVueMyBatisMySQL这套组合几个难点要单独拎出来讲MyBatis怎么设计才能既灵活又不失控MySQL在大表场景下怎么做索引和优化Vue这边怎么优雅地接入m3u8流媒体播放这些我都会在后面的章节展开细说。1.2 技术栈选型SpringBootVueMyBatisMySQL的优势在哪里这套技术栈在2024年依然非常能打虽然市面上出现了很多新东西但对企业级项目来说稳定、招人容易、生态成熟才是最重要的。后端SpringBoot现在做Java web开发基本不会有人从零搭SSH了SpringBoot把繁琐的配置全部自动化内嵌Tomcatjava -jar就能启动。企业级项目的核心诉求是快速交付和稳定运行SpringBoot恰好都满足。我这里推荐选择SpringBoot 2.7.x分支不要一上来就追SpringBoot 3.x因为3.x基于Jakarta EE和Java 17很多老依赖还没适配完踩坑成本比较高。这个我后面有一整节专门讲。前端Vue无论Vue2还是Vue3组件化开发带来的维护优势是jQuery时代没法比的。在线教育平台页面交互复杂课程列表筛选、视频播放器、后台数据大屏组件化能把逻辑拆得很干净。Vue的响应式数据绑定和生态Vue Router、Pinia/Vuex、Element UI让开发效率非常高。这个项目我会以Vue3 Vite作为推荐基线但也兼容Vue2的迁移理解。持久层MyBatis很多人纠结MyBatis和JPA到底选哪个。在线教育这种偏互联网风格的业务系统SQL经常需要复杂join、统计报表、动态条件查询MyBatis能把SQL完全控制在自己手里便于DBA优化。配合MyBatis-Plus单表CRUD也不用写SQL两者结合既灵活又高效。数据库MySQL商业软件照搬Oracle的成本太高开源方案里MySQL是当之无愧的王者。InnoDB引擎支持事务行锁对于订单支付这种强一致性场景完全够用。配合主从复制、读写分离几万学员的规模也能扛得住。这套组合还有一个隐性的优势会的人多。无论招人还是找资料网上大把现成的解决方案和源码可以参考。踩坑有前车之鉴不会被卡死在某个冷门问题上。1.3 前后端分离架构与接口设计规范在线教育平台的架构我没有采用传统服务端渲染的方式而是严格的前后端分离。后端只提供JSON接口前端Vue独立部署两者通过HTTP/RESTful协议通信。这样做的直接好处是前后端团队可以并行开发而且后端接口可以被PC端、移动端H5、小程序共用一鱼多吃。接口设计上有几个规范我强烈建议你一开始就定好不然后期重构会哭统一返回格式。后端所有接口统一返回{ code: 200, message: success, data: {} }这样的结构前端axios在拦截器里统一处理而不是每个接口单独写状态判断。RESTful风格。比如GET /api/course/{id}获取课程详情POST /api/order创建订单PUT /api/course/{id}更新课程。语义清晰也好维护。JWT无状态认证。用户登录成功后后端签发一个JWT Token前端每次请求在请求头里带上Authorization: Bearer token。后端通过拦截器解析Token识别当前用户身份。统一异常处理。用RestControllerAdvice全局捕获业务异常返回统一的错误提示避免把堆栈信息直接暴露给前端。这套架构跑起来之后最大的感受就是调试问题不再需要前后端互相扯皮。用Swaggerspringdoc自动生成接口文档前端对着文档就能自己Mock数据开发联调效率提升非常明显。2. 后端核心模块设计与关键实现2.1 用户认证与权限控制方案在线教育平台的用户体系要区分三类角色学员、讲师、管理员。三者的权限边界完全不同必须在后端做好硬校验不能只靠前端隐藏按钮来实现控制。我用的是Spring Security JWT Redis的组合方案。流程拆开是这样的用户发起登录请求后端校验用户名密码。校验通过后生成JWT Token将用户基本信息用户ID、角色放入Token的claims中。Token返回给前端前端存到localStorage或Pinia状态里。后续请求携带Token后端通过OncePerRequestFilter解析Token将用户信息放入SecurityContext。Redis里再存一份Token对应关系用于实现“强制下线”和“同一账号单点登录”等功能。权限控制这块我在Spring Security里配置了三套拦截规则http.authorizeHttpRequests() // 无需登录即可访问的接口 .requestMatchers(/api/auth/**, /api/course/list, /api/course/{id}).permitAll() // 需要登录的接口 .requestMatchers(/api/order/**, /api/study/**).authenticated() // 需要讲师角色的接口 .requestMatchers(/api/lecturer/**).hasRole(LECTURER) // 需要管理员角色 .requestMatchers(/api/admin/**).hasRole(ADMIN);这里有一个细节值得注意/api/course/{id}虽然是公开接口但不要在该接口中直接返回视频的真实播放地址。我通常只返回视频相关的元信息真正的播放地址需要前端再携带Token去申请后端根据当前用户是否已购买来决定是否发放。这样就算别人拿到了课程详情接口的返回值也拿不到真正的视频流。关于密码安全千万不要明文存储用户密码。我用的是BCryptPasswordEncoder做哈希每次加密结果都带随机盐即使两个用户密码一样密文也不同。注册的时候入库登录的时候校验整个链路不需要自己手写任何加盐逻辑。2.2 课程管理、视频点播与m3u8流媒体播放方案课程模块是这个平台的核心资产模块。讲师上传原始视频可能是几个G的MP4系统转码成HLS切片m3u8 ts文件学员端通过Vue播放器拉流播放。这里有两条路线可选取决于你有没有自己的转码服务器。方案一自建FFmpeg转码服务。在Linux服务器上安装FFmpeg把上传的MP4文件转成m3u8索引文件加ts切片ffmpeg -i input.mp4 -codec copy -bsf:v h264_mp4toannexb -start_number 0 -hls_time 10 -hls_list_size 0 -f hls output.m3u8参数说明-hls_time 10表示每个ts切片时长为10秒-hls_list_size 0表示不限制m3u8索引中最多的ts条目数。转码完成后把m3u8和ts文件上传到对象存储OSS或者MinIO数据库记录播放地址即可。自建方案的好处是成本和数据自主可控缺点是要自己处理转码队列和服务器负载。方案二使用云厂商的视频处理服务。上传原始视频到OSS后触发云函数或媒体处理服务自动转码完成后回调通知你的后端更新课程状态。这个方案省心按量付费适合不想运维转码集群的团队。两种方案殊途同归最终落到数据库表里就是两个关键字段video_url原始文件地址用于后台预览和hls_urlm3u8播放地址用于学员端播放。HLS流媒体的好处很明显支持码率自适应网络差时自动降低清晰度切片是独立的ts文件可以做CDN缓存播放压力大时能横向扩展。而直出MP4在高并发下对带宽和内存都是巨大压力所以现在基本没人那么干了。2.3 MyBatis持久层设计、缓存机制与批量操作优化MyBatis在整个项目中扮演的角色是把Java对象和数据库记录映射起来。我先说几个设计上的建议都是实操总结。第一个建议SQL写XML还是注解复杂SQL建议放到XML文件里。Java注解拼复杂动态SQL可读性太差而且不方便DBA直接review。XML里可以用sql标签抽公共字段用where和if处理动态条件用foreach做批量操作。当然单表CRUD用MyBatis-Plus的BaseMapper就够了没有必要手写。第二个建议缓存设计要区分场景。MyBatis自带一级缓存和二级缓存。一级缓存是SqlSession级别的默认开启同一个SqlSession里执行相同SQL会命中缓存。但要注意Spring整合MyBatis后每次查询都新建SqlSession所以一级缓存实际作用不大。二级缓存是Mapper级别的默认关闭开启后多个SqlSession可以共享。不过我的建议是二级缓存可开但不要依赖它。在线教育平台的课程列表、分类信息这类改动不频繁的数据用二级缓存很合适。但涉及订单、学习记录这种高频更新的数据开了缓存反而容易出现脏数据。更稳妥的做法是引入独立的Redis做业务级缓存把控制权握在自己手里。第三个建议批量写操作慎用foreach拼接单条SQL。很多新手喜欢用这种方式插一批数据insert idbatchInsert INSERT INTO course_tag (course_id, tag_id) VALUES foreach collectiontagIds itemtagId separator, (#{courseId}, #{tagId}) /foreach /insert数据量小的时候没问题但单条SQL过于庞大时MySQL的max_allowed_packet可能扛不住而且大批量SQL在高并发下会长时间占用数据库连接。我更推荐在Java代码里用ExecutorType.BATCH模式分批执行每500条提交一次既稳定又快。批量更新同理电商系统里常见的批量上下架、批量改库存用ExecutorType.BATCH模式能大幅减少数据库连接次数。我在项目中实测过一次性更新5000条数据BATCH模式比逐条update快10倍以上。第四个建议动态查询用where不要拼where 11。虽然where 11加if也能跑但SQL语句里总带着一个永真条件对MySQL优化器不友好也不美观。MyBatis的where标签会自动去掉第一个多余的AND/OR写起来干净得多。3. MySQL数据库设计、存储过程与部署实战3.1 核心表结构设计与索引规划在线教育平台的数据库设计我按照业务域拆分成几大块用户域user、user_role、课程域course、course_section、video、course_tag、交易域order、order_item、cart、payment_log、学习域study_record、favorite、course_comment。这里挑几个核心表重点说。用户表user字段类型说明idbigint主键自增usernamevarchar(50)用户名唯一索引passwordvarchar(100)BCrypt哈希后的密码phonevarchar(20)手机号可用于登录avatar_urlvarchar(255)头像地址role_typetinyint角色类型0学员 1讲师 2管理员statustinyint账号状态0正常 1禁用create_timedatetime注册时间课程表course字段类型说明idbigint主键titlevarchar(100)课程标题subtitlevarchar(200)课程副标题category_idbigint分类IDlecturer_idbigint讲师IDcover_urlvarchar(255)课程封面pricedecimal(10,2)售价original_pricedecimal(10,2)原价statustinyint上架状态sales_countint销量冗余字段detail_htmllongtext课程详情富文本这张表是查询最频繁的表有两个索引设计重点一是category_id单独建索引支撑分类筛选二是(status, sales_count)建联合索引支撑“上架且按销量排序”的查询场景。因为学员打开首页最典型的行为就是看“热门课程榜”这条SQL要能走索引。学习记录表study_record字段类型说明idbigint主键user_idbigint用户IDcourse_idbigint课程IDvideo_idbigint视频IDprogressint观看进度秒watch_durationint累计观看时长秒update_timedatetime最后观看时间这张表数据量增长很快建议复合唯一索引(user_id, video_id)保证一个用户对一条视频只保留一条记录进度更新走ON DUPLICATE KEY UPDATE避免先查再改。表设计出来后建议配合定时存档和分表策略学习记录单表超过千万级后可按 user_id 做水平分表。3.2 MySQL存储过程编写与复杂业务查询优化存储过程在这个项目里的主要场景有两大类一类是统计报表一类是定时任务处理。举一个实际的例子运营后台需要一个“每日销售汇总”报表统计当天新增订单数、成交总额、退款金额等指标。如果每次都在Java代码里聚合查询多个SQL往返的性能很差。我写了一个存储过程来实现DELIMITER $$ CREATE PROCEDURE sp_daily_sales_stat(IN stat_date DATE) BEGIN INSERT INTO daily_sales_stat ( stat_date, order_count, total_amount, refund_count, refund_amount ) SELECT stat_date, COUNT(DISTINCT id), COALESCE(SUM(total_amount), 0), COUNT(DISTINCT refund_order_id), COALESCE(SUM(refund_amount), 0) FROM order_info WHERE pay_time stat_date AND pay_time DATE_ADD(stat_date, INTERVAL 1 DAY); END$$ DELIMITER ;用存储过程的好处是逻辑集中在数据库端Java代码只需要调用一次减少应用与数据库之间的交互开销。但也要注意存储过程不要写太复杂的业务逻辑涉及外部接口调用、消息通知的东西还是放到Java服务里更合适。存储过程适合做纯数据库内部的数据加工和统计。另外再说几个MySQL查询优化的经验避免SELECT *只查需要的字段。虽然写起来方便但MySQL的InnoDB是聚簇索引组织表SELECT *会拖慢范围查询的性能网络传输也浪费。分页查询深翻页时LIMIT 100000, 20会全表扫描前10万条再丢弃性能很低。改用“延迟关联”或者基于ID游标的方式-- 延迟关联 SELECT c.* FROM course c INNER JOIN (SELECT id FROM course ORDER BY id LIMIT 100000, 20) t ON c.id t.id;联合索引遵循最左前缀原则比如(status, sales_count)能同时支撑WHERE status1 ORDER BY sales_count DESC和WHERE status1但支撑不了单独ORDER BY sales_count。3.3 MySQL数据库主从复制、备份迁移与高可用实践在线教育平台上线后数据库的单点风险必须解决。我的实践经验是采用一主一从再加每日全量备份的组合方案。MySQL主从复制的配置核心步骤主库开启binlog日志[mysqld] log-binmysql-bin server-id1从库配置[mysqld] server-id2 relay-logrelay-bin在从库执行主从关联CHANGE MASTER TO MASTER_HOST主库IP, MASTER_USERrepl, MASTER_PASSWORD密码, MASTER_LOG_FILEmysql-bin.000001, MASTER_LOG_POS位置; START SLAVE;主从搭建好之后业务上可以做读写分离写操作走主库统计报表、课程列表等读多写少的接口走从库。在SpringBoot中配置动态数据源或者使用ShardingSphere这类中间件最简单的做法是配置MyBatis多个数据源报表查询走从库路由。备份这块我使用mysqldump每日凌晨做全量备份binlog实时归档到备份服务器。这样一旦主库故障可以用全量备份加binlog恢复最近的数据数据丢失控制在分钟级以内。这里有个容易忽略的地方MySQL 8.0默认密码加密方式是caching_sha2_password很多旧版客户端连接时会报认证失败。连接串里的驱动版本要对应升级JDBC驱动至少用8.0.x。另外时区参数serverTimezoneAsia/Shanghai必须显式配置不然Java程序连接数据库会出现时间和本地差8小时的问题我当时排查了大半天才定位到是时区问题。4. 前端Vue工程搭建与视频播放器集成4.1 Vue工程结构、路由管理与请求封装前端部分我推荐采用Vue3 Vite Pinia Element Plus这套组合。相比Vue2 WebpackVite的开发服务器启动速度快太多了大型项目几乎是秒开热更新也流畅开发和调试体验完全不在一个量级。工程结构按模块划分src/ ├── api/ # 接口请求模块 │ ├── auth.js # 认证相关接口 │ ├── course.js # 课程相关接口 │ └── order.js # 订单相关接口 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── store/ # Pinia状态管理 ├── views/ # 页面组件 │ ├── course/ # 课程模块 │ ├── user/ # 用户中心 │ └── admin/ # 管理后台 └── utils/ # 工具函数路由管理需要根据角色做动态路由。我实现了路由前置守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else if (to.meta.role !hasRole(to.meta.role)) { next(/403); } else { next(); } });请求封装方面axios拦截器统一处理Token注入和错误提示axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); axios.interceptors.response.use( res { const { code, message } res.data; if (code 200) return res.data.data; ElMessage.error(message); return Promise.reject(new Error(message)); }, err { if (err.response err.response.status 401) { localStorage.removeItem(token); router.push(/login); } return Promise.reject(err); } );这里有一个实际经验Token过期时间不要设置太长。我见过很多项目把JWT过期时间设成7天甚至30天安全风险极大。建议Token有效期2小时左右前端在401后自动跳登录页用户重新登录一次的成本是可以接受的。4.2 m3u8视频流播放hls.js与Video.js的踩坑与实践前端播放m3u8是很多初学者的拦路虎因为HLS协议并不是所有浏览器原生支持的。Chrome、Firefox、Edge这些主流浏览器都不能直接播放m3u8只有Safari原生支持。好在有hls.js这个库能在支持Media Source ExtensionsMSE的浏览器上把HLS流改成MP4片段再播放。我的集成方式是封装一个播放器组件底层用hls.js同时兼容原生支持的场景import Hls from hls.js; export function playM3u8(videoElement, url) { if (Hls.isSupported()) { const hls new Hls({ maxBufferLength: 30, // 最大缓冲时长30秒 maxBufferSize: 60 * 1000 * 1000, // 最大缓冲60MB lowLatencyMode: true, }); hls.loadSource(url); hls.attachMedia(videoElement); return hls; } else if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // Safari原生支持 videoElement.src url; } }播放器选型上Video.js生态成熟插件丰富适合快速开发。它内部也集成了hls.js的能力只需要引入videojs-contrib-hls插件即可。如果不想被框架束缚直接用原生video加hls.js也完全够用。实际项目中踩过几个播放相关的坑提醒大家注意跨域问题。如果m3u8放在OSS或CDN上而OSS的CORS配置不允许前端域名访问播放会直接失败。需要在OSS控制台配置CORS规则允许你的前端域名来源并允许GET请求和Range头。Autoplay策略。现代浏览器禁止带声音的视频自动播放用户进入课程页后必须手动点击播放按钮或者后端返回的播放地址需要支持静音自动播放再开启声音。产品层面要接受这个限制不能跟浏览器死磕。播放进度的保存时机。不要每秒钟都调后端保存进度太消耗接口。我的做法是每15秒上报一次并且在组件销毁前beforeUnmount和页面visibilitychange变为hidden时强制上报一次保证用户切后台时进度不丢。4.3 前端打包部署与常见布局兼容问题前端开发完要部署上线这里有个高频问题Vue项目打包后白屏或者图片、字体全部404。根因基本出在静态资源引用路径上。Vite默认构建产出的资源路径是绝对路径/assets/xxx.js如果你的站点部署在域名根路径下没任何问题但部署在子路径下比如https://example.com/edu/这些绝对路径的请求就会全部404。解决办法是在vite.config.js里设置baseexport default defineConfig({ base: ./, // 使用相对路径适配子目录部署 // ...其他配置 });另外Vue Router如果启用了history模式在服务器端必须做重写配置。比如Nginx要把所有不存在的路径都重写到index.htmllocation / { try_files $uri $uri/ /index.html; }如果不配这个用户访问/course/123时直接刷新页面Nginx找不到/course/123这个真实文件就会报404。这个坑非常隐蔽很多人只在路由跳转时测试没在刷新场景下测试上线就被用户反馈“页面一点就白屏”。CSS布局兼容问题在后台系统中也很常见。Element Plus的表格在数据量多时固定列和滚动区域会出现错位通常是因为列的宽度计算依赖父容器的加载状态。简单有效的做法是调用table.doLayout()重新计算布局或者在v-if渲染数据后就执行。5. 常见问题排查与避坑大全5.1 MyBatis中单个数字字符比较的经典坑MyBatis在写XML动态SQL时遇到if teststatus 1这种单个字符判断很多人会踩一个非常隐蔽的坑。原因是MyBatis的OGNL表达式解析中单引号内是单个字符时会被当成Java里的char类型而不是String。比较时char和String的类型不同结果永远是falseSQL里的条件永远不会生效。示例if teststatus 1 AND status 1 /if这段代码中如果status传进来是字符串1永远进不了if分支。解决方式有两种用双引号包字符串if teststatus 1 AND status 1 /if用toString()方法if teststatus 1.toString() AND status 1 /if这个坑我第一次遇到时排查了将近一小时因为代码逻辑看着完全没问题。后来翻MyBatis源码才确认是OGNL的类型判断机制导致的。现在我看到这种单字符比较就会条件反射地检查引号形式。5.2 SpringBoot版本过高引发的依赖冲突SpringBoot的版本选择很讲究。SpringBoot 3.x发布后很多新人直接创建最新版本项目结果集成第三方组件时接连碰壁。比如SpringBoot 3.x强制要求Java 17及以上很多老项目的JDK还是8而且3.x的javax包改成了jakarta包所有引用javax.servlet、javax.annotation的旧依赖全部报编译错误。我自己项目里踩过一个具体问题SpringBoot 3.2集成某个版本的MyBatis-Spring-Boot-Starter时启动直接报NoSuchBeanDefinitionException查了官方文档才发现是需要升级Starter版本到3.0.3以上而且必须额外引入mybatis-spring3.0以上的依赖。给新项目的建议是如果你的JDK是8或11直接用SpringBoot 2.7.x这是2.x的最后一个稳定分支各种坑都被填平了。如果你的项目从零开始且能用JDK 17可以上SpringBoot 3.x但第三方依赖要逐一比对兼容矩阵特别是MyBatis、ShardingSphere、Redis客户端这类的底层依赖。IDEA的Spring Initializr创建项目时默认会选SpringBoot 3.x创建完需要手动改pom版本。这个细节很多新手没注意项目一跑就报错。5.3 MySQL安装配置与连接常见问题MySQL的安装和配置看似简单但网上教程鱼龙混杂很多新手照着教程装完Java程序一连接就报错。我归纳几个高频问题服务无法启动。Windows下最常见的原因是3306端口被占用。用netstat -ano | findstr 3306查端口占用找到占用进程后结束它或者改MySQL默认端口。连接报Communications link failure。大概率是连接串没写useSSLfalse或者时区参数没配。推荐标准连接串jdbc:mysql://localhost:3306/edu?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8mb4root用户密码登录不了。MySQL 8.0默认有个随机密码在安装日志里。如果忘了用mysqld --initialize-insecure重新初始化一个无密码root用户再登录修改。字符集乱码。建库时一定要指定字符集CREATE DATABASE edu DEFAULT CHARSET utf8mb4;。utf8mb4比utf8多支持四字节的emoji表情使用范围更广。5.4 Vue打包后布局异常的处理套路Vue项目打包后布局异常除了路径问题之外还有几个常见原因。一个是CSS样式顺序错乱。开发环境加载样式是异步的打包后样式合并为一个文件如果不同组件之间类名冲突就会出现“开发环境好好的打包后样式变乱了”。解决办法是给组件加scoped属性隔离样式或者使用CSS Modules。另一个是完整参数中后端返回的时间格式。如果后端返回的是2024-06-01T10:20:30这种ISO格式前端直接用会显示不正确需要做格式化处理。我一般在项目里封装一个formatDate工具统一处理所有时间显示。还有一个容易忽略的是Element Plus按需引入。如果手动配置了按需引入但漏掉了某个组件打包后这个组件直接不渲染。排查方法是在控制台看有没有对应的组件警告或者干脆先全量引入等稳定后再做性能优化。6. 一套源码怎么从“能跑”变成“能交付”6.1 项目初始化与目录规划建议很多刚接触SpringBootVue的开发者从网上下了一套源码跑起来之后只能对着界面感叹“好强”但真要改需求就无从下手。根本原因是项目缺少清晰的工程化约定。我建议初始化项目时不管代码量多少先把后端包结构定下来com.example.edu ├── config/ # 配置类Redis、Security、WebMvc ├── controller/ # 接口层 ├── service/ # 业务层 ├── mapper/ # 数据访问层 ├── entity/ # 数据库实体 ├── dto/ # 请求/响应DTO ├── common/ # 通用类返回结果、异常处理 └── util/ # 工具类这样分层的核心动机是依赖方向是从上到下单向的controller依赖serviceservice依赖mapper每一层都只在它该在的位置做事情。很多烂项目的通病就是controller里直接写SQL逻辑后面想复用都拆不动。前端后端统一加RestController(/api/v1)这种前缀方便做网关路由和版本管理。我建议一上来就带/api/v1而不是裸路径后续升级接口时能平滑过渡不用改前端就能实现灰度切换。6.2 日志规范、统一异常与线上问题追踪企业级系统和一个demo最大的区别之一是对可观测性的重视程度。日志不规范线上出了问题根本没法查。我的日志配置建议使用logback-spring.xml按天滚动生成日志文件区分error级别和info级别appender nameERROR_FILE classch.qos.logback.core.rolling.RollingFileAppender filelogs/edu-error.log/file filter classch.qos.logback.classic.filter.LevelFilter levelERROR/level onMatchACCEPT/onMatch onMismatchDENY/onMismatch /filter /appender每个请求打印出统一的访问日志请求路径、请求参数、耗时、用户ID。用拦截器实现方便做耗时分析。异常日志必须把堆栈打全配合全局异常处理返回友好提示给前端但堆栈只在服务端保留。线上排查问题我一般流程是先看error日志根据异常堆栈缩小范围再看对应接口的访问日志确认请求参数是否正常最后看数据库慢查询日志确定是不是SQL问题。这个流程走下来90%的问题都能定位。6.3 部署上线流程与常见运维技巧源码交付后的部署是“最后一公里”也是很多项目翻车的地方。我推荐一个简单可靠的部署方案前端构建产物dist/用Nginx托管配置Gzip压缩和静态缓存。后端SpringBoot打成jar包用systemd管理进程支持开机自启和崩溃重启。数据库单独服务器或者RDS避免和应用抢资源。Redis用于缓存和Token管理。Nginx配置静态文件缓存的关键字段server { listen 80; server_name edu.example.com; location / { root /var/www/edu; index index.html; try_files $uri $uri/ /index.html; gzip on; gzip_types text/css application/javascript application/json; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里有个小细节location /要配置try_files解决Vue路由的刷新404问题location /api/设置反向代理到后端。静态资源类的请求图片、js、css会自动命中location /下的缓存逻辑后端接口请求走location /api/互不干扰。写到这里我个人的经验总结是一套在线教育系统源码的价值不在代码本身而在于你能否理解每个技术选型和设计背后的取舍。SpringBootVueMyBatisMySQL这套组合之所以能成为企业级项目的主流配置是因为它在开发效率、运行稳定、成本控制之间找到了一个非常平衡的点。这套系统后续还可以往直播课、题库系统、企业内训定制等方向扩展底层的用户、订单、课程体系都能直接复用前期把架构设计得干净一些后面扩展会省掉很多返工的痛苦。