尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
ThinkPHP+Layui进销存系统:部署、二次开发与库存管理实战解析
简介点可云进销存系统是一套面向中小企业的开源企业资源计划源码包基于ThinkPHP与Layui开发覆盖采购、销售、零售、多仓库管理、财务核算及报表分析等核心供应链环节适合需要搭建企业级管理系统的开发者与运维人员。压缩包共一千七百余个文件约二十兆字节源码以PHP业务逻辑、JavaScript交互、HTML页面、CSS样式及DAT数据文件为主并附带SQL数据库脚本、安装说明、更新日志和部署批处理文件便于搭建与二次开发。目前已有110人学习下载目录结构清晰完整串联从采购订单、供应商管理、库存盘点、销售开票到资金对账的业务流程内置超详细报表模块可生成采购、销售、零售、仓库和资金等多维统计直接辅助运营决策。通过研读源码可深入理解ThinkPHP与Layui的应用组合掌握进销存、ERP系统从数据建模到模块划分的工程化思路适合毕业设计、技术培训或企业内部管理系统建设参考。1. 点可云进销存系统ThinkPHP Layui 搭建的企业库存与财务一体化方案进销存系统是中小企业的刚需但市面上的 SaaS 产品要么按月收费、数据不在自己手里要么功能臃肿、光权限配置就能劝退一半人。点可云进销存系统走的是另一条路基于 ThinkPHP 6 Layui 构建把采购、销售、零售、多仓库、财务和报表全部打包在一套开源代码里部署到自己的服务器上数据完全可控。对于需要私有化部署、又不想从零开发的企业应用项目来说这套系统的性价比很高。它适合三类人一是做传统贸易、需要管多仓库库存的老板二是接企业定制单的 PHP 开发者拿它当基础脚手架改业务逻辑三是高校或培训机构做 ERP 教学演示的教师。系统代码结构清晰菜单权限、数据字典、操作日志这些企业应用该有的底座都有更重要的是报表模块做到了按日、按月、按商品、按往来单位多维统计省掉了大量 SQL 手写工作。接下来我会从技术底座拆到业务模块最后落到部署踩坑和二次开发技巧。2. 技术底座拆解ThinkPHP 6 分层与 Layui 组件化的前后端配合逻辑2.1 为什么是 ThinkPHP Layui而不是前后端分离先聊选型。thinkphp 在国内中小企业项目中占有率一直不低它的 ActiveRecord 模型、中间件机制和自带的分页/验证器让 CRUD 开发效率远高于手写原生 PHP。这套系统用的是 ThinkPHP 6.x属于 LTS 版本PHP 7.2 即可运行对服务器要求不高阿里云 2 核 4G 的机器跑起来毫无压力。对比 LaravelThinkPHP 的部署成本更低不需要 composer install 时拉一堆扩展包生产环境出错概率小。Layui 这边则是经典的模块化前端框架。它的亮点不是组件多好看而是表单、表格、弹层这些后台管理最常用的东西全封装成了 jQuery 插件直接在 HTML 里写layui.use([table,form])就能用没有 Webpack、Vite 那套心智负担。进销存系统有大量动态表格——采购单明细、销售单明细、库存流水——Layui 的 table 模块用url属性指向后端接口再配cols定义列数据渲染和分页就自动完成了开发效率确实高。这套系统选择前后端不分离对实际部署是加分项打包后不需要配 Nginx 反向代理到 Node 服务也不用处理跨域 Cookie 问题一个域名、一份 PHP 代码直接开箱跑。我实际部署过不少这种单体应用上线速度比前后端分离快太多了特别是客户端急着要验收的时候。2.2 项目目录结构与核心配置文件拿到代码包后第一件事是看目录结构。系统沿用了 ThinkPHP 6 的标准骨架但有几个目录需要重点注意。假设你的站点根目录是/www/wwwroot/erp结构大致是这样/www/wwwroot/erp ├── app/ │ ├── admin/ # 后台管理模块主业务 │ │ ├── controller/ # 控制器按业务拆Purchase、Sales、Inventory... │ │ ├── model/ # 模型层继承 BaseModel │ │ └── view/ # 模板文件Layui 页面 │ ├── common/ # 公共模块登录、权限、工具类 │ └── index.php # 入口绑定 ├── config/ │ ├── database.php # 数据库连接配置 │ └── app.php # 应用调试、时区、默认模块 ├── public/ # Web 根目录Nginx 指向这里 │ ├── static/ # Layui 的 css/js 文件 │ └── index.php # 前端入口 ├── route/ # 路由配置 └── extend/ # 自定义扩展类比如 Excel 导出先说config/database.php。数据库连接是整套系统的命根子我习惯把debug true先打开跑通流程确认无误后再关掉避免 SQL 报错直接暴露在页面上。系统默认数据库是 MySQL 5.7字符集用utf8mb4不然商品名称里有特殊符号会存不进去。config/app.php里有个default_module admin参数决定了访问根域名时自动跳转后台登录页。然后是路由。ThinkPHP 6 默认是控制器/方法的 PATHINFO 模式比如访问采购单列表URL 是/admin/purchase/lists.html。这套系统没有做复杂路由重写保持默认即可。但有一点要注意route/route.php里可能有自定义的伪静态规则如果你用 Nginx 部署务必在server块里加一句配置location / { if (!-e $request_filename) { rewrite ^/public/(.*)$ /public/index.php?s/$1 last; } }不加这段配置的话所有二级页面都会 404——这是我第一次部署时踩的第一个坑后面会展开说。2.3 后台权限与数据字典的设计思路进销存系统的权限控制不是简单的「登录才能看」而是需要精细到按钮级别。比如仓管员能看到采购单和库存但看不到财务成本和利润。点可云的权限设计是经典的 RBAC基于角色的访问控制一张管理员表关联角色角色关联菜单和操作节点。菜单表里的pid字段实现树形层级icon字段控制侧边栏图标。实际操作中登录后系统会查两次数据库第一次拿当前用户的角色 ID第二次用角色 ID 查权限菜单列表存入 Session。前端渲染侧边栏时Layui.menu会按层级关系生成菜单树后端控制器里则有个基类BaseController每个业务方法的开头都会调checkAuth()校验操作权限。这种方法的好处是权限判定集中在基类里业务控制器只管写逻辑不用重复写权限检查。checkAuth()的核心逻辑是这样的protected function checkAuth() { $admin session(admin_info); $module app(http)-getName(); // 当前模块名 $controller request()-controller(); // 当前控制器 $action request()-action(); // 当前方法 $url $module . / . $controller . / . $action; // 超管不受限制直接放行 if ($admin[is_super] 1) { return true; } // 角色关联的权限节点列表已经存入 session $accessList session(access_list); if (in_array($url, $accessList)) { return true; } else { // 记录非法访问日志 Log::write(权限拒绝 . $url . 操作人 . $admin[username]); $this-error(无操作权限请联系管理员); } }这段代码有三个参数值得关注is_super是超级管理员标记通常是账号 ID 为 1 的那个人跳过所有权限校验access_list是登录时查出来的权限节点数组注意它是按模块/控制器/方法三层拼接的字符串数组Log::write会把非法访问写进日志表这个对排查「谁动了成本数据」很有用。数据字典方面系统在app/admin/model/Dict.php里维护了一个字典表像「单位类型」「结算方式」「仓库类型」这类可配置选项都不写死在代码里而是在后台字典管理里维护前端下拉选项直接查字典接口。3. 进销存核心业务模块详解采购、销售、零售与多仓联动3.1 采购入库到应付账款单据流与库存流如何同步采购模块是最能体现进销存系统设计功力的地方。操作流程是先建采购单选供应商、选择采购仓库添加采购明细商品、数量、单价、折扣确认后审核系统会同时完成两件事——增加仓库库存、生成一笔应付账款。这套系统的关键设计是把「单据」和「流水」分开存采购单主表存业务头信息采购单明细表存商品行库存变动表单独记录每次出入库的增量。为什么要分开因为业务上允许「反审核」。比如采购单录错了数量仓管员在审核状态下是不能直接改的必须先反审核让单据回到草稿状态修改后再重新审核。审核动作本身要触发库存回滚和重新增加如果直接把数量和库存存在一张表里反审核时会很难追溯差异。仓储日志表stock_log里有一条关键字段change_type值 1 是采购入库、2 是销售出库、3 是盘点调整所有库存报表都是从这个日志表聚合出来的。采购入库的核心代码在app/admin/controller/Purchase.php里的approve方法我把关键段落贴出来public function approve() { $id input(post.id/d); // 采购单ID $purchase PurchaseModel::find($id); // 状态校验只有未审核的单据才能审核 if ($purchase[status] ! 0) { $this-error(只有未审核的单据才能审核); } // 事务开启确保库存和应付账款要么同时成功、要么同时回滚 Db::startTrans(); try { // 遍历采购明细逐个增加库存 $items PurchaseItemModel::where(purchase_id, $id)-select(); foreach ($items as $item) { $this-addStock([ warehouse_id $purchase[warehouse_id], goods_id $item[goods_id], quantity $item[quantity], change_type 1 ]); } // 写入应付账款流水 AccountPayableModel::create([ supplier_id $purchase[supplier_id], purchase_id $id, amount $purchase[total_amount], status 0, // 0未付 1部分付款 2已结清 ]); PurchaseModel::update([status 1, approve_time time()], [id $id]); Db::commit(); $this-success(审核成功); } catch (\Exception $e) { Db::rollback(); $this-error(审核失败 . $e-getMessage()); } }这段逻辑有三个点值得说。第一Db::startTrans()和Db::rollback()成对出现这才是真正保证库存和应付账款一致性的关键如果去掉事务库存加成功了、应付账款写入SQL失败账实就不符了。第二addStock方法是公共的销售出库、盘点调整也调它所以库存变动入口统一方便后续对账。第三total_amount在创建采购单时已经计算好存进了主表审核时直接用效率高但有一个隐患——如果商品单价在单据创建后被修改主表总金额不会自动同步后面排错时我会讲这个坑。3.2 销售出库与应收款核销零售 POS 与仓库扣减的差异处理销售模块可以分两条线普通销售出库和零售出库。普通销售面向企业客户流程是选客户 → 选商品 → 填数量单价 → 审核后扣库存、生成应收款零售则更像超市收银直接扫码枪扫商品条码选收银员、选支付方式现金/微信/支付宝确认后立即扣库存并收款不产生应收账款。两条线的核心差异在价格策略。普通销售可以针对不同客户设置不同的销售价比如大客户打 9 折、批发客户走批发价零售则必须用固定的零售价扫码枪扫完就直接出价格不能再弹窗改价。系统在商品表里设计了多个价格字段purchase_price进货价、wholesale_price批发价、retail_price零售价和member_price会员价。普通销售开单时默认带出批发价零售收银台带出零售价。扣库存的逻辑两者也不一样。普通销售审核时系统要记录应收款挂账后续在财务模块做收款核销零售则是即时收款的「钱货两清」直接把收款单和销售单绑定。零售收银的实现里有段doRetail代码我一般建议二次开发时重点看这段public function doRetail() { $barcode input(post.barcode); $quantity input(post.quantity/d); $goods GoodsModel::where(barcode, $barcode)-find(); if (!$goods) { $this-error(未找到该条码对应的商品); } if ($goods[stock] $quantity) { $this-error(库存不足当前库存 . $goods[stock]); } $session session(retail_cart); // 购物车操作同一商品累加数量 if (isset($session[$goods[id]])) { $session[$goods[id]][quantity] $quantity; } else { $session[$goods[id]] [ goods_id $goods[id], goods_name $goods[name], price $goods[retail_price], quantity $quantity ]; } session(retail_cart, $session); $this-success(已加入购物车); }这段代码有两个边界问题值得注意。一个是库存判断用的是主表stock字段的实时值如果在高并发场景下多个收银台同时卖同一款商品这里会有超卖风险系统的解决方案是扣库存时再用UPDATE ... WHERE stock quantity做条件更新第二次判定会执行两次查询。另一个是价格快照——加入购物车时把retail_price直接存进 Session但如果此时后台改了零售价购物车里的价格不会更新必须清空购物车重新扫。自己改的时候建议加上「结算时重新读取最新价格」的逻辑。零售与普通销售另一个差异是库存汇总维度。普通销售单审核时系统会校验单据统计的仓库是否匹配商品所在仓库零售单则默认从「默认零售出库仓库」出货——这个仓库是在系统设置里配置的。所以如果仓库数量多零售收银的库存报表只统计指定的出库仓别拿总库存去核对零售流水会差出一截。3.3 多仓库管理:调拨单、库存锁定与实际可用库存计算多仓库是这个系统相对花心思的部分。它支持两种仓库类型实体仓库一号仓、二号仓和虚拟仓库比如「损坏品仓」用于隔离残次品。多仓库管理的核心不是「多建几个仓库字段」而是三件事仓库间调拨要有独立的单据流程、出入库时要指定仓库、报表统计要按仓库维度聚合。调拨流程是先建调拨单选择调出仓库和调入仓库添加调动商品明细审核后系统会生成两条库存流水——调出仓扣减一条、调入仓增加一条。这里有一个容易懵的点调拨单审核前后所有库存都是「在途状态」没有独立字段标记。如果你需要管在途库存就得自己加扩展表。系统原生的逻辑很简单调拨审核就是一个事务里的两次addStock调用一个change_type为 3调出、一个为 4调入。实际可用库存的计算是进销存系统最容易被账实不符打击的地方。系统在查询库存列表时用了这样一个思路// 查询实际库存当前库存 入库总量 - 出库总量 $list Db::name(stock_log) -field(goods_id, warehouse_id, SUM(CASE WHEN type1 THEN quantity ELSE 0 END) as in_total, SUM(CASE WHEN type2 THEN quantity ELSE 0 END) as out_total) -where(is_delete, 0) -group(goods_id, warehouse_id) -select();这段聚合查询有两个坑。第一type1是入库、type2是出库但是注意盘点调整、调拨产生的流水 type 值不是这两个如果漏掉了盘点类型实际库存会算错。系统在盘点模块里用的 type5 和 type6我自己把代码改成枚举常量后就不容易再记混了。第二is_delete0这个条件是软删除过滤——如果业务上允许删除历史单据那删掉的单据不会影响库存流水因为关联的流水也做了软删除但如果有人直接操作数据库把流水 delete 了库存报表就会对不上。生产环境务必禁止手删stock_log表数据这是血泪教训。多仓的报表还有一个独特的「库存分布」概念系统库存列表页默认按商品汇总总和所有仓库的数量但点击某个商品可以展开看每个仓库各自的库存数。这个功能对应的就是上面那段 group by 仓库的聚合逻辑。在goods_stock视图模型中每个商品在每个仓库会有一条独立记录如果你发现某些商品在列表页没有按仓库分开展示大概率是初始化数据时没有跑仓库库存初始化脚本。4. 财务模块应收应付、成本核算与利润报表的连动设计4.1 应收账款与收款核销核销是「两清」而不是「一锤子」财务模块是进销存系统最容易做崩的地方。简单粗暴的做法是销售单审核后直接把金额累加到客户欠款字段里客户付款后再把这个字段减掉。这种设计的问题是只能算个总数根本查不清哪笔销售单没回款。点可云的方案是「销售单 收款单双表模式」销售单审核后生成应收记录收款时创建收款单现在两个单的关系是「核销」——一张收款单可以核销多张销售单一张销售单也可以被分多次收款。核销页面里有个经典组件左侧是客户列表右侧是待核销的应收单据列表点「生成收款单」后会弹窗让你输入本次收款金额然后选择要核销哪些销售单。系统在后台Recount.shou逻辑里会循环要核销的销售单 ID把本次收款金额按顺序分配直到收款金额分配完或销售单全部核销完。具体代码逻辑是这样的public function writeOff() { $customer_id input(post.customer_id/d); $amount input(post.amount/f); // 本次收款金额 $sale_ids input(post.sale_ids/a); // 选中核销的销售单ID数组 $leftAmount $amount; // 剩余待分配金额 Db::startTrans(); try { foreach ($sale_ids as $sale_id) { if ($leftAmount 0) break; // 钱分完了就停 $receivable ReceivableModel::where(sale_id, $sale_id)-lock(true)-find(); $needMoney $receivable[total_amount] - $receivable[received_amount]; $paidMoney min($needMoney, $leftAmount); // 本次核销金额 ReceivableModel::where(id, $receivable[id]) -inc(received_amount, $paidMoney) -update(); // 更新应收单状态 $newReceived $receivable[received_amount] $paidMoney; if ($newReceived $receivable[total_amount]) { ReceivableModel::where(id, $receivable[id]) -update([status 2]); // 2已结清 } else { ReceivableModel::where(id, $receivable[id]) -update([status 1]); // 1部分收款 } $leftAmount - $paidMoney; } Db::commit(); $this-success(核销成功剩余未分配金额 . $leftAmount); } catch (\Exception $e) { Db::rollback(); $this-error(核销失败 . $e-getMessage()); } }这里要特别讲lock(true)这个参数。它是 ThinkPHP 的悲观锁语法执行时会生成SELECT ... FOR UPDATEMySQL InnoDB 会在这一行上加锁。多人在后台同时给同一个客户做收款核销时没有这行锁就可能出现「超收」——钱比欠款还多。加了锁之后第二个核销操作会等待第一个事务提交后再执行代价是并发稍低但对 ERP 场景来说数据准确比并发重要得多。另外注意inc(received_amount, $paidMoney)不是直接set一个新值而是自增这样在多线程下也安全。4.2 采购成本与销售毛利:移动加权平均法的落地实现利润报表是进销存系统里最受老板关注的页面也是最容易算错的地方。核心问题在于「成本」怎么算。这套系统用的是移动加权平均法每次采购入库时重新计算商品的平均成本公式是原库存金额 本次采购金额÷原库存数量 本次采购数量每次销售出库时用当前平均成本作为出库成本。这个算法不要求先进先出实现简单对大多数中小企业足够用。移动加权成本的更新逻辑在GoodsModel的一个方法里每次库存变动时都会被调用public function updateAvgCost($goods_id, $newQuantity, $newCost) { $goods GoodsModel::find($goods_id); // 原库存金额 新入库金额 $totalAmount $goods[stock] * $goods[cost_price] $newQuantity * $newCost; // 原库存数量 新入库数量 $totalQuantity $goods[stock] $newQuantity; // 计算新的加权平均成本保留4位小数避免精度误差累积 $avgCost round($totalAmount / $totalQuantity, 4); GoodsModel::where(id, $goods_id)-update([ cost_price $avgCost, stock $totalQuantity, // 注意库存金额不作为字段存储每次实时计算 ]); }这个方法有个前置条件——必须在采购审核的同一事务里调用。如果采购单审核成功但成本更新失败会导致后续销售毛利全部算错。所以采购审核那段代码里updateAvgCost的调用放在addStock后面、提交事务之前。销售毛利的计算方式则是在销售报表模块里用 SQL 联表查询销售明细表关联商品表出库成本取当前商品的cost_price快照所以每一行销售明细里会存两个关键字段sale_price卖价和cost_price出库时的成本。毛利 销售收入 - 出库成本毛利率 毛利 ÷ 销售收入 × 100%。这里有一个非常重要的坑销售单审核时系统会把商品当时的成本快照进销售明细表——注意是「快照」。如果审核后采购价格变动历史销售的毛利不会被影响这符合会计核算原则但如果你去商品表看到的是最新成本拿它手算历史毛利一定会对不上。用系统的利润报表页看数据就对了别自己重新算。4.3 财务日报 vs 经营日报两个报表各管什么系统报表菜单里有两个名字很像的报表很多人容易点混。财务日报统计的是当天发生的收付款流水收款、付款、预收、预付数据来源于收款单和付款单经营日报统计的是当天的销售业绩和毛利数据来源于销售单。两者的时间口径也不同财务日报按「实际收付款时间」统计经营日报按「销售单审核时间」统计。举例来说昨天审核的销售单今天客户才转账那这笔金额只出现在今天的财务日报里但出现在昨天的经营日报里。所以老板和对账会计看报表的时候如果发现两个表的数字对不上先看口径是不是混了。经营日报的聚合 SQL 是报表模块里比较有代表性的SELECT DATE(approve_time) AS business_date, SUM(total_amount) AS sale_amount, SUM(total_cost) AS sale_cost, SUM(total_amount - total_cost) AS gross_profit, (SUM(total_amount - total_cost) / SUM(total_amount)) * 100 AS profit_rate FROM cdk_sale WHERE status 1 AND approve_time 2025-01-01 00:00:00 AND approve_time 2025-01-02 00:00:00 GROUP BY DATE(approve_time);注意total_cost这个字段不是从销售单主表读的而是销售明细行里sale_quantity * cost_price的汇总。系统在销售单审核时就已经算好这个汇总值存到了主表里所以查日报时不需要逐行关联商品表。设计上这是刻意为之的——为了报表速度牺牲一点存储冗余。当单量到了几十万条这个量级这种预汇总策略的价值很大MySQL 不需要实时聚合明细表。但副作用是如果发现某天的毛利数据异常你得去反查销售明细因为日报本身不存明细关联关系。5. 部署避坑手册ThinkPHP 项目从上传到跑通的常见问题5.1 访问首页直接报 404伪静态规则没写对这是 ThinkPHP 项目部署最常见的翻车点我几乎每次帮人排查都会遇到。现象是域名打开后直接 Nginx 404但public/index.php单独访问却能弹出安装页面。原因是 ThinkPHP 6 的入口在public/index.php但所有业务 URL 都是index.php?s/admin/login/index这种格式Nginx 默认配置没有把请求转发给入口文件所以除了首页之外的所有路由全部 404。解决方法是按 Nginx server 块加一条 try_files 规则location / { try_files $uri $uri/ /index.php?s$uri$args; }加完之后记得nginx -s reload。如果你是 phpstudy 或宝塔面板宝塔的 ThinkPHP 配置模板已经内置了这条规则直接选用即可自建 Nginx 就必须手写。还有一种现象是首页能打开、点击登录按钮后 URL 跳转 404那大概率是public/目录没设为运行目录Nginx root 指向了项目根目录而不是public/子目录。5.2 安装完打开提示「数据库连接错误」字符集与端口两大坑数据库连接报错的排查路径其实很固定。第一个坑是 MySQL 端口不是默认的 3306尤其宝塔面板经常把 MySQL 端口改成 3307但安装向导里的数据库配置只让你填账号密码后台配置文件里写着 3306结果连不上。第二个坑是字符集——ThinkPHP 6 默认 PDO 连接参数里charset是utf8但如果你建库时选了utf8mb4个别生僻字比如商品名称里有 emoji 或特殊符号会存不进去或查询时报Illegal mix of collations。改法是找到config/database.php把charset改成utf8mb4同时数据库排序规则也要选utf8mb4_general_ci。还有一个不太明显但非常常见的坑服务器防火墙没放行 3306 端口。本地连不上、服务器 localhost 却连得上基本就是这个。如果你用 Navicat 远程连接测试在服务器上执行mysql -uroot -p先确认本地能连再排查安全组。5.3 登录成功后页面空白或 JS 报错static 资源路径与 HTTPS 混合内容被拦截登录页能打开账号密码输进去也验证通过了但跳转后台后页面一片空白控制台报一堆 404。90% 的原因是public/static目录下的静态资源配置被改错了。Layui 的前端文件都依赖__STATIC__模板常量的路径这个常量在app/common.php里定义。如果项目不是部署在域名根部比如挂在http://ip:8080/erp/public这种子目录路径会算错。找一下app/common.php里的__STATIC__定义改成绝对路径/erp/public/static即可。另一类是 HTTPS 环境特有的问题页面部署成 HTTPS 后如果静态资源引用的是http://开头的绝对路径浏览器会拦截所有「混合内容」。比如背景图、Layui 的 CSS、JS 全部加载不出来。控制台会明确提示Mixed Content: The page at https://... was loaded over HTTPS, but requested an insecure resource http://...。解决就是在 Nginx 层做一次强制跳转或改写sub_filter http://yourdomain.com https://yourdomain.com; sub_filter_once off;这个方案不需要改后端 PHP 代码Nginx 会在输出 HTML 时自动替换。如果你用的是宝塔也可以在「网站设置 → 伪静态」里配rewrite ^(.*) https://yourdomain.com$1 permanent;做整体跳转然后统一改数据库里存的域名配置。5.4 采购单反审核后库存没变多表事务与状态机的约束一个让人抓狂的场景采购单审核后库存加了接着点反审核页面提示成功但库存数字纹丝不动。原因通常在两个代码分支——审核走的是一个方法反审核走的是另一个方法如果后者忘了调addStock反向减库存或者减库存的 SQL 条件没匹配上记录事务也不会报错因为是成功执行了 0 行 update。排查方法很直接打开采购单详情页看审核状态如果状态已经回到 0草稿但stock_log里没有对应反向流水那基本就是反审核逻辑里有分支漏调了。解决方法是给反审核方法加库存回滚操作并且用条件where(stock_log_id, $logId)更新流水表打上「已作废」标记public function unApprove() { $id input(post.id/d); $purchase PurchaseModel::find($id); if ($purchase[status] ! 1) { $this-error(只有已审核单据才能反审核); } Db::startTrans(); try { // 反向扣减库存审核时加了库存反审核要减回来 $items PurchaseItemModel::where(purchase_id, $id)-select(); foreach ($items as $item) { $this-addStock([ warehouse_id $purchase[warehouse_id], goods_id $item[goods_id], quantity -$item[quantity], // 负数扣减 change_type 9 // 9 采购反审核 ]); } // 作废待付款记录 AccountPayableModel::where(purchase_id, $id)-update([is_delete 1]); PurchaseModel::update([status 0], [id $id]); Db::commit(); $this-success(反审核成功); } catch (\Exception $e) { Db::rollback(); $this-error(反审核失败 . $e-getMessage()); } }这里特别注意quantity传了负数是因为addStock内部默认做库存累加传负数就能复用同一套流水逻辑。如果系统自带的反审核逻辑少了这种反向流水你在二次开发时必须补上不然库存账永远对不平。另外反审核后应付账款记录要软删除而不是物理删除保留历史轨迹方便审计。5.5 PHP 版本与扩展缺失报Call to undefined function怎么办这代 ThinkPHP 项目对 PHP 版本有硬性要求6.x 要求 PHP 7.2.5 以上。如果你从 PHP 5.6 环境直接迁移会直接数据库连接都建立不起来。除了版本之外phpredis、fileinfo等扩展如果没装系统有些功能会静默报错——比如图片上传依赖fileinfo扩展没装的话上传商品图片时提示未知错误但 PHP 错误日志里会写Call to undefined function finfo_open()。用宝塔面板的话直接在「软件商店 → PHP 设置 → 扩展安装」里勾选装上即可。如果是自建源编译的 PHP重编译时加上--enable-fileinfo参数。排查原则我记得很清楚先看 PHP 错误日志/var/log/php-fpm/error.log或宝塔面板的日志路径再逐项看扩展列表php -m。大部分「页面白屏无响应」都是 PHP fatal error 被日志吞了不是业务代码的问题。6. 进阶技巧报表扩展与多部门利润核算的轻量改造很多人在基础功能跑通后会遇到一个新需求老板要的不只是总毛利而是每个业务员、每个客户、每个品类的分维度利润。这套系统原生报表只有「总览型」统计想按维度钻取就得自己扩展。其实不用大改只需要在现有销售明细表上做两个小扩展。第一个扩展是给销售明细表加一个「业务员」字段。系统销售单主表有operator_id字段记录开单操作人但默认报表没有按它对毛利分组。直接在cdk_sale_item表加一个salesman_id字段开单时从主表拐过来。SQL 扩展报表时就能这样查SELECT s.salesman_id AS salesman_id, u.real_name AS salesman_name, SUM(i.quantity * i.sale_price) AS sales_amount, SUM(i.quantity * i.cost_price) AS cost_amount, SUM(i.quantity * (i.sale_price - i.cost_price)) AS profit FROM cdk_sale_item i LEFT JOIN cdk_sale s ON i.sale_id s.id LEFT JOIN cdk_user u ON s.salesman_id u.id WHERE s.status 1 AND s.approve_time 2025-01-01 AND s.approve_time 2025-02-01 GROUP BY s.salesman_id, u.real_name ORDER BY profit DESC;这个查询跑不了全表的cdk_sale_item扫描因为月份条件过滤了主表范围MySQL 会用索引先定位销售单 ID 范围再联查明细即使几十万条明细单页报表也在百毫秒内返回。记住一个原则明细联表必须先用时间范围把销售单主表缩小再关联明细否则索引失效全表扫描页面必卡。第二个扩展是做「部门核算」。多部门管理时可以复用系统的仓库维度——把部门对应到仓库然后所有利润报表都按warehouse_id分组。实现上不需要改表结构因为每个销售明细都带出库仓库 ID。报表 SQL 只需把 group by 从业务员改成仓库即可代码复用率很高。但要注意如果把一个仓库当作一个部门零售出库仓需要单独设一个「零售部」否则零售数据会挂在某个实体仓库下部门核算就不准了。最后说一个很实用的工作量验证方法。上线后每周跑一次「库存总账平账检查」SQL 很直白SELECT goods_id, SUM(CASE WHEN change_type IN (1,3,5) THEN quantity ELSE 0 END) AS total_in, SUM(CASE WHEN change_type IN (2,4,6,9) THEN quantity ELSE 0 END) AS total_out, SUM(CASE WHEN change_type IN (1,3,5) THEN quantity ELSE 0 END) - SUM(CASE WHEN change_type IN (2,4,6,9) THEN quantity ELSE 0 END) AS calc_stock FROM cdk_stock_log GROUP BY goods_id;把calc_stock和商品主表的stock字段对比有差异就说明有单据没走审核流程、或者有人直接改了库存表。从那以后我每次给客户交付前都会强制跑一遍这个检查顺便核对所有反审核单的流水有没有成对出现基本能把 90% 的库存隐患挡在上线之前。希望这套系统的拆解思路能帮你快速上手少走我走过的那些弯路。本文还有配套的精品资源点击获取
RELATED

