首页 / 资讯中心 / 文章详情

从0到1搭建同城社区生鲜电商:Python+小程序+AI抢单实战

从0到1搭建同城社区生鲜电商:Python+小程序+AI抢单实战 ★ FEATURED ARTICLE
这个项目上线到现在半年多了我是在一次社区团购的失败案例里找到灵感的。当时某小区的业委会在群里抱怨楼下的菜店又贵又不新鲜而几十公里外的蔬菜基地因为没人收购一批批绿叶菜直接烂在地里。那一刻我突然意识到同城社区 绿色蔬菜 抢单这三件事组合在一起可能真的是一个值得认真做的方向。于是我用 Python 做后端、把 AI 塞进推荐和预测里、前端落在微信小程序上做了这套青蔬到家平台的原型一直迭代到正式运营。这篇文章把我从 0 到 1 的完整过程、关键代码、翻车记录全部摊开希望能给想做同城生鲜或者社区电商的朋友一些真正能用的参考。先说结论这套系统的价值不在于又一个卖菜小程序而在于它把非标品生鲜的时效损耗和社区高频复购这两个矛盾点用一个抢单机制和一套轻量 AI 体系给协调起来了。对开发者来说它同时涉及小程序双端、Redis 并发控制、Python 算法服务、微信支付和订阅消息麻雀虽小但五脏俱全。对运营者来说它解决了今天该摘多少菜、谁来送、送到哪、怎么推荐四个每天都会重复的问题。1. 这个项目解决的痛点社区买菜难与菜农损耗高之间差一个调度层1.1 生鲜电商和普通电商最根本的区别损耗做普通商品电商库存放三个月可能都问题不大但生鲜不一样。绿叶菜从地里摘下来那一刻就在损耗水分蒸发、黄叶、磕碰每过一小时都在贬值。我当时去某蔬菜基地调研负责人告诉我他们一亩地的小白菜如果当天卖不掉次日就只能按半价甚至三折处理这还是在有冷库的前提下。社区这边又是另一番景象。很多老旧小区周边菜场配套不足年轻人下班晚去菜场的时间成本太高老年人倒是愿意跑但拎着大袋小袋爬楼梯也是负担。所以下班前下单、到家正好送到的预售制非常适合社区场景。这就引出了第一个设计决策我们不坐库存走订单制采购。用户当天 18 点前下单基地晚间统一采摘次日早晨配送到社区自提点或骑手直送。这套路其实和拼团很接近但同城属性让它能承诺更短的送达时间也就能承接今天就要吃的即时需求。1.2 为什么一定要有抢单这个环节项目最初我并没有设计抢单想着统一排单、按顺序分配就行。但在模拟推演时发现一个问题配送半径不一样骑手对路线的熟悉程度也不一样。系统派单给一个不熟悉路况的骑手配送时长可能比熟悉地形的骑手多出一倍以上。生鲜配送的时效敏感度又极高这就导致系统派单这种方式在实战中很容易积压订单。抢单机制的本质其实是把派单权从中心化调度器转移到一线执行者手里。骑手最清楚自己此刻在哪儿、能不能接、走哪条路最快。系统要做的不是强行指派而是把订单特征广播出去让合适的人在合适的时机认领。这个思路不新鲜网约车和外卖平台早就验证过但把它用在小体量社区生鲜场景里起步成本低、容错率高骑手也更愿意干。1.3 项目定位和核心用户画像我给青蔬到家画了三类用户社区家庭用户25-45 岁工作忙希望回家前菜已到自提点。骑手/团长可以是小区便利店老板、全职宝妈、退休大爷他们有闲散时间和熟人网络。基地/供应商希望稳定出货、减少损耗能提前知道明天的采购量。这三类角色对应小程序端的三套界面C 端用户下单骑手端抢单基地端上架和接单。后文我会详细拆每端的差异化设计。2. 技术架构与数据模型Python后端撑起小程序Redis是抢单的并发闸门2.1 总体架构三端一后台四个服务先看整体结构我把系统分成六块微信小程序端用户端、骑手端、商家端共用一个代码仓库用条件编译区分角色。API 网关层Python 的 FastAPI 框架负责鉴权、路由、参数校验。业务服务层订单服务、商品服务、用户服务、配送服务。AI 算法层销量预测、智能推荐、图像识别三个模型的推理服务独立部署通过内部 HTTP 接口调用。数据层MySQL 8.0 存储业务数据Redis 6.x 处理抢单热点和会话缓存ES 暂时没上搜索靠 MySQL LIKE。定时任务层APScheduler 做每日库存预测、订单汇总、骑手对账。为什么用 FastAPI 而不是 Django生鲜项目天然是 IO 密集型FastAPI 的异步支持非常好而且配合 Pydantic 做参数校验写接口效率极高。后来加了 WebSocket 推送订单状态FastAPI 原生支持也很省事。Python 在这类项目里的另一个优势是 AI 生态无缝衔接后面我讲 AI 落地时你会体会到。2.2 MySQL 核心表设计订单、商品、社区、骑手生鲜电商数据模型最需要注意的是批次的概念。蔬菜不是标准 SKU今天的小白菜和明天的小白菜品质可能差很多价格也不同。所以我没有用常规的 product 表直接存商品而是设计了商品品类 每日批次的组合-- 品类表商品是抽象品类不是具体某一批菜 CREATE TABLE product_category ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, unit VARCHAR(10) NOT NULL COMMENT 计价单位份/斤/捆, category_tag VARCHAR(20) COMMENT 叶菜/根茎/水果 ); -- 每日批次表每个品类每天一个批次对应采摘量和价格 CREATE TABLE daily_batch ( id BIGINT PRIMARY KEY AUTO_INCREMENT, category_id BIGINT NOT NULL, batch_date DATE NOT NULL, total_stock INT NOT NULL, remain_stock INT NOT NULL, price DECIMAL(10,2) NOT NULL, origin VARCHAR(100) COMMENT 产地, status TINYINT DEFAULT 1 COMMENT 1上架 0下架, UNIQUE KEY uk_batch (category_id, batch_date) ); -- 订单表 CREATE TABLE order_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id BIGINT NOT NULL, batch_id BIGINT NOT NULL, quantity INT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL COMMENT 10待支付 20已支付待抢单 21已被抢 30配送中 40已完成 50已取消, grab_runner_id BIGINT DEFAULT NULL COMMENT 接单骑手, paid_at DATETIME DEFAULT NULL, grabbed_at DATETIME DEFAULT NULL, created_at DATETIME DEFAULT NOW(), INDEX idx_status_time (status, created_at) );这套设计的核心好处是库存控制在批次层面而不是商品层面。用户买的是今天的小白菜系统知道这一批还有多少份。抢单成功后要扣减的是骑手的运力而不是商品库存所以不会出现下单了却没人送的情况。2.3 为什么要单独维护一个社区维度同城社区模式必须有社区维度因为各种运营策略都基于它每个社区配置默认自提点坐标。同一社区订单合并批次采摘。骑手抢单规则按照当前订单所属社区 骑手常驻社区距离推荐排序。我设计了community表和user_community表用户首次进入小程序时通过定位推荐最近的社区也可以手动切换。有了这个维度AI 推荐才能做隔壁老王常买什么这类社区协同过滤后面讲推荐算法时会专门展开。3. 抢单模块的完整设计从订单广播到派单完成的闭环3.1 抢单的业务规则哪些人能看到订单能抢什么订单支付完成后进入待抢单池。骑手刷新骑手端时系统拉取符合以下条件的订单订单所属社区在骑手的配送范围内默认 3 公里半径可配置。订单当前状态为已支付待抢单。骑手当前没有正在配送中的订单同一时刻只能抢一单配送完成释放。订单未被其他骑手锁定。这是第一版规则比较简单。后来加了一个优先级排序系统根据骑手历史送达效率、当前骑手位置与社区的距离、订单中菜品体积估算给每单算一个match_score分数高的排前面。但排序只影响展示顺序不限制抢单范围——这个取舍很关键限制太死就又变回派单制了骑手会感觉被操控。3.2 用 Redis Lua 脚本实现原子抢单抢单和秒杀本质上是同一个问题多个并发请求同时修改同一份资源必须保证只有一个能成功。我用 Redis 存储待抢单的订单池每个订单一个 key状态用一个短值表示抢单时执行一个 Lua 脚本完成状态检查 状态修改 写骑手信息三步操作。# 抢单接口的核心逻辑使用 Lua 保证原子性 GRAB_LUA local order_key KEYS[1] local runner_key KEYS[2] local status redis.call(HGET, order_key, status) if status waiting then redis.call(HSET, order_key, status, grabbed) redis.call(HSET, order_key, runner_id, ARGV[1]) redis.call(HSET, order_key, grabbed_at, ARGV[2]) redis.call(SADD, runner_key, ARGV[3]) return 1 else return 0 end 在 FastAPI 里这样调用app.post(/api/runner/grab) async def grab_order(payload: dict, runner: Runner Depends(get_current_runner)): order_id payload.get(order_id) redis_conn await get_redis() # order_id 对应的 key例如 order:10086 ok await redis_conn.eval(GRAB_LUA, 2, forder:{order_id}, frunner_orders:{runner.id}, runner.id, now_str(), order_id) if not ok: raise HTTPException(status_code400, detail订单已被抢走) # 异步更新 MySQL 订单状态 await update_order_grabbed(order_id, runner.id) return {code: 0, msg: 抢单成功}你可能注意到了这里 Redis 命中后还要异步回写 MySQL。必须做这一步否则 Redis 数据一旦重启丢失订单状态就错乱了。但顺序必须是Redis 先成功MySQL 后更新万一 MySQL 更新失败要有补偿任务定时扫描Redis 已抢但 MySQL 未更新的差值重新回放。3.3 防超卖和防重复抢单这是最容易翻车的地方防超卖在生鲜场景里有两层一是商品库存超卖二是骑手运力超卖。商品库存我用daily_batch.remain_stock做条件更新扣减时 SQL 加WHERE remain_stock 0PDO 框架层再补一个乐观锁版本号。运力超卖则完全依赖 Redis 抢单锁也就是上面 Lua 脚本里grabbed状态互斥。防重复抢单指骑手重复点击、同一订单被两个骑手同时抢到。Redis Lua 是单线程原子执行的天然解决这个并发问题。我压测过 500 个并发请求同一订单最终只有一个return 1其他全部返回 0成功率 100%。但这里有个隐藏坑Lua 脚本执行时间过长会阻塞 Redis 其他命令所以脚本里绝对不要做耗时操作比如网络请求、遍历大集合。我的经验是脚本只做状态判断和简单写入最多再加一个过期时间的设置。3.4 订单状态机状态流转必须靠事件驱动抢单模块的状态机看起来简单做起来容易漏。我最终定义了一套事件流转待支付 - 已支付待抢单 - 已被骑手抢单 - 配送中 - 已送达 - 已完成 v 超时未抢 - 自动转人工调度/次日优先派单超时未抢的订单最危险。每次抢单池扫描任务每 5 分钟一次都会检查那些支付后超过 15 分钟仍未被抢的订单。这些订单会被打上timeout_flag并在骑手端以高额补贴单的方式置顶展示。如果 30 分钟还是没人抢就直接派给最近的自提点团长配送同时给用户补一张优惠券作为安抚。这个兜底机制非常重要生鲜订单体验崩一次用户可能就永久流失了。3.5 超时自动关单防止僵尸单占用库存订单超时未支付必须自动关闭。我用的是订单创建时间 15 分钟判断定时任务每分钟扫一次。关闭后要回补批次库存否则就会出现明明没卖掉、库存却显示不充足的情况。这里有个细节用户取消订单的库存回补和超时关单的库存回补要区分消息类型因为超时关单会让用户觉得是自己错过了系统不会发推送而主动取消则要温柔提醒一句库存已释放欢迎下次再来。4. AI能力是怎么落地的预测、推荐、识别、调度四件事4.1 销量预测决定基地今天该摘多少菜生鲜最浪费的场景是什么种了没人买或者不够卖。所以第一个 AI 需求就是次日销量预测。我选型的是 LightGBM训练特征包括历史 30 天同批次销量。星期几周末买菜量明显高尤其叶菜类。天气特征最高气温、是否降雨、是否极端高温。社区维度每个社区自提点订单量。节假日标记法定节假日前后囤菜需求激增 20% 以上。模型输出的是每个品类每个社区的预测销量。这个数字直接对接到基地端的明日采摘计划页面同时经过一个调整系数预测值乘以 1.15 作为上限保留安全库存应对临时爆单。实际运营下来损耗率从最初没有预测时的约 18% 降到了 11% 左右效果非常明显。需要提醒的是生鲜销量预测不要追求复杂的深度学习模型。样本量不大、噪声极大下雨、社区团购节日活动、附近新开菜场都会猛烈干扰LightGBM 这种梯度提升树在小样本表格数据上就是最可靠的。深度模型我的团队也试过调参成本高、上线后反而更不稳定。4.2 智能推荐每个用户首页的菜不一样推荐模块我用的是社区协同过滤 规则兜底的组合方案。思路很朴素同一社区的用户对菜品的偏好往往相似因为受同一群邻居评价的影响。用用户的历史购买记录构建一个用户-品类打分矩阵用协同过滤找相似用户再推荐他们买过而你没买过的品类。新用户没有行为数据就用社区维度当前社区最近 3 天最热销的品类 当季时令菜组合推荐。协同过滤我直接用 Python 的surprise库试跑了一个 SVD 模型但线上实际跑的是召回触发的规则版。为什么SVD 在几百个用户的数据上性能极不稳定而且要把「新鲜」「本地」这类标签做大特征很难。反而是同小区的人爱买什么今天是雨天推荐菌菇汤套餐这种组合效果立竿见影。所以我的建议是小规模项目先把推荐做成数据看板 人工规则调整等用户量过万后再上真模型。AI 不是用来炫技的是要解决实际问题的。4.3 图像识别给蔬菜的品质做分级基地端每天要处理大量到货蔬菜每个批次都要人工检查有多少是残次品。为了提高效率和定级一致性我训练了一个简单的图像分类模型用来在装车前对蔬菜做新鲜度粗分级。数据集是我们在基地拍的 800 张照片包括小白菜、生菜、菠菜、韭菜等常见叶菜标注等级为优/良/差三档。模型用的是经典的 ResNet18 迁移学习PyTorch 训练完导出 ONNX 格式再由 FastAPI 拉起 ONNX Runtime 做推理。部署时做了个硬限制推理超时超过 1 秒就直接返回人工检测不能阻塞上架流程。实测效果优/良两档的准确率在 87% 左右差的召回能达到 91%。肯定不能完全替代人但在批量收货时能把明显不新鲜的筛选出来把人工精力省下来放到抽检上。4.4 AI 与抢单的结合给骑手算推荐排序这可能是标题里AI抢单最直接的结合点。抢单列表的排序分数match_score由 4 个因素加权骑手当前位置到社区自提点的距离权重 0.4。骑手对该社区的历史送达次数权重 0.3送过说明熟路。订单金额权重 0.2高价值订单排前面但也考验运力。订单包含的菜品重量/体积估算权重 0.1太重的小单和轻快小单骑手偏好差异很大。def cal_match_score(runner_pos, community_pos, history_count, order_amount, order_weight): dist_km haversine(runner_pos, community_pos) dist_score max(0, 1 - dist_km / 5) # 5 公里外分数为 0 history_score min(history_count / 20, 1.0) amount_score min(order_amount / 80, 1.0) weight_score max(0, 1 - max(order_weight - 3, 0) / 5) return 0.4 * dist_score 0.3 * history_score 0.2 * amount_score 0.1 * weight_score这个排序不需要 GPU不需要训练但它的效果立竿见影。上线头两周最明显的变化是同一社区订单的平均配送时长从 45 分钟降到了 31 分钟。因为熟路的骑手总能优先看到更匹配的订单抢单的动力也更强了。5. 三个端的小程序开发要点用户端、骑手端、商家端的差异化处理5.1 为什么不直接做一套 App很多朋友听说我做生鲜电商项目第一反应是为什么不顺手做个 App。原因很简单获客成本。社区买菜是高频低客单价的生意用户不愿意为此额外下载一个 App。微信小程序即用即走还能通过订阅消息做履约提醒非常契合社区场景。另一个现实因素是小程序的代码发布要过微信审核虽然流程繁琐但也等于多了一道质量关卡。5.2 用户端核心是下单前尽一切可能降低决策门槛用户端的首页我做成了一个今日菜篮子而不是传统电商的分类瀑布流。这是因为买菜决策往往很偶然——用户不知道今天吃什么需要有人给他灵感。首页从上到下依次是顶部搜索框 定位信息当前社区。今日推荐卡片区来自推荐算法包含一个组合套餐比如今日汤锅组合小白菜豆腐菌菇。品类滚动区时令优先。购物车浮层直接展示当前社区自提点剩余截单时间。下单流程控制在三步内浏览 - 加入购物车 - 选自提点/配送方式 - 支付。这里必须做的两个细节是获取用户收货地址时用微信的chooseAddress接口不要自己开发地址管理支付用微信支付的分账能力让基地、骑手、平台三方资金自动分账。5.3 骑手端核心是快而且要抢得到骑手端界面更像一个任务大厅。新订单到达时振动 声音提醒同时推送一条订阅消息到微信。订单卡片上直接显示社区名称和地图距离。预估配送时长。订单金额和预计配送费。菜品清单方便骑手预判重量。一个醒目的抢按钮点击后 2 秒内必须给反馈防止重复点击。骑手的核心诉求其实就是多跑单多赚钱系统要让他觉得公平。所以每个抢到的订单都会记录抢单时间并定期公示同城骑手跑单排行。我还给骑手端加了防误触机制抢单按钮在 500ms 内连续点击只触发一次请求按钮进入 5 秒冷却。5.4 商家端一键上架与明日计划基地商家端的使用者普遍年纪偏大UI 必须极简。主要功能就三个上架当季蔬菜选择品类、填数量、设价格。查看明日采收建议这是销量预测模块生成的数字。确认订单汇总按社区批量打标签方便分拣装袋。这端的开发难点不在技术上而在交互设计上。我和基地负责人聊了几轮之后把所有操作都简化成填表确认两段式抛弃了复杂的商详编辑流程。录入一棵菜只需要选品类、重量、价格三个字段10 秒搞定。5.5 小程序端的共性问题定位与推送三个端都用到了定位和推送这里有几个经验值定位小程序wx.getLocation需要用户主动授权首次拒绝后再弹授权会非常难。我的策略是首次进入用一个半屏弹窗解释需要定位才能为你匹配最近的社区并提供手动选择社区的备选路径。推送订单状态变化用订阅消息一次性订阅用户支付成功后引导勾选发货通知骑手端抢单成功用wx.vibrateLong() 后台声音提醒。本地缓存商品列表、社区配置这些低频变化数据在小程序 storage 里缓存 5 分钟能显著降低首屏白屏时间。实测从冷启动到首屏可交互1.8 秒压到了 0.9 秒。6. 测试与上线后的踩坑实录超卖、雨天失灵和审核驳回6.1 并发压测你以为的抢单 vs 实际的抢单上线前我做了一轮模拟抢单压测500 个骑手账号同时刷新任务大厅然后随机抢 100 个订单。结果第一轮就发现了问题——任务大厅的接口在并发 500 时响应时间从 120ms 飙到 2.8 秒骑手端直接卡死。定位发现是列表接口里做了 foreach 循环查社区信息N1 查询问题。修复方案是先把订单关联的社区信息一次性 JOIN 查出来再在内存里组装。优化后同压测场景下响应时间稳定在 400ms 内。这个教训很简单抢单系统的热点不仅是抢单那一刻抢单前的列表查询同样是热点。压测时我还特意关注了极端情况1000 个并发抢同一订单最终只有 1 个成功其余全部返回手慢了。Redis Lua 脚本的原子性保证了这一点没有出现两个骑手同时抢到同一订单的脏数据。6.2 超卖的真正原因不是 Redis 没锁住而是回补逻辑放错了位置第一次上线运营的第三天出现了年度最严重的翻车事故一个批次的菠菜卖出去了 130 份但库存只有 100 份。排查下来发现不是抢单锁的问题而是用户订单超时自动取消时库存回补操作被重复执行了两次。具体链路是定时任务扫描到超时订单 - 执行取消并回补库存 - 同时微信支付回调也到达 - 支付成功后再次回补。一次回补、一次二次回补等于凭空多了 30 份。修复方案是给订单取消操作加一个幂等锁每次取消前生成一个cancel_token通过 RedisSETNX保证同一订单只有一次取消能执行成功。生鲜电商的库存问题往往不是高并发抢出来的而是回调幂等没做好。6.3 AI 预测在雨天的集体失灵销量预测模型上线后前两周很准MAPE 在 12% 左右但第三周下了一场大雨预测值整体低估了 60%。原因排查后明白过来模型训练时用的天气特征字段是当日是否降雨但训练集里 60 天只有 4 个雨天样本量严重不足模型学不到下雨会导致线上订单暴增这种非线性关系。修复方案分三层增加训练集把历史数据拉到 180 天并做天气特征交叉特征比如降雨高温组合。规则兜底气象台预报降雨概率超过 70% 时自动触发rainy_boost 1.5的预测系数。实时反馈当天实际销量和预测值偏差超过 30% 时触发告警运营人工介入调整。这个坑让我很受教在数据量不足的情况下任何 AI 模型都扛不住小概率事件的冲击规则的兜底永远不能省。6.4 小程序审核被驳回的两件事微信小程序审核是每个上架者必经的一道坎。我被驳回过三次两个典型问题类目问题销售蔬菜属于食品-生鲜类目需要提供《食品经营许可证》。个人主体无法入驻必须用企业主体。如果不小心选了电商平台类目会被要求提供增值电信业务许可证门槛更高。内容安全问题商家端上架商品时如果商品描述里出现最好第一这类绝对化用语也会被拦截。这套合规审查不止在首页购买流程的每个页面都要扫一遍。建议大家在项目启动前就先把企业主体和资质办下来不然后端代码写得再好也只能干瞪眼。6.5 上线后的真实数据损耗率、履约率、复购率项目在某社区试点运营 90 天核心数据如下指标数值注册用户数326 人试点社区约 800 户日均订单量42 单客单价37.6 元次日复购率34%订单超时未抢率2.8%平均配送时长33 分钟货源损耗率约 11%含采摘后淘汰用户投诉率1.7%说实话日均 42 单不算高但试点阶段的重点是验证跑通流程和积累数据。复购率 34% 意味着每天有约 14 个用户在持续使用这个数字说明社区生鲜的黏性确实存在。随着单量增长AI 预测的准确性也在逐步提升——这就是数据飞轮的价值。7. 项目复盘与后续迭代一些真实体会和下一步想法如果让我重新做一遍有两件事我会在第一版就做对。第一是把骑手抢单改成骑手抢单 社区团长预分配双通道。抢单模式灵活但在单量少、骑手不足的社区可能没人抢单这时社区团长便利店老板反而是最稳定的配送节点。第二是商品展示要加非标预告同一品类不同批次涨跌价不要太频繁否则用户会困惑为什么昨天菠菜 3 块今天变 4 块。后续我想把精力放在三件事上一是用真实购买数据训练一个更细粒度的用户偏好模型做千人千面的今日菜谱二是给抢单模块引入更复杂的路径规划让骑手一次能顺路送 2-3 个社区三是把现有的图像分级能力扩展到水果品类降低收货端的人工成本。这些功能要想落地核心还是先把用户量和单量做起来数据多了AI 才能越用越准。最后分享一个小技巧生鲜项目不要一开始就追求系统复杂度。很多团队上手就是微服务 容器编排 数仓结果一个月过去了还在搭基建。我整个系统初期就是一台 4 核 8G 的云服务器MySQL、Redis、FastAPI、模型推理全挤在上面照样撑住了试点阶段的全部流量。先跑通业务闭环再谈扩展这个顺序别搞反。
阅读完成 · 觉得有帮助?
咨询建站