ARTICLE DETAIL

资讯详情

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

Python Flask进销存系统开发实战:从需求拆解到部署上线

Python Flask进销存系统开发实战:从需求拆解到部署上线 做企业进销存系统几乎是每个Python学习者入门Web开发之后都会想碰的项目类型。不管是课程设计、毕业设计还是公司内部的小工具进销存这个场景几乎覆盖了Web开发的所有基本功数据建模、增删改查、登录权限、关联事务、报表统计。前阵子我刚把一套以Gucci品牌为业务背景的进销存系统从零到尾完整做了一遍技术栈就是Python Flask顺手把整个设计思路、核心代码和踩坑记录都沉淀了下来。这篇就当是给正在做类似项目的朋友一份能直接参考的实操笔记从需求拆解到部署上线全程不藏私。1. 立项背景与需求拆解1.1 为什么选Gucci这种业务背景很多人看到Gucci两个字第一反应是奢侈品、高冷跟写代码有什么关系。其实恰恰相反奢侈品零售的进销存管理比普通快消品更讲究细节非常适合拿来当业务案例——因为奢侈品的库存金额高容错率极低一件货品动辄几千上万盘点错一件都是不小的损失。用这种高要求的业务场景去设计系统你的表结构、字段设计、报表口径都会不自觉做得更严谨。具体来说Gucci这类品牌零售有几个明显特点首先是SKU维度复杂同一个款式下面有颜色、尺码、系列、批次等多个组合维度商品的唯一标识不能只靠一个名称字段其次是价格体系复杂进货价、吊牌价、折扣价、会员价并存不能混为一谈第三是客户管理重视VIP体系销售单要能追溯到具体导购方便后续做客户关怀和销售提成核算。虽然我们实际项目里商品表未必真的填Gucci的商品数据但整套业务逻辑是完全可复用的你把它换成任何中高端服饰、化妆品、珠宝品牌都一样成立。1.2 进销存的业务闭环到底长什么样进销存三个字拆开看进是采购入库销是销售出库存是库存状态。系统要解决的其实就是一条完整的货物流转闭环——从供应商那里采购商品商品进入仓库形成库存再通过销售环节把商品卖给客户库存相应减少。听起来简单但实际操作中每个环节都牵着两张关联单据一个主表记录单据本身一个明细表记录单据里包含的商品明细这样才能支持一张采购单里同时进多款商品。我设计系统之前先画了一遍业务角色和权限采购员负责创建采购单、确认入库仓库管理员负责库存查询和盘点调整销售员负责开销售单和退货单管理员负责用户管理、商品维护、数据报表。不同角色的操作边界不同所以登录之后的权限控制必须从最开始就纳入设计。很多新手一上来就建表写界面结果做着做着发现角色权限、库存流水这些关键点全漏了返工成本极高。我建议任何进销存项目都先花半天时间把业务角色、单据流转、库存变化规则理清楚这个前置工作比写代码更有价值。进销存的库存变化是核心中的核心。采购单确认入库时库存增加销售单确认出库时库存减少盘点时发现账实不符可以做盈损调整。每次库存变动都要在流水表里留痕记录变动前后的数值、变动类型、操作人、操作时间。这样一来哪怕后面某些单据录错了也可以通过流水回溯到底哪一步出了问题而不是看着一个孤零零的库存数字无从下手。2. 技术选型与架构设计2.1 Flask和Django之间我为什么选了Flask技术选型这件事很多初学者喜欢问“选哪个框架好”。我的判断标准很简单项目复杂度、团队熟悉度、交付周期。如果是做一个后台管理系统Django确实开箱即用自带Admin后台和大量生态组件但Django的“全家桶”在项目小的时候反而显得笨重。Flask的优势在于轻、灵活、可控性强你可以按需引入扩展对于理解Web框架底层的请求响应、路由、模板渲染机制很有帮助。这套进销存系统的核心是CRUD加报表Flask完全hold得住体量也正合适。具体到技术组合我用的是Flask Flask-SQLAlchemy Flask-WTF Jinja2。Flask-SQLAlchemy是ORM层负责把Python类映射成数据库表写代码的时候不用拼SQL字符串通过模型类就能操作数据Flask-WTF负责表单渲染和CSRF防护避免自己手写校验逻辑Jinja2是Flask自带的模板引擎配合Bootstrap做前端页面开发效率很高。数据库开发阶段用SQLite就行零配置、文件型存储方便本地调试部署到生产环境时再换MySQL因为Flask-SQLAlchemy的统一ORM接口切换成本很低。2.2 数据库模型设计主表、明细表、流水表三层结构进销存的数据库设计是整个项目的地基。我按照“主表 明细表 流水表”的三层结构来组织。先看供应商和客户这类基础资料表供应商表存名称、联系人、电话、备注客户表还要额外加客户等级字段方便后面做VIP区分class Supplier(db.Model): __tablename__ supplier id db.Column(db.Integer, primary_keyTrue) name db.Column(db.String(64), uniqueTrue, nullableFalse) contact db.Column(db.String(32)) phone db.Column(db.String(32)) remark db.Column(db.Text) created_at db.Column(db.DateTime, defaultdatetime.now)商品表是核心基础表除了基础信息外我给每个商品加了唯一编号、分类、品牌系列、进价、售价、库存量。有人可能觉得库存量应该单独建表存但实际业务中直接在商品表冗余一个stock字段查询列表时效率最高。注意这里的进价和售价都用Numeric类型而不是Float因为浮点数在做金额累加、比较时会有精度误差这在财务场景里是不能接受的。采购和销售两块采用主表加明细表的结构。以采购单为例class PurchaseOrder(db.Model): __tablename__ purchase_order id db.Column(db.Integer, primary_keyTrue) order_no db.Column(db.String(32), uniqueTrue, nullableFalse) supplier_id db.Column(db.Integer, db.ForeignKey(supplier.id)) total_amount db.Column(db.Numeric(12, 2), default0) status db.Column(db.Integer, default0) # 0未入库 1已入库 operator_id db.Column(db.Integer, db.ForeignKey(user.id)) created_at db.Column(db.DateTime, defaultdatetime.now) class PurchaseDetail(db.Model): __tablename__ purchase_detail id db.Column(db.Integer, primary_keyTrue) order_id db.Column(db.Integer, db.ForeignKey(purchase_order.id)) goods_id db.Column(db.Integer, db.ForeignKey(goods.id)) quantity db.Column(db.Integer, nullableFalse) price db.Column(db.Numeric(10, 2), nullableFalse)为什么主表和明细表要分开因为一张采购单对应多个商品如果把所有商品塞在一行里后续想按单号统计、按商品维度汇总都会很痛苦。主表存单据级别的公共信息单号、供应商、总金额、状态明细表存商品级别的行数据哪个商品、多少数量、什么单价两张表通过外键关联。销售侧的SaleOrder和SaleDetail结构完全对称只是多存了客户ID、导购ID和折扣金额。流水表是很多人容易忽略的设计。库存不能只靠商品表的stock字段还要有StockLog流水表记录每一次变动class StockLog(db.Model): __tablename__ stock_log id db.Column(db.Integer, primary_keyTrue) goods_id db.Column(db.Integer, db.ForeignKey(goods.id)) change_type db.Column(db.String(16)) # purchase_in / sale_out / adjust quantity db.Column(db.Integer) before_qty db.Column(db.Integer) after_qty db.Column(db.Integer) operator_id db.Column(db.Integer, db.ForeignKey(user.id)) created_at db.Column(db.DateTime, defaultdatetime.now)流水表的价值在于“可追溯”。库存有变动的时候把变动前数值、变动后数值、变动数量全部记录下来后面对账、审计、找错都有据可查。我见过很多项目不做流水库存错了只能靠人肉翻单据那体验真的酸爽。2.3 项目目录结构用蓝图拆分模块Flask项目最大的坑之一就是所有路由堆在同一个文件里写到最后几千行代码根本没法维护。我一开始就把项目按照功能拆成了多个蓝图Blueprint目录结构长这样gucci_erp/ ├── app.py # 应用入口 ├── config.py # 配置项 ├── extensions.py # db实例和扩展初始化 ├── models.py # 所有数据模型 ├── forms.py # 表单类定义 ├── views/ │ ├── __init__.py │ ├── auth.py # 登录认证 │ ├── goods.py # 商品管理 │ ├── purchase.py # 采购管理 │ ├── sale.py # 销售管理 │ ├── inventory.py # 库存管理 │ └── dashboard.py # 首页统计和报表 ├── templates/ # Jinja2模板 ├── static/ # CSS、JS、图片 └── requirements.txt注意我把db实例放在了extensions.py里而不是models.py。很多人习惯在models.py里顺便初始化db结果后面视图、表单都要import模型和db导入顺序一乱就报循环导入错误。单独搞一个extensions.py让models和views都从它那里拿db实例这个坑就能彻底避开我在第5节会详细展开讲这个问题。3. 核心功能模块实现3.1 采购入库事务、单号和库存联动采购入库是“进”的核心环节。用户在页面上选择供应商、录入商品明细商品、数量、进价提交后系统要做三件事生成采购主单、生成采购明细、更新商品库存并写流水。这三件事必须在一个数据库事务里完成任何一个步骤失败都要整体回滚否则会出现“单据没了但库存加了”这种不一致状态。我实现采购入库的核心代码大概是这样的bp.route(/purchase/add, methods[POST]) login_required def add_purchase(): form PurchaseForm() if form.validate_on_submit(): order PurchaseOrder( order_nogenerate_order_no(PO), supplier_idform.supplier_id.data, operator_idsession.get(user_id), remarkform.remark.data ) db.session.add(order) db.session.flush() total 0 for item in form.details.data: goods Goods.query.get(item[goods_id]) before_qty goods.stock quantity item[quantity] price item[price] detail PurchaseDetail( order_idorder.id, goods_idgoods.id, quantityquantity, priceprice ) db.session.add(detail) goods.stock quantity db.session.add(StockLog( goods_idgoods.id, change_typepurchase_in, quantityquantity, before_qtybefore_qty, after_qtygoods.stock, operator_idsession.get(user_id) )) total quantity * price order.total_amount total db.session.commit() flash(采购入库成功) return redirect(url_for(purchase.list))这里有几个细节值得说明。第一db.session.flush()的作用是先把主单写入数据库拿到自增ID这样明细才能通过order_id关联但flush之后事务还没提交后续出错仍然可以rollback这是事务控制的常见技巧。第二库存增加和流水写入必须同时进行顺序上先拿before_qty再更新stock保证流水里记的前后数值是准确的。第三单号用generate_order_no生成格式是PO加日期加随机数保证唯一性避免后续按单号关联时撞车。3.2 销售出库库存不足校验和数量回滚销售出库的逻辑和采购入库相反但多了一道“库存不足校验”。用户开销售单时系统先遍历明细里的每个商品检查当前库存是否够卖。如果任何一个商品库存不够整个订单都不能提交——注意校验必须在事务内完成不能用“先扣库存再发现不够改回去”的土办法那样容易因为并发问题产生负库存。销售出库还有个常见需求是退货。客户的退货单本质上是把商品重新加回库存同时生成负向的销售记录。我在销售模块里单独提供了一个退货入口退货时也会更新库存并写一条change_type为sale_return的流水退货金额直接从客户应付款里扣除。这里建议金额计算单独封装成函数避免采购金额和销售金额混在一起算错。3.3 库存查询和报表统计库存模块承担的是“存”的视角。用户需要能随时查当前库存列表包括库存不足预警——我给商品表加了min_stock字段当现货库存低于这个阈值时列表里用红色标识提醒补货。库存盘点则走调整功能盘点人员录入实盘数量系统自动计算盈亏数并生成调整流水。报表统计是这套系统的亮点。进销存数据积累起来之后管理者最关心的几个数字是本月的采购总额、销售总额、毛利、动销商品排行。我在首页dashboard用SQLAlchemy的聚合查询做了这几个统计from sqlalchemy import func today date.today() first_day today.replace(day1) sale_total db.session.query( func.coalesce(func.sum(SaleOrder.total_amount), 0) ).filter(SaleOrder.created_at first_day).scalar() sale_count db.session.query( func.count(SaleOrder.id) ).filter(SaleOrder.created_at first_day).scalar() top_goods db.session.query( SaleDetail.goods_id, func.sum(SaleDetail.quantity).label(qty) ).group_by(SaleDetail.goods_id).order_by(func.sum(SaleDetail.quantity).desc()).limit(10).all()查询结果直接传给模板用Jinja2循环渲染成表格。这里用scalar()拿单个数值比query.one()更稳尤其是没有数据的时候不会抛异常。报表这一块不需要做得很花哨先把口径定义清楚采购总额按入库单统计、销售总额按出库单统计、毛利按销售金额减商品成本金额计算口径清楚了数字才有意义。4. 实操中的关键细节4.1 环境搭建为什么必须用虚拟环境很多初学者一开始图省事直接在系统Python里pip install flask装了一堆全局包最后项目之间互相冲突想卸载都不知道哪个依赖谁。这套项目我强烈建议创建独立虚拟环境步骤很简单python -m venv venv source venv/bin/activate # Windows下用 venv\Scripts\activate pip install flask flask-sqlalchemy flask-wtf python-dotenvPython 3.8以上都自带venv模块不用额外安装。激活虚拟环境之后pip安装的所有包都进当前项目的venv目录不会污染系统环境。依赖装好后用pip freeze生成requirements.txt锁定版本换机器或者部署时一键恢复环境。如果pip下载慢配置一下国内镜像源能省很多时间pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple4.2 登录认证和权限控制的实现进销存系统不能裸奔至少要区分登录用户和匿名访客。我用Flask的session实现最简单的登录态用户登录成功后把user_id和username写进session之后每次请求通过装饰器检查session里有没有user_id。以下是login_required装饰器的标准写法from functools import wraps from flask import session, redirect, url_for, flash def login_required(f): wraps(f) def wrapper(*args, **kwargs): if not session.get(user_id): flash(请先登录) return redirect(url_for(auth.login)) return f(*args, **kwargs) return wrapper登录之后还需要控制不同角色的权限。我的做法很朴素但够用User表里存一个role字段取值是admin/warehouse/sales/purchase然后针对每个蓝图判断当前用户角色是否允许访问。比如采购蓝图的视图函数加一个role_required(purchase)装饰器销售蓝图只允许purchase和admin角色进入。Flask没有内置用户系统但用Session加装饰器这套组合足够应付进销存这种内部系统不需要为这点需求引入Flask-Login。写登录认证时还有几个必须注意的地方。第一app.secret_key务必设置session才能被签名防篡改开发环境随便写个随机字符串生产环境用环境变量传入不要硬编码在代码里。第二密码不能明文存至少用werkzeug.security自带的generate_password_hash存哈希值校验时用check_password_hash。第三CSRF防护必须做Flask-WTF的表单默认自带CSRF Token渲染表单时要在模板里加hidden_tag()这是很多人会漏的一步漏了之后表单提交会报400错误。4.3 前端模板的继承和分页组件前端层面我用Jinja2的模板继承做了一个公共布局base.html把导航栏、侧边栏、消息提示都放在基础模板里每个页面的模板只要继承它再填充content块即可。页面风格用Bootstrap表格、按钮、表单控件都是现成的配合Jinja2的条件判断和循环渲染数据。封装分页组件这个事值得单独说一说。数据量小的时候不分页没问题但进销存跑几个月之后一张采购单列表可能有几百上千条记录不分页页面会卡。Flask-SQLAlchemy的paginate方法很好用page request.args.get(page, typeint, default1) pagination PurchaseOrder.query.order_by( PurchaseOrder.created_at.desc() ).paginate(pagepage, per_page10, error_outFalse) orders pagination.items把pagination对象整体传给模板前端用pagination.iter_pages()渲染页码实现上一页、下一页、页码跳转。我这里把小技巧记下来error_outFalse这个参数很重要意思是当page超出最大页数时不抛404而是返回空列表避免用户点下一页点到边界报错。5. 常见问题与排查实录5.1 循环导入错误百思不得其解的ImportError这个坑我几乎每次写Flask项目都会遇到排查过很多次所以单独拿出来讲。典型报错长这样ImportError: cannot import name db from partially initialized module models问题根源在于models.py和views文件互相importmodels.py里是模型的定义里面用到db.Columnviews文件里要import模型来查询。如果models.py在import db之前就被views引用Python解释器就会进入“部分初始化”状态报出这个看似莫名其妙的错误。解决办法就是我在目录结构里提到的把db单独放在extensions.py中models.py和views全都从extensions import db谁都不互相依赖导入顺序问题天然消失。5.2 中文乱码和模板404中文乱码一般有两个来源。一个是数据库层面的字符集问题如果用MySQL建表时没指定utf8mb4存中文就会变问号或乱码解决办法是建库时就用CREATE DATABASE ... CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。另一个是浏览器页面乱码多半是模板文件头没加meta charsetutf-8Jinja2模板继承的base.html里把这个标签写上就行。模板404的典型场景是路由写对了、视图函数也正常但访问时总是404。排查步骤很简单——检查templates文件夹的位置。Flask默认从应用根目录下的templates目录找模板如果你的项目把app实例建在子目录里记得在创建Flask实例时指定template_folder参数否则Jinja2找不到模板就报404。我见过有人把模板放在了static目录下面那也算一种可用方案但建议还是按规范来。5.3 端口冲突和启动失败开发调试阶段最常见的是启动时就报“Address already in use”。这是因为上一个Flask进程没完全退出5000端口还被占着。解决方式有两种杀掉占用进程或者换个端口启动。Flask的app.run支持指定端口if __name__ __main__: app.run(debugTrue, host127.0.0.1, port5001)顺带一提debugTrue模式在开发阶段很有用代码改了自动重载报错页面会直接显示堆栈信息。但上线生产环境务必关掉debug否则任何人访问都能看到你的源码报错信息这是很基础也很致命的安全隐患。5.4 常见问题速查表我把自己在这个项目里遇到的典型问题整理成了表格给后面做类似项目的朋友当参考问题现象可能原因解决方案ImportError: partially initialized module模型和views互相importdb实例抽到独立文件页面访问404模板没渲染templates目录路径不对检查template_folder参数库存变负了销售出库没做库存校验或并发竞争事务内先校验后扣减表单提交400缺少CSRF Token模板加hidden_tag中文存库后变问号数据库字符集不是utf8mb4建库时指定字符集端口被占用旧进程未释放换端口或杀进程6. 项目测试与部署实践6.1 功能自测清单上线前必须过一遍系统写完不是结束还要做一轮完整的自测。我习惯在交付之前拉一个功能清单逐项手动验证同时配合pytest写一些关键接口的自动化测试。下面是我这套系统的自测清单模块测试点预期结果登录认证错误密码登录提示错误不跳转商品管理新增商品、编辑价格列表实时更新采购入库新增采购单含多商品库存增加、流水生成销售出库库存不足时提交拦截并提示退货对已售商品退货库存回补、金额冲减库存盘点盘点调整数量流水留痕报表切换月份查询统计数值正确自动化测试这边我用pytest写了几个简单的接口测试核心是验证采购入库后库存确实增加、销售出库库存确实扣减以及未登录用户访问受保护页面会被重定向到登录页。进销存是重业务逻辑的系统自动化测试不一定覆盖所有界面操作但关键事务路径库存变更必须有测试兜底这是我最看重的一点。6.2 部署上线从开发模式切换到生产模式部署这块我在Linux服务器上用的方案是Gunicorn Nginx反向代理。Flask自带的开发服务器只适合调试并发能力有限上线必须换Gunicorn这类生产级WSGI服务器pip install gunicorn gunicorn -w 4 -b 127.0.0.1:8000 app:appNginx在这里负责接收外部请求并转发到Gunicorn同时处理静态文件。这里有个细节如果项目用了SQLite部署时注意数据库文件的路径要写对最好用绝对路径因为工作目录不同时相对路径经常找不到数据库文件如果切换到MySQL记得把数据库连接串改成mysql驱动格式安装pymysql库SQLALCHEMY_DATABASE_URI mysqlpymysql://user:passwordlocalhost/gucci_erp?charsetutf8mb4上线前还务必改几处配置关闭debug模式、换新的secret_key、用环境变量管理数据库密码。这些看起来琐碎但每一条都是实际项目中可能炸雷的点。我在部署时还踩过一个坑Gunicorn启动时如果提示bind地址不对先确认Nginx配置文件里的proxy_pass地址和Gunicorn监听地址一致两个端口对不上时反代就会报502。收尾前的几句体己话这套系统完整做下来我最大的感受是进销存一点都不难难点全在业务细节的严谨性上。表结构设计时多想一步流水留痕写销售逻辑时多写一行库存校验部署上线时多看一眼配置项这些看似不起眼的动作决定了系统是“能跑”还是“好用”。我自己也是在踩过库存对不上、查询慢、被CSRF拦了半天之后才把这些细节逐个补上的。最后再分享一个小技巧做这种管理系统的时候不要把商品名称、客户姓名这种基础资料硬编码在代码里全部走数据库维护后续加字段、改分类都会轻松很多。把基础数据治理好后面的采购、销售、报表模块写起来就是水到渠成的事。希望这份笔记能帮你在自己的Flask进销存项目上少走几个来回把功夫花在真正重要的事情上。
返回列表