多商户多仓库SaaS进销存源码:数据隔离与库存并发实践

发布时间:2026/10/9 15:45:40
多商户多仓库SaaS进销存源码:数据隔离与库存并发实践 简介这是一套面向企业信息化与进销存SaaS开发场景的多商户ERP管理系统源码包。系统支持总公司—子公司—门店三级组织架构各企业数据完全隔离同门店多仓库共享基础数据但单据隔离不同门店的仓库也支持调拨总公司与子公司账号可按需切换到门店操作适合需要多租户隔离、多级权限与库存协同管理的云进销存项目二次开发。压缩包共1317个文件整体约22.36MB以php文件承载后端业务js、css、html构成前端交互与页面png、gif、jpg等图片素材用于界面另含sql、csv、txt等配置说明便于按模块检索。目前已有231人学习/下载。对开发者和部署运维者而言这套源码提供了从扫描开单、多仓库调拨到SaaS营销版无限商户的完整闭环可直接复用或改造也能帮助理解多商户数据隔离与多级组织架构的设计实现。1. 多商户多仓库SaaS进销存源码真正难的从来不是扫码枪那一声响一套标着“多商户、多仓库、带扫码、SaaS营销版、无限商户”的进销存ERP源码功能听起来很杂但真正难的不是扫码枪那一声响而是“一套实例服务多家商户每家商户还有多个仓库”时数据隔离和库存并发这两件事。部署一套这样的系统平台方可以不断开通新租户每个租户自己开仓库、建SKU、做采购和销售单据扫码枪扫一下就能完成出入库。这类源码解决的是中小商贸企业的实际问题要开分店、要管多个仓、要用手机或扫码枪快速录单还要能发优惠券、做会员营销。对于有技术基础的个人开发者和软件服务商来说在一套成熟源码上做二次开发比自己从零建模快得多。下面从选型、部署、核心代码、避坑、验证五个方向拆开讲代码和表结构尽量给到可以直接抄的程度。如果你准备接手这类源码做交付这篇应该能帮你省下几个周末的排查时间。2. 源码架构与数据模型多商户隔离、多仓库库存、扫码落库怎么设计拿到压缩包先别急着解压跑起来。先花半小时把数据模型和目录结构读明白比盲跑一通省时间。这类源码一般拆成平台管理端和商户端两套入口平台端负责开通商户、配置套餐商户端负责仓库、员工、SKU和日常单据。扫码也不是一个独立模块它只是把条码转换成SKU的快捷录入方式真正干活的是出入库单据和库存流水。2.1 选型先想清楚PHP单体为什么是这类源码的主流市面上这个定位的源码包主流实现是PHP单体加MySQL加Redis前端走H5或者小程序后台管理用单独的后台入口。和Java微服务版本相比PHP单体在中小规模交付中有明显优势部署轻、一台常规云主机就能跑、二次开发门槛低、改完刷新就能看到效果。Java版本虽然并发扩展性更好但运维成本和开发环境要求高几十个商户的体量根本用不上那套能力。这里要先把标题里的“无限商户”说透。它通常指的是源码在授权层面不限制商户创建数量并不是一台服务器能无限扛并发。真实影响上限的是数据库连接数、库存表的数据量、以及定时任务的执行速度。遇到动辄说“无限”的源码第一反应应该是去看它有没有做Redis缓存、有没有读写分离的预留而不是先高兴。对比项PHP单体Java微服务部署成本低LNMP一套就够高需要构建和容器环境二开门槛较低改完即生效较高链路长编译慢商户规模几十到几百商户够用成百上千商户更稳适合团队个人、小团队、区域服务商平台型产品团队2.2 多商户数据隔离的三种方案与merchant_id索引陷阱SaaS多商户第一问题是数据隔离。常见三种做法共享库共享表加merchant_id共享库独立schema独立库。独立库隔离最彻底但开通新商户要建库建表运维成本高独立schema在MySQL里实际还是同一套存储只是逻辑命名空间用得最多的是共享表加merchant_id所有业务表都带商户字段查询靠条件过滤区分。这套源码常见的就是第三种。优点是开通商户零成本缺点是代码漏掉一个条件就串号。先看一个标准查询长什么样SELECT s.sku_id, sk.sku_name, s.available FROM erp_stock s JOIN erp_sku sk ON sk.id s.sku_id WHERE s.merchant_id 10 AND s.warehouse_id IN (1, 2);逻辑很简单坑在索引。stock表的数据量上来之后如果只在warehouse_id上建索引商户条件无法走索引会扫全表。正确做法是建联合索引商户字段放最左(merchant_id, warehouse_id, sku_id)。查询条件里固定有merchant_id的索引才能稳定命中。没有索引的SaaS系统做到每天几万条库存流水后单据打开就明显变慢。2.3 多仓库库存模型分仓库存表为什么不需要再加总库存多仓库和多商户是两个独立维度。商户确定后仓库之间是物理隔离的库存关系A仓的库存和B仓的库存不能互相顶替调拨得走调拨单。核心库存表的设计通常是这样CREATE TABLE erp_stock ( id int unsigned NOT NULL AUTO_INCREMENT, merchant_id int unsigned NOT NULL DEFAULT 0, sku_id int unsigned NOT NULL DEFAULT 0, warehouse_id int unsigned NOT NULL DEFAULT 0, available int NOT NULL DEFAULT 0, locked int NOT NULL DEFAULT 0, updated_time datetime DEFAULT NULL, PRIMARY KEY (id), UNIQUE KEY uk_merchant_sku_wh (merchant_id, sku_id, warehouse_id), KEY idx_merchant_wh (merchant_id, warehouse_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;available是可用库存locked是订单占用库存。下单时先锁库存收款后再扣available并释放locked取消订单则回滚锁定的数量。很多新手会再加一张总库存表觉得查询方便但实际上总库存和分仓库存同时更新事务一致性很难保证对不上账时根本不知道信哪张表。总库存需要时直接用SUM聚合实时算不需要单独维护。每次库存变动都要落流水表这是对账的最后防线。stock_flow表至少要有商户、SKU、仓库、变动数量、业务类型采购入库、销售出库、调拨、盘点、期初、关联单据号。流水只增不改出问题还能顺着单号倒查。2.4 扫码模块是怎么组成的扫码枪、摄像头和服务端条码匹配进销存里的“带扫描”通常指两种设备USB扫码枪和手机摄像头。扫码枪本质上是一个键盘输入设备扫一个条码等于敲一串字符再带一个回车手机摄像头则是调用摄像头识别条码后填入输入框。所以前端要做的事情并不复杂真正的匹配逻辑在后端。服务端拿到条码要按优先级找SKU先精确匹配条码字段匹配不到再尝试去掉前后缀的模糊匹配再不行就让操作员手动选择商品。这里给出一段接收扫码输入的入口示意$code trim(input(code)); $sku Sku::where(barcode, $code)-first(); if (!$sku) { // 去掉常见前缀后缀比如 0 开头补位 $cleanCode ltrim($code, 0); $sku Sku::where(barcode, $cleanCode)-first(); } if (!$sku) { throw new \Exception(条码未识别请手动选择商品); }注意手机端调摄像头扫码H5页面必须在HTTPS环境下才能调起相机权限这是浏览器安全策略决定的。扫码枪则没有这个限制但要注意输入法状态这个问题后面避坑章会单独展开。3. 把源码在本地跑起来环境配置、数据库初始化与首个商户创建本地跑通的最小目标是看到一个后台登录页并且能创建出第一个商户。建议直接在Linux云主机或虚拟机上操作Windows下PHP版本坑多路径带空格还会引出一堆玄学报错。整个部署过程按环境初始化、代码放置、数据库导入、后台建商户四步走顺序不要乱。3.1 本地运行环境PHP版本、扩展清单与安装命令先确认依赖组件。这套源码常见组合是PHP 7.4或8.0、MySQL 5.7或8.0、Redis 6、Nginx。PHP版本直接看源码里的语法兼容性如果代码用了构造函数属性提升这类特性至少要PHP 7.4起步。扩展方面最常缺的四个是bcmath、gd、mbstring、redis。组件推荐版本用途PHP7.4 / 8.0后端运行环境MySQL5.7 / 8.0业务数据存储Redis6.x缓存、扫码防抖、商户配置Nginx1.20静态资源与PHP转发以Debian系为例装完基础环境后补扩展apt-get update apt-get install -y php8.0-cli php8.0-fpm \ php8.0-mysql php8.0-redis \ php8.0-gd php8.0-bcmath php8.0-mbstring php -m | grep -E bcmath|redis|gd|mbstringbcmath负责金额精度计算缺失时营销金额计算会直接报未定义函数gd负责验证码和商品图片缩略安全登录页离了它转不出来mbstring处理中文SKU名和导出文件没有它导出Excel时中文全变乱码。装完之后用grep确认扩展已加载再进入下一步。3.2 解压后先读目录入口文件与nginx根目录指向解压后先看目录结构通常长这样erp/ ├── app/ │ ├── controller/ │ ├── model/ │ └── service/ ├── config/ ├── public/ │ └── index.php ├── runtime/ ├── extend/ ├── route/ └── install/绝大多数404问题出在站点根目录指错。Nginx的root必须指向public目录而不是项目根目录。否则访问后台时PHP能找到文件但URL重写全部失效路由全跑到前端控制器上。伪静态配置也要跟着开把不存在的文件请求都转发到index.phplocation / { try_files $uri $uri/ /index.php?s$uri$args; }runtime目录要给写权限否则编译模板、生成日志、写缓存全都会失败。权限命令是chmod -R 777 runtime生产环境改成php-fpm用户拥有即可。3.3 导入初始化数据库并配置连接prefix参数不要乱动拿到源码包通常会附带初始化SQL文件名字可能是erp_init.sql或install.sql。先导入mysql -u root -p erp_init.sql导入完成后去看config下的数据库配置。一般会有一个database配置文件也可能跟.env环境变量文件配合。核心配置写出来是这个意思return [ type mysql, host 127.0.0.1, port 3306, database erp_saas, username erp_user, password YourStrongPass, prefix erp_, charset utf8mb4, ];prefix是表前缀千万别看它不顺眼就改。改前缀相当于把所有表名换一遍除非你连源码里所有模型文件名一起改否则启动第一时间就是各种“表不存在”。另外密码里如果带特殊字符注意配置文件是单引号还是双引号包裹省得被转义坑一次。3.4 初始化顺序先建商户再建仓库最后建员工账号数据库通了你就能打开安装向导或默认后台登录页。第一次进来初始化顺序有讲究在平台端创建第一个商户设置套餐和到期时间。进入商户后台先建仓库仓库名称编码要规范。创建SKU和商品分类SKU编码建议按品类规则生成。最后创建员工账号绑定角色和仓库权限。顺序不能反。很多源码的收银端登录后要拉取“当前员工可操作的仓库列表”这个列表是按员工和仓库的绑定关系过滤的。如果先建员工再建仓库员工登录后仓库选择器是空的操作员会以为系统坏了。建员工的时候还要分配权限组扫码出入库、盘点、查看报表这些菜单都在角色权限里漏配一个菜单前端表现是“按钮消失”排查起来容易误导人。4. 核心代码走读商户权限过滤、库存扣减、扫码出入库和营销计算环境跑通之后二开碰得最多的是四块代码模型查询自动带商户条件、库存并发扣减、扫码出入库接口、营销金额计算。这四块也是决定系统后期稳不稳的关键。4.1 商户权限隔离的关键用模型全局作用域兜底多商户系统最怕的是开发者在某个新接口里忘写merchant_id数据串得一塌糊涂还不好查。很多源码会在模型基类里做一层兜底常见写法是全局作用域。这是一个Eloquent风格示例ThinkPHP的模型过滤器思路完全一样trait MerchantScope { public static function bootMerchantScope() { static::addGlobalScope(merchant, function ($builder) { $builder-where(merchant_id, merchant()-id()); }); } }这个Trait挂到所有业务模型的基类上之后每次静态调用查询都会自动追加商户条件。merchant()是一个全局助手函数从当前登录态里取商户编号。但要注意全局作用域只保护模型查询和关联查询。你用原生SQL、查询构造器或者写子查询时它管不到。所以避坑守则是二开一律走模型层禁止为了图方便写裸SQL查业务表。如果实在要写必须显式拼上merchant_id并在代码评审时重点盯这一行。4.2 库存扣减别用先查后改事务加锁或条件更新很多第一次做进销存的人写扣库存都是“先查出来判断够不够再减掉”。这个逻辑在并发场景下必翻车。两个收银员同时买同一个SKU都读到库存还剩1件都判断“够”都执行减1结果是库存变成负数。正解是让“判断扣减”成为一个原子操作。用事务加行锁的写法看这一段DB::transaction(function () use ($skuId, $warehouseId, $qty) { $stock Stock::where(merchant_id, merchant()-id()) -where(sku_id, $skuId) -where(warehouse_id, $warehouseId) -lockForUpdate() -first(); if (!$stock || $stock-available $qty) { throw new \RuntimeException(库存不足); } $stock-available - $qty; $stock-save(); StockFlow::create([ merchant_id merchant()-id(), sku_id $skuId, warehouse_id $warehouseId, change_qty -$qty, biz_type sale, biz_no $orderNo, ]); });lockForUpdate会给这条库存记录加行级排他锁第二个请求必须等第一个事务提交后才能读所以不会出现并发扣超。库存流水和库存扣减放在同一个事务里保证任何一方失败都能回滚。如果不方便用事务也可以换成条件更新UPDATE erp_stock SET available available - ? WHERE merchant_id ? AND sku_id ? AND warehouse_id ? AND available ?;执行后检查受影响行数等于0说明库存不足。这种写法少了行锁等待并发量高时更推荐。4.3 扫码出库接口条码解析、防重复提交与扣减联动扫码出库是收银端最频繁的接口。它的完整路径是前端收到扫码枪回车事件→把条码传给后端→后端按条码找SKU→校验库存→扣减并生成销售明细分录。接口里要处理两件大事条码清洗和防重复提交。条码清洗解决的是全角字符、空格、回车残留问题防重复解决的是扫码枪连击导致同一商品被扫两次。public function scanOut(Request $request) { $code trim($request-input(code)); $sku Sku::where(barcode, $code)-first(); if (!$sku) { $sku Sku::where(barcode, like, $code . %)-first(); } if (!$sku) { return error(条码未识别请手动选择商品); } // 当前订单下做3秒防抖防止扫码枪连击重复提交 $lockKey scan_lock_ . $orderNo . _ . $code; if (!Cache::put($lockKey, 1, 3)) { return error(条码已提交请勿重复扫码); } // 复用库存扣减逻辑 deductStock($sku-id, $warehouseId, 1, $orderNo); return success([ sku_name $sku-sku_name, price $sku-price, ]); }Cache::put的第三个参数是过期秒数这里设3秒既不影响同一种商品连续扫多件也能拦住误触连击。注意防抖只是第一层真正的幂等判断还是要靠订单状态再次提交相同订单明细时应该走“已经存在则加数量”而非重复插入。扫码枪的输入框要留意回车事件别阻止默认提交否则扫码枪回车唤不醒查询。4.4 营销版金额计算会员价、满减、优惠券、分摊的执行顺序营销版和基础版的差别主要在订单金额计算这一层。常见执行顺序是会员价优先→满减→优惠券→尾差分摊。顺序错了同一笔订单能算出完全不同的金额。金额计算必须在服务端做不能信任前端传来的合计金额。前端可以看后端必须重新算。一个简化的计算流程// 所有金额统一转成“分”计算避免浮点误差 $amount (int) round($sku-price * 100); // 第一步会员价覆盖原价 if ($memberTier $sku-member_price 0) { $amount (int) round($sku-member_price * 100); } // 第二步满减按商品行参与金额判断 if ($orderTotal $rule[min_amount]) { $amount - (int) round($rule[discount] * 100); } // 第三步优惠券抵扣不超过当前剩余应付 $couponAmount min($amount, $coupon-value); $amount - $couponAmount;优惠券、满减规则都要带merchant_id和时间窗口字段跨商户的营销规则是常见的数据泄露点。分摊时处理尾差最后一行的明细金额用“订单总额减前面明细合计”来填保证订单头与明细总和永远一致。营销规则启用之后注意后台要能配置适用仓库否则多仓库场景下A仓的活动被B仓用掉对账时会产生莫名差异。5. 部署与二次开发避坑几个容易让系统翻车的配置点这套源码跑通不难真正花时间的是那些不报错但结果不对的问题。下面五个坑是我每次接手类似系统都要排查一遍的地方每条按现象、原因、解决展开。5.1 坑一商户A看到了商户B的订单和会员数据现象商户A的管理员登录后台在会员列表或订单列表里能看到其他商户的数据统计数据偶尔是全部商户的汇总需要手动还原本商户数据才能对上。原因模型层全局作用域管住了常规查询但二开时有人图方便用了原生SQL或者直接查询了中间表、关联表漏写merchant_id条件。也可能是登录时没有正确初始化商户上下文导致全局作用域里的merchant_id取出来是空值查询条件被直接忽略。解决先确认入口中间件有统一的商户身份注入再全局搜索代码里的Db::query和原生select语句逐一补上商户条件。排查时可以临时打开SQL日志看每条业务SQL是否都带merchant_id条件。后期团队约定所有跨表查询必须走模型关联原生SQL需要额外提交说明原因。5.2 坑二并发销售时库存被扣成负数现象两个收银员同时卖同一个SKU订单都成功生成了库存却变成了负数而且库存流水里的变化数量看着也没错就是汇总对不上。原因扣库存用了“先查后改”的非原子操作。两个请求都查到剩余1件都判断够卖然后分别执行扣减最后一个请求把库存扣到了-1。解决改事务加行锁或者用条件更新。条件更新的方式简单直接UPDATE带available ?条件执行完判断影响行数0就说明库存不足。注意一条SQL把扣减和判断合并不要拆成两步。另外库存扣减、流水写入、单据状态更新必须放在同一个事务里单独在外面先扣库存再写单据失败时就得人工对账。5.3 坑三扫码枪扫出来的条码多字符或者丢位现象扫码枪扫同一件商品有时候扫得出来有时候提示条码未识别。把扫进去的码打出来看前面多了个“a”或者全角数字占了两字节甚至最后少了一位。原因扫码枪本质是键盘输入输入法在中英文切换状态时会干扰录入结果中文全角状态下数字和字母会被替换成全角字符。还有一部分扫码枪会按厂商配置追加回车符或前缀码代码没做清洗就直落库。解决前端扫码输入框加正则清洗只保留数字和字母后端在接收条码时统一trim并过滤非可见字符。识别不了的时候可以尝试按条码后几位或者去掉前导零再匹配但模糊匹配范围别放太宽否则两个相似条码会对应错商品。收银端页面上加一个输入法锁定提示让操作员保持英文输入态能省掉一半这种问题。5.4 坑四定时任务没挂库存快照和报表一直不更新现象系统跑了一周日常单据都正常但库存预警、积分过期、优惠券失效这些功能全没反应日结报表数字停留在部署当天。原因源码里用命令行执行定时任务部署时只配了Web环境没有配置crontab计划任务从来没运行过。有些源码甚至把库存快照生成也放在定时任务里不跑库存统计就是旧数据。解决找到源码里的command或task目录确认计划任务入口然后用crontab挂上。一般像这样# 每天凌晨 2 点执行库存结转与优惠券清理 0 2 * * * cd /data/www/erp php think schedule:run runtime/cron.log 21日志一定要落文件。这样定时任务哪天没跑还能通过日志时间定位是脚本卡住还是cron本身没触发。上线前把任务列表打印出来核对一遍别漏了。5.5 坑五核心文件加密或绑定域名本地跑不起来现象代码部署好后登录页正常但一进入核心业务页面就白屏查看文件发现是乱码或者页面提示“授权域名不匹配”必须改成特定域名才能访问。原因交付方对核心模块做了代码加密保护加密文件绑定了授权域名或服务器IP。本地用localhost或测试IP访问时授权校验失败导致整个模块不可用。解决先看源码包里有没有license、授权key之类的配置文件按有效渠道的流程绑定当前测试域名。注意这里的核心是确认你拿到的授权范围是什么能不能用于本地开发和二开调试。如果加密文件无法解密要评估加密部分覆盖了哪些模块是否影响你的改造计划。遇到这种情况最稳妥的做法是在正式采购前要求对方提供完整可读源码并在合同里写清交付物形态避免二次开发时被加密文件挡住。6. 上线前的完整验证用最小订单流把系统跑一遍源码部署完别急着给客户演示。先按最小业务流完整走一遍能发现八成以上隐藏在配置里的问题。这里列一张验证表照着走每一步都确认结果。步骤操作预期结果建商户平台端创建商户并分配套餐商户登录成功独立空间建仓库商户后台创建仓库并绑定员工员工端能看到该仓库建SKU录入商品编码、条码、价格商品列表可见采购入库录采购单或扫码入库库存增加库存流水生成扫码销售收银端扫码出库库存减少销售单生成营销订单用优惠券或满减下单金额计算正确明细可查盘点录入盘点数量差异生成盘盈盘亏单报表查看日结与库存汇总数字和单据明细一致这八步走完核心链路基本通了。再加两个进阶点能少走很多弯路。第一移动端对接时H5扫码页面必须提前准备HTTPS证书本地开发可以用内网穿透临时顶一顶正式环境直接配证书。第二SKU和期初库存多半要走Excel导入导入模板的列名不要手动改代码是按模板列名解析的多一列少一列都会报错导入前先清一遍模板里的格式符号省得把不可见字符带进数据库。库存这条路最怕对不上账。我做这类项目最深的一个教训是线上库存和账面库存不一致时不要急着改库存先查流水。流水完整问题一定在单据创建逻辑流水不全问题就在事务边界。先保流水再修余额账才能平。希望这些踩过的坑能帮到你至少在你上手这套系统时少熬几个盯着日志发呆的凌晨。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询