果蔬批发管理系统实战:从Flask到Django的进销存选型与部署
2026/9/9 21:46:24 网站建设 项目流程

做果蔬批发系统这个项目,起因特别接地气:我一个在农贸市场做批发的朋友,每天凌晨三点开始忙活,收了十几家散户的货,再批发给市里的超市和小摊贩。他记账用的还是手写小本子,一天的流水几百条,到了月底对账对到头大。他一口气说了三个需求:能管进销存、能打单、能算账。我跑了几个批发市场看了一圈,发现市面上的进销存软件全是给零售商家设计的——扫码、收银、小票那套逻辑,根本覆盖不了批发场景。于是这个用 Python Flask 和 Django 搭建的果蔬批发系统,从一次酒桌闲聊变成了正经开发项目。

这篇文章不打算写成"项目说明书",而是把整个设计决策和实现过程摊开讲:果蔬批发业务的真实痛点在哪里、Flask 和 Django 怎么选、数据库怎么建模、订单和库存怎么联动、部署上线以后踩了哪些坑。适合正在做类似进销存项目的开发者、准备拿"管理系统"做毕设或作品集的同学,也适合刚学完 Flask/Django 基础、想知道框架到底怎么用在真实业务里的朋友。

1. 果蔬批发业务剖析:这根本不是普通进销存

1.1 批发和零售的三个本质差异

做这个系统之前,我花了一个星期蹲在朋友的摊位上看他们怎么干活。越看我越确定一件事:批发业务流程和零售是两个物种。零售是"选品、付款、拿走",批发是一条更长的链路:"报价、下单、过磅、记录、装车、记账、结账"。这个链路里的每个环节都有自己的特殊规则。

第一个差异是计量单位。零售按"份"按"件",批发按"斤"按"箱",而且很多时候同一商品两种单位并存。土豆可以按斤卖给小摊贩,也可以整箱卖给食堂;叶菜基本只能按斤,水果则看规格。我的系统里商品表必须同时支持两种单位的换算,不能像零售系统那样写一个固定单位了事。

第二个差异是价格机制。零售价贴上标签一周不变,批发价一天能变好几次——早市价、午市价、下午清场价,老客户和散客还不一个价。更麻烦的是,批发的"成交价"通常不是一个冷冰冰的数字,而是双方口头撮合的结果。系统要能支持一天多次调价,并且每次调价最好都有记录,月底别人问"这个月白菜怎么越卖越贵"的时候,你能翻出调价日志来。

第三个差异是结算方式。很多批发客户不是当场付款,而是月底统一结账,也就是常说的账期。系统里每一笔订单除了"已支付"和"未支付",还要有"已记账、未结清"这类批发特有的状态,并且要能按客户累计欠款。

1.2 果蔬商品的"娇气"属性

再说果蔬本身的特殊性,这里是最容易让没做过生鲜系统的人翻车的地方。第一个是损耗。叶菜早上进货一筐是100斤,下午卖出可能只有92斤,那8斤是自然脱水和摘除烂叶造成的。如果系统里库存只记"入100出100",账面没两天就乱了。我的处理方式是把损耗单独建模,后面第三章会细讲。

第二个是规格差异。听起来奇怪,但批发市场里的"同一种菜"其实分好几个规格。同一批山东大葱,统货一个价,挑出来的粗葱一个价,细葱一个价。如果不做规格维度,你会发现价格表根本维护不下去。

第三个是保质期压力。水果蔬菜的生命周期短则两三天,长则一周。库存表上的数字和冷库里的实物经常对不上——并不是记账记错了,而是货真的烂掉了。所以系统里必须有一个"报损"入口,店员发现坏了一批货,要能快速从库存里扣掉。

1.3 用户画像与使用场景

系统最终服务四类人:老板(管理员)维护商品、价格,看报表;店员(操作员)下单、过磅、打单、报损;财务月底对账、生成应收表;还有客户——有些老客户想在手机上提前看第二天报价、预约留货,所以系统后来加了一个面向客户的报价查询接口。

使用场景按时间轴看也很鲜明。凌晨3点到5点是下单高峰,几十个客户同时来要货,系统必须扛得住快速录入。白天10点以后是补货和整理时间,农残检测单、进货单要录入。月底是财务最忙的时候,对账、催款、打印汇总表,系统要能一键生成。

