
1. 项目背景与技术选型思路做这个前后端分离智能学习平台系统其实是源于我自己一个很直接的痛点市面上很多在线学习系统要么是前后端耦合在一起的老古董改个页面都得重启整个服务要么就是功能堆得很满但对学习这件事本身缺少追踪。所以我花了大概三周时间用 SpringBoot Vue MyBatis MySQL 这套主流技术栈从零搭了一个前后端分离的学习平台包含课程中心、学习计划、进度追踪、笔记管理、在线测验等核心模块。这个项目既能给应届生当毕业设计也适合想系统了解前后端分离项目怎么落地的开发者参考代码量不算大但该有的东西都有。之所以强调前后端分离是因为这个架构在现在的企业开发里几乎是标配了。前端团队和后端团队可以并行开发后端只负责提供稳定的接口前端只关心页面交互和状态管理双方靠一份接口文档协作效率比传统的单体模板渲染高太多。我在这套系统里把两部分的边界拆得比较清楚后端只暴露 JSON 接口前端纯静态资源单独部署这也是你以后进公司、做外包项目时最常遇到的协作模式。1.1 为什么选前后端分离架构你可以把前后端分离想成餐厅的前厅和后厨。前厅前端负责点菜、上菜、展示环境后厨后端负责备料、炒菜、管理库存两者之间靠一张菜单API 接口文档约定。以前那种前后端耦合的项目相当于前厅后厨不分家改个菜单牌得惊动后厨所有人现在把职责拆开前端调接口就像客人对着菜单点菜想换菜品样式不影响后厨做菜。落到工程上好处非常明确前端资源由 Nginx 这类静态服务器承载后端可以独立扩容接口能同时供给 Web 端、移动端甚至第三方系统复用系统上线后的维护边界也清晰很多。当然前后端分离也不是没有代价。最直接的代价就是联调成本上来了接口返回什么、字段命名是什么、错误状态怎么约定都得提前定清楚否则就会出现前端说接不到数据后端说接口明明返回了的尴尬局面。我在这个项目里用了一套统一的结果封装和状态码约定来解决这个问题后面后端部分会详细说。1.2 技术栈为什么是 SpringBoot Vue MyBatis MySQL技术选型这件事我一直坚持成熟优先、够用就好。SpringBoot 不用多说它把 Spring 繁琐的配置全部自动化了内嵌容器打一个可执行 jar 就能跑是目前 Java 后端绝对的主流框架招聘市场上十个后端岗位有七个要求你会这个。Vue 在国产项目里的普及率非常高中文文档友好生态里像 Element UI / Element Plus 这类组件库直接拿来就能做后台管理系统对一个人开发全栈项目来说学习成本最低。MyBatis 虽然简单但它的 SQL 可控性非常强复杂的多表查询、动态条件都可以自己写 SQL 精准控制相比 JPA 那些自动生成的 SQL我更喜欢这种数据库里发生了什么我一清二楚的感觉。MySQL 则是免费、稳定、几乎每个后台岗位都要求熟悉的数据库对于学习平台这种中等数据量级的业务完全够用。1.3 智能学习平台的模块设计与功能拆分这个智能主要体现在两点一是学习进度自动追踪用户看到哪个课程、哪个小节后端都记录在案下次登录可以直接续看二是基于学习数据给用户生成简单的学习计划和掌握度分析比如你学了哪些课、笔记写了多少、测验得分如何首页看板会把数据汇总展示出来。模块拆成六个主业务域我整理成表格放在下面开发的时候按这个颗粒度去建表写接口基本不会乱。模块核心功能涉及表认证中心登录、注册、JWT 发放与校验user课程中心课程分类、课程列表、课程详情course, course_section, course_lesson学习中心学习计划、进度上报、续播定位learn_plan, learn_progress, learn_record笔记社区笔记新增、列表查询、学习圈互动note, comment测验中心试题管理、在线答题、成绩统计exam, exam_question, exam_result数据看板学习时长、课程完成率、测验趋势聚合查询以上多张表模块划分好之后开发节奏就非常清晰了先做后端建表写接口再写前端页面调接口最后统一联调和部署。下面我先从后端讲起这是整个项目的地基。2. 后端SpringBoot MyBatis MySQL 核心实现2.1 项目分层设计与分包规范我见过不少初学者把 Controller 里面直接写 SQL 逻辑然后看着像能跑但一遇到需求变更就到处改乱成一锅粥。我的习惯是严格分层Controller 只做参数接收和结果转发Service 层放业务规则Mapper 层只写数据库访问Entity 对应表结构另外单独建 config、exception、util 包来处理公共逻辑。这样分包以后每一个类的职责一眼就能看出来排查问题的时候不用把整个项目翻一遍。比如用户提交一条学习进度Controller 先接收 userId、courseId、lessonId、progress 这些参数调用 Service 层的service.reportProgress()方法业务规则在这里判断是新增记录还是更新时间、是否需要更新学习计划状态。具体操作数据库的 SQL 写在 Mapper 接口和 XML 里Service 层拿到的只是 Mapper 的返回值。这种层层递进的结构在前任离职接手、或者项目迭代半年后再打开看都能很快定位问题比什么都挤在一起要省心得多。2.2 MyBatis 的实用配置与缓存机制如果你是用 SpringBoot 集成 MyBatis除了引入mybatis-spring-boot-starter之外有几项配置是必须注意的。首先是驼峰映射我习惯在 application.yml 里开启map-underscore-to-camel-case: true这样数据库字段learn_progress可以自动映射到 Java 属性learnProgress不用写一堆冗余的映射关系。其次是 SQL 日志打印开发阶段配置logging.level.com.example.mapper: debug可以直观看到每条语句的执行参数和耗时排查问题效率翻倍。再有就是连接池参数我这边用的是 HikariCPSpringBoot 2.x 之后默认就是它稍微把最大连接数和超时时间调一下就行。MyBatis 的缓存是个高频考点也是大家容易踩坑的地方。一级缓存默认开启它是 SqlSession 级别的同一个会话里执行两次相同 SQL 会命中缓存。但实际开发中很多人没意识到一个问题Spring 整合 MyBatis 之后每次执行完数据库操作SqlSessionTemplate 默认就会关闭会话导致一级缓存基本没有发挥作用。二级缓存是 namespace 级别的跨 SqlSession 共享需要在 Mapper XML 里配cache/标签实体类实现Serializable。不过我要提醒一句如果一张表被多个 Mapper 关联查询开启二级缓存很容易读到脏数据因为某个 Mapper 更新了数据其他 Mapper 命名空间里的缓存根本感知不到。所以在学习平台这种频繁读写、多表关联的业务里我干脆没有开启二级缓存让每次查询都落到真实数据上换来的是可靠性。2.3 动态 SQL 与条件不生效排查实战这个项目的课程筛选、测验筛选用到了大量动态 SQLif、where、foreach、choose这几个标签基本是天天用。动态 SQL 虽然好用但有一个全社会通病问题条件不生效。我排查过很多次最后发现绝大多数都是同一个原因——if test里面写的参数名字和 Mapper 接口里的Param名称对不上。举个例子如果你的接口方法是ListCourse selectByCondition(Param(categoryId) Integer categoryId)那 test 里面必须写categoryId ! null写category_id ! null在 XML 里根本没这个变量条件直接被忽略。还有一个特别容易犯的错误是字符串和数字的判空写法。当参数是 Integer 类型时只判断! null就够了不要去写! 否则在某些版本下会出现数值 0 被当成空值而丢失条件的情况。反过来字符串类型判断空串时必须前后拼and如果你懒得多加空格就会出现生成的 SQL 里出现AND关键字直接贴着上一个条件、或者在 where 子句后面第一个条件就是and ...这样的低级语法错误。动态 SQL 拼接字符串向来是最考验细心程度的环节我的习惯是写完每个if之后把控制台里打印出来的完整 SQL 复制到数据库客户端里跑一遍确认语义正确再继续往下写。2.4 学习进度表的设计与 SQL 优化学习进度是整个平台的核心数据之一表结构我设计得比较精简主键 id、用户 id、课程 id、小节 id、进度百分比、最后学习时间。为了让同一个用户对同一个小节只有一条进度记录这个约束在数据库层面就能保证我加了一个联合唯一索引UNIQUE KEY uk_user_lesson(user_id, lesson_id)。这样应用层就算重复上报进度数据库也会拒绝重复插入保证数据干净。查询当前用户的所有课程进度时我会用一个关联查询把课程名称、封面、最近学习小节都带出来这里涉及两张表联查我给外键字段都建了索引查询基本都在毫秒级。排序和分页也是要提前设计好的。列表页默认按最后学习时间倒序排时间字段建了索引避免 filesort。分页用的 LIMIT 加偏移如果数据量到了几十万深分页会慢下来这个阶段可以改成上一页最后一条记录的 ID这种游标分页或者直接用子查询延迟关联。另外在学习记录表上我做了轻量级汇总每隔几秒前端会静默上报一次播放位置如果每次都写一条记录表会很快膨胀。所以我采用存在即更新的策略通过ON DUPLICATE KEY UPDATE把同一用户同一节课的观看记录不断刷新这样表格始终只保留一份有效记录查询压力和存储压力都小很多。2.5 统一返回结果与全局异常处理前后端分离的项目里接口返回结构必须是稳定的否则前端处理异常时会疯掉。我封装了一个ResultT通用对象字段是code、message、data成功时 code 为 200业务异常时返回对应的业务码前端拿到之后根据这个消息提示用户。可能你会觉得加一层封装是麻烦但它保证了接口的稳定性也照顾了调用方的体验后端再怎么抛异常前端拿到的永远是同一个结构体。全局异常处理用的是RestControllerAdvice在类里面定义几个异常处理方法MethodArgumentNotValidException处理参数校验失败BusinessException处理我主动抛出的业务错误Exception兜底处理未知异常。有了这个机制代码里就不需要到处写 try-catch 了业务层遇到问题只管throw new BusinessException(课程不存在)外层拦截器统一转换。还有一点要注意兜底异常不要把堆栈直接返回给前端否则容易暴露内部细节只返回系统繁忙请稍后重试就好。2.6 JWT 认证与接口权限接口不能裸奔我用了无状态的 JWT 来做认证流程是用户登录成功后后端生成一个 token里面包含 userId 和用户角色设置过期时间然后返回给前端。前端把 token 存起来每次请求在请求头里带上后端用一个拦截器统一校验token 无效或者过期就直接返回 401。登录接口要加白名单注册接口也一样不能拦这个可以在 WebMvcConfigurer 里配置 excludePathPatterns把/api/auth/login、/api/auth/register排除掉其余接口全部校验。至于权限细分这个项目我分了两种角色普通用户和管理员。管理员可以操作课程管理、用户管理接口普通用户只能访问学习相关接口。拦截器校验 token 有效后把 userId 塞到请求上下文里Service 层从上下文取当前用户这样就不用每个接口都把 userId 当参数传进来省事不少。如果你以后想扩展成更复杂的 RBAC可以再引入权限表但现阶段 JWT 加角色判断已经能满足学习平台的绝大部分权限需求。3. 前端Vue 完整实现与关键细节3.1 工程初始化与环境配置前端用的是 Vue 3 加 Vite组件库选的 Element Plus路由用 vue-router状态管理用的 Pinia。创建项目的命令很简单npm create vitelatest选 Vue 模板然后安装依赖。但这里有个新手高频问题依赖安装失败或者特别慢。大多数情况是 npm 默认源在国外遇到网络波动就一直卡着。解决办法是切换成国内镜像源我用的是npm config set registry https://registry.npmmirror.com换完之后速度立竿见影。Vite 项目比 Vue CLI 写起来更轻快配置都在vite.config.js里。开发阶段需要解决跨域问题我会在配置里加一个 server.proxy 代理把请求/api开头的接口全部代理到后端的http://localhost:8080这样浏览器里看起来接口就是同源的完全避免了开发环境跨域的麻烦。Vite 的热更新速度很快改一行代码浏览器几乎瞬间刷新这也是我后来不愿回 Vue CLI 的主要原因之一。3.2 动态路由与权限控制后台系统最核心的一个体验是不同角色登录后看到的菜单不一样。实现方案有很多我选的是后端动态返回菜单数据前端通过router.addRoute()动态注入路由。流程是这样的用户登录成功后请求/api/user/menus拿菜单列表里面包含菜单名、路由路径、图标、组件路径等字段前端拿到后遍历解析成 RouteRecordRaw 数组然后逐个 addRoute 加进路由表同时生成侧边栏菜单。这里必须处理刷新页面路由丢失的问题否则刷新一下就白屏了。我的做法是在路由守卫beforeEach里加一个全局标志比如 Pinia 里存一个isRoutesLoaded。每次跳转前先判断如果已经登录但路由还没加载就先去调菜单接口重新生成路由再放行当前导航。还有一个细节是路由的 404 页面要放在最后兜底否则动态路由匹配不到的时候会被 404 直接吞掉。3.3 Axios 封装与请求拦截如果每个页面都自己拿 axios 发请求那重复代码会多到让人崩溃。我把 axios 封装成一个模块在请求拦截器里统一做四件事从 localStorage 读取 token 并塞到请求头、判断请求超时时间、请求开始时在页面上显示全局加载状态、请求结束时隐藏加载状态。响应拦截器则统一处理返回结果code 为 200 就直接返回 data 给业务层code 为 401 就清除本地登录态并跳转到登录页其他错误码弹出对应的错误提示。另外文件下载、表单提交和 JSON 请求在 axios 里的处理方式不一样我封装时把Content-Type和responseType做成可配置参数避免每次写重复代码。我见过很多项目封装的 axios 根本没法处理文件流后面遇到导出功能全是页面卡死这些都是提前规划就能避免的坑。3.4 核心页面课程列表、视频播放与学习进度上报课程列表页用到了滚动分页加载触底时请求下一页数据比传统的翻页体验好很多。卡片上展示课程封面、标题、学习人数用户点击进入课程详情页后能看到章节列表。播放页用的视频格式是 m3u8这是目前流媒体播放的主流格式但浏览器原生不支持需要引入 hls.js 来处理。具体步骤是通过Hls.isSupported()判断浏览器是否支持支持的话new Hls()然后加载播放地址attachMedia 到 video 元素上不支持就降级用video.canPlayType(application/vnd.apple.mpegurl)判断原生播放。播放器要上报进度给后端我用的是节流策略每隔 10 秒上报一次当前播放位置和进度百分比同时在页面卸载前再补报一次。这里有个容易忽略的点用户拖动进度条跳转播放时video 的timeupdate事件会高频触发如果不做节流一次拖拽可能触发几十个请求把后端打崩都不奇怪。我直接封装了一个reportProgress函数内部判断时间间隔超过阈值才真正调用接口实测下来请求量骤减。3.5 Vue 插槽与组件复用组件复用是 Vue 的最大优势之一而插槽slot则是让组件灵活性翻倍的关键。举一个非常实在的例子项目里的空状态提示组件我做一个通用组件主题是插槽方案默认插槽放一个缺省图标和文案但调用方如果需要在空状态里放一个去创建课程按钮那就通过具名插槽传进去。这样同一个组件可以适配多个业务场景不用复制粘贴一堆几乎相同的模板。还有表格里的操作列删除、编辑按钮在不同业务里的逻辑不同我会在封装的表格组件里开放一个operation插槽让父组件自己决定操作区域长什么样。用插槽的本质是把变化的部分交给调用方把公共的部分留在组件内部理解了这个思路你写组件的时候就不会再为了一个按钮去写一堆v-if分支了。3.6 前端常见的小需求速查有些需求项目里不一定用到但热搜词里很多人问我顺手记录一下。比如 Vue 里能不能显示 PDF答案是能简单场景用 iframe 直接嵌一个.pdf?tokenxxx地址就行但要控制播放进度、做水印之类的复杂场景就建议用 pdf.js 自己渲染。又比如内容折叠展开Element Plus 自带 collapse 组件但如果只是简单文字展开收起用布尔变量加 v-show 就够了不用上组件。再比如动态路由和 slot 的掌握是面试官特别爱问的 Vue 进阶点建议平时动手实操一下光看文档记不住。4. 前后端联调与排查实录4.1 跨域问题的来龙去脉联调阶段遇到最多的就是跨域问题。开发环境我用 Vite 代理生产环境我用 Nginx 代理所以从原理上讲这两个阶段都不会有真正的跨域。但如果你把前端和后端部署在两个不同的端口或域名下又没有做任何代理和跨域处理那浏览器就一定会拦截响应报Access-Control-Allow-Origin相关错误。处理跨域的后端手段主要有两种一种是直接在每个接口上或 Controller 类上加CrossOrigin注解简单粗暴但只能针对单一来源往往要配合 allowedOrigins 配置另一种是写一个全局的 CorsFilter设置允许的域名、方法、请求头适合多个端口联调。更推荐的做法还是统一走反向代理通过 Nginx 把/api开头的请求转发给后端服务这样前后端在浏览器眼里始终是同源跨域问题就根本不存在了。4.2 参数传递与时间格式的坑前端联调时最容易出问题的是参数传递方式不一致。我在项目里定的规范是查询参数用RequestParam配合 GET 请求实体数据用RequestBody配合 POST/PUT 请求。如果你前端用了 axios 的 data 发送后端接口却是RequestParam接收就会导致后端拿到一个全是 null 的对象或者直接报 400 参数缺失。反过来前端如果是在 URL 后面拼参数后端却用RequestBody接收那就会报 415 Unsupported Media Type。联调前先把接口文档里每个字段的传输方式核对清楚可以帮你省下半天时间。还有一个特定坑是时间字段。后端默认返回的 LocalDateTime 序列化出来是一大串带 T 的格式前端要处理非常麻烦。我的解决办法是在 yml 里配置spring.jackson.date-format和time-zone让接口返回统一为 yyyy-MM-dd HH:mm:ss 这种格式。这样前端展示时间就不用再写乱七八糟的格式化函数了。如果你用 fastjson 而不是 Jackson记得也要单独配置序列化规则这个坑我踩过换了项目照样会遇到。4.3 常见问题速查表我把项目联调和部署中遇到的高频问题整理成一张表你照着排查基本能覆盖 90% 的情况。现象可能原因解决方案前端请求一直 pending 后报 404请求路径 /api 前缀与后端接口不一致核对 axios baseURL 和后端 controller 的 RequestMapping登录成功后刷新页面菜单丢失动态路由没有持久化或未重新加载在路由守卫中检查路由加载状态未加载时重新 addRoute接口返回 415前端发送方式和后端接收方式不匹配统一用 RequestBody 或 RequestParam 二选一中文乱码数据库连接 url 未指定字符集或表字符集不是 utf8mb4连接串加characterEncodingutf8建表指定 utf8mb4MyBatis 传参报There is no getterXML 里使用参数名与实际 Param 不符检查 Param 名称和 SQL 中的 #{xxx} 完全一致动态 SQL 条件不生效test 属性内参数名写错或类型判断不当精确匹配参数名数字类型只判 null 不判空串播放视频拿不到 m3u8 数据跨域限制或请求头携带了多余的认证信息针对视频请求路径做跨域配置或排除 token 校验4.4 踩坑后的几点心得联调阶段我的最大体会是接口文档的维护一定要跟上。我这次项目用 Apifox 在线管理接口每写完一个接口就标记一下参数和返回示例前端对照文档联调效率非常高。如果你一个人写全栈可能会觉得我自己都清楚不用写文档但这个念头在项目规模变大后会立刻反噬你——三天前的接口返回了哪些字段你根本记不住。另外建议前端同学学会看浏览器的开发者工具的 Network 面板后端同学学会看应用日志。前端说接口 500 了后端如果只看日志里那句 SpringBoot 默认输出的 ERROR 级别堆栈没有打业务参数排查起来会很痛苦。我的习惯是在网关或拦截器里打印一个统一的请求日志记录请求路径、参数、耗时、状态码一条日志能解决绝大部分联调纠纷。5. 部署上线完整教程5.1 环境准备JDK、Node、MySQL 的安装与避坑部署环境的准备是第一道坎。后端要求 JDK 1.8 及以上我建议直接装 JDK11 或 JDK17SpringBoot 2.x 和 3.x 都能兼容。Maven 装好后配置一下本地仓库路径和镜像否则拉依赖也会很慢。前端要求 Node.js 16 以上装完用node -v验证版本。MySQL 我推荐装 8.0性能和默认字符集都比 5.7 好但要注意 8.0 的认证插件是caching_sha2_password某些老版本驱动连不上一定要用mysql-connector-java 8.0。MySQL 安装是新手翻车重灾区。Windows 上装 MySQL 8.0 时安装包里有个 Initialize Database 步骤很多人直接跳过了结果服务起不来或者启动后却不知道 root 密码是什么。我的建议是下载 ZIP 包手动初始化写一个 my.ini 指定 basedir 和 datadir然后执行mysqld --initialize-insecure这样 root 初始密码为空登录后立刻改密码。如果你是 Linux 服务器用 rpm 安装最省事但要注意安装完默认密码可能写在日志/var/log/mysqld.log里用grep temporary password就能看到。Docker 部署 MySQL 也有讲究常见失败原因是用-v挂载了空的目录却忘记设置数据初始化命令导致容器起来了但数据库无法连接建议查看容器日志docker logs mysql定位多数是权限或端口冲突。5.2 后端打包与运行后端打包非常简单在项目根目录执行mvn clean package -DskipTests等构建完成target 目录下就会生成一个可执行的 jar 包名字一般是smart-learning-platform-0.0.1-SNAPSHOT.jar。如果有测试类需要跳过一定要加-DskipTests或者是-Dmaven.test.skiptrue否则默认执行测试可能会因为数据库连接不上而失败。启动命令也很直观java -jar target/smart-learning-platform-0.0.1-SNAPSHOT.jar --server.port8080。生产环境我建议用nohup后台启动同时把日志重定向到文件方便排查。如果你想开机自启可以写一个 systemd 服务文件指向这个 jar 的绝对路径和启动参数实现崩溃自动重启比手动 nohup 管理要稳得多。这一点对长期稳定运行的学习平台来说非常关键。5.3 前端构建与 Nginx 部署前端打包执行npm run build构建完成后会生成一个dist目录里面就是纯静态资源。部署方式很简单把 dist 目录里的文件丢到服务器上配置 Nginx 指向该目录即可。但有两个关键点必须注意一是 Vue Router 如果用的是 history 模式刷新深链接页面时会报 404必须在 Nginx 配置里加try_files $uri $uri/ /index.html;让所有找不到的路径都回退到 index.html二是接口代理要把/api开头的请求转发到后端服务地址同时设置proxy_set_header Host $host;和proxy_set_header X-Real-IP $remote_addr;否则后端拿到的用户 IP 都是 Nginx 的地址。我给一份最小可用的 Nginx server 配置参考root 指向 dist 目录location / 里做 try_files 兜底location /api/ 里做 proxy_pass 转发。注意 proxy_pass 后面带不带斜杠含义不同带斜杠表示把/api前缀去掉再转发不带则是原样转发后端接口前缀能不能匹配上就取决于你这里怎么写了。5.4 将 Vue 打包放进 SpringBoot 的单体部署方案很多人问 Vue 打包能不能直接放进 SpringBoot 里答案是能而且操作很简单。把前端打包生成的dist目录里的文件全部复制到后端的src/main/resources/static目录下重新打包 jar访问http://服务器地址:8080就能看到前端页面。但要注意如果你前端路由用的是 history 模式SpringBoot 内部默认是不认识这些路由路径的直接刷新/course/1会返回 404需要写一个控制器做通配转发把非/api开头的所有路径转发到 index.html。还有前端请求的 baseURL 要改成相对路径/api这样部署后就无需额外处理跨域。这种方案适合什么场景呢适合你要快速演示项目、或者只有一个服务器的轻量部署场景。它牺牲了前后端独立扩容的能力换取了一个 jar 包搞定的部署便利性。但如果你想展示真正的企业级前后端分离部署还是应该用 Nginx 独立托管前端资源后端单独跑 jar两者通过代理互通。5.5 数据库初始化与生产环境优化第一次部署时要执行项目里的sql/init.sql脚本创建数据库和全部表结构。建库语句一定要加上DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci否则默认字符集可能是 latin1存中文直接乱码。初始化数据方面我会往课程表里插入几门演示课程和对应的章节、课时这样登录进去就能立刻看到效果不用在空页面里摸索。生产环境还有几个建议。数据库账号不要用 root单独创建一个业务账号只授权目标数据库的增删改查权限降低风险。连接池参数要控制好比如 HikariCP 的maximum-pool-size设成 20 左右connection-timeout设成 30000 毫秒避免高并发时连接被耗光。MySQL 的max_connections也要同步调大一般默认 151 在并发稍高时就不够用了。最后可以开个慢查询日志把执行时间超过 1 秒的 SQL 捞出来针对性地加索引或改写这对系统上线后的长期稳定运行帮助很大。5.6 部署后的一些扩展想法项目上线稳定运行之后我还在思考怎么把智能做得更深入一些。现在系统中的学习记录和笔记数据已经有一定积累可以基于用户的学习轨迹做一个简单的推荐逻辑比如根据已经学完的课程分类去推荐同分类下的其他课程。再往后数据量大了可以考虑把埋点日志通过 Flink 做实时分析把统计结果同步到 ClickHouse 这类分析型数据库里看大屏但那对硬件和运维要求都高目前这个阶段 MySQL 加定时统计任务完全够用。个人不建议一上来就把架构搞得很重先把核心业务跑通、数据积累起来再按需迭代。我个人在实际部署中的体会是前后端分离项目的上线难度绝大部分不在写代码而在环境准备和配置细节。很多问题看起来是服务挂了白屏了数据没了追下去全是环境变量、路径、字符集、代理这些小事。所以最后再分享一个建议如果你是一个新手拿到这套完整源码后不要急着读代码先按照部署教程把项目跑起来亲眼看到整个系统在浏览器里能跑通再回过头去改代码、理解每个模块的作用。有了看到结果的反馈学习效率会高非常多。做完整套系统我最大的感受是前后端分离看似简单真正难的是接口约定和边界划分。把一个复杂的学习平台拆成一个个职责清晰的模块后端只管数据、前端只管交互再去谈 Vue 的组件复用、MyBatis 的动态 SQL、MySQL 的索引优化这些技术点才有了落地的载体。这套源码和部署流程是我自己从零踩出来的路希望能帮你少走几个弯路。