SpringBoot+协同过滤:校园课程推荐系统与AI推荐理由实战

发布时间:2026/9/7 6:24:03
SpringBoot+协同过滤:校园课程推荐系统与AI推荐理由实战 如果只把“推荐”做成一个下拉框搜索加课程列表那它只能算一个管理系统不能叫推荐系统。很多毕业设计题目里有“AI推荐”四个字但答辩时评委打开数据库看一眼就会发现所谓的推荐其实是一句固定 SQL 排序。这件事恰恰是校园课程推荐系统最容易被忽视的地方推荐能力不是靠页面堆出来的而是靠用户行为建模和推荐策略分层撑起来的。这篇文章就以一个基于 SpringBoot Thymeleaf AI 推荐的校园课程推荐系统为例完整拆解从数据库设计、推荐策略实现到页面渲染的整个过程。读完你可以得到三样东西一是能直接在毕业设计中落地的最小推荐系统代码二是理解“协同过滤 AI 辅助推荐理由”这类混合策略的工程写法三是知道作为毕设项目哪些功能点更容易在答辩中讲出深度。1. 这门毕设真正要解决的问题1.1 为什么很多课程管理系统“推荐”做得很假先看一个真实场景某高校的课程管理系统里有几百门选修课学生登录后看到的是按上课时间排的课程列表查询只能靠课程名称模糊搜索。学生找不到感兴趣的课教务也说不清哪些课受欢迎。最后系统上线了但使用率很低。这不是个例。绝大多数课程管理类毕设项目功能集中在“增删改查”管理员维护课程、学生选课、老师录成绩。推荐模块要么没有要么写了一行ORDER BY course_score DESC就当作热门推荐。这种做法在答辩时经不起追问你的推荐依据是什么用户的历史行为在哪里推荐结果为什么对他有意义1.2 课程推荐系统真正的技术难点课程推荐和电商推荐的差异在于课程是低频、高决策成本、强内容属性的物品。学生一学期可能只选 5 到 10 门课不能用“买完再来一单”的逻辑去思考。它同时要考虑课程难度、开课院系、相似课程、历史选课学生群体等多个因素。因此这套系统的核心设计目标不是追求算法复杂度而是做到三点能记录学生在系统中的真实行为浏览、选课、评分。能基于已有行为给出可解释的课程候选集。能用 AI 能力生成推荐理由让学生理解“为什么推荐这门课”。这里真正值得在毕业设计中展开的是第二点和第三点的结合。协同过滤负责找相似课程和相似兴趣学生AI 推荐理由负责解释推荐结果。两者一组合系统从“能跑”变成“有说服力”。1.3 什么样的读者适合参考这篇文章如果你正在准备 Java 方向的毕业设计技术栈是 SpringBoot Thymeleaf想用数据库 推荐策略做一个校园课程推荐系统这篇文章可以直接作为实现蓝本。如果你只是对推荐系统的工程落地感兴趣重点看数据建模和第 6 章的策略分层方式。如果你已经写完课程管理功能想让项目在答辩时更有亮点重点看冷启动处理和 AI 辅助推荐理由的设计思路。2. 系统总体架构与设计思路2.1 SpringBoot Thymeleaf 为什么还适合做毕设这几年前后端分离很流行但 Thymeleaf 作为服务端渲染模板引擎在毕设和企业传统项目中依然常见。它的价值在于不需要单独部署前端工程后端返回 ModelAndView 直接渲染 HTML项目结构简单部署方便排查问题链路短。对于校园课程推荐系统来说页面交互并不复杂不需要 Vue Element UI 那套复杂工程。用 Thymeleaf 可以减少一整套前端依赖把精力集中在推荐模块本身。这在毕业设计的时间安排上是很现实的选择。整套系统的分层如下表现层Thymeleaf 模板页面负责课程列表、推荐结果展示、学生操作入口。控制层Spring MVC Controller负责接收请求、调用服务、返回视图。业务层课程管理、选课管理、推荐策略、AI 推荐理由生成。数据层MyBatis-Plus 操作 MySQL完成用户、课程、行为记录的持久化。2.2 推荐模块在业务层中的位置推荐模块被设计成独立的业务服务不直接穿透到 Controller。这样做的原因是推荐策略后期会调整比如从协同过滤换成内容推荐或者接入新的大模型服务只要 Service 接口不变上层页面不需要跟着改。推荐流程大致如下学生访问首页时Controller 调用推荐服务。推荐服务优先基于当前学生的历史选课记录计算相似课程。如果当前学生没有历史记录则使用热门课程与随机策略兜底这就是冷启动处理。对候选课程生成推荐理由由 AI 服务接口统一输出。Controller 将推荐结果封装后交给 Thymeleaf 页面展示。这样的流程写进论文里本身就是一套完整的“软件工程设计”论述策略模式 接口隔离 冷启动兜底。答辩护身符不只是代码而是设计思路。3. 数据库设计与业务建模3.1 数据建模的核心是“记录行为”很多毕设把选课功能做成简单的外键表学生选了哪门课就插入一条记录。这对于管理功能来说足够但对于推荐系统远远不够。推荐系统需要知道的不只是“选了”还有“学生对这门课的评价”。推荐系统里最经典的数据模型是“用户-物品-评分”三元组。放到校园场景中就是“学生-课程-行为”。行为可以是评分也可以是浏览、选课、退课等隐式反馈。为了让推荐有数据支撑这里设计了几张核心表学生表、课程表、选课评分表和推荐日志表。3.2 核心表结构与建表 SQL学生表用于存储学生基础信息课程表用于存储课程名称、所属院系、任课教师、难度、学分等业务属性选课评分表是整个推荐系统的关键记录学生对课程的打分和评价推荐日志表用于记录每次推荐内容方便后续分析和复盘。下面是一套可直接执行的 MySQL 建表脚本-- 文件路径sql/init_schema.sql CREATE DATABASE IF NOT EXISTS course_recommend; USE course_recommend; -- 学生表 CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_no VARCHAR(32) NOT NULL UNIQUE COMMENT 学号, student_name VARCHAR(64) NOT NULL COMMENT 姓名, major VARCHAR(64) COMMENT 专业, grade VARCHAR(16) COMMENT 年级, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生表; -- 课程表 CREATE TABLE t_course ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, course_name VARCHAR(128) NOT NULL COMMENT 课程名称, course_code VARCHAR(32) NOT NULL UNIQUE COMMENT 课程编号, college VARCHAR(64) COMMENT 开课院系, teacher_name VARCHAR(32) COMMENT 任课教师, credit DECIMAL(3,1) DEFAULT 2.0 COMMENT 学分, difficulty TINYINT DEFAULT 3 COMMENT 难度1-5, course_type VARCHAR(32) COMMENT 课程类别通识/专业/公选, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT课程表; -- 选课评分表推荐系统的核心行为数据 CREATE TABLE t_student_course_score ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_id BIGINT NOT NULL COMMENT 学生ID, course_id BIGINT NOT NULL COMMENT 课程ID, score TINYINT COMMENT 学生评分1-50表示未评分, comment_text VARCHAR(255) COMMENT 学生评价, select_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, KEY idx_student (student_id), KEY idx_course (course_id), CONSTRAINT uk_student_course UNIQUE (student_id, course_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生选课评分表; -- 推荐日志表 CREATE TABLE t_recommend_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键, student_id BIGINT NOT NULL COMMENT 学生ID, recommend_strategy VARCHAR(32) COMMENT 推荐策略标识, course_ids VARCHAR(512) COMMENT 推荐课程ID列表逗号分隔, reason_text VARCHAR(512) COMMENT 推荐理由, create_time DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 推荐时间, KEY idx_student_time (student_id, create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT推荐日志表;这张表模型的好处在于选课记录本身是可扩展的。如果你想记录学生浏览课程的行为只需要新增一张浏览日志表推荐策略可以从“选课 评分”扩展到“浏览 关注”。这个扩展性要在论文和答辩中单独说明。3.3 测试数据的准备推荐系统离不开数据验证。建议在开发阶段先准备一部分模拟数据方便看到推荐效果。比如插入几个学生让他们对不同的课程打不同分数再观察推荐结果是否符合直觉。可以写一段简单的 insert 语句插 3 到 5 个学生和 10 门左右课程每门课有 2 到 3 条评分记录即可。4. 环境准备与工程基础配置4.1 开发环境说明建议使用 JDK 8 或 JDK 17Maven 3.6 以上IDEA 作为开发工具MySQL 5.7 或 8.0 均可。SpringBoot 版本以你本机稳定可用的版本为准建议 2.7.x 系列稳定且资料多。下面代码中的版本号只是示例实际依赖版本请以项目为准核心是学会整套实现思路。如果你的 IDEA 创建 SpringBoot 项目一直超时通常是网络问题。可以选择阿里云镜像的 Initializr 地址或者先本地生成项目再导入 IDEA。4.2 创建 SpringBoot 工程并引入依赖新建一个 Maven 工程后在pom.xml中引入 Web、Thymeleaf、MyBatis-Plus 和 MySQL 驱动依赖parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent properties java.version1.8/java.version mybatis-plus.version3.5.3.1/mybatis-plus.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里选择 MyBatis-Plus 而不是原生 MyBatis是因为它内置了BaseMapper通用方法单表查询基本不用写 XML毕设开发效率高很多。4.3 application.yml 中的关键配置在src/main/resources/application.yml中配置数据源、MyBatis-Plus 和 Thymeleafserver: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_recommend?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: yourpassword thymeleaf: cache: false suffix: .html prefix: classpath:/templates/ mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: id-type: auto注意几个细节map-underscore-to-camel-case负责把数据库字段course_name映射成实体类属性courseNameThymeleaf 开启cache: false是开发阶段改模板免重启serverTimezone参数能避免数据库连接时的时间报错。如果启动时报数据库驱动类错误先检查 MySQL 驱动版本是否和本地 MySQL 版本匹配。5. 实体类、Mapper 与推荐数据准备5.1 实体类设计建立Student、Course、StudentCourseScore三个实体类。以课程实体为例// 文件路径src/main/java/com/example/course/entity/Course.java Data TableName(t_course) public class Course { TableId(type IdType.AUTO) private Long id; private String courseName; private String courseCode; private String college; private String teacherName; private BigDecimal credit; private Integer difficulty; private String courseType; }这里使用 Lombok 的Data自动生成 getter/setterTableName指定数据库表名TableId声明主键类型。Student 和 StudentCourseScore 的结构与上述类似分别对应t_student和t_student_course_score表。注意 StudentCourseScore 里应该还有一个score字段和commentText字段它是推荐系统的关键输入。5.2 Mapper 层Mapper 接口继承 MyBatis-Plus 提供的BaseMapper即可// 文件路径src/main/java/com/example/course/mapper/CourseMapper.java Mapper public interface CourseMapper extends BaseMapperCourse { }StudentMapper、StudentCourseScoreMapper 采用同样的写法。不写 XML不写复杂 SQL单表操作全部由 MyBatis-Plus 提供。真正复杂的推荐计算不是在 SQL 层完成的而是在 Service 层用 Java 代码完成这样推荐逻辑更清晰也更容易测试。5.3 推荐服务需要的数据获取方式推荐服务运行时需要两类数据当前学生已经选过并打分的课程集合。课程的全量列表以及所有学生的选课评分记录。这些都可以直接用selectList加上LambdaQueryWrapper条件查询获得。比如用new LambdaQueryWrapperStudentCourseScore().eq(StudentCourseScore::getStudentId, studentId)查某个学生的行为用selectList(null)查全量课程。数据量不大时内存计算完全够用这也符合毕设场景。6. 推荐策略实现协同过滤 AI 辅助生成推荐理由6.1 推荐策略的分层设计推荐模块在 Service 层建立三个层次策略接口、核心算法、AI 理由生成。先定义一个统一的推荐服务接口// 文件路径src/main/java/com/example/course/service/RecommendService.java public interface RecommendService { /** * 为学生推荐课程 * * param studentId 学生ID * param limit 推荐数量 * return 推荐结果列表 */ ListRecommendItem recommendForStudent(Long studentId, int limit); }RecommendItem包含课程信息、推荐分数和推荐理由// 文件路径src/main/java/com/example/course/service/RecommendItem.java Data public class RecommendItem { private Course course; private Double score; private String reason; }用接口而不是具体实现是为了后续可以加入新的推荐算法而不影响上层调用。课程推荐主服务实现这个接口内部组装策略。6.2 基于课程相似度的协同过滤实现协同过滤有很多种这里选择比较适合毕设讲解的“基于物品的协同过滤”思路核心是先找到用户喜欢的物品再找到与这些物品相似的物品。用在课程场景里可以简述为如果很多学生同时喜欢“高等数学”和“线性代数”那么当一个学生喜欢“高等数学”时系统就该把“线性代数”推荐给他。课程相似度计算使用余弦相似度。每一门课都有一个向量向量中的每个维度对应一个学生维度值就是该学生对这门课的行为值。如果某个学生没有学过这门课该维度就是 0。两门课向量越接近说明它们的受众越重叠课程就越相似。// 文件路径src/main/java/com/example/course/service/Impl/SimCourseRecommendService.java Service public class SimCourseRecommendService { public ListLong recommendCoursesBySimilarity( ListStudentCourseScore myScores, ListStudentCourseScore allScores, ListCourse allCourses, int limit) { // 1. 计算所有课程之间的相似度只保留每个目标课程最相似的课程集合 MapLong, ListCourseSimilarity simMap new HashMap(); ListCourse courses allCourses; for (int i 0; i courses.size(); i) { Course c1 courses.get(i); for (int j i 1; j courses.size(); j) { Course c2 courses.get(j); double sim cosineSimilarity(c1.getId(), c2.getId(), allScores); if (sim 0) { simMap.computeIfAbsent(c1.getId(), k - new ArrayList()) .add(new CourseSimilarity(c2.getId(), sim)); simMap.computeIfAbsent(c2.getId(), k - new ArrayList()) .add(new CourseSimilarity(c1.getId(), sim)); } } } // 2. 找到当前学生评分较高的课程再找出这些课程的相似课程 MapLong, Double candidateScore new HashMap(); MapLong, Double explainScore new HashMap(); for (StudentCourseScore my : myScores) { if (my.getScore() null || my.getScore() 3) { continue; } ListCourseSimilarity simCourses simMap.getOrDefault(my.getCourseId(), Collections.emptyList()); for (CourseSimilarity sim : simCourses) { if (myScores.stream().anyMatch(s - s.getCourseId().equals(sim.getCourseId()))) { continue; } candidateScore.merge(sim.getCourseId(), sim.getScore() * my.getScore(), Double::sum); explainScore.putIfAbsent(sim.getCourseId(), sim.getScore()); } } // 3. 排序并返回课程ID return candidateScore.entrySet().stream() .sorted((e1, e2) - Double.compare(e2.getValue(), e1.getValue())) .limit(limit) .map(Map.Entry::getKey) .collect(Collectors.toList()); } private double cosineSimilarity(Long courseA, Long courseB, ListStudentCourseScore allScores) { MapLong, Double vectorA new HashMap(); MapLong, Double vectorB new HashMap(); for (StudentCourseScore s : allScores) { if (s.getCourseId().equals(courseA)) { vectorA.put(s.getStudentId(), s.getScore() null ? 0.0 : s.getScore().doubleValue()); } if (s.getCourseId().equals(courseB)) { vectorB.put(s.getStudentId(), s.getScore() null ? 0.0 : s.getScore().doubleValue()); } } SetLong allStudents new HashSet(); allStudents.addAll(vectorA.keySet()); allStudents.addAll(vectorB.keySet()); double dot 0, normA 0, normB 0; for (Long studentId : allStudents) { double a vectorA.getOrDefault(studentId, 0.0); double b vectorB.getOrDefault(studentId, 0.0); dot a * b; normA a * a; normB b * b; } if (normA 0 || normB 0) { return 0; } return dot / (Math.sqrt(normA) * Math.sqrt(normB)); } Data AllArgsConstructor public static class CourseSimilarity { private Long courseId; private double score; } }这段代码里值得在答辩中说明的点有三个一是为什么要跳过当前学生已经选过的课程二是score * sim的加权重叠含义三是相似度只保留正值避免负相关课程的干扰。把这三点讲清楚比单纯背余弦相似度公式更有说服力。6.3 冷启动处理没有历史数据时推荐什么新学生没有选课记录协同过滤算法无法算相似度。这时候要有一个兜底方案。最简单的做法是按课程的综合热度推荐热度可以由选课人数和平均评分计算出来。同时可以加入少量随机扰动避免所有学生进入系统看到的推荐课程完全一样。// 冷启动返回热门课程 public ListCourse hotCourses(ListStudentCourseScore allScores, ListCourse allCourses, int limit) { MapLong, Long courseCount allScores.stream() .collect(Collectors.groupingBy(StudentCourseScore::getCourseId, Collectors.counting())); return allCourses.stream() .sorted((c1, c2) - Long.compare( courseCount.getOrDefault(c2.getId(), 0L), courseCount.getOrDefault(c1.getId(), 0L))) .limit(limit) .collect(Collectors.toList()); }这个方案虽然简单但逻辑通顺。毕业论文里可以把它上升为“冷启动问题下的默认推荐策略”核心思路是在用户画像不完整时优先使用群体统计特征而不是强行套用个性化算法。6.4 AI 辅助推荐理由生成推荐理由是本系统区别于普通课程管理系统的关键功能。它让推荐结果不再是一条冷冰冰的数据记录而是一句学生能看懂的话。考虑到毕设环境里不一定有可用的外部大模型 API这里采用接口 模板实现的方式。先定义一个 AI 推荐理由服务接口再写一个模板实现作为默认方案。如果之后想接入真实大模型服务只需要新增一个实现类替换 Bean 即可。// 文件路径src/main/java/com/example/course/service/AiRecommendReasonService.java public interface AiRecommendReasonService { String generateReason(Course course, Double score, boolean coldStart); }// 文件路径src/main/java/com/example/course/service/Impl/TemplateReasonServiceImpl.java Service public class TemplateReasonServiceImpl implements AiRecommendReasonService { Override public String generateReason(Course course, Double score, boolean coldStart) { if (coldStart) { return 近期选课人数较多、整体评分良好的课程适合作为通识拓展方向尝试。; } if (score ! null score 3.0) { return 与你已选课程有较高的受众重合度往届选课学生普遍反馈课程内容扎实。; } return 根据课程多维特征分析该课程在难度与开课院系方面与你当前学习方向较为匹配。; } }模板实现并不神奇但它完成了一个更重要的事情把“推荐解释模块”的边界确定下来。后期如果你的毕业设计想展示更前沿的 AI 能力比如接入大模型生成个性化推荐语只需要替换这个 Service。Controller 和 Thymeleaf 页面完全不用改。这一点在毕业设计论文中非常好写也是评委愿意听的设计亮点。6.5 主推荐服务的完整流程主推荐服务按以下逻辑组装查询当前学生的历史评分记录。查询全部课程和全部评分记录。如果学生没有历史记录走热门课程冷启动逻辑。如果学生有历史记录调用协同过滤推荐逻辑。用 AI 推荐理由服务生成每门候选课程的推荐理由。封装成RecommendItem列表返回给 Controller。// 文件路径src/main/java/com/example/course/service/Impl/CourseRecommendServiceImpl.java Service public class CourseRecommendServiceImpl implements RecommendService { private final StudentCourseScoreMapper studentCourseScoreMapper; private final CourseMapper courseMapper; private final SimCourseRecommendService simCourseRecommendService; private final AiRecommendReasonService aiRecommendReasonService; public CourseRecommendServiceImpl(StudentCourseScoreMapper studentCourseScoreMapper, CourseMapper courseMapper, SimCourseRecommendService simCourseRecommendService, AiRecommendReasonService aiRecommendReasonService) { this.studentCourseScoreMapper studentCourseScoreMapper; this.courseMapper courseMapper; this.simCourseRecommendService simCourseRecommendService; this.aiRecommendReasonService aiRecommendReasonService; } Override public ListRecommendItem recommendForStudent(Long studentId, int limit) { ListStudentCourseScore allScores studentCourseScoreMapper.selectList(null); ListCourse allCourses courseMapper.selectList(null); ListStudentCourseScore myScores studentCourseScoreMapper.selectList( new LambdaQueryWrapperStudentCourseScore() .eq(StudentCourseScore::getStudentId, studentId)); boolean coldStart myScores null || myScores.isEmpty(); ListRecommendItem result new ArrayList(); if (coldStart) { ListCourse hotCourses simCourseRecommendService.hotCourses(allScores, allCourses, limit); for (Course course : hotCourses) { RecommendItem item new RecommendItem(); item.setCourse(course); item.setScore(0.0); item.setReason(aiRecommendReasonService.generateReason(course, 0.0, true)); result.add(item); } return result; } ListLong courseIds simCourseRecommendService.recommendCoursesBySimilarity( myScores, allScores, allCourses, limit); MapLong, Course courseMap allCourses.stream() .collect(Collectors.toMap(Course::getId, c - c)); for (Long courseId : courseIds) { Course course courseMap.get(courseId); if (course null) { continue; } RecommendItem item new RecommendItem(); item.setCourse(course); item.setScore(0.0); item.setReason(aiRecommendReasonService.generateReason(course, 0.0, false)); result.add(item); } return result; } }主流程并不复杂但结构完整。答辩时顺着这个方法讲条理非常清楚。7. Controller 与 Thymeleaf 页面渲染7.1 Controller 层实现Controller 负责接收学生 ID、调用推荐服务、把结果放进 Model返回到 Thymeleaf 模板。为了演示方便这里用studentId作为请求参数实际项目应该从登录会话中获取。// 文件路径src/main/java/com/example/course/controller/RecommendController.java Controller public class RecommendController { private final RecommendService recommendService; public RecommendController(RecommendService recommendService) { this.recommendService recommendService; } GetMapping(/recommend) public String recommend(RequestParam(value studentId, defaultValue 1) Long studentId, Model model) { ListRecommendItem items recommendService.recommendForStudent(studentId, 5); model.addAttribute(items, items); model.addAttribute(studentId, studentId); return recommend; } }7.2 Thymeleaf 页面模板在src/main/resources/templates/recommend.html中渲染推荐结果。这里用th:each遍历推荐列表用th:text输出课程属性和推荐理由。!DOCTYPE html html langzh xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 title校园课程推荐系统/title link relstylesheet th:href{/css/style.css} /head body div classcontainer h2为你推荐的课程/h2 p学生IDspan th:text${studentId}1/span/p div classcourse-list div classcourse-card th:eachitem : ${items} h3 th:text${item.course.courseName}课程名称/h3 p课程编号span th:text${item.course.courseCode}/span/p p开课院系span th:text${item.course.college}/span/p p任课教师span th:text${item.course.teacherName}/span/p p学分span th:text${item.course.credit}/span/p p难度span th:text${item.course.difficulty}/span/p p classreason推荐理由span th:text${item.reason}/span/p /div /div /div /body /html这个页面不需要复杂的 JavaScript。它的价值在于学生打开页面能看到推荐课程列表还能看到一行推荐理由。对于课程推荐系统来说“有推荐理由”和“没有推荐理由”在观感上是质变。7.3 给页面增加基本样式可以增加一个style.css样式的引用让课程卡片横向排列或纵向堆叠提高演示视觉效果。CSS 可以根据自己的审美调整这里只提供一个基础思路使用display: grid或flex布局展示卡片课程卡片内部包含课程名称、教师、学分、难度和推荐理由。8. 运行验证与常见问题排查8.1 启动与访问流程先用 Maven 启动 SpringBoot 应用mvn spring-boot:run启动成功后浏览器访问http://localhost:8080/recommend?studentId1如果数据库里有学生和选课评分数据页面会展示推荐课程列表和推荐理由。没有历史数据的学生走冷启动逻辑会看到热门课程推荐。8.2 如何判断推荐结果正确判断推荐系统是否正常工作不能只看页面是否渲染出课程。按照下面几个步骤验证把学生 1 的选课记录人工构造出来比如他给“高等数学”打了 5 分给“大学物理”打了 4 分。插入一批其他学生对课程的打分记录确保“线性代数”和“概率论”这些课程与“高等数学”有相似受众。访问学生 1 的推荐页面确认推荐结果中包含相似课程且不包含他已经选过的课程。检查控制台 SQL 日志确认 MyBatis-Plus 正常打印了查询语句。如果推荐结果为空优先检查数据库里是否有足够的选课评分数据以及当前学生是否把所有课程都选过了。8.3 常见问题与排查思路问题现象可能原因排查方式解决方案启动失败端口被占用8080 端口被其他进程占用查看启动日志里的端口异常更换server.port或释放端口连接数据库报错MySQL 服务未启动或账号密码错误检查数据库连接 URL、用户名、密码确认数据库已启动密码正确页面出现Could not resolve viewThymeleaf 模板路径不对检查templates目录和文件名确认文件存在于resources/templates后缀为.html推荐列表为空学生历史评分为空或评分过低打印推荐日志构造足够测试数据或检查冷启动逻辑中文乱码数据库或页面字符集不一致检查数据库字符集和连接参数使用utf8mb4并在 JDBC URL 中加入characterEncodingutf8Maven 依赖下载缓慢未配置国内镜像检查本地 Maven settings配置阿里云镜像仓库这些问题是 SpringBoot MyBatis-Plus 项目里最常见的几类也是答辩时评委爱问的工程问题。能在现场答出“这个报错是因为模板解析路径不对”本身说明你是真的做过。9. 最佳实践与后续学习方向9.1 推荐系统别一上来就上复杂模型很多做毕设的同学容易走进一个误区认为推荐系统一定要用矩阵分解、深度学习、大模型。但在校园课程推荐这个场景里数据量只有几百条复杂模型不仅训练不出来解释起来也费劲。毕设场景下最务实的路径是先做基于规则的推荐再叠加协同过滤最后用 AI 生成推荐理由。这套方案数据要求低、逻辑可解释、代码量可控而且每一层都可以单独写进论文。9.2 把“可解释性”作为项目亮点普通推荐系统只输出结果你这套系统输出结果的同时还输出推荐理由。这个点非常值得在答辩时展开。讲讲 AI 推荐理由服务为什么用接口隔离模板实现和真实大模型实现的差异在哪里以及可解释推荐对用户体验的价值。9.3 继续深入的学习方向如果时间充裕可以从三个方向升级这套系统方向一是引入真实大模型服务将模板推荐理由替换为大模型生成的个性化推荐语让推荐理由更丰富。申请好合法的模型服务后构建一个实现类调用模型接口解析返回内容即可原有页面结构不用变。方向二是引入向量数据库把课程名称、课程介绍、课程评价文本转成向量用语义相似度替代稀疏评分矩阵解决新课程没有评分数据的问题。方向三是增加更多隐式反馈比如记录学生浏览课程的时间、点击次数、关注课程的行为用这些信号训练一个更完整的用户画像让推荐结果更准。9.4 工程上要注意的细节数据库连接密码不要硬编码在配置文件里提交到公开仓库这是基本安全意识。如果多人共同开发推荐日志表和课程表等核心表结构变更时要提前沟通。上线前用测试数据跑一遍完整流程包括冷启动和协同过滤两种场景确保页面不会因为推荐结果为空而报错。这里强调一句所有数据库变更建议先用测试库验证操作前备份遵循最小权限原则不要在生产环境直接改表。10. 总结基于 SpringBoot Thymeleaf AI 推荐的校园课程推荐系统核心不是页面有多花哨而是数据建模是否合理、推荐策略是否分层、推荐结果是否能解释。学生选课评分表是这套系统的数据基础协同过滤负责生成课程候选集AI 推荐理由服务负责解释推荐结果冷启动逻辑负责处理无历史行为的学生。代码量不大但每一层都经得起追问。建议你把这套工程跑通之后再思考一个问题如果数据量扩大一百倍内存计算会变成性能瓶颈那时候应该怎么设计带着这个问题去研究缓存、异步推荐、离线计算结果等方案你的推荐系统能力会上一个台阶。这篇内容建议收藏备用动手写代码时对照着实现会顺利很多。