2. 为什么我从 Flask 转到了 Django:一次真实选型复盘

2.1 先说结论:Flask 不是不行,是项目体量到了临界点

项目一开始,我其实用的是 Flask。为什么?因为我偏爱轻量框架的灵活性和透明感——路由自己定义,ORM 用 SQLAlchemy 自己装配,模板想用 Jinja2 就用 Jinja2,不想用拉倒。前两周在 Flask 里做原型,写了大概 1200 行代码,把"商品管理+下单+库存"跑通了,当时觉得挺顺的。

转折点是第三周,开始加后台管理功能。具体来说,老板需要一个能筛选商品、分页展示、导出 Excel 的列表页,店员需要一个能查看"今天的订单"并按状态筛选的界面。在 Flask 里,这些功能每个都要手动实现或者调研第三方库:分页有 flask-paginate,导出有 openpyxl,筛选要自己拼查询条件。功能确实都能做,但每加一个功能就要做一次选型和配置,那种"为了攥一个螺丝要跑一趟五金店"的感觉越来越强。

2.2 Django 的"白送"能力正好踩中需求

试了一圈之后,我把主体迁移到 Django,发现几个能力简直是给这种管理系统量身定做的。

第一是 Django Admin,这个不用多废话。模型一定义,后台管理页面自动就有了,添加商品、改价格、查订单,几乎零成本。对于果蔬批发场景这种内部管理系统,Admin 完全可以当员工操作界面用,省下的开发时间不是一点半点。

第二是认证权限体系。Django 自带 User、Group、Permission 三个模型和完整的登录会话机制。老板、店员、财务的权限直接建三个组就完事:店员能录订单但不能看毛利,财务能看应收不能改库存。在 Flask 里要搞到同等能力,至少得接入 Flask-Login 加 flask-principal,还要手动写装饰器和中间件。

第三是 ORM 和迁移。Django ORM 的 QuerySet 链式查询在写业务报表时非常顺手,migrations 机制让我从开发到部署改了大大小小十几次表结构,从来没手动写过一条 ALTER TABLE。

2.3 最终架构:Django 为核心 + Flask 守边缘

不过,Flask 并没有被我从技术栈里拿掉,而是换了个位置继续服役。最终架构是:Django 跑在 8000 端口,负责全部核心业务(商品、客户、订单、库存、报表);Flask 跑在 8081 端口,只提供一个面向客户微信小程序的当日报价查询接口。

两个服务共用同一个 MySQL 数据库。Flask 侧只读,不做写操作,所以不用考虑事务冲突。这样设计的好处是:客户的报价查询请求不占 Django 的主业务进程,凌晨高峰期报价接口再频繁也不会拖慢店员录单。Flask 那个服务从启动到跑通只花了一个下午,代码不到 100 行,部署上也只要一个 systemd 服务托管,省内存启动快。

如果要说选型建议,我的排序很明确:只想做个几百行的小工具或者简单 API,用 Flask;做一个需要后台管理、权限、数据报表的完整业务系统,用 Django 可以省掉一大半重复劳动。

维度FlaskDjango本项目选择
原型速度快,代码量少稍重,初始样板多原型期 Flask,正式开发 Django
后台管理需手动集成自带 Admin,即建即用Django
用户权限需第三方库手动拼自带完整体系Django
ORM/迁移SQLAlchemy + AlembicDjango ORM + MigrationsDjango
轻量 API非常合适可以做但略重Flask

3. 数据模型设计:把"报价-过磅-开单"固化进数据库

3.1 先梳理业务流程,再动笔建表

动手写 models.py 之前,我把批发业务按实际发生顺序完整捋了一遍,列出六个关键动作:供货商送货验收入库、每天早晨维护当日售价、客户来下单、过磅确定实际重量、记账并确认应收款、月底对账。

由此拆出八张核心表:用户表(User)、客户表(Customer)、商品分类表(Category)、商品表(Product)、价格日志表(PriceLog)、订单表(Order)、订单明细表(OrderItem)、库存流水表(StockLog),外加一张收款记录表(Payment)。有人可能会说水果批发系统怎么没有供应商表——我把它并进客户表逻辑了,用 type 字段区分"客户"还是"供应商",因为两者在系统里信息字段几乎一样,早期没必要拆两张表。

3.2 核心模型与关键字段

