☰
微信小程序个性化电影推荐系统:从协同过滤到完整落地
2026/10/13 2:22:43 网站建设 项目流程

毕设选这个题的时候,身边不少同学都在做图书管理系统、校园二手交易平台之类,我当时想法很简单:要么做点有意思的,要么做点能讲出东西的。"基于微信小程序的个性化电影推荐系统"恰好两个都占——前端是现在企业里需求量很大的微信小程序,后端核心是一个能讲清楚原理的推荐算法,而且最终能交付一套可运行的源码,无论是自己学东西还是应付答辩,都站得住。

这篇文章我会从开题思路、技术选型、推荐算法落地、数据库设计、小程序端细节、源码跑通、答辩加分这七个维度完整过一遍,把当时做这个项目踩过的坑和摸索出来的方法都写出来。无论你是刚拿到这个题目不知道怎么下手,还是已经有了源码但跑不起来、推荐逻辑看不懂,这篇文章应该都能帮上忙。

1. 开题之前先想清楚:这个毕设的"个性化"到底值多少分

很多拿到这个题目的同学第一反应是:小程序我会写,电影列表我也能调接口拿到,那不就完了吗?如果真这么做,答辩的时候老师问一句"个性化体现在哪里",基本就卡住了。这个题目的核心从来不是前端页面,而是"个性化推荐"这四个字背后的算法逻辑。

1.1 推荐算法的选题价值:为什么不是"大家都在看"排行榜

先做一个最简单的对比。如果一个电影推荐系统只是把热度最高的电影列出来,那叫排行榜,不需要任何算法,SQL按评分排序就够了。但"个性化"意味着每个用户打开小程序看到的首页推荐列表应该不一样——你喜欢悬疑片,那推荐位就应该多给你悬疑;你最近看了三部王家卫,那就不该再给你推漫威爆米花。这个差异怎么来的?就是靠推荐算法算出来的。

毕业设计评分时,老师最看重的就是"工作量+技术难点"。管理系统类题目之所以容易做得平淡,是因为它们本质上是增删改查,技术难点约等于零。但推荐系统天然自带一个算法层,哪怕你只做了最基础的协同过滤,也足够在答辩时讲十分钟。这就是这个题目的天然优势:它不需要你发明新东西,只需要把一个经典算法正确落地,就比纯CRUD项目高一个档次。

1.2 三类推荐方案的取舍:协同过滤、基于内容、混合

我个人把这个项目的推荐方案分成三个层次:

  • 基于内容的推荐(Content-based):根据电影的类型、导演、演员、标签等属性,给用户推荐和他曾经看过/喜欢过的电影相似的片子。优点是冷启动对老用户友好,不需要其他用户的数据;缺点是推荐结果容易单调,永远推荐同一类东西。
  • 协同过滤(Collaborative Filtering):又分为基于用户(User-based)和基于物品(Item-based)。核心思想是"品味相似的人看的东西也相似"或"喜欢这部电影的人也喜欢那部电影"。这是毕设中最容易讲清楚、也最能体现算法含量的方案。
  • 混合推荐(Hybrid):把上面两种结合,加权或分策略融合,工程复杂度上升,但效果和答辩亮点都会更好。

我当时选的是"基于用户的协同过滤为主、基于内容为辅"的混合方案。主要原因是:纯协同过滤有冷启动问题——新用户一条行为数据都没有,谁也跟他相似不了;纯基于内容又不能体现"个性化交互"的感觉。两者结合,既能处理冷启动(新用户先用内容推荐 + 热门兜底),又能在用户积累了十几条评分后切到协同过滤,让推荐列表明显随着用户行为变化。这一点在录演示视频时非常有用:你给某部冷门片打高分,再刷新首页,推荐列表立刻不一样,老师一看就知道这不是写死的。

1.3 毕设的合理边界:MVP方案 + 两条加分扩展线

做毕设最忌讳的就是贪大。一开始我也想过要不要用深度学习模型(比如NCF、YouTubeDNN那套),后来冷静算了一下账:训练要数据,数据量不够;部署要环境,答辩演示时模型加载慢、推理慢,反而翻车;老师问"你损失函数怎么设计的"还要花大量时间补理论。最后决定用经典机器学习方法(近邻/相似度计算)作为主实现,把深度学习作为"后续改进方向"放在论文末尾和答辩PPT里。这个策略既保证了项目能稳定跑通,又留出了提升空间。

