news 2026/9/7 6:24:00

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

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
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 接口不变,上层页面不需要跟着改。

推荐流程大致如下:

  1. 学生访问首页时,Controller 调用推荐服务。
  2. 推荐服务优先基于当前学生的历史选课记录计算相似课程。
  3. 如果当前学生没有历史记录,则使用热门课程与随机策略兜底,这就是冷启动处理。
  4. 对候选课程生成推荐理由,由 AI 服务接口统一输出。
  5. 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 '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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 '创建时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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-5,0表示未评分', 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 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) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='推荐日志表';

这张表模型的好处在于:选课记录本身是可扩展的。如果你想记录学生浏览课程的行为,只需要新增一张浏览日志表,推荐策略可以从“选课 + 评分”扩展到“浏览 + 关注”。这个扩展性要在论文和答辩中单独说明。

3.3 测试数据的准备

推荐系统离不开数据验证。建议在开发阶段先准备一部分模拟数据,方便看到推荐效果。比如插入几个学生,让他们对不同的课程打不同分数,再观察推荐结果是否符合直觉。可以写一段简单的 insert 语句,插 3 到 5 个学生和 10 门左右课程,每门课有 2 到 3 条评分记录即可。

4. 环境准备与工程基础配置

4.1 开发环境说明

建议使用 JDK 8 或 JDK 17,Maven 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> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent> <properties> <java.version>1.8</java.version> <mybatis-plus.version>3.5.3.1</mybatis-plus.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> </dependencies>

这里选择 MyBatis-Plus 而不是原生 MyBatis,是因为它内置了BaseMapper通用方法,单表查询基本不用写 XML,毕设开发效率高很多。

4.3 application.yml 中的关键配置

src/main/resources/application.yml中配置数据源、MyBatis-Plus 和 Thymeleaf:

server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/course_recommend?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/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映射成实体类属性courseName;Thymeleaf 开启cache: false是开发阶段改模板免重启;serverTimezone参数能避免数据库连接时的时间报错。如果启动时报数据库驱动类错误,先检查 MySQL 驱动版本是否和本地 MySQL 版本匹配。

5. 实体类、Mapper 与推荐数据准备

5.1 实体类设计

建立StudentCourseStudentCourseScore三个实体类。以课程实体为例:

// 文件路径: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/setter,@TableName指定数据库表名,@TableId声明主键类型。Student 和 StudentCourseScore 的结构与上述类似,分别对应t_studentt_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 BaseMapper<Course> { }

StudentMapper、StudentCourseScoreMapper 采用同样的写法。不写 XML,不写复杂 SQL,单表操作全部由 MyBatis-Plus 提供。真正复杂的推荐计算不是在 SQL 层完成的,而是在 Service 层用 Java 代码完成,这样推荐逻辑更清晰,也更容易测试。

5.3 推荐服务需要的数据获取方式

推荐服务运行时需要两类数据:

  • 当前学生已经选过并打分的课程集合。
  • 课程的全量列表,以及所有学生的选课评分记录。

