ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

多门店进销存管理系统PHP源码解析:从部署到二次开发

多门店进销存管理系统PHP源码解析:从部署到二次开发 做进销存系统这个方向我前前后后折腾了快三年。去年帮一个开连锁水果店的朋友整理多店进销存管理系统源码的时候被各种细节折磨得够呛——单店逻辑谁都会写一旦摊上多门店库存同步、调拨流转、汇总报表、权限隔离每一个环节都得重新想一遍。后来项目收尾我把整套源码做了脱敏和整理去掉里面客户的私有配置补上通用安装说明再放到干净的服务器上从头部署验证了两遍确认可用才敢说是“亲测可用”。今天这篇要聊的就是这份亲测可用的多店进销存管理系统源码的完整拆解。这套源码的核心不是炫技而是把多门店进销存最常见的业务场景全部跑通总部统一维护商品资料各门店独立开单、独立看库存总部可以跨店调拨、汇总利润。部署门槛不高PHP加MySQL就能跑前端用Bootstrap加jQuery后端是原生PHP按简单MVC分层写的懂点PHP的人改起来不费劲。接下来我把这套系统的来龙去脉、数据库设计、多店逻辑、部署流程、测试中踩过的坑以及二次开发的建议全部摊开讲一遍方便你快速判断它适不适合接过来用。1. 为什么最后我做了这套系统而不是直接用现成的进销存软件1.1 现成软件补不上的三个缺口当时我朋友那家连锁水果店规模不大不小五家门店加一个总仓。最开始用的是一款很常见的进销存SaaS单店版本免费多店要按门店数收年费五家店一年下来也是一笔不小的开支。更麻烦的是他们的损耗管理跟别人不一样——进口水果当天到货当天结账坏果要单独在系统里记损耗否则月底利润算出来全是虚的。SaaS产品的报表字段是写死的你想加一列“损耗金额”都只能找客服提需求等一个季度都排不上。数据就更别说了进销存跑了一年想导出一份连续12个月的各店毛利对比后台居然没有这个维度导出来的是乱七八糟的流水。当时市面上其他几个系统我也挨个试过要么数据结构封闭、导出还要单独收费要么多门店只是个伪概念门店之间调拨还得靠线下手工单。1.2 技术选型为什么是PHP而不是Java、Python很多朋友看到源码第一反应是问怎么不用Java、不用Python原因有三个。第一是部署门槛。进销存这种东西很多中小商户的服务器就是一台便宜的云主机甚至有的就扔在一台旧电脑上装了个面板。PHP在这类环境里几乎是零成本跑起来的装个Nginx、装个MySQL复制过去就能用。Java写一套得打包、配JDK、配Tomcat对维护的人来说是灾难。第二是修改效率。多店进销存的业务逻辑就是增删改查加上一堆条件判断。PHP动态语言改完刷新就能看到效果不需要等编译。做二次开发的时候恨不得一天迭代三版这种“改完即所得”的节奏相当舒服。第三是生态。进销存相关的PHP开源项目、代码片段、踩坑帖非常多遇到了问题搜一下基本都有答案。如果你要服务的是几千家门店的连锁巨头那就得上Java或者Go做高并发这套代码显然不是给那种规模准备的。十家店到几十家店这种量级PHP单机扛几百并发完全够用。1.3 第一版的功能边界我当时跟朋友对需求的时候没有一上来就想着做得多花哨而是列了一张“必须要有”的清单门店管理新增、停用门店设置门店负责人员工账号与权限总部管理员、店长、收银员三种角色各自看到不同的菜单和数据商品管理总部统一建商品档案支持多规格同一款苹果分“大果”“中果”两个SKU采购入库门店可以独立采购也可以由总部统一采购后分拨销售开单快速选择商品、自动带出价格、支持打折和抹零库存调拨门店之间调拨总部审批后发货、收货确认库存盘点与库存流水所有变动都有记录可回溯到人、到时间汇总报表按门店、按商品、按月统计销售额、毛利、库存成本这个边界很重要很多进销存项目死在“什么功能都要”第一版加了几十个菜单结果一半没人用一半没做好。先守住这个边界跑通后面再迭代比什么都强。2. 数据库设计多店进销存的底子怎么打2.1 门店、商品、库存三张主表这套系统采用的是原生PHP按简单MVC分层数据库是MySQL 5.7。多店的“多”字本质上是靠表之间的关联字段撑起来的所以建表的时候我特别在意三个字段store_id、goods_id、sku_id。门店表CREATE TABLE store ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, store_name VARCHAR(100) NOT NULL COMMENT 门店名称, contact_name VARCHAR(50) DEFAULT NULL COMMENT 负责人, contact_phone VARCHAR(20) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1营业 0停用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT门店表;商品表CREATE TABLE goods ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, goods_name VARCHAR(200) NOT NULL COMMENT 商品名称, unit VARCHAR(20) DEFAULT 件 COMMENT 计量单位, category_id INT UNSIGNED DEFAULT 0 COMMENT 分类ID, status TINYINT NOT NULL DEFAULT 1, created_by INT UNSIGNED DEFAULT 0 COMMENT 创建人, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表;商品如果有多规格就拆出一张goods_sku表把“大果”“中果”这种规格存到sku_name字段单价和进价挂在SKU级别而不是商品级别。这一点如果不拆后面做销售开单选规格的时候会很痛苦。库存表CREATE TABLE stock ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, store_id INT UNSIGNED NOT NULL COMMENT 门店ID, sku_id INT UNSIGNED NOT NULL COMMENT SKU ID, quantity DECIMAL(14,2) NOT NULL DEFAULT 0 COMMENT 当前库存数量, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uniq_store_sku (store_id, sku_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存表;这里最核心的设计是唯一索引(store_id, sku_id)保证同一个门店同一个SKU只有一条库存记录。如果这里不约束两条记录各自攒数量最后报表对账的时候能让你怀疑人生。2.2 流水表每一笔变动都要留痕库存表保存的是“当前有多少”但它解释不了“为什么变成这么多”。所以必须有一张stock_flow流水表把所有变动记下来。业务上的采购、销售、调拨、退货、盘点最终都要转化为流水只有这样你才能回答老板最爱问的那句话“这个月库存少了二十箱到底少在哪”CREATE TABLE stock_flow ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, store_id INT UNSIGNED NOT NULL, sku_id INT UNSIGNED NOT NULL, biz_type VARCHAR(20) NOT NULL COMMENT purchase/sale/allocation/return/check, biz_no VARCHAR(50) NOT NULL COMMENT 关联单号, change_qty DECIMAL(14,2) NOT NULL COMMENT 正数为入库 负数为出库, before_qty DECIMAL(14,2) NOT NULL DEFAULT 0, after_qty DECIMAL(14,2) NOT NULL DEFAULT 0, operator_id INT UNSIGNED NOT NULL COMMENT 操作人, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_store_sku (store_id, sku_id), KEY idx_biz_no (biz_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存流水表;注意before_qty和after_qty这两个字段。很多进销存系统偷懒不记录变更前数量只写一条“入库50”结果后面一对账发现流水和库存对不上又只能干瞪眼。记录前后数量等于给每次操作都拍了前后两张照片出问题一查就能定位。2.3 订单用主从表不要图省事全部塞进一张表销售开单、采购入库这种业务前端展示的是一张单但数据库里要拆成订单主表和订单明细表两张表。主表存单号、门店ID、客户或供应商、总金额、操作人、状态明细表存这个单子里的每一行商品数量、单价、金额。为什么这么拆因为订单汇总表按天、按月统计的时候只需要扫主表很轻很快明细表只在打开某张单的时候用。如果你把商品明细直接拼在一行里用逗号隔开后面做统计、做利润分析、做退货的时候都要先拆字符串维护成本极高。我见过不少新手项目这么干最后全都返工了。3. 多店逻辑的核心数据隔离、调拨、汇总3.1 所有数据以store_id为界这套系统最大的一个坎是搞清楚“哪些数据是总部的哪些数据是门店的”。我最后定的规则很简单——商品档案、客户档案归总部统一管门店只能引用不能新建订单、库存、流水天然属于门店每一条记录都强制带store_id员工账号属于某一个门店总部管理员可以看所有门店的数据。那代码上怎么落实我在所有查询入口都加了一层强制过滤不允许传入一个不带store_id的查询条件。具体做法是写了一个公共的查询基类在底层把store_id拼进WHERE条件业务代码里拿不到原始模型对象只能通过基类方法取数这样就堵住了“写代码忘加过滤”这个最大的数据泄漏口子。3.2 门店调拨一个事务解决双店库存变化调拨是多店进销存比单店系统多的一个核心场景也是最容易出事的地方。不少调拨实现是发货店扣库存收货店却没加库存最后盘库发现凭空少了一批货。这套系统的调拨流程是A店发起调拨单选择要调的SKU和数量系统先冻结A店这部分库存调拨单状态为“待发货”。A店确认发货后系统在事务里扣减A店库存同时生成一张调拨流水。B店收到货确认收货后再在事务里增加B店库存更新调拨单状态为“已完成”。这里的关键是不能在一个事务里同时扣A店库存又加B店库存。很多看似“一步到位”的实现一旦发货但货丢了B店根本没收到账上却已经多了库存后面盘库必炸。物流单据和库存数据必须分离这是调拨模块的底线。3.3 总部汇总报表跨店统计怎么算总部看板要展示的是“所有门店加起来今天卖了多少钱”“哪家店库存积压最严重”。用SQL做也不复杂核心就是按store_id分组汇总然后联表把门店名称带出来SELECT s.store_name, COUNT(DISTINCT o.id) AS order_count, IFNULL(SUM(o.total_amount), 0) AS total_amount, IFNULL(SUM(o.total_amount) / NULLIF(COUNT(DISTINCT o.id), 0), 0) AS avg_order_amount FROM sale_order o LEFT JOIN store s ON s.id o.store_id WHERE o.order_date 2025-01-01 AND o.order_date 2025-02-01 GROUP BY o.store_id ORDER BY total_amount DESC;毛利率的计算稍微绕一点因为毛利等于销售额减销售成本销售成本要按每个SKU的加权平均进价去算不能简单用最后一次进价。这套系统在销售开单时就把当时的成本价快照到明细表里毛利报表直接按明细汇总就行不用每次重算历史成本。4. 从空服务器到跑通业务部署实测全程还原4.1 环境准备我当时用的是一台2核4G的轻量云服务器CentOS 7上面先装了宝塔面板。宝塔的好处是图形化装PHP、装MySQL都是点两下的事对没有运维基础的朋友很友好。如果你的服务器上已经装了LNMP环境跳过这一步直接配站点即可。PHP版本建议用7.4。不要用太老的5.6也不要急着上8.2因为这套源码里有些语法是PHP 7.x风格8.2偶尔会有兼容性提示7.4是性能和兼容性之间最平衡的版本。4.2 上传源码、导库、改配置在宝塔里添加一个站点域名或IP都能用。把源码压缩包上传到站点根目录解压。在宝塔的数据库管理里创建一个新库把根目录下的schema.sql文件导入。修改config/database.php里的数据库地址、用户名、密码、库名。设置站点运行目录为public伪静态规则按源码包里的nginx.conf配置。浏览器访问站点看到登录页说明基础部署成功。这里有个容易踩的坑站点根目录和运行目录public不是一回事。很多朋友把源码直接扔到根目录访问时看到的是文件列表或者404。必须把运行目录指向public同时保证config这类目录不能被外部直接访问里面存了数据库密码暴露出去后果很严重。Nginx里要加一条配置禁止访问.sql、.env、config等敏感文件。4.3 创建管理员、门店、员工部署完之后的第一步用种子账号登录后台。安装说明里初始管理员一般是admin初始密码我看大家习惯用123456但上线前第一件事就是改掉它否则系统一旦放到公网默认密码分分钟被扫描器拿到后台权限。接着进“门店管理”把五个门店和总仓都建出来。再进“员工管理”给每个门店配一个店长账号和一个收银员账号角色不同进来的菜单就不一样。这里我把角色权限做得比较细收银员只能开销售单店长多一个调拨和盘点权限总部管理员才有报表和数据导出的入口。4.4 初始化商品和期初库存商品档案可以手工一个个加也可以做Excel批量导入。我当时给朋友做的时候顺手写了个CSV导入功能第一行是字段名后面每行一个SKU导入前先校验格式避免编码乱码。期初库存的录入比较讲究要在正式启用系统之前一次性录完然后在“库存盘点”里把这批数量做一次期初盘点登记让库存流水表从同一起跑线开始记录否则后面所有对账都会差一个“期初”。5. 亲测过程中踩过的坑和对应修复5.1 并发扣库存数量变成负数第一个坑是典型的并发问题。门店两个收银员同时卖同一款商品系统先查询库存数量判断是否足够再扣减库存。两个请求同时进来都查到库存“还有5件”各自减了3件最后库存没有变成-1件反而可能变成2件但实际上已经超卖了。原因就是查询和扣减不是原子的。修复方式是在事务里对库存行加排他锁// 在事务内加锁读取库存 $sql SELECT quantity FROM stock WHERE store_id ? AND sku_id ? FOR UPDATE; // 锁住这一行后其他请求只能等待 // 判断足够后再执行 UPDATE stock SET quantity quantity - ? WHERE ...用FOR UPDATE锁住库存行其他事务必须等当前事务提交或回滚后才能操作同一行库存数量就不会被两个请求同时改掉。这个方法对中小门店的并发量来说完全够用不用引Redis分布式锁那种复杂方案。5.2 金额精度被四舍五入吃掉进销存里最不能出错的就是钱。最初的设计里单价和金额直接用DECIMAL(10,2)以为两位小数够了。结果发现分钱有时候对不上因为一张销售单里同款商品买了三件单价按两位小数存储单价乘数量之后还要保留两位小数中间过程一旦四舍五入和客户那边按另一个顺序算出来的结果就对不上。后来我统一了规则金额计算全程用DECIMAL(14,4)运算只在最后展示或者写入订单主表时才转成两位小数。比如单价9.99买3件是29.97没问题但如果是9.995这种单价中间保持四位精度最后才四舍五入到分就不会出现系统性误差。另外小数处理逻辑不要分散在三四个地方最好统一封装成一个金额工具类全系统都用它改一处全局生效。5.3 调拨单发货未确认两个店库存都出问题调拨模块第一版我写得比较乐观想着“总部确认调拨直接扣A店、加B店”一步到位。结果跑了两天发现问题物流送三天才到B店可B店账面早就显示收到货了营业员以为有货开单卖了一批实际上货还在路上最后到货少了一批两家店账面全乱。修复方案就是我前面讲的调拨状态机待审核、待发货、已发货、待收货确认、已完成、已作废。每一笔调拨单只有在目标门店点击“确认收货”之后目标门店库存才会增加。同时加了一个保护逻辑调拨单处于“已发货”状态时发货门店的对应库存被冻结不能用它继续开销售单。这个逻辑多了一步操作但实测避免了对账扯皮。5.4 后台接口没有鉴权改个参数就能看别店数据这个坑比较隐蔽也比较致命。一开始我把接口安全放在前端菜单控制上以为“页面里没显示这个按钮用户就点不了”结果朋友那边一个懂的店长直接在浏览器控制台改了请求参数把store_id从3改成4调用接口居然能看到4店的销售报表。这才意识到服务端必须对每一次请求做权限校验前端隐藏菜单只是体验优化不是安全手段。修复分两层第一层登录后在Session里记下当前用户的门店ID和角色ID所有查询接口自动带上Session里的门店ID忽略客户端传的store_id参数除非是总部角色且确实有跨店查询权限第二层在公共基类里统一检查操作权限分店收银员即使手动构造调拨接口也会返回无权限。这个修复做完后我又把所有接口过了一遍确保没有一个是裸奔的。6. 拿到这套源码之后建议往这些方向扩展6.1 先加这四个功能性价比最高一是条形码和扫码枪支持。实体门店每天大量销售开单手工搜商品名太慢。商品表加一个barcode字段销售开单输入框支持扫码自动带入效率提升立竿见影。二是简易客户管理。给销售单挂一个客户ID再凑上会员号和手机号就能做积分、挂账、赊账这些老客刚需功能。三是多计量单位转换。很多商品是进货按箱、卖货按件换算比例要在商品SKU上配置好开单时自动换算。不加这个功能采购和销售数量在报表里会打架。四是给店长推送提醒。比如某店库存低于预警值给对应店长的企业微信发一条消息。这个用定时任务扫一下库存表就行不复杂但对门店运营帮助很大。6.2 性能优化先看SQL再看缓存十几家店、几十万单量的情况下优化优先级是这样的第一条所有订单和流水表的日期字段建索引统计报表按日维度聚合时速度会快一个数量级第二条把汇总统计写成定时任务每天晚上凌晨跑一次把结果写入汇总表白天看板直接查汇总表不实时扫全部明细第三条如果真到了普通MySQL扛不住的时候再考虑Redis缓存热点数据和读写分离这套。6.3 二次开发最容易犯的错最后提醒一句别因为好看就急着换前端框架也别因为“顺手”就动数据库表名。这套源码的表之间有大量关联改动字段名或类型可能一个模块没影响另一个模块的SQL拼接里就报错了。改动之前先全局搜索这个字段在哪些地方出现过列个清单再动手。我吃过这种亏有一次把goods表的unit字段从char改成varchar看起来只是放宽长度结果某个自定义报表因为类型不匹配直接白屏查了半天才定位到是字段定义触发的问题。源码这种东西分享出来最大的价值就是让遇到同样问题的人少走弯路。我整理这份多店进销存系统的体会是进销存项目真正的难点不在代码本身而在业务逻辑的完整性和边界条件的处理上。你拿到这份源码可以直接拿去给客户演示也可以在它的基础上继续加功能。只要能跑通前面讲的调拨、盘点、汇总这三条核心链路后面加什么功能心里都有底。
返回列表