资讯动态

基于协同过滤的外卖点餐系统:Android推荐算法与工程实现

发布时间:2026/10/15 5:06:13 来源:尧图企业网站定制
先聊个很多人会问的问题你打开外卖APP首页推给你的商家凭什么是那几家而不是隔壁评分更高的店这里面最核心的逻辑就是“个性化推荐”。我个人做了两个月的“安卓Android app基于个性化推荐的外卖点餐系统”这个项目从需求分析、后端搭建到算法选型踩了不少坑也积累了一些很实用的经验。今天这篇就把这套系统的完整设计和实现思路捋一遍重点讲清楚推荐算法怎么选、数据怎么建模、Android端怎么做适配以及那些课堂和教程里基本不会告诉你的细节。这篇文章适合谁看你是做毕设、课设的同学或者刚接触移动端推荐算法想找个完整案例练手的开发者都应该能从里面拿到一套可以直接落地、也能讲清楚原理的方案。内容不要求你有很深的理论基础但我会尽量把“为什么这么做”讲明白。1. 整体设计与思路拆解个性化推荐外卖点餐系统的核心逻辑1.1 这个系统到底要解决什么问题先拆需求。一个外卖点餐系统站在用户角度核心动作就四个搜、看、点、付。搜是主动找看是逛点是下单付是交易闭环。但传统外卖平台现在都面临一个很实际的问题——用户面对几百上千个店主推不知道从哪下手商家面对庞大的用户池也没法把最合适的菜品推给最可能下单的人。个性化推荐要解决的就是这个信息过载问题让用户在最短时间内看到最想吃的。从技术角度拆这套系统又分成两大块底层的数据支撑系统和外层的业务交互系统。底层要干的事是把用户的行为数据浏览、点单、收藏、评价、搜索攒起来清洗好算出用户的偏好画像和菜品的隐语义特征。外层要干的事是把这些计算结果变成用户看得见摸得着的界面——首页推荐流、商家列表、菜品详情、购物车、下单结算。我在做设计时最一开始纠结的点是“推荐系统到底要放在服务端还是客户端”。这个选择的后果很严重会直接决定后续所有代码怎么写。如果把推荐算法写在Android端内置看着很简单很独立但数据源会很受限——只能靠本地记录的分析用户换个手机或者清掉缓存画像就没了。所以最终我选择把推荐引擎放在服务端Android端作为展示和交互层通过RESTful接口拿结果。这个方案的优点是画像可以持续积累、数据可以集中式分析、算法迭代不用重新发版。1.2 技术方案选型为什么用这套组合技术栈我选得很务实服务端Java Spring Boot搭建RESTful接口负责业务处理、数据库读写、推荐结果计算。数据库MySQL存核心业务数据Redis做实时热数据缓存和中间结果存储。推荐策略初期用基于物品的协同过滤ItemCF作为主干再配合基于用户的协同过滤UserCF做冷启动用户兜底标签内容推荐做补充。Android端原生Java开发MVP模式网络层用OkHttp Retrofit图片加载用Glide。这套组合打法很稳原因有三点。第一Spring Boot生态成熟资料多出问题好查。第二ItemCF在电商、外卖场景下的效果比UserCF更稳定计算开销也相对可控。第三Android原生方案在复杂交互和推荐流展示上更灵活比套壳H5体验好得多。说实话一开始我也纠结要不要直接上个深度学习模型比如用embedding做双塔召回再用精排模型打分。后来我把这个问题想透了——外卖场景的候选集规模还不至于大到非深度学习不可而且冷启动、数据稀疏、实时性这些工程问题简单模型如果调校得当反而更容易出效果。你没看错推荐系统真正难的从来不是模型多高级而是数据能不能支撑起这个模型的“performance expectation”。2. 核心细节解析推荐引擎与数据库设计的关键点2.1 数据建模推荐系统的地基就是数据表先建表这是很多新手上来就栽跟头的地方。推荐系统没有像样的数据后面的一切都是空谈。我建的核心表有这些用户表user存基础信息。商家表business存店铺信息。菜品表dish存菜品信息注意要有food_type字段做标签。用户行为表user_behavior记录浏览、点击、下单、收藏、评价五类行为。订单表orders和订单明细表order_detail。重点说一下用户行为表这张表是整个推荐系统的“燃料”。我当时的设计字段如下user_behavior 表结构 ( behavior_id bigint primary key auto_increment, user_id bigint not null, dish_id bigint not null, behavior_type tinyint not null comment 1浏览 2点击 3下单 4收藏 5评价, behavior_score float default 0 comment 行为得分, behavior_time datetime not null, location varchar(64) comment 用户位置, device_id varchar(128) comment 设备标识 )这里最大的设计坑在于行为权重打分。很多教程会随手给你一个固定权重浏览1分、收藏2分、下单3分、评价4分然后就直接用。你真按这个上线推荐效果会非常飘。我的做法是把行为权重设计成动态调整的区间值根据实际业务反馈去优化。比如用户下单之后超过20分钟没有取消这个行为才算有实际价值给5分光浏览不点击甚至十几秒就滑走只能给0.5分。用户停留时间也是关键信号——在菜品详情页停留超过30秒说明真的产生了兴趣额外加2分。再补充一点行为数据要做时间衰减这个很关键。用户上周爱吃黄焖鸡不代表现在还想吃。我设计了一个衰减因子score base_score * pow(0.95, days_diff)也就是距离当前时间越久旧行为的影响力越低7天前的一次下单权重只剩0.7左右30天前的基本可以忽略。2.2 用什么算法ItemCF基于物品协同过滤为主、UserCF为辅推荐系统经典算法一般分两种基于内容的过滤和协同过滤。协同过滤又分基于用户UserCF和基于物品ItemCF我推荐用ItemCF作为核心。先说一下两者的区别。UserCF的原理是找“和你口味相似的其他用户”然后把那些用户点过的、而你没点过的菜推荐给你。ItemCF的原理则是找“和你之前点过的菜相似的菜品”然后推荐这些相似项。举个例子你点过番茄炒蛋系统发现点番茄炒蛋的人也经常点紫菜蛋花汤那就把紫菜蛋花汤推给你。外卖场景为什么用ItemCF更合适核心原因是用户兴趣的“瞬时性”。你今天想吃辣的明天可能就想吃清淡的基于用户相似度去推荐很容易推出一堆你现在根本不想吃的东西。而基于物品相似度只要锚定当前选中的几个菜品推荐的延伸关系会更稳定可靠。用户冷启动阶段怎么办呢用户第一次进来没有任何行为数据我会先用UserCF里面的“热门兜底”策略——也就是把全平台最近3天销量TOP50的菜品做成一个热门榜按“热度 下单数 * 0.6 收藏数 * 0.3 浏览数 * 0.1”排序。另外再配合一个“基于用户地理位置”的强制过滤只展示当前位置3公里内有配送能力的商家否则推荐出来用户也下不了单体验很差。菜品相似度的计算我用了余弦相似度这个公式很经典cos_sim(A, B) A·B / (|A| * |B|)把每个菜品表示成一个“用户评分向量”向量的维度是用户值是该用户对菜品的“综合行为得分”。然后计算任意两个菜品向量之间的余弦夹角。举个具体例子菜品A被用户1、用户2、用户3分别打过分4.0、3.5、0.0菜品B被用户1、用户2、用户4分别打过分4.5、3.0、2.0那么cos_sim(A, B) (4.0*4.5 3.5*3.0 0*2.0) / (sqrt(4.0^2 3.5^2 0^2) * sqrt(4.5^2 3.0^2 2.0^2))算下来两个菜品的相似度大概在0.4到0.6之间说明不算特别像但有一定的关联度。有了这个相似度矩阵后对用户曾评分过的每个菜品取最相似的TopN菜品再按照“用户评分 * 相似度”的加权汇总就可以得到候选菜品的推荐分数。实际调试中要注意一个点先做一次“同类目过滤”。比如用户点的是米饭类主食相似的米饭类商品直接跳过这点很违反直觉但你有真实体验就明白了——用户点了一个菜不代表他还想点一模一样的另一个菜。相似推荐应该往配菜、饮料、小食方向去延伸这才是“加购逻辑”。2.3 服务端架构Redis为什么是必需品推荐系统最怕的是响应慢。外卖APP对实时性要求极高用户从点开首页到看到推荐流超过两秒就需要优化了。服务端我做了两级缓存第一级Redis缓存推荐结果。用户请求推荐接口时key用user_id:recommend:dishvalue是序列化之后的JSON字符串过期时间设为30分钟。第二级数据库数据变更时实时更新。比如用户下单成功后立刻删除该用户的推荐缓存保证下次重新生成。另外高频访问的菜品信息、商家信息、分类信息都会单独缓存到Redis里减轻MySQL的压力。我们模拟压测过加了Redis缓存之后推荐列表接口的平均响应时间从800ms左右降到了200ms以内这个变化用户感知非常明显。2.4 Android端接口通信怎么设计才不踩坑Android端不是核心算法所在但它是用户最终体验的决定者。接口返回推荐列表后客户端要做的事情主要是渲染推荐流、和推荐位交互、处理下拉刷新和上拉加载。服务端推荐接口我按下面这种格式返回{ code: 0, msg: success, data: { recommendList: [ { dishId: 10086, dishName: 香辣鸡腿堡, businessName: 某快餐店, dishImg: https://xxx.com/xx.jpg, price: 19.9, recommendReason: 因为你常点炸鸡类食品, rank: 1 } ], hasNext: true, cursor: 1500 } }为什么推荐接口里要带一个recommendReason字段这是我实测之后强烈建议保留的。用户看到“猜你喜欢”这种冷冰冰的字眼点击率并不高但改成“因为你常点炸鸡类食品”这种解释型文案后点击率提升了大约30%。个性化推荐的“透明度”是很有效的交互策略。Android端这边的技术点网络请求用Retrofit封装推荐接口走单独的ApiService不跟业务接口混在一起。推荐流界面用RecyclerView实现多类型item布局顶部banner位、中部推荐菜品列表、底部热门商家。图片加载一定要用Glide因为推荐菜品图片是动态加载的随手写个new Thread加载图片很不可靠。3. 实操过程与核心环节实现一步一步把推荐系统跑起来3.1 搭建服务端基础工程用Spring Boot三步起项目spring init --dependenciesweb,mysql,redis,mybatis train-cloud-auth我习惯把推荐相关的代码独立成一个模块recommend-core和recommend-web。recommend-core里放算法逻辑recommend-web提供接口给Android端。千万别把推荐算法跟商家管理、订单管理混在同一个模块里否则后面迭代模型时牵一发动全身改都改不动。然后配置数据源spring: datasource: url: jdbc:mysql://localhost:3306/food_rec username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379注意数据库连接池我用的是HikariCPSpring Boot默认就带性能很强不需要额外引入。3.2 实现ItemsSimilarity核心算法类这个类是整个推荐系统的发动机。核心思路已经在上面讲过了细节上我补充几个处理点。第一矩阵太稀疏怎么办。外卖平台刚起步时用户量小、行为数据稀薄直接计算出来的相似度矩阵几乎全是0。我的处理办法是引入“默认回退逻辑”——当相似度矩阵中找不到足够相似项的时候自动用“同品类、同价位、高评分”组合来兜底。也就是从候选集中挑选和当前菜品同品类、价格差在8元以内、评分排前5的菜品补位。第二相似度计算要不要实时做。这个问题很多教程会忽略我一开始也是打败仗才明白的——如果每一次推荐请求都去实时算相似度MySQL根本扛不住。后来我改成“离线计算 周期性更新”策略每天凌晨2点运行一次批量计算把结果写入推荐评分表或者Redis中白天对外只读缓存结果。写代码时最核心的逻辑大概是public ListRecommendItem recommendForUser(Long userId, int topN) { // 1. 取用户最近30天有效行为 ListUserBehavior behaviors behaviorMapper.selectRecentBehavior(userId, 30); if (behaviors.isEmpty()) { return hotDishService.getHotDishes(topN); } // 2. 汇总候选集 MapLong, Double candidateScoreMap new HashMap(); for (UserBehavior b : behaviors) { ListSimilarDish simList similarityMapper.findTopK(b.getDishId(), 10); for (SimilarDish s : simList) { candidateScoreMap.merge(s.getSimilarDishId(), b.getScore() * s.getSimilarity(), Double::sum); } } // 3. 过滤已下单/已收藏的菜品 candidateScoreMap.keySet().removeIf(dishId - behaviorMapper.isOrdered(userId, dishId)); // 4. 过滤超出配送范围的商家 // 这里需要联合商家表和用户GPS信息省略细节 // 5. 排序取TopN return candidateScoreMap.entrySet().stream() .sorted(Map.Entry.Long, DoublecomparingByValue().reversed()) .limit(topN) .map(entry - new RecommendItem(entry.getKey(), entry.getValue())) .collect(Collectors.toList()); }第二步里面的similarityMapper.findTopK()就是查离线算好的相似度表这一步的速度很快也是性能优化最关键之处。3.3 Android端推荐列表实现与优化Android端我按照MVP模式拆分成三层。Model层负责从Retrofit拿数据View层负责更新UIPresenter层做业务逻辑组装。推荐流页面主要分三个部分第一个部分是顶部的横向分类标签。外层是“为你推荐”“热门商家”“附近好店”“新店特惠”内层是数据驱动的动态标签。分类标签的排序也是推荐系统的一部分这个细节很容易被忽略——我一直采用服务端下发的排序结果而不是写死在前端。第二部分是推荐菜品卡片。每个卡片展示菜品图、名称、简介、价格、“猜你喜欢”理由标签。这里的图片地址我统一走CDN否则高并发下列表会卡顿。第三部分是上拉加载更多。推荐流的翻页我用的是“游标”cursor模式而不是传统的页码模式核心原因是推荐结果列表可能会因为用户行为更新而动态变化页码模式会出现重复或漏推。游标模式则是返回当前列表最后一个item的rank值服务端从该rank之后继续取数据。RecyclerView的布局管理LinearLayoutManager manager new LinearLayoutManager(this); manager.setStackFromEnd(false); binding.recyclerView.setLayoutManager(manager); FeedAdapter adapter new FeedAdapter(this); binding.recyclerView.setAdapter(adapter);注意千万不要在onBindViewHolder里面去做数据库操作或者网络请求RecyclerView的复用机制会让这些操作成倍放大直接导致卡顿。图片加载我也踩过坑一开始图省事用setImageResource结果列表一长就崩。后来统一改成Glide并且在Glide里加了磁盘缓存策略、缩略图加载、圆角变换等参数配置Glide.with(context) .load(url) .placeholder(R.drawable.ic_placeholder) .error(R.drawable.ic_error) .thumbnail(0.1f) .apply(RequestOptions.bitmapTransform(new RoundedCornersTransformation(8, 0))) .into(holder.ivDish);3.4 购物车与订单模块的衔接推荐流的目的不能只停留在“展示”最终目标是促成转化。所以购物车模块必须跟推荐流打通。我加了一个很关键的设计从推荐列表点“加购”按钮时会把这个菜品的来源追踪带上比如sourcerecommend。服务端收到加购请求后会把这个动作写回行为表behavior_type2点击加购分数给3.5分。这样推荐系统就能不断学习哪些推荐位真正能促成转化。购物车使用本地数据库SQLite存储同时在下单时上传到服务端。我用的Room数据库定义了一个CartItem实体类字段有dishId、dishName、price、quantity、selected。订单状态流转模块和普通外卖APP的逻辑类似重点说一下推送模块。用户下单之后商户接单、骑手取餐、配送中、已送达这些状态我用的是WebSocket实时推送。因为这个项目是Android端原生App服务端用Spring的WebSocket模块客户端用OkHttp WebSocket连接体验比定时轮询好很多。4. 常见问题与排查技巧把推荐系统真正调“通”的经验4.1 推荐结果太热门看不出个性化怎么办这是最让人头痛的问题。写着协同过滤跑出来的结果全是全站热门菜用户感觉不到“个性化”评分和留存都拉不上去。排查后发现两个原因一是行为矩阵太稀疏大多数用户之间没有共同点过的菜品相似度计算退化成了热门推荐兜底二是我的兜底逻辑写得太“宽”——相似度计算结果低于某个阈值时就直接用热门榜代替导致热门菜霸屏。解决办法增加召回路径不只是“相似菜品”还要加入“同商家推荐”“同类目推荐”作为补充。对热门菜品加“个性化衰减系数”也就是热度乘以一个惩罚因子例如热度排名前10的菜品惩罚系数是0.5前50的是0.8这样用户画像中已有的个性化菜品能排到前面。给每个用户增加多组候选集最后合并排序时不是简单按分数排而是用“多样性的重排策略”——相邻两个推荐位尽量不同类目。4.2 用户首次打开App推荐列表为空这是典型的冷启动问题。用户第一次进来没有行为数据推荐接口直接返回空列表。我最终的解决方案是“三级冷启动策略”第一级定位权限拿到后推荐用户当前GPS坐标1.5公里内评分最高的8家店加它们的招牌菜。第二级没有授权定位时推荐全平台销量最高的10款菜品。第三级用户手动选择偏好标签后按“标签匹配度”推荐匹配度计算公式是match_score 0.5 * 标签命中数 0.3 * 销量排名分 0.2 * 评分排名分。这套策略上线之后新用户首页推荐流的空白率从15%降到了0.3%左右。4.3 推荐列表刷新后大量重复前文提到的游标分页就是为解决这个问题的。但如果你用页码分页很容易出现第一页的数据和第二页的数据有30%以上是重叠的。原因还是推荐结果整体偏移——用户的实时行为反馈会改变排序导致下一页内容和上一页高度相似。游标模式的实现也有一点要注意游标本身要带上“排序向量指纹”比如取当前列表最后20个item的dishId组成的Hash值作为筛选条件。这样就算排序变化只要列表末尾的特征没变就不会推重。这个方法是我实测下来最有效的做法。4.4 推荐接口响应慢如何快速排查如果接口响应时间超过1秒典型的排查链路有三条先看SQLEXPLAIN SELECT ... FROM user_behavior WHERE user_id xx AND behavior_time xxx确认有没有走索引。我给user_id和behavior_time建了联合索引查询性能能提升一个数量级。再看Redis确认命中率如果缓存命中率低于50%就要看看是不是过期时间设置太短。最后看推荐计算逻辑候选集会不会太大比如相似菜品查了前50而不是前20导致计算量爆炸。我实际用到的排查工具是开源的Arthas直接在服务器上观察方法的调用耗时能精准定位到是网络IO慢还是算法逻辑慢。4.5 用户反馈“推荐的东西乱七八糟”怎么办在系统里加一个“不感兴趣”的反馈入口长按推荐卡片可以点击“不感兴趣”。服务端收到反馈之后给这个菜品打一个负反馈标记未来一个月内不再推荐。同时把这个反馈也记录进行为表行为类型是-1分数是-5分。之后还要加入“推荐解释”功能告诉用户“因为最近点了XX才推荐这个”减少莫名感。这一块看起来简单却是算法外最影响体验的交互设计几乎每个主流平台都会有但多数自建项目会忽略掉。5. 项目进一步扩展的思路这个项目做完基本功能之后还能顺着几个方向继续延伸都是非常加分的方向。第一个方向是把推荐算法换成“实时推荐”。现在用的是离线计算 定时更新间隔太长。改成基于用户最近一次行为实时触发推荐更新——用户刚下单五分钟内刷新推荐流系统就要立刻给他推“这个商家的其他菜品”或者其他同类商家的相似款。实时度接上之后转化率提升很可观。第二个方向是引入“LBS位置智能”。目前我只做了最简单的按位置过滤没做位置相似度的建模。实际上外卖的推荐有一个明显特点不同商圈的用户偏好差别巨大。可以把商铺所属商圈作为特征加入推荐模型推荐的时候首先匹配用户当前所在商圈的“主题消费场景”比如写字楼周边推荐轻食简餐住宅区周边推荐家常菜。第三个方向是给推荐系统加一个简单的A/B测试平台。推荐位展示A方案还是B方案用两个接口实现后端用流量切分来控制。这是比较接近工业级实践的事情搞清楚了面试时能够讲的地方就特别多了。最后再分享一个我在做这个项目过程中的体会推荐系统不是纯算法项目算法只占两成数据工程、工程落地和交互设计占剩下的八成。你在应用市场下的一个外卖APP首页推荐流展示得自然流畅背后其实是一整条很长的链路在支撑。做项目的时候踏踏实实把每一个模块处理到位比把模型名写得天花乱坠要更有实际意义。这个项目我前两天重新翻出来改了一版发现当时记录的很多决策思路到今天还是适用推荐你按这个思路去完成你自己的版本。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