资讯动态

Python+微信小程序搭建农产品团购商城系统全流程实战

发布时间:2026/10/5 2:56:36 来源:尧图企业网站定制
“基于 Python 的农产品商城销售团购系统小程序”听起来就是把“买菜”搬到手机上但真落地要做的事远比你想象的多。上个月我帮一家做社区蔬菜配送的合作社从零到上线整了这么一套系统微信小程序端负责浏览、开团、参团、下单、支付Python 后端负责商品、库存、订单、成团判定最后部署在云服务器上。这篇文章把我这次实操里所有的设计思路、踩坑记录、代码片段和排查方法都写出来给准备做同类的朋友一个能直接抄的作业。1. 做之前先想清楚这个系统到底解决什么问题农产品商城不是普通的电商系统它有两个明显的行业特征一是蔬菜水果这类生鲜有保质期库存和时效性很敏感二是社区的消费习惯是“今天下单明天自提”或“满多少人开团”对拼团玩法有天然需求。如果完全照搬普通商城订单会乱库存容易超卖用户也不会有持续购买的冲动。1.1 为什么选微信小程序而不是App农产品商户的用户群体里很多人不愿意为买一箱西红柿专门下载一个 App但都会用微信。小程序天然具备“用完即走”的属性开团信息发到微信群里用户点一下就能参团不需要额外安装。对技术团队来说小程序端的开发成本也比 App 低很多不用处理 iOS、Android 双端发版也不用折腾证书签名那一套。1.2 为什么后端选Python选 Python 不是因为“网上教程多”而是这个项目本身的业务复杂度恰好适合 Python 生态来表达。商品、SKU、订单、拼团、用户这些模型用 SQLAlchemy 写起来很干净提供 RESTful API 用 Flask 就够了不需要像 Spring 那样搞一堆配置文件。项目跑起来之后用 Python 写报表脚本、做数据导出、接支付回调效率也都高得多。也有人说农贸项目是不是该用 Java我个人的经验是如果你没有一个大团队也没有极端并发需求Java 的学习和开发成本会吞掉你本就不多的迭代时间。Python 后端配合 MySQL、Redis完全扛得住一个中型社区团购项目的日常流量。对比项Python FlaskJava SpringNode.js Express上手成本低高中业务CRUD速度快中快文档和案例多多中适合团队规模1-5人5人以上1-3人运行资源占用低高低2. 整体架构和功能模块拆解系统按照“会员端 管理端 Python后端 微信小程序”四部分切分每一部分只做自己那件事。前端小程序只负责展示和交互所有业务判断都放到后端这样后续如果要把小程序换成 H5 或 uni-app 打包的多端应用后端基本不用动。2.1 核心功能模块套餐设计上我习惯先列出一张模块表把所有功能点写清楚再开始动代码避免写着写着需求膨胀。端模块功能点用户端登录微信授权、静默登录、token缓存用户端商品分类浏览、搜索、列表分页、详情用户端团购开团、参团、成团进度、倒计时用户端订单下单、取消、支付、退款用户端个人中心地址管理、订单列表、团购记录管理端商品上下架、价格、库存调整管理端订单发货、退款处理管理端数据每日订单统计、销售报表2.2 请求链路和数据流小程序端发一个请求大致走这条链路用户在商品详情页点击“发起团购”小程序把sku_id和typegroupbuy发给后端。后端先校验用户登录态再检查这个 SKU 是否还有库存。如果库存充足后端创建一条拼团记录同时生成一笔待支付订单。前端跳到支付页调用支付接口开发阶段用模拟支付。支付成功后后端更新拼团人数人数达标就成团未达标就等倒计时结束自动退款。任何环节都不能只在前端做判断。比如“库存是否还有”这个问题前端看到的数字只是展示真正的扣减必须由后端完成否则用户多点几次按钮库存就负数了。3. 后端设计与核心模型实现后端这层是整个系统的命根子设计重点放在数据模型、用户登录、库存控制、拼团状态四个方向。下面给的都是我实际用过的核心代码你可以直接拿来改。3.1 数据库表设计农产品团购和普通商城不太一样的地方在于一个商品会有多个规格比如“苹果”有“5斤装”和“10斤装”价格和库存都不同所以不能简单把库存挂在商品表上切到 SKU 表更合理。CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT DEFAULT 0 ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, title VARCHAR(100) NOT NULL, cover VARCHAR(255) NOT NULL, detail TEXT, status TINYINT DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE product_sku ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, spec_name VARCHAR(50) NOT NULL, price DECIMAL(10,2) NOT NULL, group_price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ); CREATE TABLE groupbuy_team ( id INT PRIMARY KEY AUTO_INCREMENT, sku_id INT NOT NULL, openid VARCHAR(64) NOT NULL, target_num INT NOT NULL DEFAULT 3, current_num INT NOT NULL DEFAULT 1, status TINYINT DEFAULT 0, expire_at DATETIME NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, user_id INT NOT NULL, sku_id INT NOT NULL, team_id INT DEFAULT NULL, quantity INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );在 ORM 里我会把订单和拼团记录关联起来每次查询订单详情时都带上团队信息前端才能展示“还差2人成团”“已成功”这类状态。3.2 用户登录与会话接入微信小程序登录的核心是wx.login拿到的code后端再用code去微信的接口换openid和session_key。注意一点这个code只能用一次而且五分钟内有效不能缓存。import requests, uuid from flask import request, jsonify, current_app from redis import Redis redis Redis.from_url(redis://127.0.0.1:6379/0) app.post(/api/user/login) def login(): code request.json.get(code) appid current_app.config[WX_APPID] secret current_app.config[WX_SECRET] resp requests.get( https://api.weixin.qq.com/sns/jscode2session, params{ appid: appid, secret: secret, js_code: code, grant_type: authorization_code, }, timeout5, ).json() if openid not in resp: return jsonify(msg登录失败), 400 openid resp[openid] user User.query.filter_by(openidopenid).first() if not user: user User(openidopenid, nickname微信用户, avatar) db.session.add(user) db.session.commit() token uuid.uuid4().hex redis.setex(ftoken:{token}, 7 * 24 * 3600, str(user.id)) return jsonify(tokentoken, user{id: user.id, nickname: user.nickname})用户每次请求都带上Authorization: Bearer token后端用一个装饰器做登录态校验比把openid放在请求体重更安全也更符合常规接口规范。3.3 库存与超卖控制的正确姿势库存问题是所有商城项目里最容易翻车的地方。农产品团购的库存不像数码产品那样可以慢慢发货蔬菜水果一旦卖超了用户第二天来取货会发现根本没那么多货投诉会直接爆炸。解决超卖最稳妥的方式是预扣库存也就是用户下单后先把库存从 Redis 里扣掉最终支付失败再回补。单纯用 Python 代码去“查库存判断扣减”是有并发漏洞的两个请求可能同时查到库存为1然后都扣成0。正确做法是使用 Redis 的原子操作。-- stock_check.lua local stock tonumber(redis.call(GET, KEYS[1])) if not stock or stock tonumber(ARGV[1]) then return 0 end redis.call(DECRBY, KEYS[1], ARGV[1]) return 1app.post(/api/order/create) def create_order(): sku_id request.json.get(sku_id) quantity request.json.get(quantity, 1) user_id current_user_id() key fstock:{sku_id} ok redis.eval(open(stock_check.lua).read(), 1, key, quantity) if not ok: return jsonify(msg库存不足), 400 order_no generate_order_no() order Order(order_noorder_no, user_iduser_id, sku_idsku_id, quantityquantity, total_amountcalc_amount(sku_id, quantity), status0) db.session.add(order) db.session.commit() return jsonify(order_noorder_no, amountorder.total_amount)这样做的好处是“占用库存”这个过程是原子的即使同一秒有几百个用户参团Redis 也会一个一个执行判断不会出现超卖。如果一个订单三分钟内没支付就调用azure或者定时任务把原本扣掉的库存加回来。注意Redis 里的库存只是“预占”最终的库存数据还是以 MySQL 里的 SKU 表为准。每天凌晨可以用脚本把 Redis 库存重新同步成 MySQL 里的真实库存避免两边数据不一致。4. 小程序端关键页面与细节实现小程序端我直接用原生微信小程序开发如果以后要兼容抖音、支付宝小程序再改成 uni-app 也不迟。原生写法的好处是微信独有的能力可以第一时间吃到比如wx.getMenuButtonBoundingClientRect这类 API兼容性最好。4.1 首页商品列表与加载更多商品列表页最常被问到的就是“加载更多”到底怎么实现。其实核心就三件事分页参数、触底事件、防重复请求。很多人写一次就崩是因为只做了onReachBottom没有设置isLoading标志位导致页面滚动到底部时接口被触发五六次。Page({ data: { products: [], page: 1, pageSize: 10, hasMore: true, isLoading: false }, onLoad() { this.loadProducts(true); }, onReachBottom() { if (this.data.hasMore !this.data.isLoading) { this.loadProducts(false); } }, loadProducts(reset) { if (this.data.isLoading) return; const page reset ? 1 : this.data.page; this.setData({ isLoading: true }); wx.request({ url: https://api.example.com/api/products, data: { page, pageSize: this.data.pageSize }, success: (res) { const list res.data.list || []; this.setData({ products: reset ? list : this.data.products.concat(list), page: page 1, hasMore: list.length this.data.pageSize, }); }, complete: () this.setData({ isLoading: false }) }); } });对应 WXML 列表渲染时底部放一个“加载中”或者“没有更多了”的提示不要让用户滑动到底部后一脸懵。4.2 团购流程与拼团状态拼团业务流比普通下单多了一个“团队”概念。用户可以选择自己开团也可以选一个还没满的团加入。前端页面里除了商品信息必须明显展示“还差N人”“XX小时后结束”。后端接口我习惯这样设计接口方法说明/api/groupbuy/createPOST开团创建团队并生成待支付订单/api/groupbuy/joinPOST参团加入已有团队并生成待支付订单/api/order/payPOST模拟支付支付成功后更新团队人数/api/groupbuy/detailGET查团队状态、成员数、倒计时/api/order/listGET当前用户订单列表成团判定不要在支付接口里用普通查询直接更新团队人数时可以加条件UPDATE groupbuy_team SET current_num current_num 1 WHERE id %s AND current_num target_num;如果rowcount等于0说明这个团已经满了不能让用户继续支付。这样做可以避免两个人同时支付同一个坑位。前端拿到拼团剩余秒数后用定时器每秒去减但最终状态一定要走后端查询。倒计时只是用户体验不是业务依据。我曾经遇到过一个“显示倒计时0却还在拼团”的线上问题原因就是前端把本地倒计时当作准了后端时间戳和服务端差了十秒钟。所有推荐做法是前端用server_time校准一次再算本地偏移。4.3 小程序细节适配动态标题、顶部导航栏高度这几个问题看起来小但特别影响上线体验。商品详情页要把标题动态设置为商品名不能所有页面都叫“商品详情”:wx.setNavigationBarTitle({ title: product.title });顶部导航栏高度在 iPhone X 这类刘海屏上非常容易踩坑。状态栏高度用wx.getWindowInfo()导航栏标题位置要结合右上角胶囊按钮的坐标来计算不是写死一个像素数。const systemInfo wx.getWindowInfo() ? wx.getWindowInfo() : wx.getSystemInfoSync(); const menuButton wx.getMenuButtonBoundingClientRect(); const navBarHeight (menuButton.top - systemInfo.statusBarHeight) * 2 menuButton.height; this.setData({ statusBarHeight: systemInfo.statusBarHeight, navBarHeight: navBarHeight });自定义导航栏时页面的navigationStyle要设置为custom否则你就算测出了高度顶栏还是会默认露出来。5. 管理员后台与运营配套多商户的农产品商城一般都有后台但社区团购这种小团队项目我觉得没必要单独做一个重后台。用 Flask-Admin 加几个自定义视图再配一个订单导出脚本运营基本就能跑起来。5.1 后台商品与订单管理Flask-Admin 里注册商品模型和订单模型后能实现最基础的上架、下架、改库存、查看订单、发货。记得把发货状态改成status3后要同步到小程序订单列表这些字段命名前后端一致不然前端又要多写一层映射。订单状态设计建议 0 待支付 1 已支付/待发货 2 已发货 3 已自提/已完成 4 已取消 5 已退款如果只有“待支付”和“已支付”两态运营根本没法处理“用户下单不取货”的情况。我这次项目里就是多了“已自提”这个状态才让团长能批量标记取货完成。5.2 每日订单统计与Excel导出需求方经常会说“我要看每天卖了多少”与其让他翻后台不如写个脚本每天自动导出。用 Python 的 openpyxl 就能写不需要折腾前端报表。import openpyxl from datetime import datetime def export_orders(start_date, end_date): orders Order.query.filter( Order.created_at start_date, Order.created_at end_date ).all() wb openpyxl.Workbook() ws wb.active ws.title 订单报表 ws.append([订单号, 商品, 规格, 数量, 金额, 状态, 下单时间]) for order in orders: ws.append([ order.order_no, order.sku.product.title, order.sku.spec_name, order.quantity, str(order.total_amount), order.status, order.created_at.strftime(%Y-%m-%d %H:%M:%S) ]) filename freport_{datetime.now().strftime(%Y%m%d)}.xlsx wb.save(filename) return filename导出时注意金额不要用浮点数直接拼字符串商品订单这块建议全程用 Decimal避免0.1 0.2之类的问题在账目里埋雷。6. 实操过程和部署要点这里记录我这次项目从装环境到上线的完整过程尤其是容易忽略的坑。6.1 Python环境准备和依赖安装Windows 上开发很简单装好 Python 3.8 以上的版本后用虚拟环境隔离项目依赖python -m venv venv venv\Scripts\activate pip install -U pip pip install flask flask-cors flask-sqlalchemy pymysql redis requests pip freeze requirements.txt如果一台机器上有多个 Python 版本别直接敲python用python3或py -3.8。生产服务器上我建议用 Python 3.8 或 3.10兼容性最好第三库基本都跟得上。6.2 小程序端构建和uni-app打包原生小程序写完后要上传到微信开发者工具点击“上传”然后在微信公众平台里提交审核。如果你一开始就打算多端发布用 uni-app 打包mp-weixin也很方便npm install -g dcloudio/uvm npm run build:mp-weixin构建出来的dist/build/mp-weixin目录用微信开发者工具导入就能预览。不过要注意uni-app 的第三方组件和原生小程序的底层渲染会有一些细微差异导航栏和页面容器的高度一定在真机上多测几台。6.3 云服务器部署后端部署我用的是 gunicorn 加 nginx。gunicorn 负责启动 Flask 进程nginx 负责静态文件和转发。服务器上不要直接用python app.py跑生产开发服务器扛不住并发请求。pip install gunicorn gunicorn -w 4 -b 127.0.0.1:5000 app:appnginx 配置里把/api/转发到本机 5000 端口同时做好 HTTPS。小程序生产环境要求所有请求域名必须是备案过的 HTTPS 域名而且要填到小程序后台的“request 合法域名”里否则真机一调接口就报url not in domain list。7. 常见问题与排查技巧实录这部分是整篇文章里最值钱的内容。很多功能看代码很简单真正跑起来后遇到的问题基本都是环境、域名、状态同步之类的事。7.1 常见问题速查表现象原因解决方案列表滚到底部重复请求没有防重复标志位加isLoading请求期间禁止再次触发商品图片不显示图片域名不在 downloadFile 合法域名小程序后台配置图片域名真机登录后偶发失效token 存的是内存变量把 token 存 Redis设置 7 天过期库存显示有下单说没货前端缓存了旧的库存下单前实时调后端拿最新库存拼团人数满了还在支付团队更新没加条件用current_num target_num条件更新顶部高度在安卓/iOS不一致写死了导航栏高度用getMenuButtonBoundingClientRect计算7.2 开发调试中的几个独家经验小程序调试别一上来就烧键盘先在微信开发者工具里把 Network 面板打开看每个接口的请求参数和返回结构。接口报错时优先看返回的statusCode和errMsg大部分前端问题都能在这里定位到不需要额外找抓包工具。第二个经验是在开发阶段把小程序后台的“不校验合法域名”打开接口可以先用http://127.0.0.1:5000调试。但上线前一定要关掉这个选项并强制把接口切到 HTTPS 域名否则到了线上你会被域名问题折磨一下午。第三个经验也是我踩过最狠的一个坑每次发布小程序版本后用户手机上的缓存还会停留旧版本。如果你改了接口字段名新旧版本混跑会直接白屏。要给小程序加版本检测在启动时请求后端接口判断是否有强制更新有就直接引导用户去重启小程序。农产品团购这个方向代码写完只是第一步真正难的是让团长愿意用、让用户愿意每天打开。系统稳定后可以再加社区自提点管理、会员积分、订阅消息通知这些功能主流程不变扩展性都在后端模型上留好了。

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

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

免费获取报价 →
↑