真正的合理边界是:

  • 小程序端:推荐首页、全部电影列表、电影详情、搜索、评分/收藏、个人中心,六个页面足够。
  • 后端:用户模块、电影模块、评分行为模块、推荐模块。
  • 算法层:相似度计算 + TopN推荐 + 冷启动兜底。
  • 数据:爬取或整理的几百到一千部电影即可,单机演示完全够用。
  • 后续展望:混合推荐更细粒度、引入深度学习排序、实时特征更新。这些只作为论文的文字内容,不一定要实现。

2. 技术选型与系统架构:从录屏演示倒推的完整链路

技术选型这件事,我给你的建议是一个原则——别用课堂上没教过、你自己也不熟的东西。毕设的核心目标是稳定运行、能讲清楚,不是炫技。选一个你掌握的、社区资料多的技术栈组合,比什么都重要。

2.1 小程序端:原生还是框架

微信小程序的前端部分,我当时用的是原生小程序语法(WXML + WXSS + JS),没有上Uniapp或Taro。原因很简单:这个项目是我自己从零写的,原生是最直接的,调试工具里报错也能看明白。Uniapp的优势是跨端复用,但毕设不需要跨端,能跑微信端就够了,原生的学习成本也低,网上搜"微信小程序 电影推荐"能搜到大量原生写法的参考。

当然如果你们学校有明确要求用Uniapp,或者你本来就熟悉Vue,那用Uniapp开发微信小程序也完全可以,逻辑层代码(请求接口、缓存、事件处理)和原生差别不大,主要区别在模板语法和生命周期写法。项目里核心的推荐逻辑还在后端,小程序端只是展示和采集行为,所以这个选择对全局影响不大。

2.2 后端:Spring Boot还是Node.js还是云开发?

这是当时纠结最久的一个点。我列一下我的考量,你也好判断自己该怎么选:

  • Spring Boot:Java技术栈,课堂上有基础,公司用得多,社区教程极多。缺点是项目结构重,写起来慢。我当时选了Spring Boot,主要是因为我熟悉Java,而且答辩时老师问到后端架构,Spring Boot的MVC三层结构非常好讲。
  • Node.js/Express:轻量,和JS语言统一(小程序端也是JS),启动快。如果你对JS比Java熟,选这个没问题,代码量比Spring Boot少不少。
  • 微信云开发:不需要自己买服务器,云函数 + 云数据库直接搞定。最大的坑是:推荐算法需要在云函数里跑,数据量稍大时冷启动超时问题很烦;而且数据库是NoSQL的JSON结构,做相似度计算要全表拉数据,逻辑绕。我个人不太推荐拿云开发做这个题目,除非你一点服务器和后端基础都没有。

一个非常现实的考虑:毕业设计有演示环节,万一现场网络不好,自建后端要维护服务器,云开发则依赖云端。稳妥起见,我当时是本地运行Spring Boot,小程序开发者工具里勾选了"不校验合法域名",演示时全程用同一个Wi-Fi,非常稳定。

2.3 推荐引擎放哪里:离线计算与实时接口的分工

"个性化推荐"在工程上有一个重要概念:推荐结果不是每次请求都现场算的。

我当时的设计是:

  • 离线计算层:启动时或每天凌晨,后端服务跑一次推荐算法,为每个用户生成一个TopN推荐列表,存入推荐结果表。这样用户请求时只需要查一次表,毫秒级返回,完全不用担心性能。
  • 实时采集层:用户每次评分、收藏、搜索行为,都会实时写入行为表,作为下次离线计算的数据来源。
  • 兜底策略:如果推荐结果表里没有某个用户的数据(比如新用户),接口自动降级为"热门电影+高分电影"列表返回。

这个设计在答辩时可以讲成"离线计算与在线服务的解耦",理论高度立刻就有了。而且对本人来说,调试时也不用每次打开首页等好几秒算相似度,体验好很多。

2.4 整体架构与请求链路描述

