基于Flask的中药材进销存系统设计与实战部署

发布时间:2026/10/1 17:52:51
基于Flask的中药材进销存系统设计与实战部署
这几年被问得最多的 Python 实战项目除了爬虫之外就是各类管理系统。而中药材方向的进销存系统从在校学生到药材商行老板都来问过。今天这篇就把基于 Flask 的中药材进销存管理系统的完整思路拆开讲一遍从技术选型到数据库建模从核心代码到部署上线一次性说透。先说明白这东西能干什么它解决的是中药材流通环节里台账混乱、批次不清、保质期记不住三个老大难问题。如果你是刚学完 Flask 想做点拿得出手的项目或者是给家里的小药材铺做一套内部工具又或者是做毕业设计需要一套业务闭环完整的系统这篇都适用。1. 被问过一百遍的问题为什么是 Flask 而不是 Django用 Flask 还是 Django几乎是每个做 Python Web 项目的人绕不开的抉择。就这套中药材进销存系统来说我的答案很简单业务复杂度没有高到需要 Django 的全家桶但 Flask 的灵活性刚好能覆盖全部需求。1.1 这个系统的业务体量决定了技术选型进销存系统本质上是三个核心操作进货、存货、销货。听起来简单但它牵扯到用户权限、库存流水、订单管理、预警通知、报表统计这些模块。在 Django 里面自带的 Admin 后台和 ORM 确实能省不少事但它的 Model 迁移、中间件、信号机制对于小团队维护来说学习成本并不低。Flask 的优势在于你想要什么就往里加什么。比如这套系统初期只需要 SQLite 就够了等数据量上来了再切换到 MySQLSQLAlchemy 的 ORM 层几乎不用改。而且 Flask 的路由设计非常直白一个进货单的提交就是app.route(/purchase/add, methods[POST])这样一个函数新人看代码也不至于一头雾水。1.2 Flask 核心优势在中药材业务里的实际体现中药材进销存有个特殊需求批次管理。同一味黄芪今天进的和三个月前进的产地可能不同炮制规格可能不同有效期也完全不同。这就意味着系统里很多业务逻辑是高度定制化的并不能直接套用现成的进销存模板。Flask 的蓝图Blueprint机制在这种场景下非常好用。你可以把采购入库、库存管理、销售出库、系统设置拆成四个蓝本彼此独立又共享同一个数据库会话。后期给系统加模块比如加一个养护记录子模块直接新建一个 py 文件注册进应用即可完全不伤筋动骨。再者Flask 周边生态的够用就好风格也和这个项目非常搭。Flask-Login 搞定登录会话Flask-Admin 可以做简单的数据管理后台Flask-Migrate 管理数据库表结构变更。每一样都有现成方案但每一样你都可以随时替换掉。对于一套企业内部使用的小型管理系统来说这种无缝感太重要了。提示不要觉得简单就等于低级。我在实际开发中见过用 Django 做出来的进销存系统由于过度依赖 Admin 默认逻辑反而在后期的自定义业务流程上一改就出连锁问题。2. 中药材进销存和卖日用品完全是两码事很多人第一次接触进销存脑子里都是商品-库存-订单这种通用模型。但中药材这个品类如果直接套用通用模型系统做到一半就会卡住。为什么因为中药材的管理逻辑里商品的身份远不止是一个名称。2.1 批次、产地、炮制中药材的三重属性普通商品进销存一条记录就是SKU 数量。中药材行不通必须加上批次维度。同一味当归甘肃岷县产的叫岷归云南产的又叫云归品质和价格差一大截而生当归和酒当归虽然都叫当归炮制方法不同药效和卖价也完全不同。所以在这套系统里我把药材档案表和批次表拆开了。药材档案表只存基础信息药材名称、通用别名、规格标准、默认存储条件。批次表才是真正的库存承载单位它记录着产地、采收年份、炮制方式、供应商、采购价格、批号、有效期、当前库存量。这样设计的直接收益是销售人员在开单时系统可以根据批次自动分配库存而不是笼统地扣一个总数。查询这批库存还有多少这种问题也会从全表扫描变成精准定位。2.2 保质期与养护成本驱动的存销逻辑中药材还有一个普通商品不太明显的问题它不是标准化的工业品存储状态会直接影响品质。有的药材需要阴凉干燥有的需要冷藏防虫有的陈放三年以上反而药效更好比如陈皮有的则越新鲜越值钱。这就导致库存管理不能只盯着数量还必须盯质量。系统里我给每个批次都加了两个字段shelf_life保质期和storage_condition存储要求。当批次临近保质期时预警模块会主动推送提醒仓库人员优先出货或者转养护处理。这种时间敏感型的库存逻辑是一般进销存模板没办法直接提供的。2.3 系统边界从采购到销售的核心链路我建议不要一上来就想着把系统做得很庞大先把核心链路跑通。这套系统的核心链路就三条采购链路供应商档案 → 采购订单 → 药材到货 → 质检登记 → 批次入库库存链路批次库存查询 → 库存调整报损/养护/移库→ 库存预警销售链路客户档案 → 销售订单 → 批次锁定 → 出库扣减 → 销售记录最初版本只做这三条链路已经能覆盖一个中小药材经营主体 80% 的日常业务。等这些稳定了再去扩展利润报表、客户应收账款、采购计划这些附加模块节奏会更从容。3. 数据库建模把一株药材的一生管起来数据库设计是这套系统的地基。我见过不少半路夭折的进销存项目不是因为代码写不出来而是表结构一开始就没理顺后面越写越别扭。这里的核心原则是库存不是一张静态表而是一条动态的流水账。3.1 基础表设计药材档案、供应商、客户先做基础档案表这些是相对静态的数据。herb_info药材档案表字段包括herb_code药材编码、herb_name药材名称、alias_name别名、specification规格、unit计量单位、default_condition默认存储条件。supplier_info供应商表字段包括supplier_name、contact_person、phone、address、qualification_no经营资质编号。中药材行业对供应商资质很敏感这个字段值得留。customer_info客户表字段包括customer_name、customer_type终端药店/医院/批发商、contact、credit_limit授信额度。这几张表没什么玄机但要注意一点药材编码不要用自增 ID最好用有业务含义的编码比如首字母缩写加流水号HQ-0001。原因是药材行业经常线下交流一个可读的编码比一个ID138好用得多。3.2 库存核心批次表与批次明细流水这是整个系统建得最慢的地方也是最值得仔细思考的部分。批次表batch_stock的字段大致是字段名类型说明batch_idvarchar(32)批次号如 20250115-HQ-001herb_codevarchar(16)关联药材档案supplier_idint供应商 IDorigin_placevarchar(64)产地harvest_datedate采收日期processing_methodvarchar(32)炮制方式生/熟/酒炙等batch_qtydecimal(12,2)批次总入库量current_qtydecimal(12,2)当前剩余量purchase_pricedecimal(10,2)采购单价shelf_life_monthsint保质期月production_datedate生产/加工日期expire_datedate到期日期可由生产日期加保质期算出statustinyint0在库 1已锁 2报损 3售罄这里的核心是batch_id。它不可重复且要能在系统里反查出一个批次的完整生命周期哪个供应商供的、哪天入库的、从哪个单子出库的、卖给了谁。这一切都由批次明细流水表batch_transaction_log来记录。每次入库、出库、报损、调整都要在流水表里插入一条记录字段包括transaction_type1入库 2出库 3报损 4盘点调整、quantity、related_order_no关联单号、operator_id操作人、created_at操作时间。这张流水表永远只增不删它是日后对账和追溯的底牌。3.3 订单与批次映射进销存最难的一环最难的不是写代码而是设计销售订单如何扣减批次库存。新人最容易犯的错是销售单只记一个总药材和总数量然后库存表直接减去总量。这样做的隐患是一旦发生退货你不知道退回来的货属于哪个批次批次的可追溯性就断了。正确的做法是引入一个中间表sale_order_detail_batch结构是order_detail_id关联销售明细batch_id关联批次allocate_qty该订单从该批次分配的数量简单说一个销售明细可以对应多个批次比如客户要 10 公斤金银花5 公斤从 A 批次扣5 公斤从 B 批次扣。这样一张订单出库完成后每个批次的扣减来源清清楚楚后续做效期追踪、退货回库都有据可查。注意这个映射表在设计阶段就要加到 ER 图里不要等到写代码时再灵活处理。基于 Flask SQLAlchemy 做迁移虽然不难但那种动了核心表结构的迁移在公司内部系统里一次就够你难受的。4. 核心功能实现手把手拆一遍代码逻辑框架选定了表结构理顺了剩下的就是功能实现。这一节我把采购入库、销售出库、库存预警三个核心环节的代码实现思路完整讲一遍重点在于业务逻辑的处理过程而不只是贴几段能跑通的代码。4.1 采购入库一次入库就是一个批次的开端采购入库的逻辑本质上是在做三件事更新采购单状态、创建新批次、写入库存流水。用 Flask 的视图函数写出来大概是这样的思路app.route(/purchase/confirm, methods[POST]) login_required def confirm_purchase(): 采购到货确认并自动生成批次 form PurchaseConfirmForm() if not form.validate_on_submit(): flash(提交数据不合法, danger) return redirect(url_for(purchase.index)) # 采购单主表 purchase_order PurchaseOrder.query.get(form.po_id.data) if not purchase_order or purchase_order.status ! 0: flash(采购单不存在或已确认, danger) return redirect(url_for(purchase.index)) try: # 逐条明细生成批次 for detail in purchase_order.details: if detail.arrive_qty 0: continue # 生成批次号日期 药材编码 序号 batch_no generate_batch_no(detail.herb_code) new_batch BatchStock( batch_idbatch_no, herb_codedetail.herb_code, supplier_idpurchase_order.supplier_id, origin_placedetail.origin_place, production_datedetail.production_date, expire_datedetail.expire_date, batch_qtydetail.arrive_qty, current_qtydetail.arrive_qty, purchase_pricedetail.purchase_price, status0 ) db.session.add(new_batch) # 写流水 log BatchTransactionLog( batch_idbatch_no, transaction_type1, quantitydetail.arrive_qty, related_order_nopurchase_order.po_no, operator_idcurrent_user.id ) db.session.add(log) purchase_order.status 1 db.session.commit() flash(入库确认成功批次已生成, success) except SQLAlchemyError: db.session.rollback() flash(入库失败数据已回滚, danger) return redirect(url_for(purchase.index))这里值得展开的是generate_batch_no。不要用 UUID在进销存系统的实际操作场景里仓库人员手抄单号的情况很常见一串无规律的 UUID 会让对账变成灾难。用20250115-HQ-001这种格式一眼就能看出是 2025 年 1 月 15 日入库的黄芪第一批货这才是真正面向用户的细节设计。事务方面整个入库流程必须保证采购单状态修改、批次创建、流水记录三件事同时成功或同时失败。所以在try块里只要任何一个环节出错就rollback。这个习惯一定要养好进销存系统最怕的就是库存与单据状态对不上。4.2 销售出库先进先出的 Python 实现药材行业出库的原则基本都是先进先出先入库的批次先卖避免旧批次压到过期。但代码层面要注意一点先进先出不是简单的按入库时间排序取前几批还要考虑批次的有效期。如果一个批次虽然入库早但效期长另一个批次入库晚但效期短实际出库时更应该优先出效期短的。实现逻辑分三步走def allocate_batches(herb_code, required_qty): 按效期优先原则分配批次 返回 [(batch_id, allocate_qty), ...] # 1. 找出所有在库批次按到期日期升序排列 batches BatchStock.query.filter_by( herb_codeherb_code, status0 ).filter( BatchStock.current_qty 0 ).order_by(BatchStock.expire_date.asc()).all() # 2. 从效期最近的批次开始逐个满足需求量 allocation [] remain required_qty for batch in batches: if remain 0: break take_qty min(remain, batch.current_qty) allocation.append((batch.batch_id, take_qty)) remain - take_qty # 3. 不够扣则提示库存不足 if remain 0: raise InsufficientStockError(f{herb_code} 库存不足还差 {remain} 单位) return allocation拿到分配结果后生成销售单时逐个扣减批次库存并给每个批次写一条出库流水。这个过程要放在同一个事务里防止订单保存了但批次没扣成功的情况。我特别想提醒的一点是销售页面里批次分配结果显示给用户看是有价值的。客户打电话来问这批还有多少客服看着系统直接答这比翻 Excel 靠谱得多。4.3 库存预警防止压货压到失效的兜底逻辑预警功能看着不起眼实际上是这套系统里最能省钱的模块。中药材压货压到过期不是小事一批货可能是几千上万的资金占用。预警逻辑不复杂关键是触发时机的设计。我的做法是每天定时跑一次预警检查用 Flask 的 CLI 命令注册成一个flask check-stock命令配合系统定时任务调度。逻辑如下def check_stock_warning(): warnings [] today date.today() # 1. 近效期预警距离过期还剩30天 near_expiry BatchStock.query.filter( BatchStock.status 0, BatchStock.current_qty 0, BatchStock.expire_date today timedelta(days30) ).all() for batch in near_expiry: batch.status 4 # 标记为近效期预警状态 warnings.append(f批次 {batch.batch_id} 将于 {batch.expire_date} 到期剩余 {batch.current_qty}) # 2. 低库存预警低于该药材最低库存阈值 low_stock db.session.query( BatchStock.herb_code, func.sum(BatchStock.current_qty) ).filter( BatchStock.current_qty 0, BatchStock.status.in_([0, 4]) ).group_by(BatchStock.herb_code).all() for herb_code, total_qty in low_stock: threshold HerbInfo.query.get(herb_code).min_stock_threshold if total_qty threshold: warnings.append(f药材 {herb_code} 库存量为 {total_qty}已低于阈值 {threshold}) db.session.commit() return warnings在界面上主页 Dashboard 直接展示两类预警卡片深色的高亮显示。同时系统把这些预警生成待办提醒操作人员处理完一个预警就标记为已办形成闭环。5. 真实项目里容易翻车的三个地方表结构和代码逻辑都理清了接下来是真正意义上的经验篇。这三处问题我在不同项目里反复见到每一处都值得单独拿出来说道说道。5.1 事务边界库存账目与单据状态的一致性进销存系统做久了你会发现绝大部分对不上账的问题根源都不是算错了而是事务边界没划对。什么叫事务边界就是一个业务流程中哪些操作必须绑定为一个整体。采购入库时采购单状态、批次库存、流水记录必须是一个事务。销售出库时订单生成、批次扣减、流水记录也必须是一个事务。有些人图方便写完一个模块提交一次结果就是订单已经生成了但批次没扣第二天一对账就露馅了。我的习惯是在 Flask 里统一使用db.session.commit()在视图函数的最末端提交过程中任何一步抛异常立即rollback。宁可提交粒度粗一点也不要出现部分成功的脏数据状态。5.2 并发扣库存两个订单同时抢最后一公斤怎么办单机部署的小系统并发量通常不高但并发扣库存这个问题还是要提前做好防护。想象一个场景客户 A 和客户 B 几乎同时下单都要买一种只剩 1 公斤的药材如果处理不当系统会允许两个订单都扣款成功库存扣成负数。解决这个问题常用的办法是乐观锁也就是在批次表加一个version字段# 扣减库存时带上版本条件 update_count BatchStock.query.filter_by( batch_idbatch.batch_id, current_qtybatch.current_qty, versionbatch.version ).update({ current_qty: batch.current_qty - take_qty, version: batch.version 1 }) if update_count 0: # 说明已经有其他请求改了这条记录需要重试 raise ConcurrentModificationError(f批次 {batch.batch_id} 库存已被其他订单占用)这样做的好处是即使两个请求几乎同时进入也只有先执行成功的那一个能更新批次另一个会因为条件不满足而失败重试用户看到的是库存不足或友好提示而不会出现账面上的负数。5.3 Flask 的 session 与操作审计内部系统的信任基石企业内部系统有个容易被忽视的需求谁在什么时候干了什么必须可追查。Flask 默认的 session 是存储在客户端的 cookie 里的虽然签名防篡改但敏感操作日志必须落在数据库里。我给系统的所有写操作都加了审计钩子核心逻辑是在调用db.session.commit()之前把当前用户、操作类型、关联单号、时间统一写入operation_log表。不要小看这个设计真到月底对账、客户投诉纠纷的时候这唯一可信的证据。操作审计另外还要注意一点不要把登录密码明文存进数据库。Flask 项目里常规做法是用werkzeug.security.generate_password_hash做密码哈希登录校验用check_password_hash。这事虽然基础但我真见过把密码直接塞进用户表的项目想想都后怕。6. 从开发到上线部署时那些绕不开的琐事系统功能做完并不是终点。把这些东西放到服务器上真正能被店里的人用起来还有几个关口要过。6.1 千万别用 Flask 自带服务器上线Flask 自带的开发服务器是单进程的写着 WARNING: This is a development server 那个它只适合本地调试。上线部署的时候我用的是 Gunicorn Nginx 的组合。Gunicorn 的启动命令按照服务器的 CPU 核数来定 worker 数量经验值是 2 到 4 个就够。比如双核小服务器可以这样启动gunicorn -w 2 -b 127.0.0.1:8000 run:app前面用 Nginx 做反向代理把 80 端口的请求转发到 8000。静态资源CSS、JS、图片交给 Nginx 直接返回Flask 只处理动态请求这样性能会有明显提升。数据库方面如果数据量预计会涨得比较快上线前就把 SQLite 换成 MySQLSQLAlchemy 配置改一下基本无痛迁移。注意记得配置app.config[SECRET_KEY]为一个随机生成的长字符串不要用默认值。这涉及到 session 的安全也是 Flask 部署时常被忽略的一个点。6.2 数据库迁移、备份与初始化数据用 Flask-Migrate 管理数据库表结构这是我能给出的最实在的建议。功能开发阶段Model 改来改去很正常没有迁移工具就想哭。具体做法就是flask db init flask db migrate -m add batch table flask db upgrade每次表结构变更都生成一个新的迁移脚本线上环境只要执行flask db upgrade就能自动同步不用手动去 SQL 里 ALTER TABLE。备份策略我用的是一天一次全量备份加上 binlog 增量备份。对于这套规模的系统每天凌晨用 crontab 跑一次mysqldump完全够用。备份文件不能只放在服务器本地我记得很清楚那台服务器硬盘坏过一次要不是当初多留了个异地备份所有库存数据和客户资料就全没了。6.3 给非技术用户做的权限与操作简化最后想聊一个很多人不太在意的地方系统是给谁用的如果是给药材商行里的工作人员用他们大概率不是技术背景。界面上的操作路径要尽量短权限划分要清晰。我的做法是分了三个角色管理员、采购员、销售员。采购员只能看到采购入库和库存查询销售员只能看到客户管理、销售开单和批次查询管理员拥有全部权限包括用户管理和数据维护。角色控制用 Flask-Login 加装饰器实现核心逻辑就是admin_required这样的自定义装饰器代码侵入性很小。数据录入体验也要优化。比如录入采购单时药材名称支持模糊搜索输入黄下拉弹出黄芪、黄连、黄柏比在一个几百条记录的表格里翻强多了。这些小优化往往比功能本身更能决定这套系统能不能被真正用起来。我在实际落地这套系统的过程中最深的一个体会是进销存系统的难点从来不在 Flask 框架本身而在于你有没有把一个药材从采购到卖出的全过程看透。框架只是工具业务逻辑理顺了代码怎么写都顺手。如果你正在做或者打算做一套类似系统建议先拿纸把业务流程画一遍再动手建表、写代码会少走很多弯路。