商品表是业务基础,字段设计上我刻意加入了两个容易被忽略的字段:spec 和 base_price。spec 表示规格(统货、精选、大果、小果),因为前面说过同一种菜在批发市场里是按规格分别报价的;base_price 是基准进价,用于和当前售价对比算毛利。

订单表是整个系统的关键,字段设计如下:

from django.db import models from django.utils import timezone class Order(models.Model): class Status(models.TextChoices): PENDING = 'pending', '待确认' CONFIRMED = 'confirmed', '已下单' WEIGHED = 'weighed', '已过磅' SETTLED = 'settled', '已结算' CANCELLED = 'cancelled', '已取消' order_no = models.CharField('订单号', max_length=32, unique=True) customer = models.ForeignKey('Customer', on_delete=models.PROTECT, verbose_name='客户') total_amount = models.DecimalField('订单总额', max_digits=10, decimal_places=2) discount_amount = models.DecimalField('抹零金额', max_digits=10, decimal_places=2, default=0) receivable_amount = models.DecimalField('应收金额', max_digits=10, decimal_places=2) status = models.CharField('状态', max_length=20, choices=Status.choices, default=Status.PENDING) created_at = models.DateTimeField('下单时间', default=timezone.now) confirmed_at = models.DateTimeField('确认时间', null=True, blank=True) settled_at = models.DateTimeField('结算时间', null=True, blank=True) operator = models.ForeignKey('User', on_delete=models.PROTECT, verbose_name='操作员')

订单明细表保存每一行的商品、单价、下单数量、过磅数量。这里我特别强调过磅字段:因为批发订单最终是按实际过磅重量结算的,而下单时的数量往往只是个预估。detail 表里同时存在 quantity 和 actual_quantity 两个字段,就是为了留一个"预估和实际"的对照。

库存流水表统一记录所有库存变动:入库、销售出库、报损、盘点调整。任何一次变动都往这张表里写一行,这样月末可以完整还原库存变化的来龙去脉。

3.3 价格字段设计的核心思路

果蔬价格一天波动多次,若只给商品表放一个 price 字段,月底复盘时根本说不清当天卖的什么价。我做了两层设计:商品表上有 current_price(当前价)和 base_price(基准进价);另有 PriceLog 表记录每次调价,包含商品、旧价、新价、调整人和时间。店员每天早晨操作的第一步就是"今日定价"——批量把参考价复制为当日价,个别商品单独微调。

订单明细里的成交单价,在新建订单时会自动从商品表 current_price 带出,但允许操作员手动修改,因为批发市场本来就有商量余地。同时,明细里保存的是"成交那一刻的快照价格",即使第二天价格改了,历史订单也不会变,这一点是靠建表时复制值入表实现,不是靠外键实时关联。

3.4 库存与损耗的联动设计

果蔬库存最大的坑是"账面数字和实物对不上"。我的模型里没有用简单的"总数减总数",而是引入了批次概念。进货时每生成一个 Batch(批次),记录数量、进价和入库时间;出货时按批次先进先出扣减。这样有两个好处:能知道某一批货还剩多少,方便报损;能准确计算每一批货的毛利。

报损入口直接在仓库模块里,店员选择商品、填报损数量、选原因(腐坏、脱水、客户退换),提交后自动产生一条库存流水并扣减库存。库存小于安全阈值时,系统在 Dashboard 上提示补货,这个功能很简单,但对批发商的经营帮助非常大——烂在仓库里的菜被量化之后,老板才会真的重视损耗控制。

4. 订单交易流程的 Django 实现:从下单到过磅到结算

4.1 订单状态机:每一次流转都有据可查

订单状态的流转是这套系统的业务主干,我的设计是五个状态:待确认、已下单、已过磅、已结算、已取消。新单从微信或前台录入后进入"待确认",店员确认价格库存后变为"已下单",装车过磅、修改实际重量之后进入"已过磅",最后财务确认收款或记账后变为"已结算"。

每个状态转换都记录操作人和操作时间,Order 表里我专门加了 confirmed_at、weighed_at、settled_at 三个时间戳字段。这样做一开始是觉得"反正加个字段又不难",后来真到了客户打电话质疑"那单是不是你们少算了一笔"的时候,翻记录一查就真相大白,省了很多扯皮。

4.2 核心视图:价格联动和事务控制