整个系统从用户点开小程序到看到推荐列表,走的是这样一条链路(配合架构图讲给老师听非常清晰):

  1. 微信小程序端启动,调用wx.login()拿到临时code。
  2. 后端收到code后调用微信接口换取openid,返回自定义登录态token(我用的是JWT)。
  3. 小程序带着token请求首页推荐接口/api/recommend/home。
  4. 后端从推荐结果表中读取该用户的前20部电影ID列表,再联表查出电影的完整信息(海报、简介、评分、演员、类型标签),返回给前端。
  5. 用户在某部电影详情页打了分,小程序调用/api/rating/submit,后端把行为写到评分表,同时标记该用户的推荐结果"已过期"——下次离线计算时会包含这条新行为。
  6. 离线任务定时触发:扫描所有有新增行为的用户,重新计算相似度矩阵,更新推荐结果表。

这套链路配合后端接口文档,是答辩演示时的"王炸级"讲解素材。

3. 推荐算法核心逻辑:协同过滤公式的代码化落地

这一部分是整个毕设的灵魂。如果只能讲清楚一个东西,就是它。我用的主算法是基于用户的协同过滤(User-based Collaborative Filtering),下面按我当时的实现步骤逐一说明,代码关键部分都给出来。

3.1 用户行为打分模型:评分、收藏、看过怎么办

协同过滤计算依赖用户对物品的"偏好分值"。电影院里不会自动打分,所以你不能只看评分表,要把多种行为统一转化成分值。

我当时建了一张用户行为明细表,把三类主要行为映射为不同权重的分值:

行为类型权重说明
主动评分(1-5分)评分数值 × 2最直接的偏好表达,权重最高
收藏6分明确表示"我喜欢"
搜索并进入详情页3分弱偏好信号
浏览短视频/预告片1分最弱的信号,过滤无效点击

在计算时,一个用户对一部电影的"最终兴趣分" = 评分数×2 + 收藏加6 + 详情页浏览加3 + 预告片浏览加1。这样做的好处是:即使用户没有对某部电影评分,只要收藏过或反复浏览过,也能计入相似度计算,行为数据利用率高很多。

这里有一个我踩过的坑:刚开始我只用评分表算,结果很多用户只有两三条评分记录,相似度矩阵稀疏得没法看。把行为统一建模之后,数据密度立刻上来了,推荐效果肉眼可见地变好。

3.2 相似度计算的核心公式:余弦相似度怎么落地

基于用户的协同过滤第一步:计算用户两两之间的相似度。通用的做法是构建"用户-电影"评分矩阵,然后计算行向量之间的余弦相似度。

脱离了数学符号,用大白话讲就是:把两个用户对所有电影的评分散成两个向量,如果这两个向量的方向越一致(夹角越小),说明两个人口味越接近。公式写出来是:

similarity(u, v) = Σ(rui × rvi) / (sqrt(Σrui²) × sqrt(Σrvi²))

其中 rui 是用户u对电影i的兴趣分,rvi 是用户v对电影i的兴趣分。分母是两个向量模长的乘积,用来归一化,避免"打分多的人天然更像别人"的偏差。

我当时的Java实现关键部分如下:

