
做了不少小程序项目之后我慢慢发现一个很扎心的现象很多人做美食推荐最后做成了点菜工具。用户能登录、能翻菜品列表、能看详情但翻来翻去全是按分类排的静态页面跟十年前的信息黄页没什么区别。推荐算法不是没想过是总觉得难等到真正动手才发现协同过滤没那么玄乎难的是把算法和业务揉在一起让一个学Python没多久的人也能跑通全流程。这篇文章要聊的项目就是一套基于Python flask uniapp微信小程序的协同过滤美食推荐交流系统。它能做的事很具体用户登录后看到的不再是固定的菜品列表而是根据他的历史行为——点赞、收藏、浏览时长、评论内容——动态算出来的猜你喜欢同时围绕美食做一个社区用户可以发动态、写评价、互相交流。后端用flask提供推荐接口前端用uniapp打包成微信小程序算法层用协同过滤实现个性化排序。整套东西可以本地跑通也能部署到线上做演示特别适合拿来当毕设、课设或者给餐饮门店做会员小程序练手。如果你正准备动手做类似项目这篇内容会给你一条相对完整的路线从架构选型到算法落地从数据库设计到小程序联调最后还有我踩过的一堆坑按性价比从高到低排好了。1. 先把方案摆到桌面上为什么是flaskuniapp协同过滤这套组合做项目的第一步往往不是写代码而是回答为什么是这套。很多人上来就纠结框架哪个好其实脱离了业务场景讨论框架都是空谈。这套组合能成立是因为三者的分工恰好各管一段互不抢活。1.1 技术选型的底层逻辑先看后端。flask是Python社区里最轻量的Web框架之一单文件能起服务蓝图Blueprint能拆模块配合SQLAlchemy操作数据库、APScheduler做定时任务几乎没有多余的概念要学。美食推荐系统的核心逻辑在算法层不在并发层所以不需要一上来就上Django的重型全家桶更不需要为了撑场面去学FastAPI的异步特性。flask的生态里还有一堆现成的扩展比如flask-cors解决跨域、flask-jwt-extended做登录鉴权这些对小程序项目来说都是开箱即用的。再看前端。uniapp的核心卖点是一套代码多端发布。在这个项目里你的目标端是小程序但保不准后面要顺手出个H5版本给朋友在手机浏览器里体验或者打包成Android应用放到应用商店。用uniapp写一次这些端都能编译出来改造成本几乎为零。另一个实际原因是uniapp的语法和Vue高度一致会Vue的人上手很快生态里有uview-plus这类UI组件库做列表、卡片、表单这些常见界面基本是拖拽式的效率。协同过滤算法则是这套系统的灵魂。它不需要任何人工标注的规则——不需要你写川菜推荐给爱吃辣的人这种每条都维护的笨办法——而是纯粹从用户行为数据里自己找规律。用户A和用户B都在牛肉面下点了收藏系统就认为他们口味相似B喜欢的麻辣香锅A大概率也感兴趣。这个逻辑用代码实现也就几十行核心逻辑但呈现出来的效果非常智能项目演示时能讲的故事完全不一样。1.2 这套组合的边界什么场景千万别硬套你得清楚这套技术栈的适用范围。flask是同步阻塞模型性能上限天然不高你要是想做成美团那种日活百万的推荐系统flask会先撑不住uniapp编译出来的小程序在复杂交互动画上跟原生开发也有差距协同过滤有冷启动和数据稀疏问题用户行为太少时推荐质量很拉胯。我的态度是做课设、毕设、门店级应用这套组合完完全全够用而且足够出彩想奔着大厂架构去那是另一套玩法。项目答辩的时候主动说清楚这套架构的边界比硬吹反而更能体现你的工程判断力。2. 协同过滤不是装上就完事美食推荐场景的算法取舍与冷启动处理协同过滤在教科书里讲得很简单但落到美食推荐这个具体业务里有一堆算法之外的问题要处理。算法本身是菜谱业务场景才是食材食材不一样烹饪手法必须跟着变。2.1 选UserCF还是ItemCF先看懂你的数据长什么样协同过滤两大流派基于用户的UserCF和基于物品的ItemCF并不存在谁更高级的说法只看谁更匹配业务形态。UserCF的思路是人以群分先算用户之间的相似度找到跟你口味最像的N个人把他们喜欢而你没看过的东西推给你。ItemCF的思路是物以类聚先算物品之间的相似度推荐和你喜欢过的食物相似的东西。美食推荐场景里我更推荐以ItemCF为主。原因很直白用户的口味偏好短期内相对稳定但用户数量通常比物品数量多得多。在小程序早期注册用户可能只有几百个但菜品可能有几千个计算物品相似度矩阵的成本远低于用户相似度矩阵更关键的是ItemCF算出来的推荐理由可以落地成一句话——因为你喜欢过毛血旺所以为你推荐水煮鱼这种解释性对用户信任度很重要而对一个交流社区来说用户信任直接决定他愿不愿意发内容。核心计算逻辑大致长这样import numpy as np from sklearn.metrics.pairwise import cosine_similarity # 构建 物品-用户 隐式反馈矩阵行为浏览/收藏/点赞 # 行是菜品ID列是用户ID值代表行为权重 item_user_matrix build_behavior_matrix() # 自定义函数 # 计算物品间的余弦相似度 item_sim cosine_similarity(item_user_matrix) # 推荐根据用户历史行为的物品加权求和找出没看过的TopN def recommend(user_id, top_n10): user_items get_user_acted_items(user_id) scores np.zeros(item_user_matrix.shape[0]) for item_id in user_items: scores item_sim[item_id] * get_behavior_weight(user_id, item_id) # 过滤掉已看过的 scores[user_items] -np.inf top_indices np.argsort(-scores)[:top_n] return [item_id_to_dish[i] for i in top_indices]这段代码只是骨架但你应该能看出关键点相似度矩阵离线算推荐排序在线算。物品数量几千个相似度矩阵是几千乘几千用sklearn的cosine_similarity几秒钟就能跑完完全可以定时离线更新用户请求推荐时只做一次加法和排序毫秒级返回。2.2 隐式反馈的权重设计比想象中更重要美食推荐不像视频网站有点赞、踩、评分这么多显式信号用户在餐饮场景里最常做的动作是看、收藏、加购物车、评论。这几个行为的含金量截然不同直接统一计为1会浪费掉大量信息。我实际用的权重表是这样的行为类型权重理由浏览0.5可能是随便看看代表弱兴趣收藏1.0明确表达我以后想吃点赞0.8社交场景的快速认可评论1.5愿意写字是最强信号下单/到店核销3.0真金白银的转化行为有了权重之后用户-物品反馈矩阵的生成就不是简单的0/1计数而是对每种行为加权累加。比如一个用户收藏了A菜品三次、评论过一次那他在A菜品上的特征值就是 3×1.0 1×1.5 4.5。特征值能拉开用户之间的口味差距相似度计算才有区分度。这个细节很简单但对推荐质量的提升立竿见影。2.3 冷启动新用户和新菜品都不该被冷落协同过滤最大的痛点就是冷启动。新用户没有任何行为数据相似度矩阵完全失效你给他推什么新菜品没人碰过谁也不会被推荐到它。我的处理方案分三层注册时强制选口味标签。在新用户注册流程里弹一个选择页让用户勾选偏好——川菜、粤菜、烧烤、甜品等这些标签直接转换成初始的行为权重注入矩阵。相当于用户还没用就已经有了虚拟行为。热门兜底策略。冷启动用户走热门榜单接口用简单的热度公式热度 浏览数×0.4 收藏数×1 评论数×2按时间衰减加权。保证首页永远不会空白也符合新用户先看看大家都在吃什么的心理。新菜品的冷启动补贴。给上架后48小时内的新菜品加一个相似度加成让它们有机会出现在推荐流中参与试错。有了曝光才有行为数据有了行为数据才能真正进入协同过滤的循环。没有曝光算法再好也永远学不会推荐它。3. 后端真正的重心在推荐服务化数据库与API的落地设计很多人写flask后端会把所有逻辑堆在app.py里路由密密麻麻两百行之后自己都找不到北。这个项目虽然不大但我强烈建议一开始就按服务化的思路拆。推荐的逻辑跟普通CRUD完全不是一回事混在一起会让你后期改算法时寸步难行。3.1 五张表足够轻量数据库设计原则我用SQLite起步零配置、单文件、本地直接跑等到要部署上线再切MySQL也很平滑。核心表五张彼此关系很清晰user表用户ID、微信openid、昵称、头像、口味标签JSON字段存储、注册时间。口味标签是冷启动的关键数据必须存。dish表菜品ID、名称、图片URL、价格、分类、描述、上架时间。这张是静态数据由管理员后台录入或者爬虫抓取注意遵守robots协议。behavior表行为ID、用户ID、菜品ID、行为类型view/favorite/like/comment/order、行为权重、创建时间。这张表是推荐算法的原油每一行都是一次用户行为的原始记录。comment表评论ID、用户ID、菜品ID、内容、评分、创建时间。交流社区的基础也是显式反馈的补充信号。post表动态ID、用户ID、内容、图片、关联菜品ID可空、创建时间。这是交流板块的主体用户可以发探店笔记或者晒图。表与表之间全部通过外键关联查询用SQLAlchemy的ORM语法不需要手写原生SQL。设计的时候特别注意一点行为表和评论表会膨胀得很快给user_id和dish_id建联合索引否则推荐算法每次全表扫数据会越来越慢。3.2 接口怎么拆推荐API与常规CRUD分开flask用蓝图拆模块最方便我按资源域划分了五个蓝图auth登录鉴权、dishes菜品查询、behavior行为采集、recommend推荐、social社区交流。前四个都和推荐链路有关最后一个撑起交流两个字。推荐模块的接口设计是重点我提供三个接口就够吃透整个推荐流程# 核心推荐接口给用户返回个性化菜品流 GET /api/recommend/dishes?user_id1limit10 # 行为上报接口前端页面每次曝光或点击都上报 POST /api/behavior/report Body: { user_id: 1, dish_id: 86, action: view } # 热门兜底接口冷启动用户和新用户的首屏 GET /api/recommend/hot?limit10接口设计逻辑有三点值得展开说第一推荐接口必须带user_id即使前端还没登录也应生成一个匿名设备ID。没有身份标识算法直接退化成热门榜个性化就无从谈起。第二行为上报接口要设计成一次性上报多条。比如用户往下滑首页一次滑动可能曝光了8个菜品卡片小程序端应该攒成数组一次性POST而不是一条条发否则请求频率会非常感人后端日志也难看到完整链路。第三推荐结果接口的返回格式要带推荐理由。字段类似recommend_reason: 因为你喜欢过【毛血旺】前端把这句话显示在卡片下方。这个字段一方面让推荐显得懂你另一方面也方便你调试一眼能看出推荐是因为哪条历史行为触发的。3.3 评分行为采集别让用户白看美食推荐系统跟普通信息流的关键差异在于——用户的每次滑动、每次停留都是有价值的信号。我在小程序端埋了三个上报点位菜品卡片的曝光进入可视区域即上报、点击详情、收藏/点赞。曝光上报用IntersectionObserver实现这个API在微信小程序原生和uniapp里都有封装不需要额外引入SDK。埋点数据上报用节流控制频率每2秒最多上报一次避免快速滑动时产生大量并发请求。后端收到行为数据后写进behavior表同时更新一份轻量的用户画像缓存Redis或者内存字典这样推荐接口不用每次都重算历史行为直接读画像就能给结果。4. 小程序前端要解决的不只是页面登录、请求封装与社区交互uniapp写小程序页面本身没什么难度难的是把登录体系、网络层、状态同步这些基础设施一次做到位。这些基础不打牢页面写得再好看一联调就崩给你看。4.1 微信登录与用户体系的打通微信小程序的登录流程和传统Web完全不同核心是拿到微信的code在后端换openid。具体链路是这样前端调uni.login()拿到微信临时凭证code把code发给后端/api/auth/wxlogin后端拿着code调用微信接口jscode2session换取用户的openid和session_key后端用openid查user表没找到就自动注册一个新用户然后签一个自己的JWT Token返回前端前端把Token存到uni.setStorageSync后续每个请求的header里都带Authorization: Bearer token。这里有个常见的坑不要把openid直接暴露给前端存储。小程序端能拿到openid就意味着任何人都能伪造身份正确的做法是后端把openid映射成自增的user_idToken里只带user_id。这是线上项目必须注意的鉴权底线。4.2 request.js封装让Token问题在源头解决uniapp项目里我习惯建一个utils/request.js统一管理请求核心逻辑是封装uni.request自动加Token、统一处理错误码、处理Token过期。const request (options) { return new Promise((resolve, reject) { const token uni.getStorageSync(token); uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Authorization: Bearer token, ...options.header }, success: (res) { if (res.data.code 401) { // Token过期跳转登录页并清除本地身份信息 uni.removeStorageSync(token); uni.navigateTo({ url: /pages/login/login }); reject(res.data); return; } if (res.data.code ! 0) { uni.showToast({ title: res.data.msg, icon: none }); reject(res.data); return; } resolve(res.data.data); }, fail: (err) reject(err) }); }); }; export default request;这段封装的核心价值是让所有页面开发人员不感知Token的存在——页面只关心业务数据身份校验的事全部在请求层完成。后来扩展社区功能、评论功能时新页面调接口都是直接request({url:/api/social/posts})完全不用改公共逻辑。4.3 交流社区的形态不止是评论区很多美食推荐交流系统把交流做成了菜品详情页里的一个评论区这远远不够。我在项目里做了两个社交层级菜品评论区围绕单道菜展开短评解决这道菜好不好吃的具体问题。社区动态流独立的发现页面用户可以发图文动态记录探店经历、晒美食照片动态可以关联一道菜品其他用户可以点赞和评论。这个板块是沉淀UGC内容的核心也是留住用户的关键。社区动态流的加载用了上拉分页模式每页10条配合onReachBottom触发下一页请求。小程序端对这个交互支持得很原生uniapp只需要在页面里配置onReachBottomDistance就行。uni-app编译到微信小程序时分页状态管理有点琐碎我的做法是封装一个usePagination组合式函数把页数、加载状态、是否到底这些状态集中管理避免每个页面都重复写一遍。5. 从本地到线上flask部署与小程序的联调细节项目写完不等于能演示。这里说的演示分两层第一层是本地把小程序跑起来能看效果第二层是把后端部署到云服务器让小程序随时随地能连。很多人在第一层就卡住了——不是卡在代码而是卡在小程序开发工具怎么访问本地flask服务这个看似小却磨人的环节。5.1 本地联调小程序开发者工具的两个关键配置微信小程序开发者工具默认只允许访问HTTPS域名本地调试时要在工具栏找到详情-本地设置勾选不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书。这不是跳过安全检查而是开发阶段访问http://127.0.0.1:5000的必要开关。第二个关键点是开发者工具里的IP地址不要填localhost。微信开发者工具模拟器里的localhost指向的是模拟器自己不是你的电脑。要填本机局域网IP比如http://192.168.1.100:5000。如果手机做真机调试还得保证手机和电脑在同一Wi-Fi下并且flask启动时设置host0.0.0.0否则手机根本连不上。这三个配置我每做一个新项目都要重新踩一遍索性记住了。5.2 部署时的几个关键命令与配置上线部署时我推荐用云服务器 Nginx Gunicorn的组合。Nginx负责静态资源和反向代理Gunicorn负责跑flask应用。关键步骤如下# 服务器上创建虚拟环境并安装依赖 python3 -m venv venv source venv/bin/activate pip install -r requirements.txt # 用gunicorn启动flask应用4个worker进程 gunicorn -w 4 -b 127.0.0.1:5000 wsgi:app # Nginx配置反向代理 server { listen 80; server_name your-domain.com; location / { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }有几个细节必须注意。Gunicorn的-w 4是根据服务器CPU核心数定的不是越大越好flask里的app.run()只在开发时用部署时不要用最关键的是小程序正式环境要求所有请求必须是备案域名的HTTPS需要提前给域名申请免费的SSL证书然后在Nginx里配置443端口。这个流程第一次走会有点繁琐但走通一次之后所有flask项目都能复用。5.3 小程序审核与发布截图小程序发布到微信公众平台时审核环节有几个高频驳回理由我在项目里提前规避了第一没有做用户隐私保护指引就调用了用户信息接口登录时只用uni.login()拿到code后静默登录不要强行弹授权框拿昵称头像微信新规则已经不允许强制授权第二社区功能里用户发动态需要做关键词过滤和内容审核哪怕是demo也要有个敏感词过滤函数放在后端审核时能拿出说明第三上线前在小程序后台配置服务器域名时记得HTTPS证书要完整不能是自签名证书。这些听起来都很基础但每年被驳回的小程序十有八九栽在这几个地方。6. 这套系统实测中踩过的坑按性价比排序这一段是我最想写的部分。理论讲得再漂亮不如实测踩坑来得深刻。我按性价比给这些坑排个序——性价比的意思是解决它所花的时间短、但对体验提升却很大。6.1 坑一协同过滤离线计算卡顿、在线响应慢第一个坑来自算法本身的实现方式。最初我把相似度计算放在每次请求推荐接口时实时算用户少的时候看不出什么问题一旦数据量涨到几百个用户、上千个菜品接口响应时间直接飙到3秒以上。后来改成离线计算内存缓存每天凌晨定时用APScheduler跑一次相似度矩阵计算结果存成Pickle文件或者放到Redis里在线请求只做矩阵乘法加排序接口响应稳定在100毫秒以内。这件事给我最大的教训是推荐系统天生是重计算、轻请求的结构把计算放在请求链路里是大忌。6.2 坑二uniapp的Canvas导出白图问题项目里做了一个生成美食海报分享的功能用uniapp的Canvas绘制海报图片然后把canvasToTempFilePath导出的临时文件传给微信的分享接口。实机测试时海报偶尔导出全白。排查后发现是因为绘制接口是异步的海报还没画完就触发了导出。解决方案很简单使用canvas绘制时必须在draw()回调函数中调用导出方法确保绘制任务已同步完成另外不要在onHide或页面卸载阶段调用导出那个时机拿不到画布上下文。这个问题在iOS的Safari内核里触发概率明显更高详见uniapp社区里大量反馈帖子不是个例。6.3 坑三微信小程序顶部导航栏高度适配自定义导航栏在iPhone X以上机型会遇到状态栏高度不一致的问题。一开始我用固定数值写顶部栏的高度iPhone 13 Pro Max上显示正常换到普通iPhone SE就整体下坠。后来改用uni.getSystemInfoSync()动态获取状态栏高度再叠加胶囊按钮的位置信息动态计算导航栏高度。这个逻辑一劳永逸地解决机型适配问题强烈建议封装成公共组件。6.4 坑四抓包检查小程序请求时发现敏感信息明文传输做安全测试时我用抓包工具看了下自己小程序的请求发现登录接口返回的数据里有用户手机号、具体地理位置等字段全部是明文传输。虽然不是被攻击但正规上线是绝对过不了隐私合规审查的。处理方式是后端给敏感字段做脱敏手机号显示为138****1234涉及地理位置时先让用户明确授权同时给所有API加上了HTTPS强制跳转。这个坑可以提醒所有做小程序的朋友上线前自己先抓一遍包看看自己的请求比别人先发现问题永远比被平台驳回好。最后再分享一点个人体会如果让我总结这个项目最值得学习的地方绝对不是某个算法或者某个框架而是怎么把一个听起来很玄的需求变成能跑的系统的整个过程。协同过滤算法教科书上讲得很干净但你得自己想清楚用户行为怎么采集、冷启动怎么兜底、接口怎么设计才能让前端好用flask和uniapp都是相对容易上手的工具但把它们真正串起来中间隔着几十个不大不小的问题比如跨域、Token过期、域名备案、Canvas导出。每解决一个你对这套技术栈的理解就深一层。这个项目做完之后我最大的体会是毕业设计也好、个人项目也好不怕功能少怕的是每个角落都藏着你没有想清楚的细节。哪怕只做三个页面、两个接口、一个算法只要把链路彻底打透讲清楚每一个决策背后的理由它就是一个拿得出手的作品。如果你正在照着这个方向做先从跑通U盘里那份flask骨架代码开始再逐步把协同过滤、社区交流这些模块填充进去——大项目永远是一小块一小块攒起来的。