这些都可以直接用selectList加上LambdaQueryWrapper条件查询获得。比如用new LambdaQueryWrapper<StudentCourseScore>().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 推荐结果列表 */ List<RecommendItem> 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 List<Long> recommendCoursesBySimilarity( List<StudentCourseScore> myScores, List<StudentCourseScore> allScores, List<Course> allCourses, int limit) { // 1. 计算所有课程之间的相似度,只保留每个目标课程最相似的课程集合 Map<Long, List<CourseSimilarity>> simMap = new HashMap<>(); List<Course> 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. 找到当前学生评分较高的课程,再找出这些课程的相似课程 Map<Long, Double> candidateScore = new HashMap<>(); Map<Long, Double> explainScore = new HashMap<>(); for (StudentCourseScore my : myScores) { if (my.getScore() == null || my.getScore() < 3) { continue; } List<CourseSimilarity> 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, List<StudentCourseScore> allScores) { Map<Long, Double> vectorA = new HashMap<>(); Map<Long, 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()); } } Set<Long> 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 List<Course> hotCourses(List<StudentCourseScore> allScores, List<Course> allCourses, int limit) { Map<Long, 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 主推荐服务的完整流程

主推荐服务按以下逻辑组装:

  1. 查询当前学生的历史评分记录。
  2. 查询全部课程和全部评分记录。
  3. 如果学生没有历史记录,走热门课程冷启动逻辑。
  4. 如果学生有历史记录,调用协同过滤推荐逻辑。
  5. 用 AI 推荐理由服务生成每门候选课程的推荐理由。
  6. 封装成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 List<RecommendItem> recommendForStudent(Long studentId, int limit) { List<StudentCourseScore> allScores = studentCourseScoreMapper.selectList(null); List<Course> allCourses = courseMapper.selectList(null); List<StudentCourseScore> myScores = studentCourseScoreMapper.selectList( new LambdaQueryWrapper<StudentCourseScore>() .eq(StudentCourseScore::getStudentId, studentId)); boolean coldStart = myScores == null || myScores.isEmpty(); List<RecommendItem> result = new ArrayList<>(); if (coldStart) { List<Course> 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; } List<Long> courseIds = simCourseRecommendService.recommendCoursesBySimilarity( myScores, allScores, allCourses, limit); Map<Long, 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) { List<RecommendItem> 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 lang="zh" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>校园课程推荐系统</title> <link rel="stylesheet" th:href="@{/css/style.css}"> </head> <body> <div class="container"> <h2>为你推荐的课程</h2> <p>学生ID:<span th:text="${studentId}">1</span></p> <div class="course-list"> <div class="course-card" th:each="item : ${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 class="reason">推荐理由:<span th:text="${item.reason}"></span></p> </div> </div> </div> </body> </html>

这个页面不需要复杂的 JavaScript。它的价值在于:学生打开页面,能看到推荐课程列表,还能看到一行推荐理由。对于课程推荐系统来说,“有推荐理由”和“没有推荐理由”在观感上是质变。

7.3 给页面增加基本样式

可以增加一个style.css样式的引用,让课程卡片横向排列或纵向堆叠,提高演示视觉效果。CSS 可以根据自己的审美调整,这里只提供一个基础思路:使用display: gridflex布局展示卡片,课程卡片内部包含课程名称、教师、学分、难度和推荐理由。

8. 运行验证与常见问题排查

8.1 启动与访问流程

先用 Maven 启动 SpringBoot 应用:

mvn spring-boot:run

启动成功后,浏览器访问:

http://localhost:8080/recommend?studentId=1

如果数据库里有学生和选课评分数据,页面会展示推荐课程列表和推荐理由。没有历史数据的学生走冷启动逻辑,会看到热门课程推荐。

8.2 如何判断推荐结果正确

判断推荐系统是否正常工作,不能只看页面是否渲染出课程。按照下面几个步骤验证:

  1. 把学生 1 的选课记录人工构造出来,比如他给“高等数学”打了 5 分,给“大学物理”打了 4 分。
  2. 插入一批其他学生对课程的打分记录,确保“线性代数”和“概率论”这些课程与“高等数学”有相似受众。
  3. 访问学生 1 的推荐页面,确认推荐结果中包含相似课程,且不包含他已经选过的课程。
  4. 检查控制台 SQL 日志,确认 MyBatis-Plus 正常打印了查询语句。

如果推荐结果为空,优先检查数据库里是否有足够的选课评分数据,以及当前学生是否把所有课程都选过了。

8.3 常见问题与排查思路

问题现象可能原因排查方式解决方案
启动失败,端口被占用8080 端口被其他进程占用查看启动日志里的端口异常更换server.port或释放端口
连接数据库报错MySQL 服务未启动或账号密码错误检查数据库连接 URL、用户名、密码确认数据库已启动,密码正确
页面出现Could not resolve viewThymeleaf 模板路径不对检查templates目录和文件名确认文件存在于resources/templates,后缀为.html
推荐列表为空学生历史评分为空或评分过低打印推荐日志构造足够测试数据,或检查冷启动逻辑
中文乱码数据库或页面字符集不一致检查数据库字符集和连接参数使用utf8mb4,并在 JDBC URL 中加入characterEncoding=utf8
Maven 依赖下载缓慢未配置国内镜像检查本地 Maven settings配置阿里云镜像仓库

这些问题是 SpringBoot + MyBatis-Plus 项目里最常见的几类,也是答辩时评委爱问的工程问题。能在现场答出“这个报错是因为模板解析路径不对”,本身说明你是真的做过。

9. 最佳实践与后续学习方向

9.1 推荐系统别一上来就上复杂模型

很多做毕设的同学容易走进一个误区:认为推荐系统一定要用矩阵分解、深度学习、大模型。但在校园课程推荐这个场景里,数据量只有几百条,复杂模型不仅训练不出来,解释起来也费劲。

毕设场景下最务实的路径是:先做基于规则的推荐,再叠加协同过滤,最后用 AI 生成推荐理由。这套方案数据要求低、逻辑可解释、代码量可控,而且每一层都可以单独写进论文。

9.2 把“可解释性”作为项目亮点

普通推荐系统只输出结果,你这套系统输出结果的同时还输出推荐理由。这个点非常值得在答辩时展开。讲讲 AI 推荐理由服务为什么用接口隔离,模板实现和真实大模型实现的差异在哪里,以及可解释推荐对用户体验的价值。

9.3 继续深入的学习方向

如果时间充裕,可以从三个方向升级这套系统:

方向一是引入真实大模型服务,将模板推荐理由替换为大模型生成的个性化推荐语,让推荐理由更丰富。申请好合法的模型服务后,构建一个实现类,调用模型接口,解析返回内容即可,原有页面结构不用变。

方向二是引入向量数据库,把课程名称、课程介绍、课程评价文本转成向量,用语义相似度替代稀疏评分矩阵,解决新课程没有评分数据的问题。

方向三是增加更多隐式反馈,比如记录学生浏览课程的时间、点击次数、关注课程的行为,用这些信号训练一个更完整的用户画像,让推荐结果更准。

9.4 工程上要注意的细节

数据库连接密码不要硬编码在配置文件里提交到公开仓库,这是基本安全意识。如果多人共同开发,推荐日志表和课程表等核心表结构变更时要提前沟通。上线前用测试数据跑一遍完整流程,包括冷启动和协同过滤两种场景,确保页面不会因为推荐结果为空而报错。这里强调一句:所有数据库变更建议先用测试库验证,操作前备份,遵循最小权限原则,不要在生产环境直接改表。

10. 总结

基于 SpringBoot + Thymeleaf + AI 推荐的校园课程推荐系统,核心不是页面有多花哨,而是数据建模是否合理、推荐策略是否分层、推荐结果是否能解释。学生选课评分表是这套系统的数据基础,协同过滤负责生成课程候选集,AI 推荐理由服务负责解释推荐结果,冷启动逻辑负责处理无历史行为的学生。代码量不大,但每一层都经得起追问。

建议你把这套工程跑通之后,再思考一个问题:如果数据量扩大一百倍,内存计算会变成性能瓶颈,那时候应该怎么设计?带着这个问题去研究缓存、异步推荐、离线计算结果等方案,你的推荐系统能力会上一个台阶。这篇内容建议收藏备用,动手写代码时对照着实现会顺利很多。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/7 6:22:32

谁在跟你抢PCIe?一文看懂链路协商与带宽排查

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:19:46

AI-Edge边缘计算实战:从模型压缩到TensorRT部署的完整指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/7 6:17:45

用Excel VBA打造进销存系统:从表结构到自动记账全解析

简介&#xff1a;一套面向中小企业与Excel进阶用户的进销存管理系统VBA实现资源&#xff0c;围绕采购、销售、库存和报表四大核心流程&#xff0c;提供低成本且可灵活定制的业务管理方案。压缩包内共3个文件&#xff0c;包括可直接运行的主工作簿xlsm文件、用于关联关系的rels文…

作者头像 李华