相关推荐

CPLEX本地部署避坑指南:Windows/Linux/macOS全平台安装与API配置

CPLEX本地部署避坑指南:Windows/Linux/macOS全平台安装与API配置

简介:《Cplex中文教程全.pdf》是一份面向优化建模初学者与工程实践者的权威学习资料,专为需要掌握数学规划求解技术的算法工程师、运筹学学习者及Java/C/Python开发者设计,解决线性规划、整数规划、混合整数规划等复杂优化问题的建模与求解难…

📅 2026/10/27 1:21:17
国产长芯微LDC7874是4通道同步采样、24 位 Δ-Σ模数转换器 (ADC),LDC7874完全P2P替代ADS131E04

国产长芯微LDC7874是4通道同步采样、24 位 Δ-Σ模数转换器 (ADC),LDC7874完全P2P替代ADS131E04

描述LDC7874 是一系列多通道同步采样、24 位 Δ-Σ模数转换器 (ADC),内置可编程增益放大器(PGA)、内部基准和振荡器。凭借 ADC 的宽动态范围、可扩展数据传输速率以及内部故障检测监测计,LDC7874受到工业电源监测和保护以及测试和测量应用的青睐。真正的…

📅 2026/10/27 1:16:17
嵌入式:STM32的启动(Startup)文件解析

