ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Python+小程序农产品团购系统实战:从架构到并发扣库存

Python+小程序农产品团购系统实战:从架构到并发扣库存 前阵子帮朋友做了一套基于Python的农产品商城销售团购系统小程序从需求梳理、数据库设计、后端接口开发到小程序端联调前后折腾了差不多两个月。中间踩过不少坑也沉淀下来一些反复验证过的方案。这篇把整套系统从架构到实现要点拆开讲一讲重点放在三块小程序端怎么做交易闭环、Python后端怎么处理农产品这种“非标又容易损耗”的商品、以及团购成团和退款里面的并发细节。如果你打算做社区团购类小程序或者正在拿这类项目当毕设课题、面试项目可以参考一下。1. “农产品团购小程序”这个组合为什么偏偏能跑通很多开发者第一次接触农产品商城会觉得浑身别扭——SKU不好定义、库存算不清楚、用户下单决策慢、物流成本还高。这不是代码能力的问题而是用做普通电商的思维去做生鲜从一开始就走错了方向。1.1 农产品电商的“三高一低”先说清楚农产品不是标准品。一件T恤有明确的尺码、颜色、材质但一棵白菜的份量、新鲜程度、产地批次很难用一个固定属性说清楚。这意味着商品模型不能照抄服装鞋帽那套“规格颜色”的SKU设计。然后是损耗。普通商品的库存“放着就是钱”农产品的库存放着就是烂。今天到货的100斤番茄明天可能只剩80斤能卖。如果按普通电商的“永久库存”来管理账一定对不上。再一个是信任问题。用户看不到实物不敢下单。水果甜不甜、青菜嫩不嫩图片看不出来。这个问题的解法不在技术上而在运营设计上——预售、可溯源、团长背书。最后是物流。单独给一个用户送20块钱的菜配送成本可能超过商品本身。1.2 团购模式怎么对症下药团购模式恰好把这几个问题一起解决了。预售制先收订单再组织货源。损耗风险转移给了供应链上游而不是平台自己赌库存。以量换价一个社区一次性采购200斤土豆采购价可以谈下来用户也愿意为低价等待一天。集单配送订单集中在自提点一辆车送到一个点用户自提。最后一公里成本从“每单配送”变成“每点配送”。信任背书自提点团长是邻居、是熟人用户的试错成本被拉得很低。1.3 为什么是小程序而不是App或者H5小程序的获客成本低用户扫个码就进来了不用下载安装。拼团活动天然适合在微信群和朋友圈传播微信的支付闭环让交易路径最短。对中小规模的创业者来说小程序是农产品社区团购目前最合适的载体。而后端用Python维护起来省心生态成熟招人也不难。我个人的判断是这类项目技术复杂度不高真正的复杂度在业务规则上。所以做系统之前先把“谁来卖、谁来买、怎么履约、坏了怎么办”这些规则定清楚代码写起来才有方向。2. 系统架构与后端技术选型项目前期最纠结的不是功能而是技术选型。这里把最终方案列出来再讲讲为什么这么选。2.1 整体架构小程序端Python后端管理后台系统分三条线用户端微信小程序商品浏览、团购活动页、下单、微信支付、订单查询、自提点选择。后端服务Python给小程序提供RESTful接口处理订单、库存、成团判定、退款等业务逻辑。管理后台Web运营人员上架商品、创建团购活动、查看订单、处理退款、维护自提点和团长信息。数据层我用了MySQL 8.0 Redis 6.x。MySQL存商品、订单、活动等核心业务数据Redis做热点缓存、库存扣减和轻量级延迟任务。对象存储放商品图片一台2核4G的云服务器在初期完全够用。2.2 FastAPI vs Flask vs Django到底怎么选Python后端框架主流就是这三个。我列了个对比表框架开发效率性能异步支持生态成熟度适合场景Flask高简单直接一般需要额外配置很成熟中小型项目、快速原型Django高全家桶一般需要额外配置极成熟大型Web应用、内容管理FastAPI高代码量少较高原生异步支持快速成长高性能API、前后端分离我最终选了FastAPI主要三个理由。第一性能好。FastAPI基于Starlette异步能力是原生的高并发下比Flask的同步模型稳得多。农产品的团购活动经常在某个时间点集中开抢接口性能必须扛得住。第二类型提示。FastAPI强制使用Pydantic做请求参数和响应模型校验小程序端传过来的数据格式对不对接口层直接帮你挡掉。开发阶段省了大量联调时间。第三自动生成OpenAPI文档。FastAPI启动后自带Swagger文档页面小程序端开发直接照着文档联调不用再单独维护一份接口文档。但如果你团队里有Django老手用Django DRF也没问题。选型的关键不是“谁最好”而是“谁能让你们最快上线、最好维护”。FastAPI在中型API项目里目前是最省心的选择。2.3 小程序端原生还是uni-app小程序端我选了原生微信小程序。主要考虑是项目只做微信端原生框架性能最好组件开箱即用排查问题也方便。如果你要同时发布到支付宝小程序、抖音小程序那uni-app更合适一套代码多端运行。两个方案对比如下方案多端支持性能学习成本调试体验原生微信小程序仅微信最优低微信开发者工具足够uni-app微信/支付宝/抖音/H5略差于原生Vue语法前端友好需要配合HBuilderX选型建议很简单只做微信端用原生要铺多端上uni-app。不用在这上面纠结太久。3. 数据模型设计把“一把青菜”变成可交易的订单这套系统的核心难点其实在数据库设计。农产品本身是非标的怎么把它容纳进标准的关系型数据库是第一步要解决的问题。3.1 商品和SKU别照抄服装电商普通电商的SKU是“红色 M码 纯棉”这样确定的组合但农产品没法这样建模。一根萝卜的规格只有“重量区间”能描述而且不同批次的品质可能差很多。我在商品表里建议这样设计CREATE TABLE product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称, category_id INT NOT NULL COMMENT 分类如蔬菜/水果/禽蛋, origin VARCHAR(50) DEFAULT COMMENT 产地, main_image VARCHAR(255) DEFAULT COMMENT 主图URL, detail_images TEXT COMMENT 详情图JSON数组, spec_desc VARCHAR(255) DEFAULT COMMENT 规格描述例如约500g±50g, storage_condition VARCHAR(100) DEFAULT COMMENT 储存条件常温/冷藏, shelf_status TINYINT DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意spec_desc这个字段。它不是一个结构化的SKU属性而是专门用来展示“约500g±50g”“约4-5个/斤”这类模糊表述的。非标品不追求精确定义而是要诚实告知避免用户收到货后觉得“缺斤少两”。3.2 团购活动与库存核心表结构普通商城的库存是永续的团购活动的库存是临时的。这两者的管理逻辑完全不同所以我把活动库存单独拆了一张表。CREATE TABLE groupon_activity ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, product_id INT NOT NULL COMMENT 关联商品, title VARCHAR(150) NOT NULL COMMENT 活动标题如本地西红柿基地直采, target_num INT NOT NULL COMMENT 成团目标人数, per_limit INT DEFAULT 1 COMMENT 每人限购份数, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, groupon_price DECIMAL(10,2) NOT NULL COMMENT 团购价, original_price DECIMAL(10,2) NOT NULL COMMENT 划线原价, pickup_point_ids VARCHAR(255) DEFAULT COMMENT 可选自提点ID逗号分隔, status TINYINT DEFAULT 0 COMMENT 0未开始 1进行中 2已结束 3已关闭, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE groupon_stock ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, activity_id INT NOT NULL, batch_no VARCHAR(30) NOT NULL COMMENT 批次号, total_num INT NOT NULL COMMENT 本批总库存, sold_num INT NOT NULL DEFAULT 0 COMMENT 已售数量, PRIMARY KEY (id), UNIQUE KEY uk_activity_batch (activity_id, batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;为什么库存要按“批次”拆分因为农产品是分批到货的第一批卖完了可能第二批还没到。拆批次一方面方便后台做进销存另一方面在订单备注里可以明确告诉用户“预计5天内自提”履约体验更好。3.3 订单表与状态机设计清楚退款省心订单表是整套系统的核心字段不能省CREATE TABLE orders ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id INT NOT NULL, activity_id INT NOT NULL, product_name VARCHAR(100) NOT NULL COMMENT 商品名快照, spec_desc VARCHAR(255) NOT NULL COMMENT 规格快照, product_image VARCHAR(255) NOT NULL COMMENT 主图快照, quantity INT NOT NULL DEFAULT 1, amount DECIMAL(10,2) NOT NULL COMMENT 实付金额, pay_status TINYINT DEFAULT 0 COMMENT 0未支付 1已支付 2已退款, order_status VARCHAR(30) DEFAULT pending_payment COMMENT 状态机值, pickup_point_id INT NOT NULL, pickup_point_name VARCHAR(100) NOT NULL COMMENT 自提点快照, pickup_address VARCHAR(255) NOT NULL COMMENT 自提点地址快照, groupon_status TINYINT DEFAULT 0 COMMENT 0成团中 1已成团 2未成团, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_user_id (user_id), KEY idx_activity_id (activity_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单状态机我建议这样设计状态值含义可流转到pending_payment待支付paid、closedpaid已支付等待成团groupon_success、refundinggroupon_success已成团preparing、refundingpreparing备货中readyready待自提completedcompleted已完成无refunding退款中refundedrefunded已退款无closed已关闭超时未支付无订单里强制冗余了商品名、规格、图片、自提点等字段。原因很简单团购活动结束商品可能下架改价如果订单再去关联活动表查商品信息历史订单随时可能查不到数据。快照是电商系统的标准做法。4. 小程序端从零搭建首页、商品详情、拼团下单小程序端的核心是三个页面首页信息流、商品详情、下单支付。农产品平台不需要复杂的推荐算法把交易路径走顺才是关键。4.1 首页信息流卖的是“活动”不是“商品”首页最好不要放成排的商品卡片因为用户来团购平台买的是“今天团什么”不是“有哪些商品”。所以首页建议用活动卡片流。接口设计很简单GET /api/v1/home?page1page_size20返回结构banner列表 活动列表。活动卡片上显示商品主图、活动名称、团购价格、已拼份数/目标份数、剩余时间。关键点接口要返回has_more字段让前端知道还有没有下一页。前端拿到数据后拼接到列表尾部而不是覆盖。4.2 列表加载更多新手最容易翻车的分页逻辑“微信小程序页面列表加载更多”是搜索频率很高的问题我一开始也在这里翻过车。原生小程序实现分页核心是三个状态page、hasMore、loading。Page({ data: { activities: [], page: 1, hasMore: true, loading: false }, onReachBottom() { if (this.data.loading) return; if (!this.data.hasMore) return; this.loadActivities(); }, loadActivities() { this.setData({ loading: true }); wx.request({ url: https://api.example.com/api/v1/home, data: { page: this.data.page, page_size: 20 }, success: (res) { const newList this.data.activities.concat(res.data.list); this.setData({ activities: newList, page: this.data.page 1, hasMore: res.data.has_more, loading: false }); }, fail: () this.setData({ loading: false }) }); } });几个容易踩的坑请求还没返回时用户快速滑动到底会连续触发onReachBottom所以要用loading状态拦截。下拉刷新时先把page重置为1activities清空否则数据会重复。判断hasMore要相信后端返回值不要靠“返回数量是否等于page_size”去猜。万一最后一页恰好满20条你的判断就会多请求一次空数据。这类问题看起来简单真到上线后高手指点你翻代码的时候改起来还是很费劲的不如一开始就写规范。4.3 商品详情与拼团进度展示商品详情页需要展示活动倒计时、团购进度条、参团列表、自提点选择、团购价和划线价。拼团进度条的数据结构{ sold_num: 35, target_num: 50, progress: 70 }前端根据sold_num / target_num计算百分比宽度。一个运营上的小技巧新用户看到“0人已拼”可能不敢参与所以展示参团列表时可以展示前几单的“虚拟拼团记录”已经成团的用户透明公开。这是常见的电商运营手段技术上不复杂但对转化率影响很大。自提点选择用picker组件先做一版上线后如果用户反馈看不清地址再上地图选点。4.4 下单流程与支付唤醒别把逻辑写在前端小程序端支付流程标准做法是用户提交订单前端调用后端接口POST /api/v1/orders后端生成订单返回pending_payment状态和微信支付所需参数。前端拿到支付参数后调用wx.requestPayment。支付成功前端跳转到“我的订单”页通过轮询或调用GET /api/v1/orders/{id}刷新订单状态。这里的关键教训支付成功与否的判断必须以微信支付回调为准不能信前端回调。用户在支付页面可能选择了“取消”也可能支付成功但网络中断导致前端没收到回调。后端在收到微信支付结果通知后才把订单状态从pending_payment更新为paid。5. Python后端核心逻辑锁库存、成团、自动退款这部分是整套系统的技术核心也是最容易出现并发问题的地方。5.1 “锁库存”到底锁在哪一层我见过很多初学项目写这样的代码stock db.query(GrouponStock).filter(...).first() if stock.sold_num quantity stock.total_num: stock.sold_num quantity db.commit()这段代码在单用户场景没问题但一旦两个人同时下单两个请求同时读到sold_num5都认为可以再买5份最后sold_num变成10而不是实际扣减后的正确值。这就是经典的“先查后写”超卖问题。正确做法是用数据库原子更新result db.execute( update(GrouponStock) .where( GrouponStock.activity_id activity_id, GrouponStock.sold_num quantity GrouponStock.total_num ) .values(sold_numGrouponStock.sold_num quantity) ) if result.rowcount 0: raise SoldOutError(手慢了已售罄)这行SQL的语义是只有“已售数量 本次购买数量 总库存”时才执行加库存操作。数据库的行锁保证了并发安全。如果影响行数为0说明库存不够直接返回“已售罄”。库存充足时再创建订单。如果创建订单失败还要把刚才加的sold_num回滚回去。严谨一点可以把加库存和创建订单放在同一个数据库事务里。5.2 成团判定什么时候通知用户“已成团”成团判定有两个时机第一个时机是每次新订单落库后立即检查。用户下完单后台逻辑先更新库存再查一下当前活动已售数量如果sold_num target_num就把活动里的groupon_status从0改成1并立刻批量更新该活动下所有paid状态订单为groupon_success。这时候再推送“恭喜你已成团”的消息体验最好。第二个时机是活动结束时兜底扫描。因为理论上可能存在一种情况某些订单还在pending_payment状态在支付后活动恰好结束。所以活动到期后需要一个定时任务把所有paid且未成团的订单统一判定成团了更新状态没成团就走退款流程。成团判定的逻辑要“幂等”。同一个活动成团判断只能触发一次即使并发请求同时进来也不能重复发通知。实现上可以用activity.status字段判断只有status1进行中的活动才允许成团更新时把status改成2已结束后到的请求看到已结束就跳过。5.3 活动到期未成团自动退款怎么做未成团的订单要自动退款。最稳妥的方案不是用time.sleep()而是用定时任务。我的做法是用Celery的beat调度每分钟执行一次扫描任务找出所有满足条件的订单WHERE order_status paid AND groupon_status 0 AND activity.end_time NOW()然后逐单调用退款接口。退款要注意幂等性。退款接口可能因为网络问题被重复调用所以订单表里必须有pay_status状态控制只有pay_status1已支付才能发起退款退款进行中置为refunding退款完成后置为refunded。重复请求看到refunding或refunded就直接返回成功不重复发起。5.4 超时未支付自动关单与库存回补用户下单后如果一直不支付订单要自动关掉否则库存一直被占着。这是和成团退款完全不同的另一个定时任务。最简单的实现也是用定时扫描查所有pending_payment且创建时间超过15分钟的订单将其置为closed同时把库存sold_num减去对应数量。这个任务粒度可以设为每5分钟一次损耗可以接受。订单量大了之后可以换成延迟队列方案下单时往MQ里塞一条延迟消息15分钟后消费检查订单是否已支付未支付就关单。前期单量不大定时扫描完全够用别上来就搞MQ运维成本不低。6. 联调排错实录抓包、域名配置、页面适配小程序开发和普通Web开发最大的区别在联调环境。这里记录几个我实测过的排错经验。6.1 用Charles抓小程序请求实测过的步骤小程序页面上的数据不对或者接口报错看不清response最直接的办法是抓包。Charles抓小程序HTTPS请求的步骤电脑和手机连同一个WiFi。Charles开启代理并开启SSL Proxying添加需要抓包的域名。手机WiFi设置代理指向电脑IP和Charles端口。手机上安装Charles的根证书并信任证书。打开微信小程序请求就能在Charles里看到了。抓包的价值有两个。一是看小程序实际发出的请求参数和后端返回的原始JSON接口问题一眼定位二是把自己小程序的请求头token、content-type抓出来直接放到API调试工具里重复测试排查问题比在页面上点来点去快得多。提示抓包只用于调试自己的小程序和接口不要拿这套方法去分析别人的小程序涉及敏感数据的问题没必要碰。6.2 顶部导航栏高度不同机型真的不一样自定义导航栏之前也踩过坑。微信小程序的胶囊按钮位置在不同机型上不一样尤其iPhone的刘海屏和安卓的挖孔屏顶部安全区高度差异很大。正确做法是动态获取胶囊位置const menuRect wx.getMenuButtonBoundingClientRect(); const statusBarHeight wx.getSystemInfoSync().statusBarHeight; const navBarHeight (menuRect.top - statusBarHeight) * 2 menuRect.height;拿到navBarHeight后再设置自定义导航栏的高度和“胶囊不遮挡内容”的边距。不要写死在样式里否则换个手机就乱套。6.3 请求合法域名、HTTPS与“真机白屏”开发模式下可以在微信开发者工具里勾选“不校验合法域名”但一旦发布上线小程序所有wx.request的域名必须已备案使用HTTPS在小程序后台的“request合法域名”里配置我上线前就吃过亏。在开发工具里跑得好好的一扫码真机就白屏。排查后发现是后端HTTPS证书链不完整微信WebView不认。解决办法是检查证书链是否配全别只配了叶子证书。HTTPS证书推荐用免费证书。云厂商提供的免费证书有效期通常一年记得设置自动续期脚本否则第二年会突然发现线上接口全挂了。7. 上线前的检查清单与运营落地细节技术做完了还得保证它能真正跑起来。7.1 一个可以直接拿来用的上线检查清单小程序已完成微信认证选择的类目覆盖“食品/生鲜/社区团购”等。微信支付商户号已申请支付回调地址已配置为HTTPS。后端API域名已加入小程序request合法域名。HTTPS证书已部署并确认证书链完整。MySQL和Redis都已配置自动备份备份策略至少每天一次。后端日志已接入按天切割错误告警能推送到群里。定时任务成团扫描、退款扫描、关单扫描已全部部署并验证过。用两个微信号实测了完整流程下单、支付、成团、退款。7.2 技术做完了为什么项目还可能挂很多团队以为系统上线就完事但农产品团购真正的难点在业务侧。预售库存和实际到货可能不一致。所以后台要提供“分批到货”的入库功能让运营把groupon_stock按批次增加同时支持“已售罄但临时补货”的追加库存操作。售后必须有人处理。生鲜商品售后率比普通商品高后台至少要支持订单改价、部分退款、整单退款、备注。不要让用户只能通过电话找团长处理更不能让技术小哥打电话问用户情况。拼团数据要看但要会看。成团率、单团平均销量、各自提点订单量这些数据后台都要有。运营要能看明白哪个商品适合开团、哪个自提点业绩差需要调整。我见过项目上线后运营问“哪里看数据”才发现后台根本没有统计页面这就很被动了。先跑通一个自提点再复制到十个。系统批量扩张的前提是把复制的流程标准化。技术层面要做的是自提点的数据模型独立团长ID、地址、营业时间、结算规则都单独成表后续复制时不用改代码。最后聊点我自己的体会。这类小程序真正难的根本不是技术——FastAPI、小程序原生、Redis这些网上教程多得是。难的是把“农产品非标、损耗、信任、集单”这些业务需求翻译到数据库表和订单状态机里。数据模型对了、状态流转清晰了、库存扣减安全了系统就成功了一大半。希望这份拆解能让你在动手前想清楚这几个关键点少走点弯路。
返回列表