public double cosineSimilarity(Map<Integer, Double> userVec1, Map<Integer, Double> userVec2) { // 取两个用户共同评价过的电影ID集合 Set<Integer> commonItems = new HashSet<>(userVec1.keySet()); commonItems.retainAll(userVec2.keySet()); if (commonItems.isEmpty()) { return 0.0; } double dotProduct = 0.0; double norm1 = 0.0; double norm2 = 0.0; for (Integer itemId : commonItems) { dotProduct += userVec1.get(itemId) * userVec2.get(itemId); } for (Double score : userVec1.values()) { norm1 += score * score; } for (Double score : userVec2.values()) { norm2 += score * score; } if (norm1 == 0.0 || norm2 == 0.0) { return 0.0; } return dotProduct / (Math.sqrt(norm1) * Math.sqrt(norm2)); }

注意几个容易翻车的地方:

  • 如果两个用户没有共同评价过任何一部电影,直接返回0,不要算分母(会出现除零)。
  • 共同物品很少时(比如只有1部),算出来的相似度可能虚高。我当时加了一个设定:共同物品数小于3就直接视为0,宁可漏掉也不误报。
  • 使用Map<Integer, Double>存储稀疏向量比用二维数组省内存,用户量几百级时效果明显。

3.3 预测评分与TopN推荐:不是把"最像你的用户"看过的全推给你

算出相似度之后,怎么给当前用户u推荐电影?我当时用的是最常见的"加权预测评分":

预测分(u, i) = Σ( similarity(u, v) × rvi ) / Σ( |similarity(u, v)| )

按所有相似用户对电影i的评分为权重相加,再除以权重和。这个分数可以理解为:跟你越像的人,他对某部电影的打分在你这边的"话语权"越大。

放到代码里就是两步:

  1. 找出TopK(我用的K=20)个与当前用户最相似的用户;
  2. 把这些用户评价过、但当前用户没有评价过的电影全部取出来,按上面的加权公式算预测分,排序后取TopN(我取的30部,前20部展示在首页)。
public List<Integer> recommendMovies(int userId, int topN) { // 1. 获取当前用户的评分向量 Map<Integer, Double> userVector = ratingService.getUserVector(userId); // 2. 计算与所有其他用户的余弦相似度,取TopK Map<Integer, Double> similarities = new HashMap<>(); for (int otherUserId : userService.getAllUserIds()) { if (otherUserId == userId) continue; Map<Integer, Double> otherVector = ratingService.getUserVector(otherUserId); // 这里用缓存加速,见5.3小节 double sim = cosineSimilarity(userVector, otherVector); if (sim > 0.1 && scores.commonSize >= 3) { similarities.put(otherUserId, sim); } } // 3. 取TopK相似用户 // 排序略 // 4. 候选电影加权打分 Map<Integer, Double> predictScores = new HashMap<>(); for (int similarUserId : topKUsers) { double sim = similarities.get(similarUserId); Map<Integer, Double> similarUserVector = ratingService.getUserVector(similarUserId); for (Map.Entry<Integer, Double> entry : similarUserVector.entrySet()) { int itemId = entry.getKey(); if (userVector.containsKey(itemId)) continue; // 已看过的不推荐 predictScores.merge(itemId, sim * entry.getValue(), Double::sum); } } // 5. 按预测分排序取TopN // 排序略 }

这里有一个经验:TopK不用太大。K=20在几百个用户规模的毕设项目里已经足够,再大一方面计算慢,另一方面顶多引入一些相似度很低的噪声用户,效果反而更差。

3.4 冷启动处理:新用户和新电影怎么办

冷启动是推荐系统里必被问到的问题。我的实现分三层:

  • 新用户冷启动:推荐结果表里没有该用户的数据时,接口直接返回全站评分最高的20部电影+近期热度最高的10部电影。等用户产生了几条行为记录后,离线任务刷新,推荐结果里才开始出现个性化内容。
  • 新电影冷启动:新上架的没有评分数据的电影,通过基于内容的方式解决。我提前为每部电影打了类型标签(动作、爱情、科幻、悬疑、喜剧等),计算电影之间的标签相似度,新电影只要标签明确,就会被推荐给喜欢对应类型的用户。
  • 用户行为极少:如果用户只有1条行为,协同过滤效果很差,这时候我用"行为对应的电影标签"为基础做一次反向的内容推荐。比如用户只给《星际穿越》打了高分,就解析出科幻、冒险、太空等标签,推荐同样命中这些标签的电影。

这三层策略在论文里对称地写出来,答辩老师一看就知道你是认真考虑过冷启动问题的。

4. 数据库与API设计:让推荐结果跑得动才算数

推荐算法再漂亮,数据表设计得不行,接口也会卡成PPT。我把数据库和接口设计的细节在这里完整拉一遍。

4.1 核心表结构:用户、电影、行为、推荐结果四张表

我当时用的是MySQL 8.0,设计了四张核心表:

-- 用户表 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) UNIQUE NOT NULL, nickname VARCHAR(64), avatar_url VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 电影表 CREATE TABLE t_movie ( id BIGINT PRIMARY KEY AUTO_INCREMENT, title VARCHAR(128) NOT NULL, poster_url VARCHAR(255), description TEXT, director VARCHAR(64), actors VARCHAR(255), genres VARCHAR(64), -- 逗号分隔的标签,如 "科幻,冒险" rating_score DECIMAL(3,1), -- 豆瓣评分,仅作为展示和兜底 release_year INT, duration INT -- 片长,单位分钟 ); -- 用户行为表(评分/收藏/浏览) CREATE TABLE t_user_behavior ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, movie_id BIGINT NOT NULL, behavior_type TINYINT, -- 1评分 2收藏 3详情页浏览 4预告片浏览 rating_value DECIMAL(2,1), -- 行为类型为1时有值 create_time DATETIME, UNIQUE KEY uk_user_movie (user_id, movie_id, behavior_type) ); -- 推荐结果表 CREATE TABLE t_recommend_result ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, movie_id BIGINT NOT NULL, predict_score DOUBLE NOT NULL, reason VARCHAR(32), -- 标记推荐来源:协同过滤/内容/热门/冷启动 create_time DATETIME, UNIQUE KEY uk_user_movie (user_id, movie_id) );

