我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

多公司多门店仓储式超市POS系统架构设计与实践复盘

多公司多门店仓储式超市POS系统架构设计与实践复盘 最近刚配合一家连锁客户上线完一套《多公司多门店·仓储式超市·POS收银系统》趁着项目细节还热乎把整套方案从头到尾复盘一遍。先给结论这类项目真正难的从来不是收银机上那几个按钮而是藏在收银背后的组织架构、库存模型和资金归集。很多团队把精力全砸在前端交互上结果第二家门店一接入就发现库存对不上、报表算不对、账目混成一锅粥。我这边客户的实际情况很典型老板手里两个公司主体一个做批发一个做零售下面挂着多家仓储式超市门店共用一个大仓库但每家门店又要独立核算。这套系统要解决的核心问题说白了就三件事多公司怎么各自独立核算、多门店怎么共享库存和协同补货、POS收银台在复杂促销和断网环境下怎么稳定收银。这篇文章适合被多公司、多门店复杂度折磨的产品经理和开发也适合准备做零售数字化的业主以及想系统搞懂POS系统背后逻辑的同行。我会按“需求拆解—数据建模—收银设计—库存协同—分账结算—问题排查”这条路线走每一步的取舍都会讲清楚为什么这么做直接可落地。1. 项目定位先想清楚这是一套“零售业务中台”不是一台收银机1.1 拆解“多公司多门店·仓储式超市”背后的真实需求标题里每个词都不是装饰背后都有明确的业务压力。先说“多公司”。仓储式超市的老板们很少只有一个营业执照。常见的是一个批发公司负责大宗采购和批发业务一个零售公司运营所有超市门店一个物业或管理公司负责收租金、管理费甚至按区域再拆几个公司来享受政策和隔离风险。这些公司之间是真实的法人关系财务必须独立核算互不掺和。但在业务上它们又共享商品、共享供应商、共享仓库。系统如果从数据库层面就用一棵简单的“公司-门店”树去建模后面跨公司调拨、成本流转会直接卡死。再说“多门店”。门店是独立的经营单元每个门店有销售目标、毛利考核、独立库存和独立资金流水。但门店和门店之间不是孤岛会互相调拨商品可能共用同一个中央大仓。仓储式超市这个定语还带来几个独特要求SKU数量大通常一两万起步大包装整箱卖也拆零卖生鲜和散称商品占比高要接电子秤批发客户多会出现整单挂账、月结这种支付方式客单价高购物车经常几十件商品收银速度要求极高。如果就按“单店版收银进销存”来做一上线就会出现一个死循环A门店缺货时不知道B门店有货批发公司给零售公司供货没法做内部结算总部想看全盘库存只能靠Excel。所以这套系统的正确姿势是以“多公司、多门店”为核心约束把组织、商品、库存、交易、结算全部抽象成平台级能力POS只是最末端的一个交互终端。1.2 为什么架构上选择“模块化单体”而不是微服务我在这个项目里最强调的一点别一上来就微服务。以常规仓储式超市的体量一台好一点的服务器支撑几个公司、十来个门店、日均几千单是完全没问题的。微服务的思路在这里会引入分布式事务、服务间调用链、重复数据同步等问题每一个都会成为稳定性黑洞。但这个项目又确实需要模块边界否则后面维护会越来越痛苦。我的落地方式是“模块化单体”组织与主数据模块公司、门店、仓库、人员、权限商品中心SPU、SKU、条码、单位换算、分类、品牌价格与促销模块多套价格、促销规则、优惠分摊库存中心实时库存、流水、预占、补货、盘点交易中心订单、支付、退款、小票、挂账结算中心日结、对账、内部结算单、导出凭证。模块之间通过内部接口调用数据库可以共享一个实例但是每张表归属模块必须清晰。后期如果真的门店量爆发式增长直接按库存中心、交易中心拆服务出来即可不需要推倒重来。部署形态上我建议门店端用“B/S收银台本地缓存”模式中心端部署一个核心服务加一个后台管理站点。门店端浏览器打开收银页面日常在线操作离线时自动切换本地模式。比起纯C/S桌面客户端省去了每台收银机装驱动和升级的运维成本。1.3 多租户数据隔离怎么处理“多公司”的数据边界多公司最容易出事的一点就是A公司的报表里混进B公司的数据。所以数据隔离在架构第一天就要定清楚。我当时对比过三种方式方案优点缺点适用场景每公司独立数据库隔离最彻底财务审计省心维护成本高跨公司报表要聚合公司间完全独立几乎无业务往来共享库、独立Schema隔离较好可跨库join迁移成本稍高公司间有协同但需要严格边界共享库、共享表带公司字段开发最方便报表聚合容易容易漏条件查到别家公司数据公司间业务协同紧密最终我建议用第三种共享库共享表每张业务表都带company_id并在所有写操作里强制注入公司上下文不允许开发人员手写查询时不带company_id。这个约束不要靠团队自觉要在ORM层面做一个拦截器或者写统一的BaseRepository所有查询自动拼上当前上下文的company_id条件。还有一点很关键主键不能用简单的自增ID跨公司、跨端合并数据时会冲突。我这边全部用雪花ID保证多门店断网本地生成订单ID也不会重复。2. 数据模型把公司、门店、仓库、商品、库存的关系理顺再写代码2.1 组织模型设计公司、门店、仓库必须解耦很多单店版系统的数据结构是公司下面挂门店门店下面挂仓库。这种设计在做多公司多门店时马上会遇到一个问题一个仓库同时给多个公司用怎么办我项目里的建模是这样的四个核心表company公司表公司ID、名称、type批发公司/零售公司/管理公司、税号、结算货币store门店表门店ID、名称、所属公司ID、地址、店长、营业状态warehouse仓库表仓库ID、名称、类型中央大仓/门店后仓/前置仓、所属公司ID、地址store_warehouse_relation门店与仓库关系表门店ID、仓库ID、是否默认出货仓。门店和仓库是多对多关系允许一个门店对应多个仓库比如卖场库存和后仓库存分属两个物理区域但一定要有一个默认出货仓POS下单扣库存时优先扣默认仓。这样设计的好处是批发公司的大仓可以同时给零售公司的门店供货按“调拨”走内部结算库存不会乱未来加前置仓、加盟店不需要改表结构。建表时我习惯把“公司”和“经营组织”分开因为现实中管理公司并不直接经营门店它只是控股。财务报表按公司走经营报表按门店走两者可以交叉但不能耦合。2.2 商品主数据一物多码、多单位换算不能省仓储式超市的商品主数据比普通便利店复杂得多。最典型的问题同一个商品在批发时按箱卖在门店零售时按瓶卖。货架上的条码是单瓶的但整箱外包装上印的又是另一个条码。所以商品模型不能只建一张“商品表”。我当时设计了这样一套结构spu商品SPU表代表一个抽象商品比如“某品牌纯净水550ml”sku规格SKU表代表具体售卖规格比如“某品牌纯净水550ml×12瓶整箱”和“某品牌纯净水550ml×1瓶”是两个SKUbarcode条码表一个SKU可以有多个条码一个条码也可以关联多个SKU这种坑后面讲unit_conversion单位换算表SKU的销售单位、采购单位、库存单位以及换算率。核心换算逻辑示例SKU条码销售单位单价库存单位换算率纯净水550ml×1瓶690000000001瓶2.00瓶1纯净水550ml×12瓶整箱690000000002箱21.60瓶12这样设计和“一物一码”思路最大的差别POS收银时扫码扫到整箱条码系统能自动算出这箱在库存上占用的是12瓶。同时报表分析销量时可以把整箱销售换算成瓶数来统计不会出现“销量单位混乱”的问题。另外仓储式超市一定有生鲜和散称商品这类商品没有固定条码或者一个条码对应一个类目但价格随重量变化。要预留PLU码计量商品码字段POS端接电子秤时称重机直接传重量和PLU码过来系统按“单价×重量”生成行。2.3 库存模型为什么不能只有“库存数量”一列很多进销存系统的库存表就一个字段qty。销售扣一下入库加一下看起来简单但一遇到并发、断网补传、手工调整账就花了。我在这套方案里坚持用“库存余额 库存流水”双表结构。库存余额表inventoryCREATE TABLE inventory ( id BIGINT PRIMARY KEY, company_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, onhand_qty DECIMAL(18,3) NOT NULL DEFAULT 0, available_qty DECIMAL(18,3) NOT NULL DEFAULT 0, allocated_qty DECIMAL(18,3) NOT NULL DEFAULT 0, version INT NOT NULL DEFAULT 0, updated_at DATETIME );库存流水表inventory_flowCREATE TABLE inventory_flow ( id BIGINT PRIMARY KEY, company_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, warehouse_id BIGINT NOT NULL, change_qty DECIMAL(18,3) NOT NULL, before_qty DECIMAL(18,3) NOT NULL, after_qty DECIMAL(18,3) NOT NULL, biz_type VARCHAR(32) NOT NULL, biz_id VARCHAR(64) NOT NULL, created_at DATETIME NOT NULL, UNIQUE KEY uk_biz (biz_type, biz_id) );两个表的关键点库存流水是append-only追加写入只允许新增不允许修改和删除。每一条流水都要带业务单据号和业务类型销售、采购、调拨、盘点、报损、退货等。库存余额表不是根据流水实时计算的它只是一份“缓存”用来快速查询和并发扣减。如果出现账实不符通过重放流水可以找到差异在哪笔单据上。allocated_qty表示预占库存也就是订单已创建但还没支付成功时占用的数量。三种数量的关系物理库存在途在库可用库存物理库存-预占。这里说一个很多人忽略的原则任何库存变动都要先写流水再更新余额表。顺序反了一旦余额表更新成功但流水写入失败系统永远都查不出差异来源。我在项目中把这个写进统一库存服务业务代码没有权限直接操作inventory表。3. 收银流程与促销POS端最容易出问题的地方全在这里3.1 收银界面与操作流程仓储式超市要的是“快”仓储式超市的收银场景和奶茶店、便利店完全不同。顾客推着一大车东西高峰期排队收银员必须尽可能在几十秒内完成一单。这个场景要求POS界面做到默认光标在扫码框扫码枪扫完一段条码直接回车商品自动加入购物车不需要再点按钮支持连续扫码同一商品扫两次自动叠加数量不会弹两次窗口问支持“数量×条码”模式比如顾客拿了10箱饮料收银员可以直接输入“10*690000000002”生鲜/称重商品显示在单独区域支持手动改重量和单价并打印第二联标签结算时一次展示所有行项目支持部分取消、临时挂单、整单删除。会员识别也放首位。仓储式超市基本都是会员制结账前先识别会员手机号、会员卡、动态码会员价和积分在整单上结算。顾客身份直接决定价格体系所以会员识别必须在选品之前完成而不是最后补。界面布局上左侧是大列表展示已扫码商品右侧是当前会员信息和常用操作按钮底部是合计和收款区域。为了方便高峰期使用所有操作都要支持快捷键比如F2快速收款F4拆单F8挂单。我当时实测过老练收银员用快捷键操作单笔结账能控制在15秒内比鼠标点选快一半以上。3.2 价格与促销引擎行级优惠分摊是重点仓储式超市的价格体系特别复杂。同一件商品面对不同顾客有完全不同的价格普通零售价、会员价、批发价、促销价促销价又分直降、满减、买赠、组合捆绑。如果价格逻辑全写在订单逻辑里每次加一个促销整个系统都要重新发版。我的做法是把价格和促销做成一个独立模块订单服务只调一个统一接口“给定顾客、商品、数量、时间返回成交单价和优惠明细”。价格计算顺序固定为先定基础价根据顾客类型选择价格带再判行级促销第N件折扣、数量满减再判单级促销整单满减、整单折扣最后分摊把整单优惠按行金额比例分摊到每一行。为什么一定要做“分摊”因为退货时只知道整单退了多少钱不知道每一行该退多少就出大问题。分摊后每一行都有一个discount_amount字段退货金额直接等于行原价 - 行分摊优惠 - 行已退金额清晰安全。促销规则的另一个坑是快照。订单生成时必须把促销规则的关键信息折扣率、满减值、赠品SKU持久化到订单明细里。如果只存一个促销ID引用当前规则表那促销一结束、规则一改历史订单的报表统计和退货退款全都会跟着乱。3.3 断网与容灾收银台永远不能停仓储式超市POS最怕的不是卡顿是断网后收银台直接瘫痪。高峰排队只要断3分钟场面就会失控。所以断网容灾是硬需求不是可选功能。我的方案是在门店端做一个轻量级本地缓存和离线队列门店启动时从中心同步商品主数据、会员基础信息、促销规则快照到本地浏览器存储或本地数据库收银时正常在线操作所有订单实时提交中心服务如果提交失败或者网络不通订单自动写入本地队列状态置为“待上传”小票正常打印网络恢复后收银台自动按顺序补传订单每条订单带一个客户端生成UUID服务端幂等表按UUID去重重复提交只返回原订单号不会重复扣库存。这里有个很现实的矛盾断网期间门店库存是准的但中心库存可能已经被其他门店卖完了。于是会出现在线时明明没货断网你照卖的情况。我的处理方式是允许断网订单产生“负库存”标记补传成功后库存按实际扣减当天日结时把这个门店的负库存项单列出来提醒后台做调拨或修改可售状态。硬要禁止负库存反而会让收银台在断网状态下彻底卡死业务损失更大。断网期间涉及支付也一样微信支付宝这类在线支付没法授权可以引导顾客走现金或“挂账”挂账单在补传时同步生成应收记录。4. 库存协同大仓、门店、调拨与盘点要闭环4.1 中央大仓与门店库存卖场和后仓要分清对于仓储式超市库存不是只有门店卖场一个概念它至少分三个物理层中央大仓总部统一进货、统一储备的区域门店卖场顾客能看到能直接买的货架库存门店后仓门店自己的备用库存卖场货架空了就补。POS收银直接扣减“门店卖场”库存。门店后仓到卖场的补货动作在系统里做成“库内移库”同一门店不同仓库ID之间做内部调拨。如果不分开盘点时永远分不清货架上的和仓库里的差异出在哪。补货逻辑我做成这个链路门店销量分析生成补货建议单参考最低库存、日均销量、在途数量—店长确认—总部审核—大仓出库—门店收货入库。整个过程单据完整每一段都能追踪到人。这里面有一个常见误区中央大仓给门店补货要不要立刻增加门店库存答案是不能。大仓出库后这批货是“调拨在途”状态既不在大仓库存里也还没进门店库存。必须等门店确认收货库存才真正加到门店。中间任何一步不确认账都会差一层。4.2 调拨流程与在途库存跨公司调拨的本质是买卖门店之间互相借货看起来只是移库存但涉及财务时必须分两种情况同一个公司内部两个门店调拨按成本价调拨不计收入只影响两个门店的库存和成本归属不同公司之间的调拨本质是内部销售调出公司确认收入调入公司确认成本生成内部结算单。我设计的调拨单状态机是草稿 — 调出方审核 — 出库确认 — 调入方收货确认 — 完成。只要不是“完成”状态库存都挂在in_transit_qty字段上。这样总部看库存报表时能清楚看到每一批货卡在哪个环节不会只说一句“货不见了”。实际操作中最容易出现的问题是调拨差异。比如大仓发出去100箱门店收货只收到98箱。这个差异不能默默改库存数量要在“收货差异”里记录并生成报损/溢余待审单。至于缺失的2箱是仓库漏发还是运输丢损由责任人线下处理完毕后在系统里补单据。4.3 盘点机制与库存差异处理盘点是最能检验库存模型是否合格的功能。仓储式超市SKU多不能动不动就闭店盘点。我的方案是循环盘点后台可按区域/货架/品类生成盘点任务每次只盘一部分区域盘点时该区域库存先冻结锁定快照终端录入实盘数后台自动比对账面库存与实盘数差异行单独列出确认无误后生成盘盈盘亏单审核通过后写入库存流水。这里有个很重要的经验盘点差异单据不要直接改库存余额。要先把盘点任务里的盘点前数量和实盘数量作为两条流水记录一正一负或一负一正保证流水账是连续的后面追责才有依据。实际项目里盘点查到差异后如果差异率超过一个阈值比如1%我建议先不急着盘盈盘亏而是去查流水有没有订单取消但库存没回滚有没有退货只退钱没退库存有没有条码串号。把根因找出来再调账否则本月调平了下个月又会冒出更大的差异。5. 分账与结算多公司多门店的钱怎么算清楚5.1 收款渠道与资金归集门店独立收款优先收银端会接很多支付渠道微信、支付宝、云闪付、现金、银行卡、预存卡、挂账。最怕的是所有渠道都收到总部一个商户号下面然后月底再手工切分到各门店。手续费怎么摊、退款时怎么区分全是麻烦。我这边强烈建议每个门店申请自己独立的微信/支付宝商户号。资金直接T1到门店对应的公司账户财务核算时天然按门店归集总部只需要在系统里做报表聚合不涉及二次清分。支付渠道的手续费也直接跟着商户号走每一笔订单都能对应到支付渠道扣费记录。如果某些连锁客户总部为了拿到更低的支付费率坚持用一个统一商户号那系统必须能下载渠道对账单按门店维度拆分明细并按比例分摊手续费。这个功能单独做一个“渠道对账”模块不要塞进订单模块里否则后面对账时查询会越来越慢。5.2 多公司独立核算日结、内部结算单与凭证导出多公司独立核算核心是把每笔业务在发生时就把归属公司定清楚销售订单通过门店归属公司生成收入凭证到门店所属公司采购订单通过仓库归属公司生成成本凭证到仓库所属公司调拨单跨公司时自动生成“内部销售单内部采购单”两张单据金额按调拨结算价计算。每日日结时每个门店独立做销售汇总、支付汇总、优惠汇总、退款汇总、交接班差异。这几个汇总数要和渠道对账单、现金盘库单三方核对全部对上才能日结完成。有任何一分钱差异收银主管都要在系统里写备注留痕。举个例子A批发公司调拨一批货给B零售公司门店系统生成两张单单据方向金额影响内部销售单AA确认收入按调拨结算价10000元A公司收入10000成本8000内部采购单BB确认成本按调拨结算价10000元B公司库存成本10000月底两个公司的往来余额对不上时直接查两边的内部结算单号关联效率比以前翻倍。这套设计不要求系统自带完整财务总账但数据结构要能导出标准凭证到财务软件。每个模块在业务单据上预留account_entry_id方便关联凭证号。等以后要上财务模块直接在现有单据上补分录即可不用动业务表。6. 常见故障与排查实录上线半年我踩过的坑6.1 并发扣减导致超卖为什么库存会变负数多门店同时卖同一个SKU时最容易出的bug就是并发扣减超卖。典型的错误代码是这样inv Inventory.objects.get(sku_idsku_id, warehouse_idwarehouse_id) if inv.available_qty qty: inv.available_qty - qty inv.save()两个请求同时读到available_qty10都判断101成立各自扣1最后available_qty还是9实际上已经卖了2件。修复方式是用数据库原子更新UPDATE inventory SET available_qty available_qty - 1, version version 1 WHERE sku_id ? AND warehouse_id ? AND available_qty 1;更新影响行数为0说明库存不够立即返回“库存不足”。不需要引入Redis分布式锁数据库行锁在这个量级完全够用。判断标准很简单只要单库TPS不超过几千行级原子更新是最可靠、最简单、性能最好的方案。6.2 库存对不上账先别急着调账按流水重放“账实不符”是零售系统的头号投诉。排查思路要固定成套路否则每次从头查太累对现有库存余额快照和库存流水余额做比对找出SKU和仓库按时段重新计算该SKU的流水累计值与余额表比对找出是哪一天开始分叉把当天所有单据销售、退货、采购、调拨、盘点、报损明细拉出来逐项核对找到差异单据后看是否是重复补传、单据状态和库存状态不一致、或手工改动过流水。我这边工具箱里长期备着一个对账SQL按biz_type分组汇总change_qty再与inventory对拍一跑就能锁定异常单据。做一个定期每天凌晨自动执行的库存对账任务差异超过阈值自动告警比等门店投诉再查强得多。6.3 退货与促销优惠分摊退得少顾客闹退得多门店亏满100减20这种整单促销顾客退其中一件商品到底退多少钱如果订单没有行级分摊优惠就只能按比例手工算POS收银员根本算不过来。我的办法是创建订单时直接做好行级分摊行分摊优惠 整单优惠 ×该行商品金额 / 整单商品总金额行可退金额 行商品金额 - 行分摊优惠 - 该行已退金额保留两位小数舍入误差单独放到订单头“分摊尾差”字段里保证所有行分摊优惠之和等于整单优惠。另外一个容易漏的坑退货不只是退钱还要回补库存。如果退货单只生成退款单没生成库存回补流水卖场库存就会越退越少最后对账对不上。我规定退货流程必须同时走“退款单入库单”两步缺一不可。6.4 硬件外设与系统运维的实战经验仓储式超市门店有很多外设标签打印机、小票打印机、扫码枪、钱箱、电子秤、自助收银机。硬件问题虽然不复杂但会非常消耗实施精力。标签打印机和小票打印机我建议优先选支持ESC/POS指令的型号直接通过打印指令控制排版比依赖Windows驱动稳定得多。尤其是POS前台驱动一更新就可能打印乱码而指令模式基本不受系统升级影响。电子秤的连接方式首选网口次选串口尽量避免USB口。称重数据要实时传到POS端USB连接容易受驱动影响突然断掉。串口只要参数配好稳定性尚可。买称重机时一定先确认协议是否开放有些品牌的数据协议要额外买接口授权合同里没写后期会很被动。数据库备份我这边用“每日全量实时binlog”流水表按月分区。POS前台产生的数据量不算大但库存流水和操作日志增长很快分区能显著提升月报查询速度。系统升级一律先灰度到一家门店稳定三天后再全量推广避免一次升级把所有门店都带崩。6.5 多公司多门店项目的实施顺序最后说说整个项目的落地顺序。第一次做这类系统时我最容易犯的错是一上来就写代码结果发现需求根本没谈清楚。这套系统的正确顺序应该是先梳理组织架构有哪几个公司公司之间什么关系哪些门店归哪个公司仓库归属谁再梳理商品和库存规则哪些商品共享库存哪些商品只能由某个公司采购内部调拨价怎么定然后才是功能开发基础资料、商品中心、库存中心、POS交易、结算分账最后是联调和试点先跑一家门店试营业跑顺了再扩到所有门店。资料收集阶段永远比想象中慢。我一个客户光整理商品主数据就花了两周但这些基础数据不整理清楚后面每上一个功能都要回头补。宁可前两周慢一点也不要后期返工。整套系统做完我的体会是多公司多门店的POS项目并不是技术难点有多高而是业务语义必须在一开始就对得非常细。公司之间怎么分账、门店之间怎么调拨、断网怎么兜底这些想透了代码只是照着实现而已。它后面还能继续扩展比如接入自助收银、移动手持PDA、供应商对账后台骨架只要没歪功能越加越顺。
返回列表