
校园课程推荐系统是很多计算机专业学生选毕业设计时容易踩坑的方向。表面看它只是把课程、学生、选课记录做成增删改查真正做起来难点全在“推荐”这两个字上。基于 SpringBoot Thymeleaf AI 推荐来搭建好处是技术栈传统、资料多、答辩好讲同时又能把个性化推荐作为亮点和普通管理系统拉开差距。这篇文章按实际落地顺序拆一遍先明确它解决什么问题再准备环境然后搭项目骨架接着讲 AI 推荐怎么接最后给出常见报错的排查思路。1. 这个系统真正要做的不是课程管理而是选课推荐1.1 校务管理的常见模块必须先理清一个校园课程推荐系统如果只从功能上看通常包含这几个模块学生管理维护学生基本信息、专业、年级、兴趣标签。课程管理维护课程名称、授课教师、学分、上课时间、限选人数、课程标签。选课管理学生选课、退课记录选课时间。课程评价学生对已选课程打星、写评语作为后续推荐的依据。推荐模块根据学生历史选课、标签偏好、课程热度生成个性化推荐列表。很多初学同学容易陷入“只做前四个模块”的舒适区。课程管理系统本身不难难点在于推荐模块怎么设计、怎么验证、怎么给答辩老师讲清楚。推荐模块才是“AI 推荐系统”这个项目名称里最关键的部分。1.2 推荐链路才是项目亮点课程推荐要解决的真实问题是每学期开课数量可能几百门学生不知道选什么只能靠问学长、刷通知、碰运气。推荐系统把历史选课数据、课程标签、学生专业和兴趣结合起来输出一份“你可能想选”的课程列表。但推荐不是简单写一个if 课程包含“AI” then 推荐。一个完整的推荐链路应该是获取学生特征专业、年级、历史选课、搜索/收藏行为、兴趣标签。获取候选课程排除已经选过的课程排除人数已满的课程。计算匹配度基于规则、标签、协同过滤或大模型理解给每个候选课程打分。排序并返回 TopN 列表。记录推荐日志用于后续效果评估。这条链路才是项目的核心价值。如果你在答辩时能把每个环节说清楚并且能演示“不同学生看到不同推荐结果”系统就已经比大部分纯 CRUD 毕设有说服力。2. 环境准备和技术选型版本坑先处理好2.1 SpringBoot Thymeleaf 的搭配逻辑SpringBoot 负责后端接口、数据库操作、事务管理和自动装配。Thymeleaf 负责服务端渲染页面它可以直接在 HTML 中写th:each循环把后端返回的课程列表渲染成表格。这种组合非常适合毕设和中小型项目因为不需要拆前后端部署也简单。不是所有项目都适合 Thymeleaf。如果你要做成小程序、App 或复杂的后台动态交互前后端分离可能更好。但校园课程推荐系统通常是“管理员后台 学生选课页面”的传统 Web 形态Thymeleaf 能把这一套完整承接住。2.2 环境清单和版本踩坑我建议不要一开始就追求“版本最新”。尤其尽量不要用太多 unreleased 版本或者跨度很大的组合。常见可用组合如下项目推荐版本说明JDK8 或 17SpringBoot 2.7.x 对应 JDK8SpringBoot 3.x 对应 JDK17Maven3.6 或 3.8多数项目直接用 IDEA 自带 Maven 也可以SpringBoot2.7.x 或 3.2.x如果参考教程是 2.x不要强行用 3.xMySQL5.7 或 8.0推荐 8.0注意驱动坐标变化ORMSpring Data JPA 或 MyBatis-Plus两者都可以选一个自己熟的前端模板Thymeleaf Bootstrap页面不用写得太复杂清爽即可推荐服务规则算法优先大模型 API 可后接见第四部分这里要特别提醒SpringBoot 版本太高不一定是好事。很多旧教程写的javax.servlet、javax.persistence在 SpringBoot 3.x 里已经变成jakarta开头代码复制过来直接编译失败。如果你看到类似 “无法解析 javax.servlet” 的错误先检查是不是 SpringBoot 3.x 和 JDK17 导致的包名变更。2.3 数据库和基础配置数据库设计要尽量贴近真实场景。核心表可以这样设计student学生表字段包括 id、name、major、grade、tags。course课程表字段包括 id、name、teacher_name、credit、tags、max_count、selected_count。course_selection选课表字段包括 id、student_id、course_id、select_time。course_rating课程评价表字段包括 id、student_id、course_id、score、comment。其中课程表的tags字段可以存逗号分隔的标签例如“AI, 编程, 计算机”也可以单独建课程标签表。对毕设来说逗号分隔简单直观推荐模块处理起来也不复杂。application.yml可以参考下面这种写法server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/course_recommend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver thymeleaf: cache: false开发阶段把 Thymeleaf 缓存关掉改完页面刷新就能看到效果。否则经常会出现“我改了 HTML但页面没变化”的情况其实是缓存在作怪。3. 从零到一搭一个能跑通的最小系统3.1 项目结构和分层我建议不要一上来写一堆代码。先把最小闭环跑通学生登录后能看到课程列表选一门课然后推荐模块给出结果。通过这个闭环你才能验证从数据库到页面再到推荐算法的整条链路。项目结构可以这样组织src/main/java/com/example/courserecommend/ ├── controller/ # 页面路由和接口 ├── service/ # 业务逻辑和推荐逻辑 ├── repository/ # 数据库访问 ├── entity/ # 实体类 └── CourseRecommendApplication.java src/main/resources/ ├── templates/ # Thymeleaf 页面 ├── static/ # CSS、JS、图片 └── application.yml分层不是死规矩但这样拆有个明显好处推荐算法可以单独放在 service 层以后想换成大模型只改 service 内部实现controller 和页面都不用动。3.2 实体、Repository、Service、Controller 的最小实现先用一个简单的课程实体举例Entity Table(name course) public class Course { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; private String name; private String teacherName; private Integer credit; private String tags; private Integer selectedCount; private Integer maxCount; // 省略 getter 和 setter }Repository 层如果使用 Spring Data JPA可以写一个接口public interface CourseRepository extends JpaRepositoryCourse, Long { ListCourse findTop10ByOrderBySelectedCountDesc(); }findTop10ByOrderBySelectedCountDesc的作用是查出当前选课人数最多的 10 门课程。这个查询在推荐系统里很重要因为当学生没有任何历史行为时我们可以用“大家都在选”的热门课程作为推荐结果也就是带冷启动兜底。Controller 不需要太复杂核心是把数据塞给 ThymeleafController RequestMapping(/course) public class CourseController { private final CourseRecommendService recommendService; public CourseController(CourseRecommendService recommendService) { this.recommendService recommendService; } GetMapping(/recommend) public String recommend(RequestParam Long studentId, RequestParam(defaultValue 10) int topN, Model model) { model.addAttribute(courses, recommendService.recommendForStudent(studentId, topN)); return recommend; } }注意这里返回的是recommend对应的是templates/recommend.html。Thymeleaf 会按照templates/recommend.html去找页面。3.3 Thymeleaf 页面渲染推荐结果推荐页面的核心可以很简单!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title课程推荐/title /head body h1为你推荐的课程/h1 table tr th课程名称/th th授课教师/th th学分/th th已选人数/th /tr tr th:eachcourse : ${courses} td th:text${course.name}课程/td td th:text${course.teacherName}教师/td td th:text${course.credit}学分/td td th:text${course.selectedCount}人数/td /tr /table /body /html运行项目后访问/course/recommend?studentId1页面就能显示推荐列表。这个阶段不要急着追求美观先把路由、数据流、页面渲染打通后面再套 Bootstrap 样式都来得及。4. AI 推荐怎么落地先规则再大模型4.1 不要一上来就接大模型很多同学看到“AI 推荐”就以为必须调用大模型接口其实不是。校园课程推荐系统的数据量一般不大一个学期的选课记录可能只有几千条。这种数据规模下复杂模型不一定比朴素方法更可靠。我建议第一版先用“基于课程标签 学生历史选课”的规则推荐。原因有三点规则算法逻辑透明答辩时容易讲清楚。不需要网络、API Key、额外费用演示稳定。推荐结果可以解释出了问题也容易排查。4.2 基于课程标签的推荐实现思路是把学生选过的课程标签统计成权重然后对候选课程计算匹配分。比如某个学生选过“Python程序设计”和“高等数学”这两门课的标签分别是“编程, 数学”和“数学, 基础”。统计后该学生对“数学”的兴趣权重为 2对“编程”的兴趣权重为 1。候选课程“线性代数”如果带“数学”标签就能得到较高分数。代码可以这样写Service public class CourseRecommendService { private final CourseRepository courseRepository; private final CourseSelectionRepository courseSelectionRepository; // 构造方法省略 public ListCourse recommendForStudent(Long studentId, int topN) { ListCourse historyCourses courseSelectionRepository.findSelectedCoursesByStudentId(studentId); if (historyCourses null || historyCourses.isEmpty()) { return courseRepository.findTop10ByOrderBySelectedCountDesc(); } MapString, Integer tagScore new HashMap(); for (Course course : historyCourses) { String[] tags course.getTags().split(,); for (String tag : tags) { tagScore.merge(tag.trim(), 1, Integer::sum); } } SetLong historyIds historyCourses.stream() .map(Course::getId) .collect(Collectors.toSet()); return courseRepository.findAll().stream() .filter(course - !historyIds.contains(course.getId())) .sorted(Comparator.comparingDouble((Course c) - scoreCourse(c, tagScore)).reversed()) .limit(topN) .collect(Collectors.toList()); } private double scoreCourse(Course course, MapString, Integer tagScore) { double score 0; String[] tags course.getTags().split(,); for (String tag : tags) { score tagScore.getOrDefault(tag.trim(), 0); } // 热门课程作为一个小加成但不要让它完全覆盖标签分数 score course.getSelectedCount() * 0.01; return score; } }这段逻辑里有两个很关键的经验没有历史选课记录时必须走热门课程兜底否则推荐列表永远是空的。历史课程已经选过不能再推荐所以要先过滤掉historyIds。4.3 通过大模型 API 扩展推荐能力规则推荐跑通之后如果你想给项目增加更多“AI 感”可以接大模型 API。让大模型根据学生信息和课程列表生成推荐理由这样推荐结果不只是“这课因为标签匹配”而是“结合你的专业基础、历史选课记录我推荐这门课”。大模型的调用流程通常是把学生专业、年级、历史选课、兴趣标签组装成提示词。把候选课程列表作为上下文提交给大模型。让大模型返回课程名称和推荐理由。程序解析返回结果再到数据库里查一遍确认课程存在。如果返回格式不对或调用超时回退到规则推荐结果。代码层面你可以直接用 Spring 的RestTemplate或RestClient调用大模型 API也可以使用 Spring AI 封装好的客户端。这里的关键不是调接口而是处理好“返回结果不稳定”的问题。比如大模型可能返回一门不存在的课程也可能返回纯文本而不是 JSON需要做兜底。一个通用提示词模板类似这样你是校园选课助手。学生是计算机科学专业的大一学生学过Python程序设计、高等数学对人工智能感兴趣。 请从以下课程中推荐5门课并给出理由人工智能导论、数据库原理、操作系统、线性代数、软件工程、大学物理。 要求只返回课程名称和推荐理由不要包含编号。之所以要限制“不要包含编号”是因为大模型容易自己编一个课程编号而我们的系统里根本没有这个编号。这个限制能减少数据查询失败的概率。如果你的答辩环境不允许外网调用也可以考虑本地部署大模型但对普通学生来说成本不低。本地部署通常需要独立显卡和大内存即使小参数量模型也要关注 CPU 推理速度和显存占用。我更建议先用在线 API 验证完整流程再根据答辩环境决定要不要本地部署。另外大模型调用会有延迟不适合在用户每次刷新页面时同步等待。正确做法是先缓存推荐结果或者做成异步任务等大模型生成后更新页面。5. 单条推荐跑通后再考虑批量、并发和性能5.1 先把单条任务跑稳很多人拿到推荐模块就急着测试几百个学生这是不合理的顺序。正确做法是先用一个学生 ID 测试确认首页推荐能加载。换一个没有历史选课的学生确认热门兜底能生效。换一个历史选课很多的学生确认标签排序不会报错。全部通过后再测试并发和批量。这一步的目的是把故障范围缩小。如果单条都跑不稳定批量跑出来的错误会非常难定位。5.2 批量生成推荐时要考虑缓存和降级假设系统里注册了 2000 个学生管理员希望后台能同时生成全部学生的推荐列表。此时如果实时遍历每个学生都走一遍数据库查询甚至大模型 API整体耗时和数据库压力都会很大。更稳妥的方案是引入缓存学生第一次查看推荐时生成结果并写入缓存。推荐结果设置过期时间比如 1 小时或 1 天。学生选课后主动清空该学生的推荐缓存。大模型接口不可用时返回上一次的缓存结果或规则推荐结果。如果是规则推荐性能通常不是问题。但如果是大模型调用批量任务一定要控制并发。不要一上来就开几十个线程同时请求大模型 API很可能被限流或 timeout。我建议先用 1 个并发跑少量样例观察响应时间和成功率再逐步增加到 3、5、10。5.3 推荐结果的验证不能只看“能不能跑”推荐系统很容易出现“能跑但推荐结果很怪”的情况。比如某个学生只选过体育课却推荐了一堆计算机专业课。课程标签为空导致分数一直为 0。学生已经选过推荐列表里的课但没有被过滤掉。这些问题需要靠日志和数据统计来发现。可以在日志中记录每个学生的历史选课数量、推荐课程数量和推荐分数。答辩前准备 5 到 10 个不同类型的测试学生包括新生无选课记录。有单一方向选课的学生。有跨学科选课的学生。选课记录很多的老生。用这些样本跑一遍看看推荐结果是否符合直觉。推荐系统不是一个“功能跑通就结束”的模块而是要不断观察结果合理性。6. 常见报错排查方法和优化方向6.1 启动失败先看环境不要急着改代码最常见的问题是项目启动时报错很多同学第一反应是怀疑代码有 bug其实大部分是环境原因。排查顺序建议如下端口占用8080被占用时SpringBoot 启动日志会直接提示换一个端口或者关掉占用进程。数据库连接失败检查 MySQL 是否启动、账号密码是否正确、数据库名是否创建。JDK 版本不匹配SpringBoot 3.x 必须 JDK17JDK8 会直接报错。依赖版本冲突SpringBoot 2.x 和 3.x 的自动配置差别很大不要照抄不同版本的项目配置。这里多说一句SpringBoot 自动装配确实方便但它不是万能的。很多“资源找不到”“仓库为空”的报错本质是自动配置没有生效因为你改了版本或依赖坐标。排查时先看启动日志前面有没有APPLICATION FAILED TO START再看具体的Description和Action一般都会提示缺什么依赖、哪个 bean 没找到。6.2 推荐结果不对先看数据再看算法推荐结果不准时不要急着调推荐算法先确认数据是不是干净的。推荐结果为空优先检查学生 ID 是否存在。学生是否有选课记录。候选课程是否已经过滤掉已选课程过滤后剩余数量是否够 TopN。课程标签是否为空空标签会导致分数全为 0。推荐结果重复优先检查是否通过 ID 过滤而不是对象 contains。是否每次查询都重新生成没有做去重。是否数据库里有重复课程记录。推荐结果偏向热门课优先检查热门课程加成是否太大。标签权重是否被覆盖率稀释。如果能写一个简单的测试用例在test目录里创建几个学生、几门课程和若干选课记录再断言推荐结果会比自己不断刷新页面高效得多。6.3 答辩和文档阶段该注意什么这类毕设通常还配套论文和演示 PPT。写文档时不要把重点放在“我用了 SpringBoot、Thymeleaf”这类技术名词上而应该把重点放在推荐链路的设计和验证上。建议文档里包含系统功能图和数据表关系。推荐算法流程图。不同测试学生的推荐结果对比。如果用了大模型 API写清楚调用流程、提示词设计和降级方案。遇到的典型问题和解决方法。答辩演示时准备几个稳定用例不要现场临时换学生 ID。因为如果数据没造好可能推荐结果为空场面会很尴尬。我自己更推荐的顺序是先演示有历史数据的学生再演示新生热门兜底最后才演示大模型推荐理由。校园课程推荐系统真正落地时最该盯住的不是功能列表而是数据、缓存和兜底逻辑。推荐模块可以很简单也可以接大模型做得很花哨但无论哪种路线都要保证单条流程稳定、数据可解释、失败有降级。踩过几次坑之后你会发现很多问题不是算法能力不够而是历史数据和输入格式没有处理干净。先跑稳一条链路再扩批量这个系统就不会从“答辩亮点”变成“现场翻车点”。