t_recommend_result这张表是我觉得整个设计里最体现工程经验的地方。很多人会把推荐逻辑写在查询接口里现场算,结果每次打开首页都慢得不行。我先把结果算好存起来,接口查询就走普通索引,返回速度从秒级降到毫秒级。

另一个小细节:行为表加了UNIQUE KEY uk_user_movie,避免用户反复操作同一部电影产生垃圾数据。比如用户给同一部电影打了三次分,只保留一条最新记录,用INSERT ... ON DUPLICATE KEY UPDATE更新rating_value和create_time即可。

4.2 推荐接口的入参/出参设计

后端接口我按RESTful风格写的,前端小程序统一走request封装。

三个最核心的接口:

GET /api/recommend/home -- 首页推荐列表 GET /api/movie/list?page=1&size=10&genre=科幻 -- 电影列表(支持分类筛选) POST /api/rating/submit -- 提交评分/收藏/浏览行为 GET /api/movie/{id}/detail -- 电影详情

推荐接口的返回结构我设计成了这样,方便小程序端直接绑定渲染:

{ "code": 200, "message": "success", "data": { "recommendType": "collaborative_filtering", "items": [ { "id": 1001, "title": "盗梦空间", "posterUrl": "https://xxx.oss.com/poster/1001.jpg", "ratingScore": 9.3, "genres": "科幻,悬疑", "director": "克里斯托弗·诺兰" } ], "total": 20 } }

recommendType字段留给前端展示"为你推荐"还是"热门精选"等不同文案,非常实用。前端拿到后,在页面上分别渲染标题栏和卡片列表即可。

4.3 相似度计算的性能问题:从全表扫描到缓存优化

第一次跑离线计算时我就吃了性能的亏:几百个用户,两两组合计算余弦相似度,看起来只有几万次运算,但每次都从数据库拉用户向量,接口能卡几十秒。后来我做了两个轻量优化,效果立竿见影:

  1. 用户向量缓存:把user_id -> Map<movie_id, score>的全量用户向量加载到内存Map中,计算时直接读内存,不再查表。几百个用户时内存占用很小(每个用户几百个键值对),却省掉了90%以上的数据库I/O。
  2. 共同物品预告检:只用userId上做一次retainAll之前,先各维护一个"用户评价过的电影ID集合",两个集合的retainAll结果小于3就直接跳过相似度计算,省掉了大量无意义运算。

实测优化后,200个用户的离线推荐计算从50多秒降到2秒以内。这个优化过程本身也是答辩素材:"我遇到性能瓶颈,通过内存缓存和剪枝策略解决了"——老师很吃这一套。

5. 微信小程序端实现要点:首页/详情/评分交互的落地细节

小程序端是用户直接看到的部分,视觉和交互不一定多华丽,但每个功能点都要能顺滑跑通。我按页面维度逐个说。

5.1 页面结构与底部Tab导航

我设置了四个底部Tab:

  • 推荐:首页,展示推荐列表,横向卡片流。
  • 排行:按综合评分/热度排序的电影列表,支持分类筛选。
  • 搜索:按电影名搜索,搜索词记录后面会进入行为采集。
  • 我的:展示用户收藏、评分历史、个人信息和登录状态。

推荐页的布局我用了最简单的上下结构:顶部一个"为你推荐/热门精选"的标题栏,下面用横向滚动的卡片列表展示海报+片名+评分。这样实现难度低,效果又直观。