新建订单时的核心逻辑是:从商品表带出 current_price 作为默认单价,计算总额时用"下单数量乘以单价"得到总额,再按操作员输入的抹零金额扣减,得到应收金额。下单动作和库存扣减必须在一个事务里完成,防止出现"订单建了库存没扣"的中间状态。

from django.db import transaction from django.db.models import F @transaction.atomic def create_order(request): customer_id = request.POST['customer_id'] items = request.POST.getlist('items') # 每项含 product_id, quantity order = Order.objects.create( order_no=generate_order_no(), customer_id=customer_id, operator=request.user, status=Order.Status.CONFIRMED, ) for item in items: product = Product.objects.select_for_update().get(pk=item['product_id']) if product.stock < item['quantity']: raise ValueError(f'{product.name} 库存不足,当前{product.stock}斤') OrderItem.objects.create( order=order, product=product, price=product.current_price, quantity=item['quantity'], ) product.stock = F('stock') - item['quantity'] product.save() StockLog.objects.create( product=product, change_type=StockLog.SALE, change=-item['quantity'], operator=request.user, ) order.recalc_amounts() return order

这里有两个要点。一是 select_for_update(),它在数据库层面给这条商品记录加了行锁,两个店员同时对同一商品下单时,第二个请求会等到第一个事务提交才继续,从根上避免超卖。二是 F('stock'),让库存扣减发生在数据库端而非 Python 层,避免"读取-修改-写回"三步操作之间的竞态条件。

4.3 过磅环节:按实际重量更新金额

过磅是批发系统特有的环节。装车时实际重量和下单重量通常有出入,操作员在过磅界面修改数量后,系统要做三件事:更新订单明细的 actual_quantity、重算订单金额、按差值调整库存。这个调整同样在事务里完成,而且库存流水里要把冲减差额记为"过磅调整",不能记成销售,否则报表会失真。

4.4 Flask 辅助报价接口

面向客户的微信小程序只需要一个接口:查今天各个商品什么价、有没有库存。这种轻量只读接口用 Flask 写非常舒服。独立服务的好处是访问量大也不影响 Django 主业务。启动方式就是常见的 flask run --port 8081,代码很少,约 80 行:

from flask import Flask, jsonify from sqlalchemy import create_engine, text app = Flask(__name__) engine = create_engine('mysql+pymysql://user:pass@localhost/veg_market') @app.route('/api/quote', methods=['GET']) def quote(): with engine.connect() as conn: rows = conn.execute(text( 'SELECT name, spec, current_price, stock FROM product WHERE is_active=1' )).fetchall() return jsonify([dict(r._mapping) for r in rows]) if __name__ == '__main__': app.run(host='0.0.0.0', port=8081)

实际部署时给 Flask 单独配上 systemd 服务,开机自启,进程挂了自动拉起。这很小,但客户小程序端每天早上七八点会集中请求报价,从几个月运行数据看,这个接口的平均响应时间在 30ms 以内,从来没给 Django 那边添过乱。

5. 果蔬批发特有的业务难点与破解方法

5.1 抹零问题

批发交易里抹零是常态,客户拿货 86 块 4 毛,双方一句"算了算 86",这 4 毛就不收了。如果系统直接把实收金额改掉,月底统计销售额时会发现和订单总额对不上,老板还会误以为有人漏单。我的做法是订单表单独放 discount_amount 字段,下单或结算时显式记录抹零金额,月底可以单独汇总"这个月共抹掉多少钱"。老板看过这个数以后反而有了谈判依据——给老客户让利,让在了明面上。

5.2 过磅差异

过磅差异我在 4.3 提过,这里补充一个容易踩的坑:整个系统的扣库存必须统一放在"过磅后按 actual_quantity 扣",而不是下单时扣一次、过磅修改时再扣一次。我早期版本是下单扣一次,过磅时按差额再修正,结果一周以后发现库存流水里全是"销售出库"和"过磅调整"交织,报表极其难看。后来改成:下单时不扣库存,只锁定预留库存;过磅确认后一次性按实际重量扣减并产生库存流水。这个改动让账务清晰很多。

5.3 账期与应收管理

