零食商城做推荐系统,这件事本身听起来没有多稀奇,但是真到动手实现的时候,你才会发现里面全是细节。很多人一上来就要搞协同过滤,觉得这是个“算法项目”所以很高端,结果连用户行为数据从哪来、商品之间的相似度怎么算、推荐结果怎么塞进前端页面都没想清楚,最终就是产出了一个调不动的半成品。我最早做这类项目的时候也踩过这些坑,所以这篇就把整个基于Python+Django+Vue3的协同过滤推荐零食商城系统拆开讲清楚,从技术选型到算法落地再到前后端衔接,直接给出一套可以照着抄的完整方案。
这套系统解决的实际问题很明确:传统商城只有“搜索+分类”两条路,用户想找零食却不知道买什么,商家想推新品却没有抓手。加上协同过滤推荐之后,系统可以根据用户过去的行为(浏览、收藏、加购、下单)找到相似人群或相似商品,从而实现“千人千面”的零食推荐,直接提升转化率和停留时长。适合正在做毕业设计、全栈项目实战,或者想在企业内部电商系统里嵌入推荐能力的朋友参考。
1. 项目整体设计与技术选型解析
1.1 为什么零食商城尤其适合协同过滤
零食这类商品有非常明显的特点:单价低、复购率高、品类多、决策成本低。用户在零食商城里买东西往往不是“精确搜索”而是“逛”,今天想吃辣的可能买几包辣条,明天想喝饮料又顺手加一包薯片。这种消费行为决定了传统的关键词搜索很难发挥作用,反而“别人买了什么”“和你喜好相近的人买过什么”这种推荐逻辑更容易命中用户需求。
所以说协同过滤和零食商城的契合度非常高。用户协同过滤(UserCF)能找到和你口味相似的其他用户,把他们的购买记录推荐给你;物品协同过滤(ItemCF)能根据你买过的零食,找到关联度高的同类或搭配商品,比如买了可乐的人很可能也买薯片。这两种方式放在零食场景里都非常自然,这也是我最终确定用协同过滤而不是内容推荐(基于商品标签做匹配)的核心原因。
1.2 技术栈的实际选择逻辑
项目标题定了Python+Django+Vue3,这套组合在当下依然是很务实的选择。
Django作为后端框架,自带Admin后台、ORM、认证体系和完整的MTV结构,开发效率非常高,尤其是做商城这种需要商品管理、用户管理、订单管理的基础系统时,Django能省下大量重复劳动。而且Django的ORM让数据建模非常直观,写协同过滤算法时可以直接通过ORM拿到用户行为数据,不需要额外拼SQL,这对推荐算法的原型验证特别友好。
前端选Vue3是因为它的Composition API在管理商品列表、推荐位、购物车这类交互密集的页面时,逻辑复用和组织都比Vue2清晰得多,配合Pinia做状态管理,购物车、用户信息、推荐结果这些跨组件数据可以很优雅地共享。更重要的是Vue3社区活跃,Element Plus这类组件库能让商城后台和前台页面快速成型,不用从零写样式。
还有一个考虑是部署和学习的平衡。Django + Vue3前后端分离是当前主流项目的常见形态,掌握了这套链路,后边换FastAPI、换React其实都是同理的。项目难度的重心不在框架本身,而在推荐算法的拆解和数据流的组织上,这反而更适合大多数人用来积累经验。
1.3 系统架构与功能模块拆解
整个系统的架构可以分成三层:
- 数据层:MySQL(或SQLite,取决于规模)存用户表、零食商品表、用户行为表(浏览、收藏、加购、下单)、订单表、推荐结果缓存表。
- 算法层:离线训练/实时计算模块,负责构建用户-物品评分矩阵、计算相似度、生成TopN推荐列表。这一层是独立于Django应用的Python模块,方便单独测试和调优。
- 应用层:Django提供RESTful API,Vue3前端负责渲染页面和交互。前端通过Axios调用后端接口获取商品列表、推荐列表、下单信息。
功能模块上,我把它拆成了几个大块:
| 模块 | 说明 |
|---|---|
| 用户模块 | 注册、登录、个人信息维护,使用Token鉴权 |
| 商品模块 | 零食商品的展示、分类筛选、关键词搜索、商品详情 |
| 行为模块 | 浏览记录、收藏、加入购物车、下单,这些行为会被记录并转化为推荐算法的输入特征 |
| 推荐模块 | 接收用户ID,调用协同过滤算法,返回个性化零食推荐列表 |
| 订单模块 | 购物车结算、订单生成、订单历史(做推荐时下单行为的权重最高) |
这套结构下来,整个项目其实是“一个商城主干 + 一条推荐分支”。主干部分所有开发者都会做,真正拉开差距的是推荐分支的数据组织和算法实现,这也是我下文要重点展开的部分。
2. 协同过滤推荐算法的工程落地
2.1 先定策略:这个场景该用UserCF还是ItemCF
很多初学者做协同过滤时第一反应是“用户协同过滤更高级”,其实这个选择完全取决于业务形态。在零食商城这个场景里,我的经验是物品协同过滤(ItemCF)通常效果更稳。
原因是零食的用户兴趣相对广泛,一个用户可能同时买辣条、坚果、饮料,如果按UserCF去找“相似用户”,很容易因为用户画像不够聚焦而推荐出奇怪的商品。而ItemCF的出发点是“你买过的商品和哪些商品经常一起出现”,它的推荐解释性更强——给用户看“购买过该商品的用户还买了什么”,用户是能理解和接受的。
UserCF也不是没有用,在用户行为非常稀疏、或者想挖掘小众新零食时,UserCF能找到你意想不到的“口味搭子”。所以我最终的做法是双路召回:ItemCF作为主推荐,UserCF作为补充召回,最后用一个简单的加权策略融合两个推荐列表。这个方案在实测里比单纯用某一种算法覆盖更全面。
2.2 数据模型设计:行为数据比商品数据更重要
协同过滤的效果上限由数据质量决定。商品表只是静态属性,用户行为数据才是算法的“原料”。我在设计数据表的时候专门为五种行为设了权重:
- 浏览:1分(只说明用户感兴趣,但没有付出成本)
- 收藏:3分(表达明确意向)
- 加购:5分(离成交很近)
- 下单:8分(已经产生交易,反馈最强)
- 复购:10分(重度偏好)
这个分值不是拍脑袋定的,而是我参考了实际电商场景中“行为到购买的转化漏斗”逻辑。浏览-收藏-加购-下单,越接近成交的动作说明用户意图越强,相应分数越高,这是协同过滤评分矩阵的基础。
Django这边的模型大致如下:
class UserBehavior(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) product = models.ForeignKey(Product, on_delete=models.CASCADE) behavior_type = models.CharField(max_length=20, choices=[ ('view', '浏览'), ('fav', '收藏'), ('cart', '加购'), ('order', '下单') ]) score = models.FloatField(default=0) # 映射后的权重分 create_time = models.DateTimeField(auto_now_add=True) class Meta: db_table = 'user_behavior'这个设计的关键点在于:每个行为记录都保存最终映射后的score,算法层拿到这张表直接就能构建矩阵,不用每跑一次推荐都重新计算权重。
2.3 算法核心代码:从矩阵构建到相似度计算
协同过滤的Python实现并不复杂,核心链路就三步:构建用户-物品评分矩阵、计算相似度、生成TopN推荐。以ItemCF为例,最常用的方法是“基于物品的协同过滤”,相似度用余弦相似度或者皮尔逊相关系数。
我贴一段核心代码,这是我项目里实际在用的简化版本,逻辑清晰,适合二次开发:
import numpy as np import pandas as pd from sklearn.metrics.pairwise import cosine_similarity def build_user_item_matrix(behavior_df): """ 行为数据转评分矩阵 行为数据格式:user_id, product_id, score """ matrix = behavior_df.pivot_table( index='user_id', columns='product_id', values='score', fill_value=0 ) return matrix def item_cf_recommend(behavior_df, target_user_id, top_n=10): """ 基于物品协同过滤的推荐 思路:根据物品相似度矩阵,把目标用户已购物品的相似物品找出来, 按相似度加权得分降序,取前N个 """ matrix = build_user_item_matrix(behavior_df) # 物品-用户矩阵转置,再算物品间余弦相似度 item_matrix = matrix.T item_sim = cosine_similarity(item_matrix) item_sim_df = pd.DataFrame(item_sim, index=item_matrix.index, columns=item_matrix.index) # 获取目标用户已购买的物品(评分>0的) user_ratings = matrix.loc[target_user_id] bought_items = user_ratings[user_ratings > 0].index.tolist() if not bought_items: # 新用户无行为数据,走热门推荐兜底 return hot_products(top_n) # 计算候选物品得分 scores = {} for item in bought_items: # 找到和当前物品最相似的K个物品 similar_items = item_sim_df[item].sort_values(ascending=False)[1:11] for sim_item, sim_score in similar_items.items(): if sim_item in bought_items: continue # 推荐时排除已购商品 scores[sim_item] = scores.get(sim_item, 0) + user_ratings[item] * sim_score rank_list = sorted(scores.items(), key=lambda x: x[1], reverse=True)[:top_n] return [item_id for item_id, _ in rank_list]这段代码里有几个细节值得说一下:
第一,为什么排除已购商品?因为商城推荐的目标是“让用户买更多”,把用户已经买过的东西再推给他没有意义。第二,为什么用用加权而不是只取最高分?因为用户可能同时对多个零食有兴趣,加权能更全面地表征这种偏好。第三,hot_products是冷启动兜底方案,这个后文单独讲。
2.4 冷启动问题的处理策略
协同过滤算法有个绕不开的天然缺陷:新用户没有行为数据、新商品没有关联记录,这就叫冷启动。我在项目里做了三层策略来缓解,实测效果还是可以的。
第一层:新用户默认推荐热门零食。所谓热门,我用的是“下单次数+浏览量的加权热度分”,而不是简单地按销量排序。原因是销量只能反映存量用户的选择,结合浏览量才能让推广中的潜力商品也有露脸机会。
第二层:新商品打标签,用内容推荐的思路补齐。零食上架时维护口味标签(香辣、甜口、咸口、坚果类、肉类、饮品等),新商品虽然没有用户行为,但可以通过“相同标签商品的协同结果”做关联召回,等它积累了真实行为再切换成纯协同过滤。
第三层:用户首次登录时做一个“口味偏好问卷”,让用户选几个喜欢的零食类型,把这个当成伪行为数据注入评分矩阵。这个操作在真实商城里会显得多此一举,但在毕设或演示项目中效果拔群,因为推荐结果立刻就有个性化反馈,演示观感非常好。
3. Django后端与Vue3前端的完整衔接
3.1 商品存储、行为埋点与API设计
后端接口我采用的是RESTful风格,Django配合DRF(Django REST Framework)实现。核心接口就几个:商品列表、商品详情、用户行为上报、推荐列表获取、购物车操作、提交订单。
不复杂,但是有个极其容易出现问题的点——行为埋点。前端用户看一眼商品、点一下收藏、加一次购物车,这些事件如果全部要在后端单独写接口,会非常啰嗦。我最后是用一个统一的行为上报接口搞定的:
POST /api/behavior/report/ Body: {"product_id": 123, "behavior_type": "view"}后端拿到behavior_type后做权重映射,插入UserBehavior表。这样前端只需要在合适的时机触发埋点请求,不用为每个动作设计独立接口,后端的算法层也只需要读一张表,两端都清爽。
3.2 推荐接口的查询与缓存处理
推荐接口是重头戏,它的基本逻辑是:
- 前端调用
GET /api/recommend?user_id=xxx请求推荐列表。 - 后端先查Redis缓存(有就直接返回,没有就执行算法)。
- 算法模块读取
UserBehavior表,构建矩阵、计算相似度、生成TopN列表。 - 根据推荐的商品ID列表,从
Product表查出完整商品信息返回前端。
为什么加Redis这层缓存?因为协同过滤的矩阵计算是O(n²)级别的操作,用户一多、数据量一上来,实时算一遍可能要几百毫秒甚至几秒。商品推荐结果又不需要秒级实时更新,完全可以让它缓存半小时甚至更久。加了这个设计之后,推荐接口从“每次计算”变成了“缓存命中直接返回”,响应时间能从两秒降到几十毫秒,体验完全两个档次。
Django里我用装饰器把推荐逻辑单独封装:
def get_recommend_products(user_id, top_n=10): """从缓存取,没有则走算法计算再回填缓存""" cache_key = f"recommend:user:{user_id}" cached_ids = cache.get(cache_key) if cached_ids: return cached_ids # 算法计算,这里调用上文实现的 item_cf_recommend behavior_df = load_behaviors_as_dataframe() product_ids = item_cf_recommend(behavior_df, user_id, top_n) cache.set(cache_key, product_ids, timeout=1800) return product_ids3.3 前端推荐位的渲染与用户体验细节
Vue3这边的推荐位我做了两个区块:首页的“猜你喜欢”和商品详情页的“看了又看”。
“猜你喜欢”用的是推荐接口返回的商品列表,展示形式是横向卡片滚动,卡片包含商品图、名称、价格和“加入购物车”按钮。“看了又看”则是在商品详情页底部,通过当前商品的相似商品接口单独拉取,它的逻辑不是基于用户整体偏好,而是基于当前商品找最相近的几款零食。
前端在页面加载时并行请求这两个接口,保证推荐位能快速渲染出来。这里我踩过一个坑:如果等所有接口都通用了再渲染页面,用户等待时间会非常长。最后改成先渲染商品主干内容,推荐位异步加载,这样不管推荐接口多慢,都不会阻塞用户浏览商品。
Vue3里的Pinia状态管理在这个场景里的作用很突出。购物车、用户Token、推荐列表都可能被多个组件同时修改,用Pinia统一管理之后,组件之间不用反复emit和props传递状态,代码组织干净很多。典型示例:
const useCartStore = defineStore('cart', { state: () => ({ items: [], }), actions: { addToCart(product) { // 判断是否已在购物车,已在则增加数量,否则新增条目 }, }, })3.4 前后端联调时的Token鉴权和跨域配置
前后端分离必然涉及跨域。Django默认不允许跨域请求,我在项目里用django-cors-headers解决了这个问题。如果只是本地开发调试,直接把所有来源放开是可以的;但如果部署到服务器上,一定要把白名单配置好,限制允许的域名,避免被人直接调用接口刷数据。
Token鉴权用的是Django DRF自带的TokenAuthentication机制。用户登录后,后端生成一个Token返回给前端,前端存到localStorage里,然后每次请求都在Authorization头带上:
Authorization: Token abc123...这里有个容易踩的坑:如果用户在无Token或Token过期的情况下请求推荐接口,后端会返回401,前端必须做全局拦截。我的做法是在Axios的响应拦截器里统一处理401,跳到登录页并清除本地用户信息,而不是让每个组件单独判断,避免代码冗余。
4. 实操过程与核心环节实现
4.1 从零初始化项目骨架
我用的是前后端分离模式,项目初始化大概分这几步:
第一步,建虚拟环境并安装Django依赖:
python -m venv env source env/bin/activate # Windows下用 env\Scripts\activate pip install django djangorestframework django-cors-headers pandas scikit-learn redis第二步,创建Django项目和APP:
django-admin startproject snackshop cd snackshop python manage.py startapp goods python manage.py startapp user_operation python manage.py startapp recommend第三步,创建Vue3前端项目:
npm create vue@latest frontend cd frontend npm install axios pinia element-plus这个步骤没什么技术含量,但能保证后续开发和调试在一个干净的环境里进行,建议不要图省事跳过去。
4.2 商品数据准备与行为数据模拟
没有真实数据怎么办?这是很多人卡住的地方。我当时也没有真实的商城数据,所以在自己写了一个mock_data.py脚本,自动生成商品和行为数据。
商品数据方面,我定义了几类零食:膨化食品、坚果炒货、糖果巧克力、饮品冲调、肉干肉脯、烘焙糕点,每个分类下造了大概15个商品。字段包括名称、价格、图片URL、分类、口味标签、库存等。图片直接用占位图URL,部署后换真实图片就行。
行为数据则是模拟了一批用户的操作序列,比如用户A浏览了商品1、3、7,收藏了商品3,加购了商品7并下单;用户B买了和A相似的商品等等。这样构造出来的数据自带一定的“结构性相似”,协同过滤能在实验数据上算出有意义的结果。
生成完数据后,需要跑一个数据检查脚本,确认每个用户至少有一定数量的行为记录、每个商品至少被足够多用户购买过,否则矩阵会严重稀疏,算法结果没有参考意义。
4.3 推荐模块与商城主流程的联调细节
核心联调点在于“推荐接口返回什么格式的数据”。
因为前端需要一个可直接渲染的商品列表,我推荐接口返回的不是简单ID列表,而是完整的商品卡片数据,直接在前端循环渲染。格式大致如下:
{ "code": 0, "data": [ { "id": 12, "name": "香辣味脆脆薯片", "category": "膨化食品", "price": 8.9, "cover_url": "https://...", "tags": ["香辣", "薯片"], "reason": "因为您购买过同品类零食" }, { "id": 28, "name": "海盐瓜子仁", "category": "坚果炒货", "price": 12.5, "cover_url": "https://...", "tags": ["咸口", "坚果"], "reason": "相似用户常买" } ] }加reason字段是我觉得特别值得推荐的做法。用户看到“因为您买过辣条所以推荐薯片”这种推荐理由时,点击意愿会明显提高,对演示和答辩都非常有帮助。
4.4 评分、订单与推荐结果的一致性处理
商城系统里用户下单之后,订单状态变更会直接影响推荐结果。比如用户买完一包坚果,坚果类零食在他的推荐列表里就应该减少曝光,避免重复推荐。
我这里的做法是在订单完成时触发一个“行为回填”动作:读取订单详情里的商品列表,把每个商品的下单行为写入UserBehavior表,同时更新Redis里的推荐缓存失效。这个方法实现不难,但能解决“用户都买了你还推荐”的尴尬,推荐体感会真实很多。
def order_created(sender, instance, created, **kwargs): """下单后回写行为数据并清缓存""" if not created: return order_items = instance.order_items.all() for item in order_items: UserBehavior.objects.create( user=instance.user, product=item.product, behavior_type='order', score=8.0 ) cache.delete(f"recommend:user:{instance.user_id}")4.5 推荐结果的可视化与调试面板
最后这个算是加分项。我给系统加了一个“推荐调试面板”,只有管理员可见。在这个面板上可以输入任意用户ID,直接查看给他推荐的商品列表,以及背后对应的相似商品、相似度和评分值。
这个面板在后端加一个接口把算法中间过程透出,前端展示的时候把相似度排名也展示出来。不要小看这个功能,运营同学或答辩评委看到推荐结果“说得清原因”的时候,对系统可信度的认可会高很多。我当时的经验是,这个面板能帮你快速定位推荐不准的案例,排查到底是数据问题还是算法参数问题。
5. 常见问题与排查技巧实录
5.1 推荐接口响应慢,页面一直转圈
这是最常被问的问题。如果你的推荐接口每次都要实时算矩阵,数据量一大自然就慢。排查分三步走:
先看耗时是不是花在算法计算上。可以在视图函数里打印一下时间,如果算法本身就要2秒多,那就该上缓存了。如果Redis里有值但接口还是慢,就检查序列化器和数据库查询,尤其是用ORM时频繁查询外键,很容易造成N+1查询问题,商品列表返回前要加select_related和prefetch_related。
根据我的实际经验,加了缓存之后,推荐接口的性能问题基本能解决80%,剩下的优化方向是把相似度矩阵预计算好存成文件,定时更新而不是每次在线算。
5.2 数据太稀疏,推荐结果千篇一律
用户少、行为少是很多项目的痛点。矩阵里大量为0的格子会让余弦相似度计算失真,推荐的每一路都倾向于热门商品,最终效果等于“所有用户拿到同一个热门零食列表”。
这个问题的解决思路有三个层次:第一,拓展行为数据来源,把浏览和收藏也算进矩阵,不要只看下单;第二,用“商品标签匹配”做冷启动兜底,行为数据确实太少时,先用内容匹配顶上去;第三,在算法层加一个“随机扰动”,以5%的概率在TopN结果里混入非热门商品,保证长尾商品的曝光。这个方法听起来很粗暴,但它能有效打破“越热越推、越推越热”的马太效应。
5.3 前端拿不到推荐列表,跨域和Token双双击穿
推荐接口默认是需要Token鉴权的,如果你的前端请求没带Token,Django会直接拒绝。我排查的时候发现很多人把代码写成了:
axios.get('/api/recommend/')这样是永远过不了鉴权的。正确做法是用拦截器统一带Token:
axios.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Token ${token}` } return config })至于跨域报错,看看浏览器控制台,如果提示CORS policy,那基本就是后端没把前端域名加进白名单,去配置CORS_ALLOWED_ORIGINS就行。
5.4 缓存失效不及时,用户反馈“我都买过了还给我推”
这个问题的本质是Redis缓存时间太长。我的建议策略是:行为数据变更(加购、下单、收藏)时立即清掉对应用户的缓存,而不是等缓存自然过期。实现起来其实就一行代码,在写UserBehavior时顺手执行cache.delete。
还有一个细节,管理员在后台修改或下架商品时也会导致推荐结果里出现失效商品。我当时的处理是,在推荐接口返回前做个商品状态过滤,把未上架和库存为零的商品直接过滤掉。这个动作不能省,否则用户点击推荐商品进详情页时发现已下架,整个推荐体验就崩了。
5.5 算法评估:怎么判断推荐效果真的变好了
很多项目做完推荐算法,但完全不知道怎么评估效果。最简单的方案是留一法评测:把用户行为数据按时间分成训练集和测试集,用前80%的行为生成推荐,然后看后20%的商品里有多少能被命中。
更实用的是算三个指标:
| 指标 | 计算方法 | 意义 |
|---|---|---|
| 命中率 | 测试集商品出现在推荐列表占比 | 推荐列表是否覆盖用户真实需求 |
| 转化率 | 推荐位点击后下单次数 / 推荐位曝光次数 | 推荐是否真正带动销售 |
| 多样性 | 推荐列表中不同分类的占比 | 配方是否过度集中在单一品类 |
命中率在离线评测时用,转化率在线上一跑就能看到。我当时用这两个指标反复调算法权重,最后得出的结论是:下单行为的权重应该明显高于浏览,一味堆浏览数据只会稀释推荐精度。
6. 从项目到生产的一点点进阶心得
这个推荐商城做完并不是终点。如果你之后想往生产级的方向走,有几个点可以先留个心眼。
第一,推荐结果不能只有协同过滤一种声音。内容推荐(基于零食口味标签、营养成分)、热销榜、新品榜都要同时存在,用户需要“稳定入口”也需要“个性推荐”,两者互补才能构建完整的商城流量分发体系。
第二,Django的ORM在数据量到达百万级时会遇到性能瓶颈,届时需要把行为数据迁移到更高效的存储中(如专门的列表型数据库或OLAP引擎),算法层再通过独立的数据管道读取,而不是每次都打主业务库。这个演进路径在早期把算法逻辑和业务逻辑分开设计就能减少后期重构成本。
第三,用户隐私和数据合规要提前考虑。推荐系统依赖用户行为数据,但这部分数据的上报、存储和使用要在产品和工程层面同时做好规范,避免违规收集非必要信息。
赶项目的时候你可能觉得“能跑就行”,但一旦站在“长期迭代”的角度看,每个结构性的小决策都在影响后边的路有多顺。至少在我经手的项目里,前期多花半小时理清楚数据流和接口边界,后期节省的时间远远不止半小时。