尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Flask+Uniapp实战:构建仓库进销存系统与库存调拨全流程

Flask+Uniapp实战:构建仓库进销存系统与库存调拨全流程 去年接了一个做五金配件的客户仓库里几千种 SKU之前全靠手工记账。每一笔出入库先登记在本子上月底再敲进 Excel光是找账实差异就能耗掉大半天。老板想上系统但市面现成的进销存产品要么太重、要么太贵最要命的是仓库管理员一天到晚在库房和货架之间跑传统 PC 端界面根本用不起来。最后我们确定用 Python 的 Flask 做后端Uniapp 做微信小程序前端从零搭了一套仓库物资进销存库存调拨管理系统。这套系统是真正在生产环境跑了一年多的东西不是 Demo。核心功能包括采购入库、销售出库、库存实时查询、仓库间调拨、库存上下限预警和基础报表前后开发加试运行用了不到三周。如果你正在做类似的中小型仓储管理系统或者你是全栈开发者想了解 Flask 小程序这套技术组合的实际落地过程这篇文章应该能帮你省掉不少试错时间。我会把选型理由、数据模型设计、核心代码实现和踩过的坑全部摊开来讲。1. 从手动记账到扫码管理Flask Uniapp 的选型理由与系统架构1.1 为什么是 Flask而不是 Django 或者 FastAPI做管理系统第一步就是定技术栈。数据库方面基本没悬念MySQL 为主开发阶段用 SQLite 凑合。后端框架反而是花时间讨论的。客户的需求非常明确一个给仓库管理员用的移动端、一个给办公室财务用的后台页面、一套能长期维护的 API。这个规模属于典型的中小型业务系统并发量不高但是业务规则复杂尤其是库存扣减、调拨流转这类逻辑必须写清楚。我最终选了 Flask。原因很朴素项目体量不需要 Django 那套完整的 Admin、Auth、Form 全家桶用了反而增加心智负担。Flask 足够轻路由、蓝图、请求上下文都很直观配合 Flask-SQLAlchemy 操作数据库非常顺手。FastAPI 虽然性能更好、自带接口文档但我们团队当时对异步体系不算特别熟而且这类企业管理项目网上积累的 Flask 踩坑案例明显更多真遇到问题能查到的资料更丰富。选框架有时候不是选最强的而是选最不容易卡壳的。1.2 前端为什么用 Uniapp而不是原生小程序客户最开始只提了做一个微信小程序。但我问清楚后发现办公室财务也要用系统而且他们习惯在电脑上操作。如果只做小程序财务端还得再单独做一个 Web 后台如果做两个前端开发和维护成本直接翻倍。用 Uniapp 的主要原因就在这里一套 Vue 代码编译成微信小程序给仓库人员用编译成 H5 部署到服务器上给办公室用。虽然 H5 和原生小程序在体验上有一点差异但对我们这种业务型系统完全够用。Uniapp 的 API 封装也比较完善uni.scanCode一行就能调用扫码uni.request封装后各端通用省掉了原生小程序里写条件编译的麻烦。另外一点非常实际国内做小程序外包的技术团队大部分都会 Uniapp后期客户想找人维护可选的人多得多。1.3 整体架构与模块划分系统整体分三层客户端层Uniapp 编译出的微信小程序端给仓管用、H5 端给办公室用服务端层Flask 提供 JSON API按业务模块划分蓝图数据层MySQL 存储业务数据开发环境直接用 SQLiteAPI 设计上全部走 REST 风格统一返回格式前端拿到数据后直接渲染不涉及模板渲染。模块划分上我拆成了六个大块认证与用户、商品管理、仓库管理、库存管理、调拨管理、报表统计。每个模块对应一个 Flask 蓝图互不掺和。这样的结构对后期维护非常有用客户后来要求加一个库存预警功能我只需要在库存模块里加接口前端加页面半小时就搞定不用碰其他模块的代码。2. 数据模型先行商品、仓库、库存与调拨单怎么设计才不翻车2.1 核心表结构商品、仓库、库存进销存系统最忌讳一上来就写接口。我做过太多系统最后返工都是因为表结构设计错了。先把表设计清楚写代码只是时间问题。商品表、仓库表都比较好理解重点说一下最容易出错的库存表。很多初学者喜欢在商品表里加一个stock_quantity字段记录库存数量。这样做在只有一个仓库的时候勉强能跑但只要有两个仓库你立刻会发现这个设计是错的同一个商品在两个仓库里分别有库存一个字段怎么存正确做法是单独建一张库存表以“仓库 商品”作为一条库存记录的唯一维度表名用途关键字段goods商品主数据id, code(唯一编码), name, spec, unit, price, statuswarehouse仓库id, name, address, managerstock仓库商品库存id, warehouse_id, goods_id, quantity, safety_quantityinbound_record入库流水id, order_no, goods_id, warehouse_id, quantity, supplier, operator_id, create_timeoutbound_record出库流水id, order_no, goods_id, warehouse_id, quantity, target, operator_id, create_timetransfer_order调拨单主表id, order_no, status, from_warehouse_id, to_warehouse_id, creator_id, create_time, complete_timetransfer_item调拨单明细id, transfer_id, goods_id, quantity, actual_quantity, statustransfer_record调拨流水id, transfer_id, goods_id, from_warehouse_id, to_warehouse_id, quantity, record_type, operator_id, create_time库存表一定要加上联合唯一索引ALTER TABLE stock ADD UNIQUE INDEX uk_warehouse_goods (warehouse_id, goods_id);这个索引能保证同一个商品在同一个仓库里只有一条记录防止程序 bug 导致数据重复。库存字段quantity用来存“当前账面库存”safety_quantity是安全库存下限库存低于这个值就触发预警。2.2 出入库流水为什么设计成只增不改流水表是整个进销存系统的账本。你完全可以把它类比成银行流水银行账户余额是变动的但每一笔存取记录是永久留存的不能改也不能删。入库流水表和出库流水表每一条记录的字段都包括单据号、商品、仓库、数量、经办人、时间以及来源或去向。单据号我习惯用日期 随机数的组合比如IN20250120001这样每一单都有唯一可追溯的凭证号。流水表设计上有几个铁律只允许插入和查询不允许UPDATE和DELETE库存扣减和流水插入必须在同一个数据库事务里完成每次盘点对账都是通过流水汇总去核对库存余额这套思路在出问题的时候优势特别明显。有一次客户打电话说某个商品库存差了一箱我直接按流水一条一条查很快定位到是两天前某次入库把数量 10 录成了 1。如果流水表可以被随意修改这种问题根本无从查起。2.3 调拨单的三层结构主表、明细、流水调拨是进销存里和普通增删改查最不一样的地方。一开始我也想过最简单的方式A 仓库减库存、B 仓库加库存一张表就搞定了。但仔细想就会发现这个做法在现场根本走不通。实际情况是仓管在 A 仓库把货装车货物在运输路上需要一段时间到达 B 仓库之后还要清点、验收。如果系统只有“减库存/加库存”两步那货物在路上的这段时间里A 仓库账上已经没了B 仓库账上还没有两边都不对。想要回答“在途物资值多少钱”“哪些货正在路上”根本没有数据支撑。所以我把调拨设计成三层结构transfer_order调拨主表保存调拨单的整体状态、从哪个仓库出发、去哪个仓库transfer_item调拨明细表保存这批调拨包含了哪些商品、各多少数量、实收多少transfer_record调拨流水表保存每一次库存变动的明细比如出库减库存一条、入库加库存一条调拨单的状态流转也很清晰草稿 - 待出库 - 已出库(在途) - 已完成每一段都有对应的操作和状态校验前端页面根据状态决定显示哪些按钮、允许谁操作。这张主表把所有环节串起来配合明细表和流水表既能查单、又能查货、还能查账始终保有一条完整的追溯链。2.4 初始化数据和测试准备表结构定完以后我先写了一个初始化脚本批量生成测试商品、仓库和初始库存。商品编码需要仔细设计我建议采用“分类前缀 流水号”的方式比如五金件用HJ开头、紧固件用JG开头扫码入库时看到编码前缀就能大概判断品类。测试数据要多准备一些最少几百条 SKU 才有模拟真实业务的效果不然列表分页、模糊搜索这些问题到上线时才暴露就晚了。3. Flask 后端实战库存事务控制、接口设计与权限管理3.1 接口清单与统一响应格式后端接口按蓝图拆分核心接口如下模块接口方法说明认证/api/auth/loginPOST微信登录换取系统 token商品/api/goodsGET商品列表分页/搜索商品/api/goodsPOST新增商品商品/api/goods/idPUT编辑商品库存/api/stock?warehouse_id1GET查询某仓库库存入库/api/inboundPOST采购入库出库/api/outboundPOST销售/领用出库调拨/api/transferGET调拨单列表调拨/api/transferPOST创建调拨单调拨/api/transfer/id/outPOST调拨出库调拨/api/transfer/id/inPOST调拨入库报表/api/report/stock_summaryGET库存汇总报表所有接口统一返回同一个格式前端只处理这一种结构不用每个接口单独判断{ code: 0, msg: ok, data: {} }错误码约定0表示成功400是业务错误比如库存不足401是未登录或 token 过期500是服务器异常。页面里一旦收到401前端统一跳到登录页这个逻辑在封装请求时一次性处理掉。3.2 库存扣减的事务实现库存扣减是整个系统最核心的代码我先贴一段简化后的出库逻辑from flask import jsonify from app.models import Stock, OutboundRecord from app.extensions import db bp.route(/outbound, methods[POST]) login_required def create_outbound(): data request.get_json() warehouse_id data[warehouse_id] goods_id data[goods_id] quantity int(data[quantity]) try: with db.session.begin(): stock Stock.query.filter_by( warehouse_idwarehouse_id, goods_idgoods_id ).with_for_update().first() if not stock or stock.quantity quantity: return jsonify(code400, msg库存不足) stock.quantity - quantity db.session.add(OutboundRecord( order_nogenerate_order_no(OUT), goods_idgoods_id, warehouse_idwarehouse_id, quantityquantity, operator_idg.user_id )) return jsonify(code0, msg出库成功, data{stock: stock.quantity}) except Exception as e: db.session.rollback() return jsonify(code500, msgstr(e))这段代码里最关键的只有一个地方with_for_update()。我的第一次版本没有加这个锁在客户现场演示时出了大笑话。两个人同时用两台手机扫同一个商品出库库存显示变成了负数账目当场就乱了。原因很简单两个请求同时读到库存是 10都判断“10 大于 8可以出库”然后各自扣减 8最后库存变成 -6。但实际上库存只够出一次。with_for_update()的作用是给这一行库存记录加一把数据库级别的行锁。第一个请求锁住这行记录直到事务提交才释放第二个请求只能排队等待。这样“读取库存 - 判断是否足够 - 扣减库存”这三步就变成了原子操作不会出现互相覆盖的情况。3.3 负库存的两道防线行级锁能解决并发问题但应用层的判断仍然可能被其他 bug 绕过。所以我加了第二道防线数据库层的约束。ALTER TABLE stock ADD CONSTRAINT chk_stock_quantity CHECK (quantity 0);如果某个逻辑漏洞导致库存被扣成负数数据库直接报错事务回滚问题能第一时间暴露。这里有个实际经验要提醒MySQL 5.7 对 CHECK 约束是不强制生效的必须升级到 8.0 才能真正用上。如果客户数据库还是 5.7就只能在应用层做好校验或者用触发器兜底。项目里我建议直接让客户用 MySQL 8.0省得后续埋坑。SQLite 对 CHECK 是原生支持的开发阶段不会有问题但生产环境还是按正式数据库来跑提前发现约束层面的问题。3.4 调拨出库与入库的状态流转实现调拨的核心逻辑和普通出库不太一样需要同时更新调拨单状态、扣减源仓库库存、写入调拨流水。bp.route(/transfer/int:transfer_id/out, methods[POST]) login_required def transfer_out(transfer_id): transfer TransferOrder.query.get_or_404(transfer_id) if transfer.status ! pending_out: return jsonify(code400, msg当前状态不允许调拨出库) try: with db.session.begin(): # 校验并扣减源仓库库存 for item in transfer.items: stock Stock.query.filter_by( warehouse_idtransfer.from_warehouse_id, goods_iditem.goods_id ).with_for_update().first() if not stock or stock.quantity item.quantity: raise ValueError(f商品 {item.goods.code} 库存不足) stock.quantity - item.quantity db.session.add(TransferRecord( transfer_idtransfer.id, goods_iditem.goods_id, from_warehouse_idtransfer.from_warehouse_id, to_warehouse_idtransfer.to_warehouse_id, quantityitem.quantity, record_typeout )) transfer.status outbound # 已出库在途状态 return jsonify(code0, msg调拨出库成功) except ValueError as e: db.session.rollback() return jsonify(code400, msgstr(e))调拨入库的代码逻辑是对称的检查状态是outbound在目标仓库加库存写入record_typein的流水状态改成completed。这里不再把整段贴出来但有一个点很关键调拨单明细表里的actual_quantity字段。收货时实际清点的数量可能和调拨数量不一致必须先登记差异再按实收数量入库这样账面才会和实物一致。4. Uniapp 小程序端落地扫码、出入库与调拨页面的实现细节4.1 页面结构与角色工作流小程序端页面我按角色拆成四个主入口仓管员工作台、扫码出入库、调拨操作、库存查询仓库主管订单审批、调拨单审核、库存预警财务入库单/出库单查询、成本报表管理员商品维护、仓库维护、用户权限页面结构上底部 Tab 分四个首页工作台、库存、单据、我的。首页放常用功能入口比如“扫码入库”“扫码出库”“新建调拨”都是高频操作必须保证到达路径不超过两次点击。库存页是当前仓库的实时库存列表支持关键字搜索和按仓库切换。单据页是历史入库、出库、调拨单的列表。我的页面放个人信息和退出登录。4.2 请求封装与 Token 管理小程序的请求我封装了一个统一模块核心代码很简单const BASE_URL https://api.example.com export function request(options) { return new Promise((resolve, reject) { uni.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: Bearer uni.getStorageSync(token) }, success: (res) { if (res.data.code 401) { uni.removeStorageSync(token) uni.navigateTo({ url: /pages/login/login }) return } resolve(res.data) }, fail: (err) reject(err) }) }) }token 用uni.setStorageSync存在本地请求时从本地读取放到Authorization头里传给后端。用户登录走的是微信授权码登录流程小程序调用uni.login拿到 code发给后端后端用 code 向微信接口换取 openid再和本地账号绑定生成系统 token 返回前端。绑定过一次之后后面再进来就是静默登录不需要用户手动输入账号密码。4.3 扫码识别和出入库表单扫码是仓库场景最高频的操作Uniapp 提供了现成的 APIuni.scanCode({ scanType: [barCode, qrCode], success: async (res) { const code res.result const result await request({ url: /api/goods/code/ code, method: GET }) this.goodsInfo result.data this.quantity 1 } })扫出来商品之后表单自动填充商品名称、规格、单位仓管只需要填一个数量点确认就完成出库。实操过程中我发现一个细节仓库里很多老旧的商品条码印得模糊或者贴在曲面上一次性扫不出来。后来我在页面里加了“手动输入编码”的兜底入口扫码失败时可以直接输入数字编码查询避免在现场卡住。这个看似不起眼的小功能反而是仓管们最认可的设计之一。4.4 调拨页面的状态控制与列表刷新调拨页面是逻辑最复杂的部分。创建调拨单时要选源仓库、目标仓库、添加商品和数量。创建之后根据状态显示不同操作pending_out显示“调拨出库”按钮点击后调后端接口源仓库库存扣减outbound显示“调拨入库”按钮点击后调后端接口目标仓库库存增加completed只展示详情不允许操作实现上我直接用v-if根据状态控制按钮重要操作加二次确认弹窗。调拨单列表用onReachBottom做触底分页加载每次加 20 条。这里有个体验问题值得注意每次提交操作成功后页面返回列表时要主动刷新数据不能沿用旧的列表缓存否则会出现“刚出库完库存列表还显示旧数量”这种让仓管一头雾水的情况。我用了一个简单粗暴的解决办法在列表页的onShow生命周期里都调用一次刷新接口。小程序页面栈的特性决定了返回上一页时会触发onShow这时候刷新永远是最新的数据不用想什么复杂的缓存策略。5. 踩坑复盘库存变负数、事务半提交与小程序审核被驳回5.1 库存变成负数的根因与修复开头讲的现场事故排查链路其实很清晰。我看数据库日志发现两条UPDATE stock语句交错执行两次都先读到 10再各减 8完全没有锁。再看代码库存判断和扣减逻辑中间隔了好几步根本不在一个事务里。最后定位到问题第一版代码把“查库存”和“扣库存”分开写了中间还用 Python 变量做中转判断这在单用户测试时没问题并发一上来就完蛋。修复方案就是 3.2 节里的写法查库存和扣库存放在同一个事务里用with_for_update()锁行判断不满足直接抛异常回滚。这个坑说明一个问题进销存系统的并发控制不是可选项而是必须项。哪怕客户说“我们仓库就两个人不可能同时操作”也要按并发场景去写代码。因为未来一定会加人、加仓库、加业务。5.2 SQLAlchemy 嵌套事务导致的部分提交还有一个坑藏在事务管理里。早期代码我在业务方法内部又开了一个db.session.begin()外层逻辑也开了事务。结果某次操作中途抛异常外层回滚了内层却已经提交导致一部分流水数据残留下来账目对不上。这个问题的本质是Flask-SQLAlchemy 的db.session是全局共享的你在同一个请求链路上反复begin()嵌套实际上底层还是同一个事务上下文但异常处理边界会变得混乱。我的修复方式是约定一条铁律每个 API 入口只允许出现一个事务边界统一用try with db.session.begin()包住整个业务逻辑不在 Model 或 Service 方法里再开新事务。这样异常回滚的范围是明确的不会出现部分提交的悬空数据。5.3 调拨在途报表的统计口径问题系统上线第二周财务反馈说库存汇总表出现负数。我查了半天发现不是 bug而是统计口径问题。调拨出库后源仓库库存已经扣减但目标仓库还没入库这段时间的商品既不在 A 仓也不在 B 仓报表如果只按stock表汇总就会让人以为少了货。修复是在报表查询里加了一个“在途库存”概念所有状态为outbound的调拨单明细它们的商品和数量就是在途库存。库存汇总页把在途量单独列一列同时在计算可用库存时把在途量算进去。商品源仓库库存在途数量目标仓库可用库存含在途HJ-001 合页502035这样财务一眼就能看出来那 20 箱不是不见了而是在路上。这个调整虽然只是查询逻辑但涉及报表口径变化我专门和客户开了一次会确认“可用库存 当前库存 在途入库 - 在途出库”的计算规则才让财务那边真正信服。5.4 小程序审核与上线配置的几道坎小程序端开发完提审时被驳回了一次。问题出在合法域名没有配置。小程序后台有个“服务器域名”设置必须配置成 HTTPS 的正式域名而且这个域名需要完成 ICP 备案。开发阶段可以在开发者工具里勾选“不校验合法域名”但真机预览和提审阶段必须配好。另外一个小程序审核的常见雷区是隐私政策。因为系统会获取用户的微信昵称、头像和 openid小程序后台要求提交《用户隐私保护指引》说明收集了哪些信息、用途是什么。这个文档最好在开发前就准备好否则提审的时候会被卡住。如果项目是给某个企业内部用的还有一个更轻量的上线方式走企业微信的“自建应用”不用过微信小程序的常规审核流程用户直接用企业微信里的工作台入口打开 H5 版本。不过这条路径需要企业有自己的企业微信组织不是所有客户都适用我当时是两条路都准备最后客户选择了正规小程序说对外展示也好看一些。最后说几句实在的整套系统做完我最深的体会是进销存这类系统表面上看就是一堆增删改查真正拉开差距的是账务一致性意识。什么时候扣库存、什么时候记流水、并发情况下怎么保证不出现负数、在途库存怎么理解这些想清楚了系统就成了一半。另外一个小建议不管是什么管理系统先把数据模型画出来再写代码。我见过太多项目代码都写了一半发现表结构不合理库存和商品混在一张表里最后全盘返工。花两天时间把表和状态流转定清楚后面能省下两周的返工时间。这套系统后续还能扩展批次管理、序列号管理、边角料回收这些功能骨架打得正往上加东西就不会太痛苦。做管理系统的项目走得稳比走得快重要得多。
返回列表