5.2 评分交互:先解决"用户愿意打分"的问题

推荐系统最怕用户不给反馈,没有反馈就是无源之水。我在设计评分交互时特意做了轻量化处理:详情页底部常驻一排五角星,点击就打分,不需要用户再点"确认"按钮,打分即提交,后台静默调用接口。评分成功的轻提示用wx.showToast({ title: '感谢评分', icon: 'none' }),不让用户觉得被打断。

这里有个小技巧:我会在打分后立即调用一次首页推荐接口的刷新,让推荐列表立刻变化。用户就会产生"我做了操作,系统真的记住了"的正反馈,愿意留下更多行为数据。这也是录演示视频的关键步骤,一定要在答辩前录制一段"打分前→打分后"的首页对比。

5.3 数据缓存策略:wx.setStorage还是内存变量

小程序页面的数据不要每次都从后端拉,能用缓存就用缓存。推荐首页的数据我做了双重策略:

  • 全局内存缓存:在app.globalData.recommendCache中存最近一次请求结果,页面onShow时先展示缓存,再异步请求新数据对比刷新。这样切Tab回来不用白屏等接口。
  • Storage持久化:冷启动时如果后端请求失败,先用wx.getStorageSync('home_cache')里上次成功的数据渲染,至少保证页面不是空的。
