最近被问得最多的一个问题就是:Java 毕设到底做什么题目,既能覆盖主流技术栈,又不至于烂大街、答辩时没有东西可讲。如果让我推荐一个稳妥的“组合题”,我的答案一直是同一个——基于 Spring Boot 的地域文化旅游推荐系统。
这个题目我前前后后带过不少学生做,包括我自己也完整落地过一版。它把 Spring Boot、MyBatis-Plus、MySQL、Redis、Vue 前后端分离这些常规技术全部串了进去,同时业务上有三个天然的内容方向:地方特产、民俗风情、旅游景点。每个方向都能做推荐,既能往协同过滤、标签匹配上讲故事,又不至于把算法做得太深导致写不完。对毕设来说,这个尺度卡得刚刚好。
这篇文章就把我当时从零到一实现这个系统时踩过的坑、用过的方案、代码结构、数据库设计、推荐算法落地方式全部整理出来。无论你是想照着一个思路自己写,还是已经买了成品代码需要二次开发和熟悉流程,都值得从头到尾看一遍。内容偏实操,按“能直接动手复现”的标准来写。
1. 项目定位与整体架构设计
1.1 选题背后的真实需求
很多人拿到“旅游推荐系统”这个题目,第一反应是:这不就是携程/马蜂窝的简化版吗?这种理解没有错,但抓错了重点。毕设题目的核心不是“做多大”,而是“能不能完整闭环”。地域文化旅游推荐系统这个题目,它的隐藏要求恰恰在于三件事:
第一,有数据区分度。地方特产、民俗风情、旅游景点是三种完全不同的实体,属性差异大,这就逼着你在数据库设计、推荐策略上做差异化处理。比如景点有地理位置、门票价格、开放时间,特产有价格、口味、产地,民俗有节庆时间、活动形式。这三类数据如果只用一张表存,后面推荐效果会很粗糙。
第二,有推荐场景可讲。系统不能只做增删改查,总要有一个“大脑”。在这里,“推荐”就是把用户的历史行为(浏览、收藏、评分)和内容特征(标签、分类、地域)做匹配,输出一个排序列表。对毕设来说,做到这一层就足够有说服力了。
第三,有前后端完整链路。这也是为什么这个题目特别适合体现工作量:用户端要有首页推荐、分类浏览、详情页、收藏/评价;管理端要有景点管理、特产管理、民俗管理、用户管理、数据统计。整个链路拉下来,工作量可观,展示效果也直观。
这个项目适合的人群也很明确:准备做 Java Web 方向毕设的本科生,以及想系统地把 Spring Boot + Vue 全栈流程走一遍的初学者。如果你已经有 Java 基础,只是想找一个“能串起来”的完整项目,这个题目同样合适。
1.2 技术栈选型:Spring Boot 为什么是那个“唯一解”
选技术栈的时候,很多学生会纠结要不要上微服务、要不要用 Elasticsearch。我的建议是:毕设阶段,Spring Boot + MySQL + Redis + Vue 这套组合已经足够稳妥,别再往复杂了加。
后端核心用的是 Spring Boot 2.7.x。选这个版本有一个非常实际的原因:它同时兼容 JDK 8 和 JDK 17,而且大部分教程、资料都是基于这个版本写的。Spring Boot 3.x 虽然也已经很稳定,但很多学生机器上装的是 JDK 8,直接上 Spring Boot 3 会遇到 javax 到 jakarta 的迁移问题,各种第三方依赖也会出现版本冲突。2025 年还坚持用 Spring Boot 2.7 不是落后,是省事。
持久层我用的是 MyBatis-Plus 而不是原生 MyBatis。如果你有实习或者工作经历就知道,国内中小型项目用 MyBatis-Plus 的比例非常高,它的 LambdaQueryWrapper 写条件查询非常舒服,分页插件也比自己手写 Limit 方便得多。而且它自带代码生成器,可以把实体类、Mapper、Service、Controller 一次性生成出来,对毕设前期搭建骨架来说能省大量时间。
缓存选型上没有纠结,直接 Redis。用途主要有三个:缓存首页推荐结果、缓存景点详情、统计用户浏览行为。面试的时候这个也相当加分,因为体现了“为什么需要用缓存”:景点的热门推荐是典型的读多写少场景,把热门前 20 的推荐结果存到 Redis 里,接口响应能从 200ms 直接压到 10ms 以内。
前端用的是 Vue 3 + Element Plus。如果你对 Vue 还不熟,建议选 Vue 3 + Vite 的组合,Vite 的冷启动速度相比 Webpack 快太多,开发体验完全是两个时代。页面不多的时候不用引入 Vuex/Pinia,直接用组件内的 ref/reactive 管理状态就够了。
1.3 系统功能模块划分
整个系统按角色拆成两个端,功能边界要清晰。
用户端(前台)包括这些核心模块:用户注册登录、首页推荐列表、目的地(城市)分类浏览、景点/特产/民俗详情页、收藏功能、评分功能、浏览历史、个人信息维护。
管理端(后台)包括:管理员登录、数据看板(用户数量、内容数量、访问量统计)、景点管理、特产管理、民俗管理、用户管理、评论管理、推荐参数配置。
这个模块划分的核心逻辑是:前台全部围绕“浏览 + 互动 + 推荐反馈”来设计,后台全部围绕“内容维护 + 推荐数据准备”来设计。前后台通过一套接口通信,接口统一走 /api/user/** 和 /api/admin/** 前缀做权限区分。
打眼一看这模块列表并不复杂,但真正写起来你要处理不少细节。比如景点详情页至少要展示图片轮播、介绍文字、地理位置、门票信息,同时还要展示“这个景点的相关特产”“附近的民俗活动”,这些相关联的内容正好就是推荐算法的数据基础。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计与核心表结构
2.1 核心实体关系梳理
数据库设计是面试答辩时最容易暴露问题的地方。很多人的表建得特别随意,字段命名不规范,表之间没有外键关联说明,答辩时老师一问“这张表为什么这么设计”就卡壳了。
我当时的做法是先梳理清楚实体关系,再动手建表。核心实体有七个:用户、景点、地方特产、民俗风情、评论、收藏、浏览记录。另外还有两个辅助实体:标签和城市(地域)。为什么要把标签和城市单独提出来?
因为推荐系统的核心匹配逻辑就是靠标签来做的。一个景点被打上“自然风光”“亲子游”“历史古迹”的标签,一个特产被打上“手工艺品”“美食”“伴手礼”的标签,用户浏览过“自然风光”的景点,那系统就可以推同样带“自然风光”标签的特产或民俗,这样跨类型推荐才能顺畅。
城市(地域)则是所有内容的最上层归属。景点的“所属城市”决定了目的地筛选的逻辑,同时“地域”本身也是推荐的一个重要维度——推荐“同城相关”内容是一个特别自然的规则,而且实现成本几乎为零。
2.2 推荐系统相关的表设计
推荐系统相关的表是整个数据库设计的重点,也是区分“普通管理系统”和“推荐系统”的关键。
我必须强调一张表:用户行为表(user_behavior)。这张表专门记录用户在产品里的所有行为,包括浏览(BROWSE)、收藏(FAVORITE)、评分(RATE)。每条记录包含用户 ID、内容类型(景品/特产/民俗)、内容 ID、行为类型、行为时间、行为分值五个字段。
为什么要有这张表?因为推荐算法的输入就是行为数据。Item-based 协同过滤计算的“物品相似度矩阵”,本质上就是对这张表做统计:两个物品被同一用户浏览过、收藏过,它们之间就产生了一条关联记录。没有这张表,你的推荐就只能是“最新发布”或“随机推荐”,没有任何个性化可言。
我建表的时候给行为分值做了明确的约定:浏览算 1 分,收藏算 3 分,评分 5 分制的用户评分直接作为行为分值。这样在计算用户偏好权重的时候,不需要再做复杂的映射,直接加权求和即可。
2.3 建表 SQL 与关键设计思路
直接给核心表的建表 SQL,这张表是我实测跑通的版本,字段命名和注释都可以直接用。
sql复制-- 用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(50) NOT NULL COMMENT '用户名',
`password` varchar(100) NOT NULL COMMENT '密码(MD5加密)',
`nickname` varchar(50) DEFAULT NULL COMMENT '昵称',
`avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
`city` varchar(50) DEFAULT NULL COMMENT '所在城市',
`phone` varchar(20) DEFAULT NULL COMMENT '手机号',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
-- 景点表
CREATE TABLE `attraction` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '景点ID',
`name` varchar(100) NOT NULL COMMENT '景点名称',
`city_id` bigint(20) NOT NULL COMMENT '所属城市ID',
`summary` varchar(500) DEFAULT NULL COMMENT '简介',
`content` text COMMENT '详细介绍',
`cover_image` varchar(255) DEFAULT NULL COMMENT '封面图',
`images` text COMMENT '图片列表(逗号分隔)',
`ticket_price` decimal(10,2) DEFAULT '0.00' COMMENT '门票价格',
`open_time` varchar(50) DEFAULT NULL COMMENT '开放时间',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`tags` varchar(255) DEFAULT NULL COMMENT '标签(逗号分隔)',
`view_count` int(11) DEFAULT '0' COMMENT '浏览量',
`status` tinyint(1) DEFAULT '1' COMMENT '状态:0下架 1上架',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='景点表';
特产表和民俗表的字段逻辑与景点表基本一致,核心差异在于业务字段:特产多了产地、价格、品尝季节;民俗多了活动时间、举办地点。标签字段建议统一用逗号分隔的字符串存,虽然这样不太符合严格的数据库范式,但对小型推荐系统来说查询和更新都特别方便。用 JSON 数组存也可以,MySQL 5.7+ 支持 JSON 类型,但对新手来说直接用字符串更不容易踩坑。
用户行为表我单独建了一个索引,这个细节你在答辩时可以重点讲:
sql复制CREATE TABLE `user_behavior` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户ID',
`content_type` varchar(20) NOT NULL COMMENT '内容类型:attraction/specialty/folk',
`content_id` bigint(20) NOT NULL COMMENT '内容ID',
`behavior_type` varchar(20) NOT NULL COMMENT '行为类型:BROWSE/FAVORITE/RATE',
`behavior_score` int(2) DEFAULT '1' COMMENT '行为分值',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
KEY `idx_user_content` (`user_id`, `content_type`, `content_id`),
KEY `idx_content_type` (`content_type`, `content_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户行为表';
idx_user_content 这个联合索引是为了快速查询“某个用户对哪些内容做过什么操作”,idx_content_type 是为了快速查询“某个内容被哪些用户操作过”。这两个索引恰好就是 User-based CF 和 Item-based CF 各自需要的最核心查询,设计数据库的时候有这个意识,后面写推荐算法的时候会顺手很多。
3. 推荐算法怎么落地
3.1 基于内容的推荐:标签匹配是底线
推荐算法是这类毕设的灵魂。但我要先泼一盆冷水:不要一上来就想着上深度学习,也不要强行搞复杂的矩阵分解。毕设推荐系统做到“有依据、可解释、能演示”就够了。
我当时最先实现的方案是基于内容的推荐。它的核心逻辑非常简单:提取用户喜欢过的内容标签,计算与待推荐内容标签的相似度,按相似度排序输出推荐列表。
具体到代码层面,就是给每条内容维护一个标签数组(tags 字段,逗号分隔),推荐的时候先找到用户最近浏览/收藏的几条内容,把这些内容的标签拆开、统计频率,形成“用户标签偏好向量”,然后对候选内容逐一计算标签重合度。
比如用户最近浏览了一个标签为 ["自然风光", "亲子游", "摄影"] 的景点,那么候选的民俗如果带 ["亲子游", "民俗体验"] 标签,重合度就有 1 个标签;如果候选景点带 ["自然风光", "徒步"],重合度也是 1。再加一个地域维度的硬性加分:候选内容和用户偏好内容的城市一致时,相似度直接乘以 1.2 的加权系数。这样推荐结果首先在“地域相关”这一点上就不会跑偏。
这个方案的优点是多快好省,推荐结果解释性很强,前端展示的时候可以直接显示“因为你看过 XX,所以推荐了 XX”。但它的缺点也很明显:完全没有利用用户群体的行为信息,推荐结果的多样性差,容易一直推同一个类型的东西。所以还需要协同过滤来补充。
3.2 协同过滤:用户行为才是指挥棒
我选择的第二层推荐方案是 Item-based Collaborative Filtering,也就是基于物品的协同过滤。
它的核心思想用一句话说就是:用户 A 喜欢了景点 X 和特产 Y,用户 B 喜欢了景点 X,那系统就把特产 Y 推荐给用户 B。因为“喜欢过同一个景点”这件事,构成了两个物品之间的相似性。
具体实现里,我维护了一个“物品关联表”(item_similarity),定期计算,不用实时算。计算步骤如下:
第一步,从 user_behavior 表里取最近 30 天的有效行为数据,过滤掉评分低于 3 的记录。
第二步,对每条行为记录按“用户+内容类型+内容ID”做聚合,形成一个“用户-物品”倒排表。
第三步,遍历用户行为列表,统计任意两个物品被同一个用户同时操作过的次数,这个次数就是物品之间的“共现数”。
第四步,用余弦相似度对共现数做归一化处理,得到一个 0 到 1 之间的相似度分值:
java复制public double calculateSimilarity(long itemAOccur, long itemBOccur, long coOccur) {
if (itemAOccur == 0 || itemBOccur == 0) {
return 0.0;
}
return coOccur / Math.sqrt(itemAOccur * itemBOccur);
}
为什么用余弦相似度而不是直接用共现数?因为直接共现数有一个严重的偏差:热门物品被操作的概率天然更高,如果直接用共现数,所有的推荐结果都会被热门物品霸占。余弦相似度通过对每个物品自身的热度做了归一化,相当于在说“虽然这个物品很热门,但更重要的是它和你喜欢的那个物品是不是真的经常一起出现”。
生成推荐列表的时候,拿用户最近有正向行为的 N 个物品,去 item_similarity 表里捞相似度最高的候选物品,按相似度加权求和排序,过滤掉已经看过的物品,输出 Top N。
这个方案虽然在工业界已经不算新颖,但作为毕设完全够用,而且可以引出很好的答辩讨论话题:物品相似度多久更新一次、冷启动怎么处理、如何改进推荐多样性。
3.3 冷启动和混合推荐的处理
冷启动是推荐系统无法回避的问题,也是答辩老师必问的考点。我当时准备了两种解决方案。
第一种,针对新注册用户。用户还没有任何行为数据,这个时候的推荐策略是:按照“城市热度 + 内容浏览量 + 内容评分”做一个加权排行榜,取综合得分最高的 Top N 展示。简单说就是推荐热门内容,这一步在 SQL 里用 ORDER BY 轻松搞定。为了让首页看起来像“推荐”而不是“排行榜”,我在前端展示时会把标题写成“热门推荐”,而不是“排行榜”,体验上自然很多。
第二种,针对新上架的内容。一条新景点或者新特产没有任何用户行为,协同过滤永远算不到它。我的处理是给新内容设置一个“冷启动加权期”,在发布时间 7 天内,给它的基础热度分乘以一个 1.5 倍的新鲜度加成,同时在标签匹配计算中,如果新内容的标签和当前用户偏好标签高度相关,直接优先推荐。
最终的推荐策略是混合式的:优先用协同过滤结果,如果协同过滤结果不足 5 条,用基于内容的标签匹配补齐;如果还是不满足,用热门榜单兜底。这个优先级逻辑写在一个服务类里,输入用户 ID,输出推荐列表,前端只需要调用一个接口。
混合推荐的核心价值在于:它让系统在任何情况下都能返回结果,且不同阶段用户的体验是有变化的——新用户看到的是热门推荐,使用一段时间后逐渐变成个性化推荐。这个“渐进式个性化”的过程在答辩现场演示时特别有说服力。
4. 后端核心接口与前端联调
4.1 统一返回结果和异常处理
前后端联调最让人头大的事情就是接口返回格式不统一。有的接口返回 {code: 200, data: [...]},有的接口直接返回裸数组,前端每个请求都要单独判断,日志排查问题的时候也麻烦。我建议在项目最开始就统一一个全局返回结构:
java复制@Data
public class Result<T> {
private Integer code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}
public static <T> Result<T> error(String message) {
Result<T> result = new Result<>();
result.setCode(500);
result.setMessage(message);
return result;
}
}
配合全局异常处理器 @RestControllerAdvice,把业务异常、参数校验异常、系统异常统一拦截,返回给前端的就是结构永远一致的 JSON。这个设计本身不复杂,但体现了一个非常重要的工程意识:接口契约要稳定。这是我带学生做项目时一定会强调的,也是你自己将来进公司工作后天天要做的事情。
4.2 JWT 登录认证
登录认证这快内容,答辩的时候也是高频考点。传统的 Session 方案需要服务端保存会话状态,现在主流做法是用 JWT(JSON Web Token)做无状态认证。
流程是这样的:用户登录成功后,后端生成一个 token,里面包含用户 ID、用户名、过期时间,用密钥进行签名,返回给前端。之后前端每次请求都在请求头里带上 Authorization: Bearer ${token},后端通过拦截器解析 token,从里面拿用户信息。
我用的 JWT 依赖是 io.jsonwebtoken:jjwt,生成 token 的核心代码如下:
java复制public String generateToken(Long userId, String username) {
return Jwts.builder()
.setSubject(String.valueOf(userId))
.claim("username", username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 24 * 7))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
这里把过期时间设置为 7 天,是为了避免用户频繁重新登录。如果你要做得更细,还可以引入 Redis 记录“已注销的 token”实现退出登录,以及用 Redis 的过期时间做“强制下线”功能。但毕设做到 JWT 解析 + 拦截器校验这一层就已经够完整了。
我当时踩过一个坑:JWT 的工具类用的是 SecretKeySpec,密钥长度不够 256 位时启动不报错,但请求接口时签名会失效。排查了很久才发现是密钥字符串太短。如果还用 HS256 签名,密钥字符串要足够的长度,建议直接写一个 32 位以上的字符串,不要图省事写 "123456"。
4.3 前端 Vue 页面结构与常见问题
前端页面的核心结构可以按路由来梳理:首页、分类浏览页、内容详情页、个人中心、管理后台。
首页是最重要的展示页面,从上到下依次是:搜索栏、城市选择器、轮播图 Banner、推荐瀑布流。推荐瀑布流的数据来自后端的 /api/user/recommend 接口,Vue 组件里直接 onMounted 的时候调用。这里我建议直接展示推荐理由,比如“根据你浏览的【XX景区】,为你推荐”,这会让推荐看起来更智能,截图放进论文里也好看。
分类浏览页非常简单,就是调列表接口,传分类参数,表格/卡片展示。Element Plus 的分页组件 + MyBatis-Plus 的分页插件配合使用,前端传 page 和 size,后端返回 total 总条数,前端分页组件根据 total 渲染页码。
管理后台用 Vue + Element Plus 的侧边栏布局,菜单和路由一一对应,每个页面就是一个表格 + 新增/编辑弹窗 + 删除按钮。这个部分没有太多技术含量,但工作量确实不低。为了省时间,你可以直接用若依框架这类开源后台脚手架改,如果项目要求自主实现,就自己写一个。
前后端联调时最大的坑是跨域配置。开发环境下前端跑在 5173 端口,后端跑在 8080 端口,不做任何处理的话浏览器会直接拦截请求。解决办法是在后端加一个全局跨域配置:
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
如果你配置了 JWT 拦截器,还要注意预检请求(OPTIONS)必须直接放行,否则前端控制台会一直报跨域错误。这个顺序问题调试起来挺折腾,我当时是把跨域配置和拦截器配置分开写,排错的时候方便很多。
5. 毕设常见问题与答辩避坑指南
5.1 Spring Boot 版本和依赖兼容问题
做毕设的人有一个惊人的共性:项目代码是在不同教程里复制拼凑的,导致依赖版本之间互相冲突。最常见的情况是:JDK 8 的环境下导入了 Spring Boot 3.x 的依赖,启动直接报 java.lang.NoClassDefFoundError: javax/servlet/...;或者 MyBatis-Plus 版本和 Spring Boot 版本不匹配,Mapper 扫描不到 Bean。
我的建议非常朴素:整个项目的依赖版本,统一从一个官方示例项目或一个已验证可运行的完整项目里复制,不要自己分别去 Maven 仓库里挑最新版。Spring Boot 2.7.x 对应 MyBatis-Plus 3.5.x,对应 MySQL 驱动 8.0.x,这个组合我跑通过很多次,非常稳。
如果你在配置里看到 spring.datasource.url 里的 serverTimezone=Asia/Shanghai 必填,不然会报时间差 8 小时的错,这也是 JDBC 驱动 8.x 的一个经典坑。一个小配置项,能让新手卡半小时。
5.2 跨域、分页、图片上传的坑
分页功能是几乎每个人都会遇到的问题。MyBatis-Plus 的分页查询必须先配置分页插件,不配置的时候 Page 对象不会自动执行 limit 语句,查询结果是全量的,前端分页完全失效。别再问我为什么 Page 返回的 total 永远是 0,十有八九就是分页插件没注入。
图片上传这块,我当时的方案很简单:后端接收 MultipartFile,保存到服务器本地 upload 目录,返回一个访问路径 /files/xxx.jpg。但是这个方案有一个隐患:Spring Boot 的静态资源默认只映射 classpath:/static/,你传的文件不在这个目录下,需要通过配置额外映射:
yaml复制spring:
mvc:
static-path-pattern: /files/**
web:
resources:
static-locations: file:${upload.path}
上传路径我建议用绝对路径存到配置文件里,这样部署的时候改一下配置就行,不用改代码。如果以后要部署到服务器上,用 Nginx 把这目录代理成一个静态资源地址,前后端分离的时候就非常顺。
5.3 论文和说明文档怎么写
很多人项目做完了,卡在论文上。我的经验是:论文写得好不好,直接决定答辩的上限。技术实现再漂亮,论文里讲不清楚,老师没法给你打高分。
写作顺序上,我建议你先画三张图再动笔。第一张是系统架构图,从上到下展示前端、后端、数据库、缓存、推荐的模块关系;第二张是功能模块图,把用户端和管理端的每个功能列全;第三张是业务流程图,选“用户从注册到获得推荐结果”这个主链路来画。这三张图放进论文里,整体框架就立住了。
正文的第一个核心章节是需求分析,要写清楚:这个系统解决什么问题(游客不知道玩什么、买什么),有哪些角色,每个角色有哪些功能需求。第二个核心章节是系统设计,包括概要设计和技术选型,这里要写出“为什么选 Spring Boot”“为什么用 Redis 做缓存”“推荐算法的思路”。第三个核心章节是系统实现,按模块写,每个模块写三样东西:界面截图、核心代码片段、功能说明。
答辩 PPT 的核心也是三块:项目背景与意义、技术架构与数据设计、亮点演示(重点是推荐效果和后台管理)。页数控制在 12-15 页就足够,不用贪多。老师提问最关注的永远是“你做了什么”和“遇到什么问题怎么解决的”,所以提前把推荐算法的原理、冷启动方案、为什么选这个技术栈这几件事想清楚,比什么都重要。
说实话,我见过太多学生把时间浪费在“反复重装环境”和“寻找完美代码”上。做这个项目的正确顺序永远是先把主链路跑通——注册、登录、发布内容、前台看到内容,然后在这条链路上加推荐、加缓存、加统计。我实际过一遍之后最大的感受是:推荐算法放在最后去优化完全来得及,但表结构和统一返回格式如果开头没做好,后面改起来会想哭。希望这份实操记录能帮你少走点弯路,项目顺利落地,答辩顺顺利利。
