SpringBoot+Thymeleaf+AI推荐:校园课程推荐系统设计与实现
2026/9/1 16:46:31 网站建设 项目流程

又是一年毕业设计季,如果你正在做或者准备做计算机毕业设计,最近大概率刷到过一类标题特别“炸”的题目,比如“基于SpringBoot+Thymeleaf+AI推荐的校园课程推荐系统(源码+LW+PPT+讲解)”。这类题目看起来功能齐全、技术栈主流,但每年答辩现场,总有人被同一个问题问倒:你的“推荐”到底是怎么实现的?数据从哪里来?为什么给这个学生推荐这门课,而不推荐另一门课?

很多同学的思路是把“AI大模型”当成主角,第一反应是去调大模型接口,或者从网上复制一份协同过滤源码,结果做出来的系统页面很花哨,逻辑却是断层。真正动手后才会发现,难点不在于接口有多高级,而在于推荐逻辑怎么和课程模块联动,没有用户行为数据时怎么办,推荐结果怎么展示、怎么向评委解释清楚。

这篇文章要讲的,就是一套基于 SpringBoot + Thymeleaf + AI 推荐算法的校园课程推荐系统,从毕业设计的真实痛点出发,把核心原理、数据库建模、SpringBoot 工程实现、Thymeleaf 页面渲染、运行验证和答辩高频问题一次性讲透。我的核心判断是:这套系统的亮点不在于页面多漂亮,而在于“推荐一门课给学生”这个动作是否可解释、可验证。抓住这一点,你的项目就超过了大部分停留在增删改查层面的毕业设计。

1. 这篇文章真正要解决的问题

先说说这类题目为什么每年都很“卷”。

校园课程系统本质上是一个典型的 CRUD 业务系统,无非是用户管理、课程管理、选课退课、成绩查询。如果只把这些功能做完,代码量确实够了,但在答辩时很难展现出技术深度——评委老师看过的课程系统,可能比你吃过的盐还多。于是很多同学会在题目里加一个“智能推荐”“AI课程推荐”的标签,希望通过算法来拉开差距。

但现实是,大部分人在“推荐系统”这个点上只是挂了一个名字:

  • 推荐列表是前端写死的;
  • 或者随机返回几门课程;
  • 或者直接调一个第三方大模型接口,让模型“编”几门课。

这三种做法都经不起追问。评委只要问一句“你用的什么推荐策略?相似度怎么计算的?为什么这么计算?”,如果答不上来,前面的功能做得再好也会被扣分。

所以要解决的核心问题,不是“怎么把项目跑起来”,而是“怎么把推荐系统讲明白”。基于内容推荐、协同过滤、冷启动处理、相似度计算,这些概念必须落到代码里,落到真实的课程标签和选课记录上。本文后面的内容,全部围绕这个目标展开。

如果你正准备做课程推荐相关课题,或者你手里已经有一套课程系统代码,但推荐模块很虚,这篇文章都值得认真看一遍。我不会只讲理论,也不会只贴一堆完整代码,而是把“理论 → 设计 → 代码 → 验证”这条链路打通。

2. 推荐系统的核心原理:先讲清楚再写代码

2.1 不是所有“推荐”都叫推荐系统

推荐系统的本质,是从一堆候选物品里,根据用户的兴趣特征和行为历史,预测用户可能喜欢的物品,并按照预测分数排序返回前 N 个结果。

在校园课程场景里,候选物品就是课程,用户特征就是学生的专业、年级、历史选课和成绩。系统要回答的问题非常具体:这个学生已经学了《Java程序设计》和《数据结构》,他接下来最适合学《数据库原理》还是《计算机网络》?

如果没有推荐系统,传统做法是让学生自己去课程列表里翻,或者按类型筛选。这种方式没有个性化,对用户是不友好的。

2.2 两个主流方向:协同过滤和基于内容

业界最经典的推荐算法有两条路线:

路线一:基于用户的协同过滤(User-Based Collaborative Filtering)

思路是“找相似的人”。先找到和你选课记录最相似的 K 个用户,然后把那些用户选过、而你没选过的课程推荐给你。核心是计算用户与用户之间的相似度。

路线二:基于物品的协同过滤(Item-Based Collaborative Filtering)

