ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Python Flask实战:汽车用品进销存系统设计与实现

Python Flask实战:汽车用品进销存系统设计与实现 1. 汽车用品进销存到底在管什么先说清楚一件事汽车用品这个行业和普通零售差别挺大——SKU多且杂同一款脚垫可能有多种车型适配机油有不同标号雨刮器分前挡后挡甚至同一品牌不同批次进价都不一样。这种业务用Excel硬扛库存明细和财务对账能让人崩溃。我当初做这个基于Python和Flask的进销存管理系统就是被朋友车品行里的一堆破事逼出来的。进销存系统的核心说白了就三件事进采购入库、销销售出库、存实时库存再往深一层还得管退货、换货、调拨、盘点、供应商结算、客户账期。我见过不少人一上来就想着做得很庞大结果数据库十来个表互相乱关联最后自己都理不清。做这个系统时我定了个原则先把进、销、存、报表四条线跑通再考虑锦上添花的功能。系统本身定位是中小型汽车用品商行或个体经营者的内部工具不需要多复杂的分布式架构但要求部署简单、维护方便、数据不丢。用Flask做后端SQLite起步、后续可换MySQL前端用Jinja2模板加轻量JS加上Bootstrap做界面这套组合对一台普通Windows机器或云服务器都友好得很。如果你是做汽配、汽车美容养护用品这块的或者你只是好奇一个进销存系统怎么用Python落地这篇文章的思路应该都能给你点参考。2. 系统设计与技术选型为什么是Flask而不是Django或FastAPI2.1 进销存系统的核心需求拆解动手写代码之前我先花了几天时间梳理业务需求。朋友的车品行经营三类商品汽车养护用品机油、防冻液、玻璃水、洗车液、汽车电子配件行车记录仪、车充、车载吸尘器、内饰外饰件脚垫、座套、方向盘套。它们的共同点是有保质期或批次概念、供应商相对固定、客户会有赊账需求。基于这个背景我把系统需求拆成下面几个模块基础资料商品信息、供应商信息、客户信息。商品要支持按品牌、类别、适用车型分组。采购管理采购订单登记、采购入库验收、退货给供应商。销售管理销售订单登记、出库发货、销售退货。库存管理实时库存查询、库存流水、盘点调整、库存预警低于安全库存自动提醒。统计报表按时间段汇总采购金额、销售金额、毛利、库存周转天数。用户权限老板看全部数据店员只能操作销售和库存查询采购员管采购模块。这套需求要是用Excel做光是对账就得把人累死尤其多 SKU 加上批号、保质期的组合Excel 的筛选和透视表根本扛不住。用系统做本质上是把“人工记账”变成“结构化数据流”每一笔出入库都留下痕迹每一条流水都能追溯到单据。2.2 Flask与Django、FastAPI的取舍技术选型上我一开始就排除了Django——不是Django不好是它太重了。Django自带Admin后台、ORM、Migration、Auth一套全家桶学习曲线陡对小项目来说很多功能用不上反而碍事。FastAPI确实性能好自带API文档但当时我需要的是快速开发几个页面配合表单操作FastAPI的模板渲染和表单处理生态不如Flask成熟。而且Flask的文档和社区讨论量非常大遇到问题基本上搜一下就有答案这对单人维护项目来说太重要了。我用Flask还有一个私人心得Flask的灵活度让你能完全掌控项目结构不会像Django那样框架替你决定怎么组织代码。当然灵活也意味着约束少容易写成“面条代码”。我的做法是采用蓝图的模块化结构把采购、销售、库存、报表各拆成一个蓝图公共方法放utils里模型统一放models.py。这样既保留了Flask的轻又不至于让代码乱成一锅粥。对比下来我的结论是中小型内部管理系统Flask就是那个最不容易出错的选择。数据量日均几百条出入库记录并发量十个八个单机部署完全够用如果你预期未来要暴露大量API给其他系统对接再考虑FastAPI。进销存这类系统页面交互是主要使用场景Flask Jinja2 Bootstrap的组合开发效率最高。3. 数据库模型设计进销存系统的地基3.1 核心表结构到底怎么建表结构是进销存系统最关键的环节现在我单独强调这部分是因为我见过太多人在这里栽跟头。我设计的核心表有六张Supplier供应商、Customer客户、Product商品、PurchaseOrder采购单、PurchaseItem采购明细、SaleOrder销售单、SaleItem销售明细、InventoryLog库存流水另外还有一张 User 表做登录权限。Product表的关键字段包括商品编码唯一、商品名称、品牌、车型适配、分类、单位、采购价、销售价、安全库存、当前库存。需要注意的是采购价和销售价我单独存了快照而不实时关联供应商报价——因为历史订单上的价格必须和下单时一致如果供应商改了报价老订单也不能变。PurchaseOrder 和 PurchaseItem 是一对多的关系PurchaseItem 里记录商品ID、数量、单价、金额、生产批次、到期日期。这里有一个很容易被忽略的点汽车养护用品是有保质期的机油一般三到五年但玻璃水两三年有的洗车液开封后保质期更短。批次和到期日期的记录直接影响后续的先进先出(FIFO)成本核算和临期预警功能所以从第一版设计开始就不能省。SaleItem 除了记录商品、数量、单价还记录成本价——这个成本价不是商品表里的当前采购价而是出库时的加权移动平均成本。进销存系统最核心也最容易算错的就是毛利毛利 销售收入 - 出库成本出库成本算不准报表上的毛利就是自欺欺人。实现上我在每次采购入库时重新算一遍该商品的加权平均成本出库时取当前平均成本作为SaleItem的cost字段快照。3.2 实时库存与流水账谁才是真相进销存系统里的“库存”有两个层面一个是Product表上的库存数字一个是InventoryLog表里的流水明细。很多新手会直接拿Product表的库存字段去做页面展示但这会出大问题——一旦发生并发操作或者数据修复库存数字就可能失真而且你完全不知道它是什么时候错的。我的做法是Product表的当前库存只是副本InventoryLog才是库存真相。每一笔采购入库、销售出库、退货、盘盈盘亏都必须在InventoryLog里写一条流水记录同时更新Product表的库存数字。这两个操作放在同一个事务里要么都成功要么都回滚。查询库存时如果怀疑数据有问题直接按商品ID把流水SUM一遍就能对账排查起来非常快。还有一个细节是批次管理。汽车用品不是批次敏感的行业像餐饮或者医药那样必须严格走批次追溯但是机油这类商品确实存在不同批次价格不同、质量问题只能追某批次的情况。我设计里允许选填批次入库时录入出库时优先扣减最早批次FIFO这样既照顾了实际操作没必要每个商品都强制批次又能在需要时查到某批次的流向。这一步一开始做后面要加保质期管理和效期预警都方便。3.3 金额存小数别踩这个坑浮点数的精度问题在进销存里是致命的。Python的float在计算0.10.2的时候给的是0.30000000000000004这个误差平时无感但在金额累计计算里会越滚越大到月底对账差几毛钱甚至几块钱你根本找不着哪里错了。处理方式也简单金额字段全部用Decimal。SQLAlchemy里对应DECIMAL类型Python端统一用decimal.Decimal计算前端展示做四舍五入保留两位小数。我见过有人图省事用Float存金额后来对账出现了莫名其妙的分差排查了一下午才发现是浮点误差累积出来的。进销存系统里“分”这个单位是底线一分钱都不能差。4. 核心功能实现从登录鉴权到库存盘点4.1 Flask项目结构布局项目目录我建议这么分层清晰度高方便后面加模块car_parts_ims/ ├── app.py # 应用入口 ├── config.py # 配置数据库、密钥等 ├── extensions.py # 初始化SQLAlchemy、LoginManager ├── models/ │ ├── __init__.py │ ├── base.py # 公共基类 │ ├── product.py # 商品模型 │ ├── trade.py # 采购/销售/库存流水模型 │ └── user.py # 用户模型 ├── blueprints/ │ ├── __init__.py │ ├── auth/ # 登录、登出、改密码 │ ├── purchase/ # 采购相关路由 │ ├── sale/ # 销售相关路由 │ ├── inventory/ # 库存查询与盘点 │ └── report/ # 报表统计 ├── templates/ # Jinja2模板 ├── static/ # JS/CSS └── utils/ ├── decorators.py # 权限控制装饰器 └── helpers.py # 公共函数库存更新、流水记录等这样做的核心思路是业务代码按领域划分公共逻辑统一收口。比如库存更新这个操作采购入库、销售出库、退货、盘点修正都要用到就写在utils/helpers.py里做成一个通用的inventory_adjust(product_id, quantity, log_type, ref_order_no)函数带事务控制谁调都不会错。4.2 登录鉴权与角色权限控制登录鉴权我用了Flask-Login这个扩展注册登录、会话管理、记住我这些功能都有不用自己造轮子。User表加一个role字段值分admin、sales、purchase、stock四种分别代表老板、店员、采购员、仓管员。权限控制用自定义装饰器实现比如from functools import wraps from flask import abort from flask_login import current_user def role_required(*roles): def decorator(func): wraps(func) def wrapper(*args, **kwargs): if not current_user.is_authenticated: return redirect(url_for(auth.login)) if current_user.role not in roles: abort(403) return func(*args, **kwargs) return wrapper return decorator用法很简洁在视图函数上直接标注bp.route(/purchase/add, methods[GET, POST]) login_required role_required(admin, purchase) def add_purchase(): ...密码存储用Werkzeug自带的generate_password_hash和check_password_hash别自己写加密。这里有个良心提醒别图省事把密码明文存数据库你永远不知道哪天项目文件会被传出公司内外网明文密码等于裸奔。4.3 采购入库与销售出库的接口编写细节采购入库的逻辑是整个系统最复杂的部分因为它涉及的事务太多了创建采购单、批量写入采购明细、逐条更新库存、写库存流水。我建议核心代码控制在事务里一层层处理from decimal import Decimal from extensions import db def process_purchase_order(form_data): 处理采购入库返回是否成功及错误信息 try: # 1. 创建采购单主表 po PurchaseOrder( supplier_idform_data[supplier_id], order_nogenerate_order_no(PO), total_amountDecimal(0), statusconfirmed ) db.session.add(po) db.session.flush() # 先拿到po.id total Decimal(0) # 2. 逐条处理采购明细 for item in form_data[items]: product Product.query.get(item[product_id]) price Decimal(item[price]) qty Decimal(item[quantity]) amount price * qty detail PurchaseItem( purchase_order_idpo.id, product_idproduct.id, quantityqty, unit_priceprice, amountamount, batch_noitem.get(batch_no), expire_dateparse_date(item.get(expire_date)) ) db.session.add(detail) total amount # 3. 更新库存流水 update_inventory_with_log( product_idproduct.id, quantityqty, change_typepurchase_in, ref_nopo.order_no ) po.total_amount total db.session.commit() return True, 采购单创建成功 except Exception as e: db.session.rollback() return False, f创建失败{str(e)}两个要点一是generate_order_no生成订单号建议用日期流水号格式如 PO20250115001避免并发时重复。二是库存更新一定要在事务里和采购单一起提交如果采购单写了库存没更新数据就分裂了。我在实际开发中测试过一种情况采购明细录入到一半页面崩溃事务回滚后库存和采购单都没有被污染这就是ORM事务管理的价值。销售出库逻辑上正好反过来检查库存充足、扣减库存、记流水、生成销售单。有一个容易踩的坑是高并发场景下两个订单同时抢同一件商品的最后一件库存后端逻辑如果不加锁就会出现“超卖”。我在update_inventory_with_log里对库存量做了条件更新def safe_deduct_stock(product_id, qty): product Product.query.with_for_update().filter_by(idproduct_id).first() if product.stock qty: return False product.stock - qty return Truewith_for_update()是数据库行级锁SQLite不生效但在MySQL下有效能确保同一时间只有一个事务在扣减某件商品的库存。对小系统来说这行代码就是防止超卖的保险丝。4.4 库存预警和盘点进销存的增值功能库存预警其实是“算”出来的不是额外功能。我在Product表里设计了min_stock安全库存字段每次库存变动后检查当前库存是否低于安全库存如果是就在首页顶栏显示一条黄色横幅提示哪些商品需要补货。报警的触发条件是库存变动事件而不是定时轮询——这样省资源而且响应也及时。盘点功能则是解决“账面库存和实际库存不一致”的问题。实践中库存不准确的原因很多收货时点数错了、销售出库时拿错了型号、退回来的商品没及时录系统、甚至货损没有记录。每月做一次盘点把实盘数量录入系统系统自动计算盘盈盘亏并生成盘点单库存流水里记录adjustment类型账面就对齐了。5. 报表统计与前端页面既要老板看得懂也要店员用得顺5.1 月度销售统计怎么算才准报表模块最核心的是月度销售统计和毛利统计。销售统计要支持按时间范围筛选按商品或商品分类汇总数量、金额毛利统计需要把我前面提到的成本快照用起来SELECT p.category, SUM(si.quantity) AS qty, SUM(si.amount) AS sale_amount, SUM(si.cost_amount) AS cost_amount, SUM(si.amount - si.cost_amount) AS gross_profit FROM sale_item si JOIN product p ON si.product_id p.id JOIN sale_order so ON si.sale_order_id so.id WHERE so.created_at BETWEEN :start AND :end GROUP BY p.categorySQL里的sale_amount - cost_amount算出的就是毛利。这里有个常见误区是把商品表里的销售价 - 当前采购价当毛利但如果期间进过两次货、价格不同算出来就完全错了。必须用SaleItem表里下单时快照的价格和成本。报表页面我用ECharts画了柱状图和饼图展示月度采购/销售趋势、销售Top10商品。前端图表用ECharts很成熟一个JS库搞定不用自己造轮子。5.2 页面交互表单校验和操作体验业务系统的页面不用炫酷但必须好用。操作频率最高的是销售开单我把表单设计成一行商品一个输入组支持动态增加多行默认带出商品的销售价数量可以手改金额自动计算。表单提交前用前端JS做基本校验必填项是否为空、数量是否为正数、库存够不够。后端再用WTForms做二次验证防止绕过前端抓接口直接调用。另外一个容易忽略的点是操作反馈。所有增删改操作完成之后用flash消息提示“采购单PO20250115001创建成功”或者“商品库存不足当前仅剩4件”让操作的人第一时间知道结果。做内部系统最忌讳的就是点了按钮没反应用户会以为卡了又点一次结果重复提交了两笔单子。6. 部署上线与常见问题排查实录6.1 部署到云服务器这几个坑我先替你踩了这套Flask进销存系统我用两种方式部署过。一种是开发环境直接跑起来测试pip install -r requirements.txt flask --app app.py init-db flask --app app.py run --host0.0.0.0 --port5000另一种是正式生产环境我推荐Gunicorn Nginx的方式。Flask自带的Werkzeug开发服务器是单进程的不适合生产环境Gunicorn可以跑多worker并发处理请求配置也简单gunicorn -w 4 -b 127.0.0.1:5000 app:appNginx负责反向代理和静态文件处理配置个server块转发/到5000端口就行。生产环境千万别直接在命令行跑flask runPython进程一断开服务就挂了要用systemd或者supervisor托管Gunicorn进程设成开机自启和崩溃自动重启。数据库方面开发用SQLite足够但正式多了并发操作后SQLite的写锁问题会暴露——多个人同时开单就可能报database is locked。后期我迁移到了MySQLSQLAlchemy的ORM层基本不用改把连接字符串换一下就行。这里也说明一开始就用ORM的好处切换数据库的成本低得多。6.2 高频问题速查表建议直接收藏我在实际运行中遇到过的问题整理成了一张速查表覆盖大部分进销存系统上线初期的常见问题问题现象可能原因排查方法解决方案登录后跳回登录页session未持久化或SECRET_KEY过期检查浏览器cookie看服务端日志设置稳定的SECRET_KEY用Flask-Login的remember_me采购单保存后库存没变库存更新代码未在同一事务提交查InventoryLog表是否有对应流水检查事务提交位置参考3.2节事务写法销售出库显示库存不足多用户并发抢库存查看库存流水是否有超扣记录使用with_for_update行级锁见4.3节报表毛利对不上SaleItem成本快照为空查老订单的cost字段确保出库时写成本快照重新生成历史订单成本金额合计差几分钱浮点运算精度问题对比Decimal和Float计算结果统一用Decimal别用Float存金额大批量查询页面很慢关联查询N1问题开启SQLAlchemy echo看启动的SQL使用joinedload预加载关联表6.3 数据备份与恢复策略内部管理系统的数据是无价的系统可以重新写但三年的出入库记录丢了就真的丢了。部署时我用crontab每天凌晨备份SQLite文件到服务器磁盘同时每周异地备份一次到对象存储。如果是MySQL用mysqldump定时导出即可。每次恢复之前先在测试环境验证备份文件的可用性——这个步骤看似多余但等你真需要恢复的时候就会发现它救过你一命。我实际踩过一次备份失效的坑SQLite备份是在程序运行状态下直接cp文件导致备份文件损坏无法打开。解决方案是使用SQLite的在线备份API或者先停服务再复制。用MySQL就没这个问题InnoDB的备份机制要稳定得多这也是我后期迁数据库的另一个原因。7. 最后分享几个实战经验整套系统从设计到上线我一个人用了大约两个星期白天调需求晚上写代码中间还推倒重来了一次数据模型——就是前面说的库存快照问题第一版没存成本价事后补数据补到崩溃。这段经历告诉我一个道理进销存这类管理系统数据模型必须先于代码想清楚尤其是快照类字段价格、成本、库存宁可在建表时多冗余几个字段也别在出问题后再想办法“追溯”。如果你也想自己做一套我给你三条非常具体的建议。第一先画数据流程图再写代码把采购、销售、退货、调拨这些单据的流转方向理清楚第二库存流水表是整个系统的账本任何库存变动都必须落流水这个习惯绝对没有坏处第三权限别贪多老板、采购、销售、仓管四种角色已经覆盖绝大多数中小商行的需求做太细自己维护也麻烦。最后再分享一个小技巧Flask的Debug模式下调试方便但生产环境一定要关闭debugTrue会把堆栈信息直接露给前端等于把系统内部结构白送给别人。配置环境变量区分开发与生产环境这个习惯越早养成越好。这套系统后续还可以扩展客户账期管理、供应商对账、条形码扫描枪对接这些功能。进销存业务说复杂也复杂说简单也简单核心就是保证“账实相符”四个字所有设计都应该围绕这个目标去倒推。希望这篇文章能给你一些启发少走一些我当初走过的弯路。
返回列表