const getHomeRecommend = () => { const cache = wx.getStorageSync('recommend_home_cache'); if (cache && Date.now() - cache.time < 5 * 60 * 1000) { // 5分钟内的缓存直接用于首屏 setData({ list: cache.list }); } // 同时发起远程请求,返回后更新缓存和页面 request.get('/api/recommend/home') .then(res => { setData({ list: res.data.items }); wx.setStorageSync('recommend_home_cache', { list: res.data.items, time: Date.now() }); }); };

一个很容易踩的坑:wx.setStorageSync的存储大小限制是10MB左右,海报URL和电影描述都是字符串,存个百八十条没问题,但不要把整张电影列表缓存数万条。只缓存推荐位和前20条搜索结果即可。

5.4 登录授权:getUserProfile与openid登录的问题

这里有一个极具时代感的坑:微信官方在2022年之后调整了wx.getUserProfile的规则,开发者调用时需要用户手势触发,不能在页面加载时直接弹窗调用。现在的稳妥做法是:

  1. 用户进入"我的"页面,点击"微信登录"按钮,触发wx.login()获取code。
  2. 后端用code调微信接口换openid(这是后端的事,不涉及敏感授权)。
  3. 用户点击"获取头像昵称"时,使用新版的头像昵称填写能力(button open-type="chooseAvatar"和input type="nickname"),不再走旧的getUserProfile弹窗。

这样既符合微信最新规范,答辩时也能顺带说明"我关注了平台政策变化,并按新版交互做了适配",加分。

5.5 请求封装:统一handle,避免到处写callback地狱

我抽了一个util/request.js,统一处理baseURL、超时、token注入和错误提示:

const request = (url, method = 'GET', data = {}) => { return new Promise((resolve, reject) => { wx.request({ url: `${BASE_URL}${url}`, method, data, timeout: 6000, header: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${wx.getStorageSync('token')}` }, success: (res) => { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data); } else if (res.statusCode === 401) { // token过期,引导重新登录 wx.removeStorageSync('token'); wx.showToast({ title: '请重新登录', icon: 'none' }); reject(res); } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }); reject(res); } }, fail: (err) => { wx.showToast({ title: '网络异常,请检查网络', icon: 'none' }); reject(err); } }); }); };

这段封装看似基础,但直接决定了整个项目的代码整洁度。所有页面统一调用request('/api/movie/list?page=1').then(res => ...),写起来非常舒服。

6. 源码到手后的第一步:跑通项目的完整流程与常见坑

如果你是从网上拿到的源码(或者参考别人的开源项目改造),跑通它才是第一道关卡。很多人死在这一步,不是代码不行,而是环境和配置问题。

6.1 微信小程序端的初始化配置

拿到源码后,先用微信开发者工具导入小程序目录。接下来有三件事必做:

  1. 修改AppID:在project.config.json里把"appid"改成你自己的测试号AppID(注册一个小程序账号即可)。不换AppID的话,前端请求中携带的openid对应的登录态会对不上。
  2. 配置合法域名:如果后端跑在本地,不好意思,微信开发者工具默认不能访问http://localhost:8080,要在"详情→本地设置"里勾选"不校验合法域名、web-view(业务域名)、TLS版本以及HTTPS证书"。这是毕设调试中最常见的卡点。
  3. 检查baseURL:找到util/config.js或request.js里的BASE_URL,如果是线上地址,改成你本机的局域网IP,形如http://192.168.x.x:8080。真机预览时必须用局域网IP,不能用localhost,因为手机上访问不到你自己电脑的localhost。

6.2 后端初始化和数据库导入

后端依赖和启动流程一般README里都有,但有几个坑我要单独提醒:

  • 数据库版本:如果源码是用的MySQL 5.7,你装了MySQL 8.0,大概率遇到"<is deprecated,please use<"这类报错。原因主要是驱动兼容和SQL模式。最省事的做法是:把所有Caused by和SQLException日志中提到的横杠、下划线符号检查一遍,确认是否是where tag = 'xxx'里混入了别的字符;如果不是,直接改连接驱动为最新版,并把连接串加useSSL=false&serverTimezone=Asia/Shanghai。
  • 建库建表:源码一般附带schema.sql或init.sql,用Navicat或命令行导入。导入后先跑一遍测试SQL,确认表和t_recommend_result的初始数据都存在。注意导入前把SQL文件里的drop table注释掉,以免误删自己的数据。
  • Redis依赖:如果后端用了Redis做缓存,本地没装Redis就是个隐藏炸弹,启动时会报连接拒绝。解决方案有两类:一是装一个Windows版Redis保持默认端口运行;二是直接搜索代码里Redis工具类,看看是否能配置为内存模式(如果源码封装的RedisUtil能优雅降级)。

6.3 数据初始化:没有电影数据,推荐算法跑了个寂寞

很多源码包里数据库是空的,连电影表都没数据。你就算启动成功,首页也是空荡荡。这个问题的解决思路是:

  • 找公开的TMDB数据集(The Movie Database开放API或CSV),筛选出国内用户熟悉的几百部电影。
  • 把电影的基本字段(中文名、导演、演员、类型、海报URL、简介、评分、年份、片长)整理成SQL,批量INSERT。
  • 人工准备的行为数据:造30个左右的虚拟用户,给每部电影打上5~30条不等的评分,形成一批有区分度的行为记录。这样推荐接口一测就能看出不同用户推荐列表不同,演示效果拉满。

造数据时有个技巧:故意让其中几个用户只看悬疑推理片、几个用户只看爱情喜剧,这样每个用户画像鲜明,推荐结果差异一眼就能分辨,答辩时可以指着屏幕讲"这位用户喜欢悬疑,所以系统给他推了《看不见的客人》和《禁闭岛》"。

6.4 常见报错自查清单

我在调试过程中遇到的几个典型报错,整理成一个自查清单,按出现频率排序:

报错现象根因解决办法
request:fail或超时域名校验未关 / BASE_URL写错勾选不校验域名;确认后端服务已启动
首页空白或数据为空推荐结果表为空 / 冷启动兜底没生效手动执行离线推荐任务,或检查/api/recommend/home的兜底逻辑
登录后openid为空code换openid失败检查小程序AppID和后端配置的AppSecret是否匹配,后端需调jscode2session接口
Could not roll back / Data too long字段长度不够后端日志会明确报字段名,把对应字段varchar长度加长
Java版本不对Spring Boot版本与JDK不匹配按源码要求切换JDK版本(比如Spring Boot 2.x配JDK8,3.x配JDK17)

6.5 我踩过的三个印象最深的坑

第一个坑:Token过期导致推荐接口401。后端生成JWT默认有效期我设置成2小时,录视频演示到一半突然接口全挂,页面疯狂弹"请重新登录"。后来我彻底理解了:演示前一定要检查JWT过期时间配置,只要能撑过答辩现场即可,设成12小时都行。

第二个坑:海报图片挂掉。我用的海报URL是网上截取的,有些直接404,小程序里图片无法加载就留一个大灰块。建议用mode="aspectFill"同时加一层binderror后替换为本地备胎图。

第三个坑:真机预览时局域网IP不通。最常见原因是手机和电脑不在同一Wi-Fi,或者Windows防火墙拦截了8080端口。解决办法是关掉防火墙(答辩场地短暂操作没问题)或加一个入站规则允许8080端口。

7. 毕设答辩与加分项:如何让推荐系统看起来"有水平"

项目做完,怎么在答辩现场把它讲得有条有理,让老师信服你确实理解了整套系统,这个问题同样重要。我总结了一套完整的演示和讲述策略。

7.1 演示的标准故事线:从冷启动到个性化

答辩演示不要一上来就打开页面戳来戳去,而是按一条逻辑线走:

  1. 展示登录流程:点"微信登录",说明后端通过wx.login换取openid并生成JWT的过程。
  2. 展示新用户冷启动:用刚注册的新账号打开推荐页,标题显示"热门精选",说明系统此时还没有用户行为数据,走了热门兜底策略。
  3. 展示行为采集:给这部电影打分,顺手点开两三部电影详情再返回,说明后端记录行为类型和权重。
  4. 展示个性化结果:切回首页,此时标题变成"为你推荐",列表中出现与该用户行为匹配的电影。可以当着老师的面再给《星际穿越》打5分,刷新后看推荐列表是否发生变化。
  5. 展示搜索和排行:快速过一遍,体现系统功能的完整性。

这条线走下来,大概5-8分钟,每一个节点都有明确的技术点,不会出现"呃,我点点看"的尴尬。

7.2 几个高频答辩问题与回答思路

  • "为什么不用深度学习?"答:深度学习模型需要大量训练数据和更高的硬件资源,本系统的数据规模属于中小型,经典协同过滤在精度和可解释性上已经能取得较好效果。论文中我分析了NCF等深度推荐模型作为后续扩展方向,但作为毕业设计,工程可落地性和算法可解释性是首要考量。
  • "数据稀疏怎么办?"答:采用两种手段缓解:一是行为数据融合,把评分、收藏、浏览统一建模,增加信号密度;二是基于内容的标签相似度做补充,即使共同评价少也能通过电影属性找到相似候选。
  • "推荐结果多久更新一次?"答:离线定时计算,同时用户每次产生新行为时会标记该用户推荐结果过期,下次计算时优先更新。接口层面不会有明显延迟,在线请求只查推荐结果表。(这题答到这个层次基本就稳了。)
  • "如何评估推荐效果?"答:项目阶段用离线指标(精确率、召回率)做过验证,把用户行为数据按时间划分为训练集和测试集,计算推荐列表中命中用户真实观看的比例。这个细节我认为是加分最大的,很多毕设根本不做评估。

7.3 三个加分改进方向(成本低、讲得响)

如果时间还有富余,以下三个方向任选其一加到PPT里,即使没实现,写出方案设计也是加分项:

  1. 混合推荐加权动态调参:给协同过滤和基于内容的推荐各算一个分数,再用一个可调的权重系数融合,权重根据用户行为数量动态变化。
  2. 实时推荐更新:引入消息队列(如RabbitMQ/Kafka),用户行为进队列异步触发该用户的增量推荐计算,不需要全量离线重建。
  3. 弹幕/影评社区互动:用户在电影详情页发短评,短评的情感分析结果作为行为权重的一部分,把"文本信息"纳入推荐特征,这个方向很有延展性。

老实说,我当时选了第三个方向做了一点短评功能演示,答辩时老师明显更感兴趣,问了十分钟的细节。如果你手头的源码里没有这些功能,也完全可以只作为论文"未来工作"章节出现,不会露怯。

这个项目从选题到跑通再到答辩,给我的整体感受是:它不像看上去那么"高大上",也不像管理系统那么"平平无奇",它刚好处在"需要动脑但不用拼天赋"的位置上。把你折腾最多的地方写进论文,把调试过程中最狼狈的那次报错变成你的答辩故事,这套系统就是你的东西了。最后再分享一个小技巧——答辩前找同学扮演用户,当着他的面给几部冷门片打分,录一段完整的操作视频备着。如果现场网络翻车,你还能用这段视频救场。

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

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

立即咨询