思路是“找相似的课”。如果很多选《Java程序设计》的用户也选了《数据结构》,那么这两门课之间的相似度就很高。当用户选过前者,系统就会推荐后者。

路线三:基于内容的推荐(Content-Based Recommendation)

思路是“找你喜欢的类型”。给每门课程打上标签,比如“计算机、编程、AI”,然后统计用户历史选课中哪些标签出现得多,形成用户偏好画像,再计算候选课程标签与用户偏好画像的相似度。

对于校园课程推荐系统,我建议把“基于内容的推荐”作为核心策略。原因有两个:

  1. 数据容易解释。课程标签是静态的,用户偏好是从选课记录中统计出来的,每个推荐结果都能说出理由,答辩时特别好讲。
  2. 冷启动相对可控。新课程没有选课记录也能靠标签参与推荐;新用户虽然没有历史,但可以用热门课程作为兜底。

当然,如果你时间充足,也可以同时实现一个“基于用户的协同过滤”作为对比,展示你对多种推荐算法的理解。但从第一版落地的角度,先把基于内容推荐做扎实,性价比最高。

2.3 相似度计算:余弦相似度怎么理解

推荐算法落地最关键的一步是相似度计算。以基于内容推荐为例,我们需要把“用户偏好”和“课程特征”变成向量,然后计算两个向量的夹角余弦值。

余弦相似度的公式是:

cos(A, B) = (A · B) / (|A| × |B|)

通俗理解:如果两个向量方向越一致,夹角越小,余弦值越接近 1,说明相似度越高;如果方向完全不相关,余弦值接近 0,说明没什么关联。它不像欧式距离那样看重数值大小,而是看重方向,非常适合描述“兴趣标签是否匹配”。

在课程推荐场景里,我们可以简化向量空间:

  • 用户偏好向量:每个标签对应的选择次数;
  • 课程特征向量:每门课的标签集合,命中标签记为 1,否则为 0。

然后代入公式计算,就能得到每门候选课程对当前用户的推荐分数。这个分数是可以排序的,也是可以解释的。

2.4 冷启动问题

推荐系统最怕冷启动,也就是没有足够数据时怎么推荐。

校园课程系统里有三类冷启动:

  • 新用户没有任何选课记录,无法统计偏好;
  • 新课程没有任何人选过,无法通过选课关系计算协同过滤;
  • 系统刚上线,数据库里几乎没有用户行为数据。

我的建议是分级处理:新用户直接返回热门课程(按已选人数排序);新课程如果标签完整,就让它参与基于内容的推荐;如果数据库整体数据太少,可以预先在数据库中准备一批模拟数据,便于演示效果。这不是作弊,而是项目演示和答辩的必要准备。

3. 系统功能规划与数据库设计

3.1 功能模块划分

第一版系统建议控制范围,把主链路做通即可:

  • 用户模块:注册、登录、个人信息维护;
  • 课程模块:课程列表、课程详情、关键词搜索;
  • 选课模块:选课、退课、我的课表;
  • 推荐模块:根据用户历史选课生成推荐列表、展示推荐理由;
  • 管理后台:课程管理、用户管理、基础数据统计。

推荐模块是核心,其他模块是为它服务的。如果时间不够,先把“我的推荐”页面做好,再补课程管理后台。

3.2 数据库表设计

数据库设计直接决定推荐算法能不能顺利实现。我建议至少设计三张表:用户表、课程表、用户选课记录表。

用户表t_user