嵌入式:STM32的启动(Startup)文件解析

相关阅读 嵌入式https://blog.csdn.net/weixin_45791458/category_12768532.html?spm1001.2014.3001.5482 启动文件(Startup File)是嵌入式系统开发中的核心组件之一,它用于初始化系统并为主程序的运行做好准备。在大多数情况下,启动文件是用汇编语言编…

📅 2026/10/27 1:16:17
MORE NEWS

更多资讯

📰

Kubernetes Pod Not Found报错排查:命名空间、上下文与标签选择器实战

1. 这个报错到底在说什么:从一次真实的排查现场说起Error: Pods pod-name Not Found这个提示,几乎每个跟 Kubernetes 打过交道的人都见过。它不像 CrashLoopBackOff 那样有一堆日志可以翻,也不像 ImagePullBackOff 那样指向明确,它…

📰

PostgreSQL主从流复制与pgpool-II高可用部署实战

简介:PostgreSQL 主从流复制与 pgpool-II 高可用部署文档,面向数据库运维工程师与 DBA,解决主备数据实时同步、读写分离与主库故障自动切换的问题。内容从方案综述入手,讲清主库 WAL 日志经 walsender 推送至备库 walreceiver 落盘…

📰

Java数据结构与算法分析:递归、复杂度与链表指针精解

