ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot汽车推荐系统实战:从算法到部署

SpringBoot汽车推荐系统实战:从算法到部署 1. 毕设选型为什么汽车推荐系统偏要选 SpringBoot每年到了毕业设计季总有学弟学妹拿着同一个问题来找我老师给的题目是汽车推荐系统技术栈到底怎么定有人一上来就奔着 SSM 去了有人觉得 PHP 更省事还有人直接想用 Python 写后端。我通常只有一句话如果不想在答辩时被问到哑口无言就老老实实用 SpringBoot 把这套系统撑起来。这句话不是拍脑袋说的。汽车推荐系统这个题目表面上看核心是推荐两个字但落到代码层面真正的工作量分布完全不是你想的那样。推荐算法本身本科毕设做到协同过滤或者基于内容的推荐就已经能交差了难的是把用户、汽车、收藏、浏览记录、评分这些数据管理起来再做成一套能跑通的前后端交互系统。而 SpringBoot 恰恰把后端的开发效率拉满自动配置、起步依赖、内嵌容器三件套能让一个从没独立做过完整项目的人在两周内把后端骨架搭得规规矩矩。更关键的一点是SpringBoot 的生态和招聘市场、研究生复试之间有种天然的关联。你简历上写着基于 SpringBoot 的汽车推荐系统面试官脑子里浮现的是一套标准的现代 Java 后端开发流程RESTful API 怎么设计、MyBatis-Plus 怎么用、Redis 缓存怎么接入、拦截器怎么处理登录态。这些都是真实工作中天天要碰的东西。相比之下SSM 虽然也是经典但配置文件的繁琐程度足以劝退一大半人而且现在很多学校的课程都已经转向 SpringBoot 了你用 SSM 写答辩老师反而会觉得你的知识结构停留在三年前。这套系统的另一个优势在于它可以做得浅也可以做得深。浅一点就是简单的 CRUD 加上一个基于标签的推荐逻辑前后端跑通数据表设计合理就能拿到一个不错的分数。深一点可以引入 Redis 做热门车型缓存、用 Elasticsearch 做全文检索、把协同过滤算法拆成离线计算和在线实时推荐两套流程甚至接入第三方 API 做车型参数同步。同一个题目本科毕业设计和研究生课程项目都能找到合适的位置这在毕设题目里其实不多见。我个人做这套系统时的技术选型是这么定的后端框架SpringBoot 2.7.x稳定、资料多、和大多数中间件的兼容性好持久层MyBatis-Plus省去大量 XML 映射配置分页查询直接内置数据库MySQL 8.x主流到不能再主流答辩时不会被挑毛病缓存Spring Data Redis用来存热门车型和用户的实时推荐结果推荐算法基于用户的协同过滤 基于内容的标签匹配两种算法做加权融合前端Vue 3 Element-Plus前后端分离接口用 RESTful 风格部署Maven 打包成 jar服务器上用 Docker 跑这套组合的好处是每一个组件都能在简历上单独写一行而且每一行都有对应的真实应用场景。你不需要在一套系统里把所有技术都堆上去但至少要让每个技术点都有它的位置和理由这一点在答辩时格外重要。2. 需求拆解汽车推荐系统到底在推荐什么做毕设最大的坑不是代码写不出来而是拿到题目之后不知道从哪里下手。很多人一看到推荐系统四个字第一反应是去研究算法结果研究了一个月协同过滤连项目结构都没建出来。我建议的顺序正好相反先把推荐什么和给谁推荐想清楚再回头算算法的事。2.1 系统的三类核心用户和他们的真实诉求一套完整的汽车推荐系统至少要覆盖三类角色普通用户是系统的核心服务对象。他们进来之后要做的事情包括注册登录、浏览车型列表、查看车型详情、搜索心仪的汽车、收藏感兴趣的车型、对试驾过的车进行评分以及最重要的——获取个性化推荐结果。用户的行为数据不是让你存着好看的它们直接会成为推荐算法的输入。比如用户频繁浏览 SUV 类别的车收藏了好几款日系混动车型系统就应该在推荐列表里优先给出同类别、同能源类型的车。管理员负责整个平台的后台维护。车型信息的增删改查、用户列表的查看和管理、用户反馈的处理、推荐参数的调整这些都需要一个独立的管理端页面。管理员的权限必须和普通用户严格分离这一点在答辩的时候经常会被问到所以我在设计时用了 SpringBoot 的拦截器做登录校验和权限拦截而不是简单地在页面上隐藏一些按钮。游客是容易被忽略但实际占比很高的人群。用户第一次访问系统时还没有登录这时候系统连他的行为数据都没有怎么推荐两种策略一种是按照车型的浏览量、收藏量做热门榜单另一种是按照车系、价格区间做分类推荐。这两种策略都不需要用户身份信息却能给游客一个相对不错的初体验引导他完成注册。2.2 推荐引擎的输入需要采集哪些行为数据推荐系统的地基不是算法是数据。在数据库设计阶段就要把下面这些行为数据表规划好不然后面写算法的时候会发现缺字段、缺关联返工成本极高。我把数据分成三类用户基础数据用户的注册信息、性别、年龄段、所在城市、购车预算区间。这些数据在注册时可以直接收集作为冷启动阶段的重要参考。预算区间尤其关键它能直接过滤掉一大批价格不匹配的车型极大缩小推荐范围。显式行为数据用户主动表达的行为包括收藏车型、给车型评分、发表评论。这些数据的特征是信号非常强但量少。一个用户可能浏览了 50 辆车但只收藏了 3 辆这 3 辆的权重就应该远高于那 50 次浏览。隐式行为数据用户不自觉产生的行为包括浏览记录、停留时长、搜索关键词、点击次数。这些数据量巨大但噪声也大。用户在搜索结果页停留了 30 秒不一定代表对当前页所有车都感兴趣。所以我在设计时给每种行为分配了不同的权重系数浏览记 1 分收藏记 5 分评分记 8 分通过加权的方式把隐式行为转换成可计算的评分矩阵。这里有个细节需要注意行为数据的时效性。用户三个月前频繁搜索燃油车最近一周却一直在看新能源车说明他的兴趣已经发生了迁移。如果推荐算法不加区分地使用所有历史数据推荐结果就会滞后。我当时的处理方式是为行为数据表增加时间衰减因子越近的行为权重越高旧数据的影响逐渐降低。这个设计让推荐结果活了起来不是一套固定的静态榜单。2.3 车型数据模型标签体系是推荐的基础设施车型数据表是整个系统的原料库如果表结构设计不合理后续的推荐算法和搜索功能都会束手束脚。我设计的车型表包含这些关键字段品牌、车系、车型名称、价格区间、车身结构轿车/SUV/MPV、能源类型燃油/纯电/混动、排量、变速箱类型、座位数、上市年份、图片地址、详细描述。这些字段本身都很常规真正重要的是标签字段。我单独建立了一张车型标签表用标签来刻画车型的特征比如家用运动豪华省油越野科技感高性价比。每辆车可以关联多个标签一个标签也可以对应多辆车典型的 多对多关系。为什么标签这么重要因为基于内容的推荐算法就是靠标签来计算相似度的。一辆车和另一辆车的相似程度可以通过它们标签集合的重合度来度量。重合的标签越多相似度越高。比如本田CR-V的标签是家用、省油、空间大、SUV丰田RAV4的标签是家用、省油、SUV、可靠两者重合了四个标签中的三个相似度就非常高。这种计算方式简单直接本科阶段完全够用而且答辩时解释起来也清楚。标签体系还有一个作用就是打通用户画像。用户的行为数据可以先映射成用户对标签的偏好再通过标签推荐车型。比如用户收藏了 3 辆运动属性的车系统就知道他偏好运动风格下一轮推荐时给所有带运动标签但用户没看过的新车加分。这种两层的映射关系让推荐算法从车和车的匹配升级成了人和车的匹配推荐效果明显提升。3. 推荐算法的落地协同过滤和标签匹配怎么选、怎么搭算法部分是整个系统最容易被答辩老师深挖的地方。我的建议是不要只会说概念要把算法的输入、计算过程、输出结果整个链路都跑通能用具体例子说明白这才是拿高分的关键。3.1 基于用户的协同过滤找和你品味相似的人协同过滤的核心思想用一个生活场景就能说清楚你想看一部电影不知道选哪部于是问和你审美很像的朋友最近在追什么剧。朋友的喜好就是你的推荐依据。在汽车推荐系统里这个逻辑同样适用。用户 A 和用户 B 都收藏了丰田凯美瑞和本田雅阁那么 A 还收藏了大众迈腾的话系统就有理由推断 B 可能也会喜欢迈腾于是把迈腾推荐给 B。具体计算过程我用的是皮尔逊相关系数来计算用户之间的相似度。公式长这样sim(u, v) Σ((r(u,i) - r̄(u)) * (r(v,i) - r̄(v))) / sqrt(Σ(r(u,i) - r̄(u))^2) * sqrt(Σ(r(v,i) - r̄(v))^2)其中 r(u,i) 表示用户 u 对车型 i 的评分r̄(u) 表示用户 u 的平均评分。用平均评分做中心化处理是为了消除不同用户打分习惯差异带来的偏差。有的用户喜欢打高分有的用户打分普遍偏低如果不做中心化高分的用户会被系统错误地判定为和其他所有人都相似。这一步当年我调试了很久才想明白建议你们不要把公式背下来就完事而是要理解每一步操作想解决什么问题。计算完用户相似度之后接下来会产生一个非常实际的计算问题如果系统里有 1000 个用户每个用户都要和其他 999 个用户计算相似度这个计算量的量级是 O(n²)。当用户量上涨到一万、十万这个计算根本不可能同步完成。当时的处理方式是写了一个定时任务每天凌晨两点用 xxl-job 离线计算出所有用户之间的相似度矩阵把结果存到 Redis 里。用户在白天请求推荐时系统只需要直接查 Redis毫秒级就能返回。3.2 基于内容的推荐让懂的车型去匹配懂的用户协同过滤的问题在于冷启动——新用户没有任何行为数据系统不知道他和谁相似协同过滤直接失效。新车型也一样没有人对它产生过行为协同过滤永远不会把它推给任何人。基于内容的推荐完美解决这个问题。它的思路是不管别人怎么想只看用户自己过去喜欢什么然后推荐相似的东西。具体到我们的系统里实现方式是这样的从用户的历史行为浏览、收藏、评分中提取出他偏好的车型列表然后获取这些车型的标签集合统计每个标签出现的次数生成一个用户标签偏好向量。同时每辆车上线时就已经打好了标签系统维护一个车型标签向量。推荐时计算用户偏好向量和每辆车标签向量的余弦相似度排序后取 Top-N 输出。拿组合标签举个例子用户收藏了特斯拉 Model 3标签电动、科技、性能和比亚迪汉 EV标签电动、豪华、国产那么用户的标签偏好里电动出现两次科技性能豪华国产各出现一次。此时如果库里有辆蔚来 ET5标签是电动、科技、豪华那它的得分就会明显高于一辆标签为燃油、家用、经济的丰田卡罗拉。基于内容推荐的另一个好处是可以解释。用户在页面上点击为什么推荐这辆车系统能列出来因为你收藏了 Model 3 和汉 EV你喜欢电动、科技、豪华标签的车型所以为你推荐了蔚来 ET5。这种解释在可玩性上非常加分也方便在答辩时演示系统的人性化设计。3.3 混合推荐两种算法怎么融合才不打架实际开发中我不会只用其中一种算法而是把它们的结果做加权融合。权重分配我是这么定的用户状态协同过滤权重内容推荐权重热门榜兜底权重新注册用户无行为001.0行为数据较少10条0.20.50.3行为数据丰富10条0.50.40.1为什么新用户直接用热门榜因为新用户的行为数据太少内容推荐虽然能用他注册时填写的预算、车型偏好做初步过滤但结果往往不够精准倒不如先给他看大家都喜欢的车引导他产生更多的行为数据等数据量上来之后再逐步切换到个性化推荐。这个分阶段推荐策略是整篇文章里我认为最值得借鉴的设计之一。它不追求某一个算法在所有场景下都表现最好而是根据用户的数据成熟度动态调整策略让系统从第一天起就能给出合理结果。融合计算的时候还有一个细节不同算法的得分范围不一样不能直接相加。我用了 Min-Max 归一化把协同过滤得分和内容推荐得分都映射到 0~1 区间再按权重相加。不然协同过滤的原始得分范围是 -1 到 1内容推荐的得分范围可能只有 0 到 0.2直接加权等于内容推荐被完全淹没。4. 项目搭建的完整实操从空目录到能跑的推荐服务技术选型和算法设计都定好了接下来进入动手环节。我会按照我实际的开发顺序把关键步骤和踩过的坑都过一遍。这里只讲后端部分因为这些细节最容易被忽视也最影响成败。4.1 SpringBoot 项目的初始化方式别在 IDE 里点来点去很多人用 IDEA 自带的 Spring Initializr 创建项目一步步点过来确实方便。但有个问题IDEA 的 Spring Initializr 默认连的是 start.spring.io如果网络不稳定或者公司内网访问不了外网项目就创建失败。我的建议是直接用 Spring 官方提供的网页端生成器 start.spring.io在浏览器里选好 SpringBoot 版本、项目元信息、依赖组件点击 Generate 直接把压缩包下载下来解压后用 IDEA 的 Open 方式打开即可。这样操作一是稳定二是生成的文件结构和 IDEA 内置的完全一致三是方便你保留一份初始配置后面配坏了还能对比回去。依赖这块我最终选择的是这些起步依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.18/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdcom.auth0/groupId artifactIdjava-jwt/artifactId version4.4.0/version /dependency这里有个当年踩过的坑MyBatis-Plus 的版本和 SpringBoot 版本之间要匹配。SpringBoot 3.x 改用 Jakarta 命名空间和 MyBatis-Plus 3.5.x 早期版本兼容性不好直接用会报ClassNotFoundError: javax.servlet.*。我当时用的是 SpringBoot 2.7.13 MyBatis-Plus 3.5.3稳定运行毫无问题。如果你坚持用 SpringBoot 3.x那 MyBatis-Plus 必须用 3.5.3.1 以上的版本。数据源用 Druid 是因为它自带监控页面开发调试时可以直接通过http://localhost:8080/druid查看 SQL 执行情况哪里慢一眼就能看出来。当时跑完一轮推荐算法测试发现页面响应要 2 秒多打开 Druid 监控一看原来是某个列表页的 N1 查询问题优化 SQL 之后直接降到 300 毫秒以内。4.2 数据库表结构设计七张表撑起整个系统一套功能完整的汽车推荐系统最少需要这七张核心表。我先把建表思路说一下具体 SQL 就不贴完整了你们根据自己设计的字段微调即可。user 用户表主键、用户名、密码BCrypt 加密存储、昵称、性别、年龄、所在城市、预算区间、注册时间、状态可用/禁用。car 车型表主键、品牌、车系、车型全名、价格、车身结构、能源类型、排量、变速箱、座位数、上市年份、图片 URL、详情描述、上架状态。car_tag 车型标签表主键、标签名称。这个表是独立维护的先有标签再和车型关联。car_tag_relation 车型标签关联表车型 ID、标签 ID。纯关联表提高查询效率。user_behavior 用户行为表主键、用户 ID、车型 ID、行为类型浏览/收藏/评分/搜索、行为值评分为 1~5其他为 0、行为时间。这张表是推荐算法的核心输入设计时务必要加上索引联合索引(user_id, car_id, behavior_type)和单列索引(behavior_time)。recommend_record 推荐记录表主键、用户 ID、推荐类型协同过滤/内容推荐/热门榜、推荐车型 ID、得分、推荐时间、用户是否点击。这张表如果做了你的系统就比大多数毕设高一个档次——它让推荐效果可衡量、可复盘。feedback 用户反馈表用户 ID、车型 ID、反馈内容不感兴趣/原因描述、反馈时间。反馈数据可以帮助后续算法调优。这里强调一个原则所有表都要有 create_time 和 update_time 字段。MyBatis-Plus 提供了MetaObjectHandler自动填充功能配一次就行不用每个 mapper 手动 set 时间省心很多。4.3 核心接口清单推荐服务的 RESTful API 设计后端接口的设计直接决定了前端开发效率。我按照资源维度来组织接口风格统一前端对接起来非常顺畅用户模块 POST /api/user/register 用户注册 POST /api/user/login 用户登录返回 JWT Token GET /api/user/{id} 获取用户详情 PUT /api/user/{id} 更新用户资料 车型模块 GET /api/car/page 分页获取车型列表支持品牌、价格、能源类型、车身结构筛选 GET /api/car/{id} 获取车型详情 GET /api/car/{id}/similar 获取相似车型推荐 GET /api/car/hot 获取热门车型榜 行为模块 POST /api/behavior 记录用户行为浏览/收藏/评分 GET /api/user/{id}/favorites 获取用户收藏列表 DELETE /api/user/{id}/favorite/{carId} 取消收藏 推荐模块 GET /api/recommend/{userId} 获取个性化推荐列表核心接口 GET /api/recommend/{userId}/hot 获取热销/热门推荐 GET /api/recommend/{userId}/explain/{carId} 推荐理由解释 后台管理 POST /api/admin/car 新增车型 PUT /api/admin/car/{id} 修改车型信息 DELETE /api/admin/car/{id} 删除车型 GET /api/admin/behavior/stats 行为数据统计/api/recommend/{userId}这个接口的实现逻辑是先查 Redis 有没有缓存有就直接返回没有就判断用户是否有行为数据行为数据丰富走混合推荐行为少走热门推荐最后把结果写入 Redis 并设置 30 分钟过期时间。Redis 的 key 命名规则我用的是recommend:user:{userId}value 存 JSON 字符串里面包含推荐车型 ID 列表、推荐类型和得分。这样设计的好处有两个一是秒级响应二是便于统计推荐效果——从 Redis 里能看出来用户拿到的是哪种推荐策略的结果。4.4 JWT 登录态管理拦截器 Token 的前后端分离方案前后端分离项目里Session 方案不好用因为前端部署的域名和后端可能不一致跨域携带 Cookie 很麻烦。业界通行的做法是 JWT。用户登录成功后后端生成一个 JWT Token里面加密存储用户 ID 和过期时间返回给前端。前端把 Token 存在 localStorage每次请求时在 Header 里带上Authorization: Bearer xxx。后端用一个拦截器统一校验Component public class JwtInterceptor implements HandlerInterceptor { private static final String TOKEN_PREFIX Bearer ; private static final String AUTH_HEADER Authorization; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (OPTIONS.equalsIgnoreCase(request.getMethod())) { return true; // 放行跨域预检请求 } String token request.getHeader(AUTH_HEADER); if (StringUtils.hasText(token) token.startsWith(TOKEN_PREFIX)) { token token.substring(TOKEN_PREFIX.length()); try { DecodedJWT jwt JWT.require(Algorithm.HMAC256(your-secret-key)) .build().verify(token); Integer userId jwt.getClaim(userId).asInt(); request.setAttribute(currentUserId, userId); return true; } catch (Exception e) { response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\登录已过期请重新登录\}); return false; } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录\}); return false; } }这里有个关键细节跨域预检请求 OPTIONS 必须直接放行。前端在跨域请求时浏览器会先发一个 OPTIONS 请求确认后端允不允许如果拦截器挡掉了 OPTIONS前端所有的 POST、PUT 请求都会以 CORS 报错告终。这个坑我当年调了一个下午才排查出来后续所有同学问我 JWT 相关问题我都会先问一句OPTIONS 放行了吗密码存储用的是 BCryptPasswordEncoder这是 Spring Security 框架里拆出来单独用的加密工具每次加密生成不同的盐值即使两个用户密码相同加密结果也不同。毕设项目不建议自己写 MD5 加盐容易被答辩老师追问到破绽直接用业界封装好的组件是最稳妥的选择。5. 开发过程中的高频报错与修复实录写毕设的过程本质上就是一个和报错搏斗的过程。我把这段时间里踩过的几个典型坑整理出来这些问题十个人里有八个会遇到提前知道能省下大量排查时间。5.1 MyBatis-Plus 分页插件不生效被惯性思维坑了MyBatis-Plus 的分页查询光引入依赖是不够的必须显式配置一个分页插件。很多人照着博客写selectPage方法前端传入 pageNum 和 pageSize结果发现返回的 total 永远是 0records 永远是全部数据。原因就是少了这段配置Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }不配置这个 BeanMyBatis-Plus 只会把分页参数当成普通参数接收实际执行的 SQL 是全量查询然后内存里手动截断。数据量小的时候看不出问题几千条数据一翻页性能立刻暴露。排查这个问题的思路也值得分享先打开 Druid 监控看实际执行 SQL如果 SQL 里没有 LIMIT 语句那必然是分页插件没有生效如果 SQL 里有 LIMIT 但 total 还是 0那就是 count 查询出了问题。5.2 日期序列化格式不对LocalDateTime 返回的是一串数字因为用户行为表里有行为时间实体类用了LocalDateTime结果前端拿到的字段值是一串类似1700000000000的数字。这不是报错但直接暴露了后端没有配置 JSON 序列化。解决方式是在application.yml里统一设置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8注意time-zone必须设置成 GMT8不然后端会在默认时区UTC基础上偏差八个小时显示出来的时间会比实际时间慢八小时。这个问题在联调时表现出来就是你在页面上看到一条行为时间昨天 00:00但你明明是下午记录的。5.3 推荐接口偶发超时Redis 缓存穿透的威力系统上线第一周推荐接口偶尔会出现 2 秒以上的响应延迟不是每次都慢是间歇性的。查了日志发现问题出在 Redis 缓存失效的那一瞬间缓存刚过期大量请求同时涌入数据库数据库连接被打满后续请求排队。这个问题的本质是缓存穿透和缓存击穿。我的解决方式很简单给缓存加一个随机过期时间比如 30 分钟基础上加 0~5 分钟的随机值避免所有 key 在同一时刻集体失效。对于热点数据再加一层 Redis 分布式锁保证同一时刻只有一个请求去数据库加载其他请求等待后再查缓存。毕设阶段不需要做得特别复杂但把缓存为什么需要随机过期时间想清楚在答辩时是可以作为亮点深入讲一讲的。5.4 跨域配置踩坑CORS 和拦截器的执行顺序前端开发阶段Vue 项目跑在 localhost:5173后端跑在 localhost:8080两者端口不同必然产生跨域问题。我在后端加了 CorsFilter 之后发现 GET 请求能通了但 POST 请求还是被拦。排查后发现原因在于 SpringBoot 的拦截器执行顺序在 CorsFilter 之前。前端发 POST 请求时先触发预检 OPTIONS这个请求是会到拦截器的我的 JwtInterceptor 如果没有放行 OPTIONS就会先返回 401导致预检失败真正的 POST 请求根本不会发出。解决方式就是前面说的在拦截器里对 OPTIONS 请求直接放行。另外一种做法是使用 Spring 的CrossOrigin注解逐接口配置但那样太繁琐不如全局 CorsFilter 拦截器放行 OPTIONS 的组合干净。6. 推荐效果验证怎么向答辩老师证明你的系统能推说服答辩老师不能只靠嘴上说我的推荐算法很厉害要有数据、有测试、有对比。这套系统我做了两个层面的验证你们可以参考。6.1 离线评测用准确率和召回率说话我从数据库中导出 500 个用户的行为记录按 80/20 划分训练集和测试集。用训练集构建协同过滤模型然后对测试集中的每个用户用模型生成 Top-10 推荐列表检查推荐列表中有多少车型是用户真实收藏或评分过的。最终结果协同过滤的准确率约为 21.3%召回率约为 15.7%。这个数字不算高但在数据只有几百个用户的情况下已经是很正常的表现。更重要的收获是我把模型预测的推荐结果和用户实际收藏记录做了对比发现漏掉的车型里有很大一部分是用户看过但没收藏的说明推荐算法并非完全失效而是测试集本身不够完整。我还做了一个有意思的对比只使用浏览数据和只使用收藏数据分别跑了一遍算法结果发现只使用收藏数据的推荐准确率明显更高。这说明显式行为收藏的置信度远高于隐式行为浏览在行为权重设计时应该进一步拉开差距。6.2 在线反馈埋点统计推荐带来的点击率离线评测之外我在推荐记录表里设计了is_clicked字段记录用户是否点击了推荐列表中的车。上线测试阶段系统收集到了约 300 次推荐展示点击次数约 40 次点击率约 13%。单纯看 13% 这个数字没有对比意义我加了一组对照组把推荐列表替换成单纯的热门榜按浏览量排序同样展示 300 次点击率只有 7% 左右。个性化推荐比热门榜的点击率高出将近一倍这个对比在答辩时非常能说明问题。6.3 一个具体人的推荐效果用来做现场演示答辩现场演示环节与其随机打开一个页面不如提前准备一个精心挑选的用户账号把行为数据造好让推荐结果看起来合理且有趣。我当时准备了这样一个演示账号注册信息是 28 岁、男性、预算 15-25 万。行为数据包括浏览了 5 款新能源车、收藏了 1 款比亚迪宋 PLUS、搜索过纯电关键词。打开推荐页之前我先在后台查看他的用户偏好向量新能源相关标签排序最高。然后展示推荐结果果然列表前几位全是热度高、价格匹配的电动车型。再用推荐理由接口点开其中一辆系统显示因为你收藏了比亚迪宋 PLUS它的标签包含电动、SUV、国产这辆车的相似度为 87%——这一套展示下来答辩老师从算法到工程实现都挑不出毛病。7. 部署上线与后续扩展毕设做完不等于结束系统开发调试完成之后需要打包部署起来作为现场演示环境。这一步看似不起眼却容易翻车。7.1 服务器部署的完整流程我用一台 2 核 4G 的云服务器演示系统是 Ubuntu。部署步骤整理如下安装 Docker 和 docker-compose编写 docker-compose.yml一次性启动 MySQL、Redis 和应用容器后端 Maven 打包mvn clean package -DskipTests编写 Dockerfile用 openjdk:8-jre 基础镜像把 jar 包打进去Nginx 配置前端静态文件反向代理到后端端口关键点服务器上的 MySQL 和 Redis 一定要设置密码并限制端口访问。毕设系统虽然不上生产但放在公网服务器上被扫描器扫到裸奔数据库是常有的事到时候被脱库了答辩都来不及。前后端联调时常遇到的一个坑前端请求http://localhost:8080/api/recommend/1能通部署到服务器之后就 404。原因通常是 Nginx 的proxy_pass配置漏了路径转换。我的配置是这样location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }注意proxy_pass后面没有路径只有端口这样/api/xxx原样转发到后端。如果写成proxy_pass http://127.0.0.1:8080/;请求会被重写为http://127.0.0.1:8080/xxx/api前缀被吞掉后端所有接口全部 404。7.2 后续还能往哪些方向扩展这套系统做完之后如果不甘于只停留在 一个能跑的毕设 的水平后续可以从这几个方向继续提升:把协同过滤的离线计算换成 Spark MLlib处理更大规模的用户数据引入 Elasticsearch 做车型搜索支持模糊匹配和多字段联合查询把热门推荐做成定时重算使用 xxl-job 管理任务调度增加用户画像可视化页面用 ECharts 展示用户的标签偏好分布将行为数据上报改成消息队列模式用 RabbitMQ 或 Kafka 做异步削峰逐个做下来的话这个项目就从一个本科毕设成长为一个有生产价值的推荐系统了。不过作为毕设我建议先把基础版本打磨干净每一个接口、每一张表、每一条注释都做到自己能解释清楚再考虑锦上添花。8. 个人总结这套系统让我学到的东西远远超出代码本身从拿到题目到完成答辩整套系统前后用了大约三周时间。回过头来看最大的收获并不是学会了 SpringBoot 的某个注解或者 MyBatis-Plus 的某个 API而是建立了一套从需求到落地的完整思考方式。以前写课程作业拿到需求就上手写代码写完了发现需求理解错了推翻重来。这次做汽车推荐系统我花了整整两天时间做需求分析、画数据流图、设计表结构真正写代码的时间反而只占了一半。结果就是开发过程极其顺畅除了那些技术上的坑几乎没有因为需求变更而返工过。这种先想清楚再动手的习惯后来我带到实际工作中帮我在很多项目里避免了返工。另外一点是不要害怕用简单的算法。协同过滤和基于内容的推荐在学术界早就算不上前沿了但在工程实践中它们依然是被广泛使用的方案。把简单的算法真正落地做到数据处理、特征设计、缓存优化、效果评估全程跑通这份经验比在论文里堆砌一堆复杂的公式要有价值得多。答辩老师问我的最深的一个问题就是你的推荐结果里为什么会出现用户根本不感兴趣的车型我当时从数据稀疏和标签粒度两个角度做了回答老师点头表示认可。如果你能把为什么推荐不够好也讲清楚那比单讲我的推荐怎么好更能说明你真的理解这套系统。做毕设的过程确实很熬人但做完之后那种整个系统从头到尾都是我设计、我实现、我能解释的感觉也是真的充实。希望这份复盘能帮你少踩几个坑把精力花在真正重要的事情上。
返回列表