
做“基于Spring Boot的课程学习平台”这活儿看起来像是后端开发里的入门级项目但真上手你会发现它跟电商、社交系统那种“重业务”不一样它的难点全藏在那些不显眼的地方用户角色怎么切、学习进度怎么记、章节顺序怎么排、接口给谁用、应用挂了怎么第一时间知道。Spring Boot的魅力也正在于此——你不需要从零搭框架但框架给你省下来的时间恰好可以拿去思考这些真正的业务问题。这篇文章我不打算贴一整份代码仓库而是把我在实际搭这类系统时踩过的坑、反复权衡过的选型、以及文档里不太会写明白的细节全部梳理出来。这篇内容适合两类人看一类是准备用Spring Boot做课程学习平台、就业推荐系统、在线教育后台这类业务系统的同学想找一套成熟的设计思路另一类是已经在用Spring Boot写业务但觉得自己的模块划分、接口管理、监控都比较随意想看看一套更规范的参考实现。1. 项目整体设计与技术选型思路1.1 课程学习平台要解决什么问题课程学习平台本质上是个“内容 用户 进度”的三角系统核心场景并不复杂学生浏览课程列表、查看课程详情、登录报名、观看视频或图文、记录学习进度教师负责上传课程、维护章节管理员负责审核、统计、管理用户。听起来就是一堆CRUD但真做起来最容易被低估的是两个点一个是学习进度怎么记录才不丢、不重复、性能好另一个是不同角色的权限边界怎么控制。为什么说进度记录是个坑因为一个用户看一门课可能要跨好几天、换好几个设备他看过的视频得记住位置下次点进来接着放。你如果只在课程表上加一个“最近看到第几集”的字段那用户看过的历史记录、完成比例、每个章节的状态就全丢了。所以必须拆出一张学习记录表按用户和课时维度逐条记录再把“这门课的总进度”作为聚合数据单独存。这个设计决定了后面统计报表、证书发放、续学提醒都好做。权限控制则是另一种坑。学生能看的接口教师和管理员不一定都能看管理员能做的操作学生绝不该碰到。很多新手项目把权限写成if判断在前端藏按钮后端接口全裸奔这就是给自己埋雷。课程学习平台的最佳实践是后端接口一律过鉴权角色写在JWT或者Session里接口层用注解或拦截器控制访问范围前端只是体验的展示层不是安全边界。1.2 技术栈选型为什么是Spring Boot为什么用MyBatis先回答一个大家经常纠结的问题持久层到底选Spring Data JPA还是MyBatis。我的判断是基于业务形态来的不是谁更先进。课程学习平台这类系统查询条件特别多变——按分类查、按关键字查、按难度查、按教师查还要做多表关联统计。MyBatis的SQL完全手写复杂查询写起来直白可控你一看XML或者注解就知道这条SQL长什么样、索引能不能命中性能问题好排查。JPA当然也能做但它胜在CRUD零代码复杂查询反而要学JPQL、Specification团队心智负担不小。再结合目前Java岗位的面试和招聘现状MyBatis的熟悉度普遍更高新成员接手也快。所以这类项目我统一选MyBatis具体整合用mybatis-spring-boot-starter这个依赖会自动配置SqlSessionFactory和Mapper扫描你只要在启动类或配置类上标一个MapperScan把Mapper接口所在的包路径写上剩下的交给Spring Boot就行。那Spring Boot本身解决了什么最明显的是起步依赖和自动配置。以前搭SSM要写一堆XML配数据源、配事务、配Spring MVC现在一个spring-boot-starter-web全带上了内嵌Tomcat也让部署变成java -jar一条命令。再加上Actuator给监控提供了标准端点Spring Boot Admin直接可视化这些能力对中小型业务系统来说省下的搭建成本非常可观。1.3 版本选型的坑2.3.x、2.6.x还是3.x热搜里有人专门问2.3.x和2.6.x怎么选还有人问Spring Boot 3跟Python的FastAPI怎么比可见版本问题真把人整懵过。我说下实打实的建议。先给一张简表把几个主流版本铺开看版本最低JDK关键变化适合场景2.3.xJava 8比较早的稳定版老项目常用维护老代码库2.6.xJava 8依赖管理调整Spring Cloud兼容需注意过渡期项目2.7.xJava 82.x系列收尾版兼容性最好新项目保守首选3.xJava 17javax改jakarta、AOT支持、Security大改新项目且愿意升级JDK重点提醒Spring Boot 3.0开始强制要求JDK 17而且包名从javax.servlet变成了jakarta.servlet。很多老教程里的import javax代码直接编译不过。Spring Security 5.7开始WebSecurityConfigurerAdapter被废弃3.x里干脆移除你从网上抄的很多“继承WebSecurityConfigurerAdapter”的写法会直接报错。我的建议如果是个人练手项目或者课程设计JDK环境你能自己说了算直接上Spring Boot 2.7.x Java 8或11跑得稳、资料多、踩坑少如果你以后要找工作建议花点时间把3.x和JDK 17摸一遍毕竟新项目越来越多。至于跟FastAPI比那就不是版本问题了是两种技术栈的选择——Java生态强在工程化、类型安全、组件多Python那套强在开发快、AI相关资源多两者没有绝对优劣看团队底子和部署环境。2. 核心功能拆解与数据库设计2.1 用户角色、课程内容、学习进度三大模块拆分课程学习平台的功能再怎么变化逃不出三个核心模块用户模块管的是“谁在用”包括学生、教师、管理员三类角色课程模块管的是“看什么”包括分类、课程、章节、课时进度模块管的是“看到哪了”包括选课记录、课时完成状态、课程总进度。这三个模块的边界一定要清晰因为它们会直接影响代码结构。比如用户模块只处理账号、角色、个人资料不要去管课程评价课程模块只管内容结构和元数据进度模块要高频写入单拎出来也方便后续做性能优化比如加缓存、做异步落库。很多项目会在这里犯一个错把学习进度塞进课程表里加一个last_video_id字段就完事了。表面上省了一张表实际上把“用户行为数据”和“内容数据”耦合在一起。用户进度是高频变化的数据课程内容可能几个月不变混在一起会导致缓存失效频繁、锁竞争、统计麻烦。正确的做法是用户行为单独成表内容表保持相对静态。2.2 核心表结构设计与关系梳理直接给一套我在项目中惯用的表设计。这不是唯一解但它是被我跑过好几轮、在性能和可维护性之间比较平衡的方案。user表id、username、password_hash、role、nick_name、avatar、status、created_at。密码绝对不能用明文至少用BCrypt加密这是底线。course表id、title、cover_url、category_id、teacher_id、difficulty、intro、status、created_at。status字段用草稿/上架/下架三个状态避免一改数据就全网可见。chapter和lesson表chapter表负责章节层字段包括id、course_id、title、sortlesson表负责课时层包括id、chapter_id、title、video_url、duration、sort。课程内容要按顺序展示所以排序字段sort必须有不然前端就没法稳定排列“第一章第一节、第二节”的顺序。course_enrollment选课表id、user_id、course_id、progress、status、enrolled_at。progress表示课程整体进度百分比status表示学习中/已完成/已退选。这张表是“用户-课程”多对多关系的体现也是进度统计的主表。learning_record学习记录表id、user_id、course_id、lesson_id、last_position、finished、updated_at。last_position存的是视频播放到的秒数finished是这节是否看完。这张表是学习行为的“流水账”它的存在让断点续播和历史记录都变得非常轻松。上面五张核心表的关系一句话讲完用户和课程通过course_enrollment关联课程内容通过course到chapter到lesson一层层展开learning_record记录用户对具体课时的行为最终聚合回course_enrollment的progress。2.3 接口设计思路对外接口应该放在哪里热搜词里有个特别现实的问题“Spring Boot对外提供的接口给第三方应该放在哪里是单独的服务还是放在对应的业务模块”这个问题我问过很多人答案五花八门我说说我的判断逻辑。判断标准其实就三条第一第三方是否需要跟你共享数据库第二第三方接口的流量和稳定性要求是否跟你主站一致第三你的团队是否有精力维护多个服务。课程学习平台这种体量的系统绝大多数情况下第三方接口不需要单独开一个服务——成本高、部署复杂、调试麻烦不划算。但接口必须从代码目录上独立出来我建议在同一个应用里单独建一个api包路径统一用/open/api/这种前缀并单独做一套鉴权逻辑。具体展开就是站内用户走/app/下的接口用Session或JWT做用户态鉴权第三方合作方走/open/api/下的接口用AppId AppSecret 签名机制来鉴权接口文档也单独维护。这样既不需要多维护一套服务又能保证第三方接入不会搅乱你主站的接口安全。如果以后第三方流量真的涨到影响主站了再把api包抽出去变成独立服务也不难因为代码边界已经在那里了。2.4 REST接口设计路径、状态码、分页约定接口的路径设计应该直接反映资源关系。课程列表是GET /api/v1/courses课程详情是GET /api/v1/courses/{id}选课是POST /api/v1/student/enrollments提交学习进度是PUT /api/v1/student/records/{lessonId}。这里有个经验路径里的版本号很有价值v1写上以后接口有breaking change就升v2老调用方不受影响。分页是这类系统绕不开的。我的习惯是统一分页参数page从1开始size默认10、最大50排序参数单独传sortBy和order。前后端约定好后用PageHelper或者手写LIMIT都可以。手写LIMIT并不复杂而且能逼你思考排序规则的一致性我反而推荐新手自己写一次理解了再上工具。统一返回结构也很关键。我常用的结构是code业务码、message、data。成功时code是200业务失败比如“课程不存在”用404或者自定义的20001不要跟HTTP状态码完全混在一起但建议保持相近语义免得前端判断精神分裂。3. 实操落地从零搭建可运行的课程学习平台3.1 环境准备IDEA社区版也能跑Spring Boot很多人以为IDEA社区版不能用Spring Boot因为没有Spring Initializr。这是个误解社区版完全能开发Spring Boot项目缺的只是图形化的“新建项目向导”绕开它太容易了。我用的做法是打开浏览器访问 start.spring.io在页面上选好构建工具Maven、语言Java、Spring Boot版本再勾选依赖比如Spring Web、MyBatis、MySQL Driver、Validation点生成下载一个zip包。然后回到IDEAFile - Open选中解压后的文件夹IDEA会识别出Maven项目并自动导入依赖。还有一个更用心的细节把生成好的zip包里的src结构先看一遍确保你理解Spring Boot项目的标准布局——src/main/java放源码src/main/resources放配置src/test/java放测试。理解这个布局比会用向导重要一百倍因为后面你手动建包、建类都是在这个结构下面做。JDK方面如果你用Spring Boot 2.7.x装JDK 8或11都行用3.x就装17。Maven建议3.6以上否则有些依赖版本解析会有问题。别装太老的Maven这个坑我碰到过本地跑得好好的一打包报错查半天发现是Maven版本太低不支持新插件的class文件版本。3.2 项目骨架与核心配置项目骨架我习惯按“controller - service - mapper - entity”四层来切再加common包放统一返回结构、异常处理、工具类。controller只做参数接收和返回service写业务逻辑事务注解标在service方法上mapper是MyBatis接口对应XML或注解SQLentity对应表结构。application.yml是核心配置文件里面几个关键项都要仔细设置。数据源部分最容易被忽略的是URL参数MySQL要加useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai不加serverTimezone的话你会惊悚地发现数据库时间比实际少了8个小时别问我怎么知道的。MyBatis的配置里把map-underscore-to-camel-case设为true这样数据库字段course_id就能自动映射到实体属性courseId省去写一堆resultMap的体力活。再配置mapper-locations把XML文件位置指到classpath:mapper/*.xml。日志级别用logging.level.com.xxx.edu.mapperdebug这样SQL会在控制台打印出来调试的时候非常直观。3.3 核心功能落地登录鉴权、课程列表、学习记录先聊鉴权。课程学习平台这种单体应用做JWT登录是比较合适的方案因为前后端分离是常见形态移动端也要用同一套接口。JWT的思路是用户登录成功后后端签一个Token里面包含userId和role前端存起来之后每个请求带在Authorization头里。后端加一个拦截器从Token里解析出用户信息放到ThreadLocal或请求上下文里供后续业务使用。下面是一段非常简化的JWT工具类核心代码能让新手看懂原理即可public class JwtUtil { private static final SecretKey KEY Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); public static String generateToken(Long userId, String role) { return Jwts.builder() .subject(String.valueOf(userId)) .claim(role, role) .issuedAt(new Date()) .expiration(new Date(System.currentTimeMillis() 24 * 60 * 60 * 1000)) .signWith(KEY) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .verifyWith(KEY) .build() .parseSignedClaims(token) .getPayload(); } }这里提个版本坑jjwt这个库在0.12.x之后API变化很大网上大量老教程用的setSubject、setExpiration、signWith都不存在了。你要么跟着新API写要么锁版本用0.9.x但注意它依赖的javax.xml.bind在JDK 11以后要额外引入。我建议直接用新版API抄错了少最稳的方法是把上面这段跟你的jjwt版本对齐编译过一次就算过。课程列表接口相对简单但有一个排序细节很关键分页查询务必带上确定的排序字段比如ORDER BY c.id DESC否则MySQL在不同版本下分页结果可能不稳定用户会看到数据重复或跳跃。学习记录是业务里最需要脑子的部分。用户每看几秒钟视频前端就会上报一次进度如果每次都直接UPDATE数据库请求量会很大。我用的模式是前端先做节流比如10秒上报一次后端拿到请求后只更新learning_record里的last_position和updated_at然后单独计算或者异步聚合出整个课程总进度。聚合逻辑可以放到选课表更新时做查出该课程总共多少课时、多少已完成算完percentage再update course_enrollment。如果课时数量在几百量级这种实时聚合完全顶得住不需要上消息队列。4. 服务监控与运维4.1 监控的需求与功能梳理热搜词里有一条是“Spring Boot实现监控都有哪些需求和功能”这是很多用过但没做好监控的人会问的问题。我的答案是先把需求从五类里对号入座第一类是健康检查应用还活着吗数据库、缓存、磁盘都正常吗第二类是性能指标内存占用、GC次数、线程数、接口QPS和P99响应时间第三类是日志尤其是错误日志出问题能查到堆栈第四类是接口层面的可用性和耗时趋势第五类是连接池状态连接数是否饱和、排队时间是否变长。Spring Boot Actuator本身就是为这个而生的。引入spring-boot-starter-actuator之后应用会暴露一批标准端点比如/actuator/health、/actuator/metrics、/actuator/info。默认情况下health端点里的数据库状态、磁盘状态已经是现成的。不过裸Actuator的展示方式不够友好纯JSON你得自己解析所以就有了Spring Boot Admin这种把数据图形化的方案。4.2 用Spring Boot Admin搭一个监控面板Spring Boot Admin的做法是单独建一个“监控中心”工程让它去拉取各个被监控应用的数据。我在课程学习平台项目里也是这么配的先另开一个极简Spring Boot项目引入spring-boot-admin-starter-server主类加EnableAdminServer配置好端口比如9000这个监控中心就跑起来了。被监控的应用需要引入spring-boot-admin-starter-client然后在application.yml里指定admin服务地址spring: boot: admin: client: url: http://localhost:9000再配合暴露全部Actuator端点management: endpoints: web: exposure: include: *启动之后打开监控中心页面你会看到实例列表点进去有健康状态、CPU、内存、线程数、HTTP接口耗时曲线应用挂了会直接显示离线。这对个人项目和中小团队来说已经非常香了不用自己写报表。有个版本兼容点必须说Spring Boot 2.2以上对应Boot Admin 2.xSpring Boot 3.x对应Admin 3.x依赖写错直接启动报错或者监控数据拉不出来。我见过有人把Admin 3.x塞到Boot 2.7的项目里启动时那些自动配置类全部报ClassNotFoundException纯属版本没对上。4.3 自定义健康检查与接口耗时统计Actuator默认的健康检查已经够用但业务上还可以更细化。比如你想让健康检查反映“数据库慢查询到了无可容忍的程度”可以自己写一个HealthIndicator。课程学习平台里我给了一个检查关键表能否在极短时间内查出来一旦超时就把状态置为DOWN这样挂在监控面板上的健康灯就会变红运维直接知道问题出在哪Component public class CustomHealthIndicator implements HealthIndicator { Override public Health health() { long start System.currentTimeMillis(); // 执行一条轻量级SQL: SELECT 1 boolean dbOk checkDb(); long cost System.currentTimeMillis() - start; if (dbOk cost 1000) { return Health.up().withDetail(dbCost, cost).build(); } return Health.down().withDetail(dbCost, cost).build(); } }接口耗时统计我习惯用AOP切面来做这个对新人特别友好不用侵入每个controller方法。定义一个切面环绕监听所有controller包下的方法执行前记录开始时间执行后算出耗时超过阈值就打成warn日志并记录方法名和路径。这样不用装任何中间件就能拿到每个接口的耗时数据排查慢接口时非常有用。Aspect Component public class ApiCostAspect { Around(execution(* com.xxx.edu.controller..*.*(..))) public Object logCost(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); long cost System.currentTimeMillis() - start; if (cost 500) { log.warn(slow api: {} cost: {}ms, pjp.getSignature().toShortString(), cost); } return result; } }5. 常见问题与排查技巧实录5.1 启动失败类问题这类问题通常一开工就遇到。最典型的是端口被占启动报Port 8080 was already in use。我一般是直接找到进程杀掉Windows用netstat -ano | findstr 8080Linux或Mac用lsof -i:8080。嫌麻烦就改server.port但对学习平台这种部署在固定环境的项目还是固定端口更省心。第二个常见的是数据库连不上要么是MySQL没启动要么是URL配置错了。检查serverTimezone、useSSL这些参数以及驱动坐标——MySQL 8以上要用com.mysql.cj.jdbc.Driver老驱动类名在8.0版本里已经去掉支持了抄旧配置就会报ClassNotFound或连接失败。还有个隐藏的启动坑包扫描。SpringBootApplication只在它所在的包和子包下扫描组件如果你的controller、service放在别的包路径下启动不会报错但接口就是404mapper就是注入不进来。新项目第一天先把包结构想清楚所有代码都放在主类同级的子包里能少掉很多“莫名其妙”的bug。5.2 数据库连接与MyBatis映射问题MyBatis的坑集中在映射上。第一个是驼峰映射数据库字段course_id映射到Java属性courseId必须开启map-underscore-to-camel-casetrue不然后台查到数据前端永远拿不到值。第二个坑是XML文件和Mapper接口的对应关系。检查mapper-locations配置确认src/main/resources/mapper/下的XML文件被正确加载。有时改了XML没生效多半是IDEA没重新编译或者target里是老文件右键recompile一下、clean再package就好。第三个是时间类型问题数据库里datetime类型映射到LocalDateTime如果驱动和配置不对查询会报“Cannot convert”之类。解决方案是确保MySQL连接参数带serverTimezoneAsia/Shanghai实体字段直接用LocalDateTime不要再用java.util.Date前端的ISO格式也好看。5.3 鉴权失效与跨域问题鉴权这块我见到的翻车现场通常有三个。第一个是前端请求带上了Token后端却没有在拦截器里读出来导致每个请求都403。排查思路很简单第一看拦截器有没有加到registry里并且排除登录、注册、课程公开列表等路径第二看前端是不是把Authorization头写对了比如Bearer开头后端解析时用的前缀要一致。第二个是JWT过期时间设得太短用户看视频看着看着跳回登录页。这种体验很糟糕我一般会把过期时间设24小时以上并且支持刷新令牌前端在Token快过期时自动调一个刷新接口换新Token。第三个是跨域。前端跑在http://localhost:5173后端跑在8080不配CORS的话浏览器一怒之下全给你拦截。要么在Spring里配置全局CorsFilter要么在网关处理。学习平台这种规模简单配置一个全局的allowedOriginPatterns就行但注意生产环境别用*允许所有来源指定可信域名列表更稳。5.4 前端联调中的踩坑经验最后分享几条我实际联调过程中的体验虽然看起来琐碎但真能帮你节省半天时间。第一个是接口返回的JSON字段命名。如果你用了camelCase前端JS拿到字段就是驼峰没问题如果你用Map返回字段可能变成下划线。前后端约定好一个格式组件间传参少很多拆来拆去的麻烦。第二个是文件上传接口。课程平台里教师传视频、传封面很常见。Spring Boot单文件上传用MultipartFile默认单文件大小限制是1MB视频肯定超必须在配置里调大。我通常设成spring.servlet.multipart.max-file-size500MB这样才能支撑教学视频。第三个是空值处理。有些字段没填JSON里会返回null前端如果用obj.xxx直接取属性倒没什么但用模板字符串或结构赋值就会报错。建议在统一的返回结构里把null值转为空串或者约定好哪些字段可能是空前端做好兜底。我个人在实际操作中的体会是做课程学习平台这种系统最大的价值不在于功能本身有多炫而在于把那些“看不见”的部分想清楚——用户行为的建模、接口的边界、监控的覆盖。把权限粒度、进度记录、接口版本、监控告警这些点在动手前敲定后面功能只会越加越顺反过来前期图省事后续每次加需求都会别扭。最后再分享一个小技巧把接口文档用统一的OpenAPI/Swagger配置生成出来前端和后端对着同一份文档做联调能减少大量“你改了我不改”的扯皮。Admin监控加上了AOP耗时打出来了日志格式固定了这套学习平台跑起来就会比大多数同类demo要靠谱得多。