简介:面向使用《数据结构与算法分析(Java语言描述)》第三版的学生与自学者,这份习题答案文档可用于课后对照、期末复习及算法基础查漏补缺,适合正在巩固Java数据结构、递归思想和复杂度分析的读者。压缩包内仅含1个doc…

📰

高效调试方法论:从基础工具到AI辅助的BUG终结术

1. 项目概述:为什么我们需要"BUG终结者"?在软件开发领域,BUG就像影子一样伴随着每个程序员。我见过太多团队在调试上浪费的时间甚至超过实际开发时间。最近一个统计显示,开发者平均花费25%的工作时间在调试上&#xff0…

📰

Linux标准IO库:原理、优化与实战应用

1. 为什么标准IO在Linux编程中如此重要?在Linux系统编程中,输入输出(IO)操作是最基础也是最频繁的操作之一。标准IO库(stdio)作为C语言标准库的一部分,提供了一套高效、可移植的文件和终端操作接…

📰

跨平台扫码功能实现方案与技术解析

1. 项目概述扫码功能在现代移动应用中几乎成为标配,但不同平台的技术实现差异巨大。最近我在一个跨平台项目中,需要同时在常规H5页面、微信H5、微信小程序和原生App中实现扫码功能,踩了不少坑也积累了一些经验。本文将分享四种场景下的完整实…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