批发客户按月结账很普遍,所以订单的"已结算"并不等于"已收款"。我把 Order.status 的 SETTLED 定义为"账务已确认",再单独用 Payment 表记录每次收款,按客户统计应收余额就是老客户欠款。再加一个简单的"账期预警":客户欠款超过 30 天或者超过预设额度,下单时给出黄色提示。老板说这个功能虽小,但真的帮他少做了好几笔坏账。

5.4 并发抢单与超卖

果蔬批发凌晨高峰期人手忙乱,两个店员在收银台同时操作很常见。如果库存处理不做并发控制,会出现明明只剩 50 斤白菜,两个单都下 40 斤,最后库存变成 -30 的诡异局面。Django 下用 select_for_update() 和事务包裹可以解决,但前提是必须保证所有修改库存的入口都走同一套逻辑。所以在项目里我把所有库存扣减统一收口到 StockService 类,不允许在视图里直接操作 product.stock,这样排查库存问题时只需要盯一个地方。

6. 部署与验收:从开发机到市场的最后一公里

6.1 开发环境准备

开发环境我建议用虚拟环境隔离,避免日常项目污染。搭建项目骨架时,我用 django-admin startproject config . 创建主项目,再用 python manage.py startapp market 创建业务应用,商品、订单、库存这些模型都放在 market 应用里。这个看似基础的命令有个小细节——把项目命名为 config 而不是项目本身名字,目的是让 WSGI 路径更固定,避免日后改目录名带来的一堆麻烦。

创建虚拟环境、安装 Django 和 MySQL 驱动后,接下来多数新手会卡在数据库连接上:Django 默认的 MySQLdb 在 Windows 下不友好,装 mysqlclient 又经常编译失败。最省事的方式是安装 pymysql,然后在项目init.py 里加一行配置。还要记得把 settings.py 里的时区改为 Asia/Shanghai,否则创建时间会差 8 小时,这个坑我花了一天才发现。

import pymysql pymysql.install_as_MySQLdb()

6.2 服务器部署

服务器我用的 Ubuntu,部署架构是 Nginx 反向代理 Gunicorn 的 Django 应用。Gunicorn 起 4 个 worker,配置 systemd 托管。关键步骤有三个:静态文件收集(Django 的 collectstatic)、生产环境关闭 DEBUG 并配置好 ALLOWED_HOSTS、给 Flask 报价服务单独配一个 systemd 单元。

部署脚本的核心配置大致如下:

# /etc/systemd/system/gunicorn.service [Unit] Description=gunicorn daemon for veg_market After=network.target [Service] User=www-data WorkingDirectory=/var/www/veg_market ExecStart=/var/www/veg_market/venv/bin/gunicorn --workers 4 --bind 127.0.0.1:8000 config.wsgi:application Restart=always [Install] WantedBy=multi-user.target

Nginx 配置里把 /static/ 直接指向静态文件目录,其余请求转给 127.0.0.1:8000。第一次上线时我发现图片偶尔加载不出,排查半天才发现是静态文件路径权限问题——Nginx 的 www-data 用户没有静态目录读权限,chown 之后解决。

6.3 上线后真实运营中的调整

这个系统上线第一周,真实使用中暴露了三个问题。第一个是订单列表分页慢,原因是最初用 Django ORM 的 filter 不加索引,数据量一大就开始卡,后来给 order_no、customer_id、created_at 都加了组合索引,问题解决。第二个是凌晨高峰期录单偶尔超时,原因是 Gunicorn worker 配置了同步类型,个别慢查询会阻塞其他请求,后来换了 gevent worker 并给关键列表页做了 Redis 缓存。第三个是打印单据格式问题,批发客户要的票据包含单位、规格、过磅重量,和常见电商小票完全不同,用 Django 模板单独写了打印页,配合浏览器的打印样式,才算是真正满足业务场景。

这些调整都是在小范围试运行期完成的,没有出现数据丢失,靠的是每次上线前都做了数据库备份和迁移回滚演练。

最后再分享一个小技巧:项目上线后,我每隔两天会跑一个简单的告警脚本,检查订单数、库存负数和接口响应时间。这个脚本是我自己用 Python 写的,每天早上八点把前一天的经营汇总推到手机。批发生意最怕的就是坏账和损耗,系统能做的不是帮你决定,而是把这些数字明明白白摆在你面前。对我来说,技术上最大的收获不是哪一行代码写得精妙,而是学会了站在批发商的角度去理解"数据"两个字的分量。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询