CREATE TABLE `t_user` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` VARCHAR(50) NOT NULL COMMENT '登录账号', `password` VARCHAR(100) NOT NULL COMMENT '登录密码,建议BCrypt加密存储', `real_name` VARCHAR(50) DEFAULT NULL COMMENT '真实姓名', `stu_no` VARCHAR(20) DEFAULT NULL COMMENT '学号', `major` VARCHAR(50) DEFAULT NULL COMMENT '专业', `grade` VARCHAR(10) DEFAULT NULL COMMENT '年级', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';

课程表t_course

CREATE TABLE `t_course` ( `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '课程ID', `course_name` VARCHAR(100) NOT NULL COMMENT '课程名称', `course_type` VARCHAR(50) NOT NULL COMMENT '课程类型:公共课/专业课/选修课', `credit` DECIMAL(3,1) NOT NULL COMMENT '学分', `teacher` VARCHAR(50) DEFAULT NULL COMMENT '授课教师', `major` VARCHAR(50) DEFAULT NULL COMMENT '建议修读专业', `tags` VARCHAR(255) DEFAULT NULL COMMENT '课程标签,逗号分隔,如 计算机,数学,AI', `description` TEXT COMMENT '课程简介', `select_count` INT DEFAULT 0 COMMENT '已选人数,用于热度推荐', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_course_type` (`course_type`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程表';

用户选课记录表t_user_course

CREATE TABLE `t_user_course` ( `id` BIGINT NOT NULL AUTO_INCREMENT, `user_id` BIGINT NOT NULL COMMENT '用户ID', `course_id` BIGINT NOT NULL COMMENT '课程ID', `score` DECIMAL(4,1) DEFAULT NULL COMMENT '成绩,未出分时为NULL', `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '选课时间', PRIMARY KEY (`id`), KEY `idx_user_id` (`user_id`), KEY `idx_course_id` (`course_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户选课记录表';

这里有两个设计点需要说明。

第一,tags字段用逗号分隔字符串存储。这在正规系统中可能不够规范,但毕业设计场景里,这种设计能大幅简化推荐逻辑的复杂度。你不需要再建一张课程标签中间表,直接拆字符串即可。

第二,select_count字段是一个冗余字段,用于热度推荐。虽然可以在查询时通过t_user_course统计,但每次实时统计会增加查询压力。为了方便演示和答辩,在选课成功后对这个字段做+1更新是很务实的做法。

除了这三张表,严谨的推荐系统通常还会记录用户的浏览行为、搜索记录、收藏记录。第一版可以不建,但你要知道这是后续优化方向。

4. 环境准备与前置条件

开始写代码之前,先把环境理清楚。

本文的工程示例基于以下技术栈,版本以你实际项目为准:

  • JDK:1.8 或 11(如果使用 Spring Boot 3.x,需要 JDK 17);
  • Spring Boot:2.7.x(稳定常用,教程多,踩坑少);
  • Thymeleaf:Spring Boot 内置 starter 管理版本;
  • MyBatis:mybatis-spring-boot-starter 2.3.x;
  • MySQL:5.7 或 8.0;
  • Maven:3.6 以上;
  • IDE:IntelliJ IDEA 即可。

这里特别提醒一点:不要盲目追求 Spring Boot 最新大版本。很多同学一创建项目就选了最新版,结果发现 Thymeleaf 的配置方式、依赖坐标跟网上教程对不上,浪费大量时间。初学阶段,建议选择一个稳定、文档丰富的大版本,先把项目跑通,再考虑升级。

如果你使用 Spring Boot 3.x,需要注意两个变化:

  • MyBatis Starter 要选择mybatis-spring-boot-starter的 3.x 版本;
  • javax包改成了jakarta包,代码中引入注解时要注意。

本文代码按 MyBatis 注解方式编写,省去 XML 配置,适合第一版快速落地。

5. 核心代码实现与关键逻辑精讲

5.1 依赖与基础配置

创建 Spring Boot 项目后,pom.xml中至少需要以下依赖:

<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>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.2</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency> </dependencies>

如果你用的是 Spring Boot 3.x,请到 Maven 仓库查看最新mybatis-spring-boot-starter版本,不要照抄 2.3.2。

再看application.yml配置:

# 文件路径:src/main/resources/application.yml 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: your_password thymeleaf: cache: false encoding: UTF-8 mode: HTML mybatis: configuration: map-underscore-to-camel-case: true

map-underscore-to-camel-case建议开启,这样数据库里的course_name可以自动映射为 Java 实体里的courseName,少写很多麻烦。

5.2 实体类与 Mapper 层

课程实体类:

// 文件路径:src/main/java/com/example/courserecommend/entity/Course.java public class Course { private Long id; private String courseName; private String courseType; private Double credit; private String teacher; private String major; private String tags; private String description; private Integer selectCount; // getter / setter 省略,建议用 Lombok 的 @Data 简化 }

用户课程 Mapper,负责查询用户选课历史和候选课程:

// 文件路径:src/main/java/com/example/courserecommend/mapper/UserCourseMapper.java @Mapper public interface UserCourseMapper { // 查询用户已经选过的课程 @Select("SELECT c.* FROM t_course c " + "INNER JOIN t_user_course uc ON c.id = uc.course_id " + "WHERE uc.user_id = #{userId}") List<Course> selectCoursesByUserId(@Param("userId") Long userId); // 查询用户没有选过的课程(候选推荐池) @Select("SELECT * FROM t_course WHERE id NOT IN " + "(SELECT course_id FROM t_user_course WHERE user_id = #{userId})") List<Course> selectCoursesNotSelectedByUser(@Param("userId") Long userId); // 查询热门课程,用于冷启动 @Select("SELECT * FROM t_course ORDER BY select_count DESC LIMIT #{limit}") List<Course> selectHotCourses(@Param("limit") int limit); }

这段 SQL 里最关键的是第二条。它先把当前用户已经选过的课程 ID 查出来,再用NOT IN排除,剩下的就是候选推荐池。这种写法性能不一定最好,但逻辑直白,适合作为毕设第一版。

5.3 推荐服务核心逻辑

这是整个系统最有技术含量的部分。推荐服务要做四件事:

  1. 查用户历史选课;
  2. 统计用户标签偏好;
  3. 遍历候选课程,计算相似度;
  4. 排序返回 Top N。

推荐结果 VO:

// 文件路径:src/main/java/com/example/courserecommend/vo/CourseRecommendVO.java public class CourseRecommendVO { private Course course; private Double score; private String reason; // getter / setter 省略 }

核心推荐逻辑:

// 文件路径:src/main/java/com/example/courserecommend/service/impl/RecommendServiceImpl.java @Service public class RecommendServiceImpl implements RecommendService { private final CourseMapper courseMapper; private final UserCourseMapper userCourseMapper; public RecommendServiceImpl(CourseMapper courseMapper, UserCourseMapper userCourseMapper) { this.courseMapper = courseMapper; this.userCourseMapper = userCourseMapper; } @Override public List<CourseRecommendVO> recommendByUser(Long userId, int topN) { // 1. 查询用户已经选过的课程 List<Course> historyCourses = userCourseMapper.selectCoursesByUserId(userId); // 2. 没有选课记录时,采用热度推荐兜底,避免冷启动空白 if (historyCourses == null || historyCourses.isEmpty()) { return convert(courseMapper.selectHotCourses(topN), "新用户暂无选课记录,先看看热门课程"); } // 3. 统计用户的标签偏好:标签 -> 选择次数 Map<String, Integer> tagPreference = new HashMap<>(); for (Course course : historyCourses) { for (String tag : splitTags(course.getTags())) { tagPreference.put(tag, tagPreference.getOrDefault(tag, 0) + 1); } } // 4. 候选课程:用户没有选过的课程 List<Course> candidateCourses = userCourseMapper.selectCoursesNotSelectedByUser(userId); // 5. 计算每个候选课程与用户偏好的相似度 List<CourseRecommendVO> result = new ArrayList<>(); for (Course course : candidateCourses) { double score = cosineScore(course, tagPreference); CourseRecommendVO vo = new CourseRecommendVO(); vo.setCourse(course); vo.setScore(score); vo.setReason(buildReason(course, tagPreference)); result.add(vo); } // 6. 按相似度降序,输出前 topN 条 result.sort((a, b) -> Double.compare(b.getScore(), a.getScore())); if (result.size() > topN) { return result.subList(0, topN); } return result; } /** * 余弦相似度计算。 * 简化思路:以用户偏好标签集合作为向量空间, * 用户向量 = 标签选择次数,课程向量 = 标签是否出现(0/1)。 */ private double cosineScore(Course course, Map<String, Integer> userPreference) { List<String> courseTags = splitTags(course.getTags()); if (courseTags.isEmpty() || userPreference.isEmpty()) { return 0D; } double dot = 0D; double normUser = 0D; for (Map.Entry<String, Integer> entry : userPreference.entrySet()) { normUser += entry.getValue() * entry.getValue(); if (courseTags.contains(entry.getKey())) { dot += entry.getValue(); } } // 课程向量中每个标签记为 1,模长就是 sqrt(标签数) double normCourse = courseTags.size(); if (normUser == 0D || normCourse == 0D) { return 0D; } return dot / (Math.sqrt(normUser) * Math.sqrt(normCourse)); } /** * 生成推荐理由,让推荐结果可解释。 */ private String buildReason(Course course, Map<String, Integer> tagPreference) { List<String> courseTags = splitTags(course.getTags()); List<String> matched = courseTags.stream() .filter(tagPreference::containsKey) .collect(Collectors.toList()); if (matched.isEmpty()) { return "根据课程热度为你推荐"; } return "匹配你的兴趣标签:" + String.join("、", matched); } private List<String> splitTags(String tags) { if (tags == null || tags.trim().isEmpty()) { return Collections.emptyList(); } return Arrays.stream(tags.split(",")) .map(String::trim) .filter(s -> !s.isEmpty()) .collect(Collectors.toList()); } private List<CourseRecommendVO> convert(List<Course> courses, String reason) { List<CourseRecommendVO> list = new ArrayList<>(); for (Course course : courses) { CourseRecommendVO vo = new CourseRecommendVO(); vo.setCourse(course); vo.setScore(0D); vo.setReason(reason); list.add(vo); } return list; } }

有几个细节值得说明。

第一,cosineScore里的向量空间做了简化。真正的余弦相似度,应该把“用户偏好标签集合”和“课程标签集合”的并集作为统一的向量空间。但在工程实现中,因为候选课程的标签如果不在用户偏好集合中,它的向量分量就是 0,对点积没有贡献,对排序结果影响不大,所以可以先按用户偏好集合近似计算。如果答辩时被问到,你可以坦诚说明这是工程近似,并说出完整理论公式,反而显得到位。

第二,buildReason非常关键。它让每个推荐结果都有了可解释的理由:“匹配你的兴趣标签:计算机、编程”。这比单纯显示一个相似度分数高明得多,评委会觉得你的系统有业务思考。

第三,冷启动兜底逻辑直接放在推荐服务里。用户没有选课记录时,返回热门课程,系统不会出现空白页面。这是很实际的产品意识。

5.4 Controller 层与 Thymeleaf 模板

Controller 层返回 Thymeleaf 视图:

// 文件路径:src/main/java/com/example/courserecommend/controller/RecommendController.java @Controller @RequestMapping("/recommend") public class RecommendController { private final RecommendService recommendService; public RecommendController(RecommendService recommendService) { this.recommendService = recommendService; } @GetMapping("/my") public String myRecommend(@RequestParam(defaultValue = "1") Long userId, @RequestParam(defaultValue = "6") int topN, Model model) { List<CourseRecommendVO> recommendList = recommendService.recommendByUser(userId, topN); model.addAttribute("recommendList", recommendList); return "recommend"; } }

注意这里的userId在真实系统中应该从登录 Session 中获取,而不是通过请求参数传进来。第一版为了演示方便,可以先用固定用户,但答辩时最好说明“正式环境会从 Session 获取当前登录用户 ID”。

Thymeleaf 模板:

<!-- 文件路径:src/main/resources/templates/recommend.html --> <!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>AI 课程推荐</title> </head> <body> <h1>为你推荐的课程</h1> <!-- 冷启动提示 --> <div th:if="${#lists.isEmpty(recommendList)}"> <p>暂时没有推荐结果,请先去课程中心选几门课吧。</p> </div> <!-- 推荐列表 --> <div class="course-grid" th:unless="${#lists.isEmpty(recommendList)}"> <div class="course-card" th:each="item : ${recommendList}" th:object="${item.course}"> <h3 th:text="${item.course.courseName}">课程名称</h3> <p>课程类型:<span th:text="${item.course.courseType}">类型</span></p> <p>学分:<span th:text="${item.course.credit}">学分</span></p> <p>授课教师:<span th:text="${item.course.teacher}">教师</span></p> <p>推荐理由:<span th:text="${item.reason}">推荐理由</span></p> <p>推荐指数:<span th:text="${#numbers.formatDecimal(item.score, 1, 2)}">0.00</span></p> <button th:onclick="'selectCourse(\'' + ${item.course.id} + '\')'">选这门课</button> </div> </div> <script> function selectCourse(courseId) { if (confirm("确认选择这门课程?")) { window.location.href = "/course/select/" + courseId; } } </script> </body> </html>

Thymeleaf 和 JSP 的最大区别在于,Thymeleaf 是 HTML 文件,可以直接用浏览器打开预览,不需要容器渲染也能看到页面骨架。这对前后端联调和毕业设计展示都很友好。

5.5 如果想把“AI大模型”真正做进去

现在回到标题里的“AI大模型”。很多同学纠结:我到底是接一个 ChatGPT 接口,还是自己写推荐算法?

我的建议是先把推荐算法做好,再考虑大模型扩展。实际上,如果你非要让系统和大模型沾边,可以在推荐模块之外增加一个“课程问答助手”功能,让大模型根据课程描述生成学习计划、课程简介或选课建议。这种功能适合作为 PPT 里的展示亮点,但不适合作为核心推荐链路的依赖。原因很简单:大模型接口不稳定、有调用成本,一旦超时,整个推荐页面就崩了。

如果真要扩展,比较稳妥的做法是先用本地假数据模拟大模型返回结果,把流程跑通,再接入真实接口。同时一定要做好超时和异常降级,不能影响主流程。

6. 运行结果与效果验证

6.1 初始化演示数据

推荐系统没有数据就谈不上推荐效果。建议在数据库里准备一组有区分度的演示数据,让答辩现场一打开页面就能看到效果。

例如:

  • 用户 A 选过《Java程序设计》《数据结构》,标签偏向“计算机、编程”;
  • 用户 B 选过《大学英语》《中国近代史》,标签偏向“英语、人文”;
  • 课程库里同时存在计算机类、数学类、外语类课程。

这样两个用户打开“我的推荐”页面时,推荐结果会有明显差异,演示效果会非常直观。

6.2 启动项目并验证

项目启动命令:

mvn spring-boot:run

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

http://localhost:8080/recommend/my?userId=1&topN=6

预期结果:

  • 用户有选课记录时,页面返回按相似度降序排列的课程;
  • 每门课都显示“匹配你的兴趣标签:XXX”的推荐理由;
  • 推荐指数越高的课程,排在越靠前;
  • 新用户没有选课记录时,推荐列表返回热门课程。

如果页面空白,先看控制台日志有没有报错,再看数据库连接是否正常。如果 MySQL 8.0 的驱动包不正确,启动阶段就会直接报错。

6.3 验证相似度排序是否合理

可以人为构造一个最简单的验证场景。假设用户选过两门课,偏好标签为“计算机=2、编程=1”。候选课程《数据库原理》标签是“计算机,数据库”,计算出来的分数应该明显高于《大学物理》这种和计算机无关的课程。

这种可解释的验证过程,在毕业设计论文中是非常好的实验素材。你可以把计算过程、分数对比、排序结果截图放进论文,作为系统效果展示。

7. 常见问题与排查方法

问题现象可能原因排查方式解决方案
页面访问出现 Whitelabel Error Page请求路径写错或 Controller 未生效查看控制台日志,确认项目启动完成检查@Controller@GetMapping路径
Thymeleaf 页面 CSS/JS 加载不出来静态资源路径映射不对浏览器 F12 查看资源请求状态,确认是否 404确认页面中引用路径以/static开头,例如th:href="@{/css/style.css}"
推荐列表为空用户没有选课记录,或候选课程 SQL 查询条件有问题先确认t_user_course是否有数据,再检查 Mapper SQL确保推荐服务中加入了热门课程兜底逻辑
Spring Boot 版本太高导致配置失效最新大版本调整了部分自动配置逻辑查看项目所用 Spring Boot 版本与依赖版本是否匹配初学建议使用 Spring Boot 2.7.x 稳定版,后续再升级
中文乱码数据库字符集或页面编码不一致检查 MySQL 表字符集、JDBC URL 中characterEncoding设置统一使用utf8mb4,JDBC URL 加上characterEncoding=utf8
MyBatis 查询结果字段为 null数据库字段下划线和 Java 驼峰没有自动映射开启map-underscore-to-camel-case: true,或写 resultMapapplication.yml中配置 MyBatis 驼峰映射
端口被占用

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询