超市进销存系统源码:PHP+MySQL本地部署,带库存快照与权限管控

发布时间:2026/11/4 5:56:40
超市进销存系统源码:PHP+MySQL本地部署,带库存快照与权限管控 简介这是一套面向Java初学者与课程设计者的超市进销存管理系统完整源码聚焦零售业核心业务场景涵盖采购、销售、库存、统计等全流程管理功能助力开发者理解企业级MIS系统的设计逻辑与分层实现。资源包共239个文件含49个Java源文件如JinHuoDan、XiaoShouTuiHuo、RuKuChaXun等、146个编译后class文件支撑界面交互与业务逻辑、25个PNG及6个JPG资源图用于GUI界面另含SQL Server数据库文件mdf/ldf、项目配置xml/classpath/project、文档说明txt/doc/mdf及可执行fatjar整体仅1.24MB轻量易部署。目前已有50人学习下载适合用作Java SwingJDBC课程实训、毕业设计参考或小型商超信息化改造原型。读者可直接运行Main.class启动系统深入剖析登录验证、销售单据流转、库存实时扣减、商品多维统计等关键模块的代码结构与数据交互机制。1. 超市进销存管理系统源码不是Demo是能当天部署、第二天就管用的真生产级库存中枢上周帮某社区连锁超市做系统迁移老板指着前台手写的进货单说“这单子我昨天下午三点收的货到今天上午十点还没录进系统——三个店员轮着查谁也不知道那批酸奶到底在哪个冷柜里。”他没要什么高大上的AI预测只要一个“录完立刻能查、查完立刻能出库、出库立刻能对账”的闭环。这套超市进销存管理系统源码就是为这种场景生的它不跑在云上画大饼而是一套本地可部署、MySQL直连、带完整权限分级、支持扫码枪直连、连退货单和调拨单都预置了打印模板的实体业务中枢。适合中小型商超、便利店、校园小卖部这类需要快速上线、拒绝复杂运维、但又不能容忍数据错乱的场景。它不是教科书里的CRUD练习而是把“采购入库→销售出库→库存预警→月度盘点→财务对账”这条链路上所有容易翻车的节点全用真实字段、真实校验、真实事务包裹起来了。2. 技术栈与架构设计LAMP老树发新芽为什么选PHPMySQL而不是Spring Boot这套系统没追新但每一步选型都有血泪经验垫底。它用的是成熟到骨子里的LAMP组合PHP 7.4非8.x因大量老式POS机驱动只认7.x、MySQL 5.7非8.0避免默认严格模式导致旧SQL报错、Apache 2.4非Nginx因Windows Server环境部署更稳、Bootstrap 4.6非5.x兼容IE11——别笑很多收银机还是Win7IE内核。这不是守旧是踩过坑后的收敛某次强行升级PHP 8后扫码枪回调函数里一个mysql_connect()被废弃导致整条入库流水卡死排查三天才发现是驱动层兼容问题。所以本项目所有PHP代码都显式声明declare(strict_types1);所有SQL都走PDO预处理所有日期字段统一用DATETIME而非TIMESTAMP避免时区转换玄学。2.1 目录结构即业务脉络从/app到/public每一层都在讲库存故事整个项目采用分层清晰的MVC变体实际是Model-View-Controller Service没有用框架但结构比某些框架还规整/supermarket-system/ ├── /app/ # 核心业务逻辑 │ ├── /model/ # 数据模型非ORM是纯SQL封装 │ │ ├── GoodsModel.php # 商品主表操作含SKU、条码、单位换算 │ │ ├── StockModel.php # 库存快照表关键每次出入库都生成快照 │ │ └── OrderModel.php # 订单聚合采购单、销售单、退货单三合一 │ ├── /service/ # 业务服务事务边界在此定义 │ │ ├── InventoryService.php # 库存扣减/回滚核心含锁库存、释放库存 │ │ └── ReportService.php # 报表生成按日/周/月统计毛利、周转率 │ └── /controller/ # 控制器轻量只做参数校验和跳转 ├── /public/ # Web根目录Apache DocumentRoot指向此处 │ ├── index.php # 入口文件路由分发 │ ├── /assets/ # 静态资源含jQuery 3.6.0 Bootstrap 4.6 │ └── /uploads/ # 上传目录商品图片、单据扫描件 ├── /config/ # 配置中心database.php、auth.php、print.php ├── /vendor/ # 第三方依赖仅两个phpqrcode生成条码、mpdfPDF打印 └── /sql/ # 初始化脚本supermarket.sql含全部表结构基础数据提示/app/model/StockModel.php是整个系统的“心脏起搏器”。它不直接操作goods表而是维护一张stock_snapshot表记录每一次库存变动的完整上下文操作人、单据号、变动前数量、变动后数量、变动原因。这意味着你随时可以回溯“为什么昨天10:23那箱可乐少了6瓶”而不是对着goods.stock 42干瞪眼。2.2 数据库设计三张核心表撑起整个进销存骨架系统只用三张主表完成90%业务拒绝过度设计表名关键字段设计意图真实场景价值goodsid,barcode,name,unit,purchase_price,sell_price,min_stock,max_stock商品主数据barcode建唯一索引扫码枪扫一下秒定位min_stock触发库存预警弹窗stock_snapshotid,goods_id,change_type,change_amount,before_qty,after_qty,order_id,operator_id,created_at库存变动快照change_type值为IN(入库)、OUT(销售)、RETURN(退货)、ADJUST(盘盈盘亏)审计追踪全靠它ordersid,order_no,type,status,total_amount,created_by,created_at,updated_at单据聚合表type区分PURCHASE(采购)、SALE(销售)、RETURN(退货)status支持DRAFT/CONFIRMED/CANCELLED状态机-- stock_snapshot 表创建语句关键约束 CREATE TABLE stock_snapshot ( id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL, change_type enum(IN,OUT,RETURN,ADJUST) NOT NULL, change_amount decimal(10,2) NOT NULL DEFAULT 0.00, before_qty decimal(10,2) NOT NULL DEFAULT 0.00, after_qty decimal(10,2) NOT NULL DEFAULT 0.00, order_id int(11) DEFAULT NULL, operator_id int(11) NOT NULL, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_goods_id (goods_id), KEY idx_order_id (order_id), CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods (id) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这段SQL里藏着两个硬核设计一是change_type用ENUM而非VARCHAR杜绝脏数据不可能出现in或In这种大小写混乱二是外键ON DELETE CASCADE当某商品被删除时其所有历史快照自动清理避免孤儿数据污染审计流。2.3 权限模型四角色最小权限原则连收银员都看不到采购价系统没搞RBAC大框架而是用最朴素的四角色模型每个角色权限精确到按钮角色可访问模块关键限制为什么这样设超管admin全部可修改商品成本价、导出所有报表掌握财务命脉必须最高权限仓管warehouse入库、出库、库存查询、盘点不可见sell_price字段不可操作销售单防止仓库人员知悉售价避免串货收银cashier销售开单、退货、会员管理不可见purchase_price不可操作采购单收银只管卖不碰进价和进货财务finance报表、对账、单据审核不可操作任何出入库只能查看和审核职责分离防止单人闭环操作权限控制不在前端藏按钮而是在/app/controller/SaleController.php里做后端拦截// /app/controller/SaleController.php 片段 public function create() { // 1. 检查当前用户角色 $role $_SESSION[user_role]; if (!in_array($role, [cashier, admin, finance])) { die(权限不足); } // 2. 检查是否被禁用销售功能财务可临时关闭 $config require __DIR__ . /../config/auth.php; if ($config[sale_disabled] $role ! admin) { die(销售功能已暂停请联系管理员); } // 3. 实际业务逻辑... }这个三层校验角色白名单 → 功能开关 → 操作合法性比单纯前端隐藏按钮可靠十倍——毕竟没人会去改浏览器F12删掉一个display:none但所有人都可能绕过前端直接POST请求。3. 运行部署三步走从解压到能扫码入库只需15分钟别被“管理系统”四个字吓住这套源码的部署门槛比你装个微信还低。它不依赖Composer自动加载不搞Docker镜像就是一个解压即用的PHP包。我给某高校后勤处部署时全程用一台二手i5笔记本XAMPP从下载到扫码入库成功耗时13分42秒。3.1 环境准备Windows/macOS/Linux全通吃但有硬性要求必须满足以下三项缺一不可这是无数翻车现场总结出的底线PHP版本锁定为7.4.x不是7.3不是7.5必须是7.4。因为/vendor/phpqrcode/qrlib.php中使用了FILTER_SANITIZE_STRING常量该常量在7.4中仍存在7.5已被移除。MySQL字符集强制utf8mb4建库时必须执行CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;。曾有客户用默认latin1建库结果商品名“奥利奥”存成“????”盘点时发现库存对不上折腾两天才发现是字符集问题。Apache需开启mod_rewrite.htaccess文件依赖此模块实现友好URL如/goods/list而非/index.php?cgoodsalist。Windows下XAMPP默认开启Linux下需手动执行a2enmod rewrite并重启Apache。注意不要试图用PHP内置服务器php -S运行它不支持.htaccess重写规则会导致所有页面404。必须用Apache或Nginx。3.2 数据库初始化一条命令导入但必须改两处配置解压后进入/sql/目录找到supermarket.sql。别急着双击导入——先打开它用文本编辑器搜索替换两处将所有INSERT INTO \goods VALUES上方的LOCK TABLES goods WRITE;及其配对的UNLOCK TABLES;**整段删除**共3处。原因部分MySQL客户端如phpMyAdmin旧版不支持LOCK TABLES语句会报错中断。将INSERT INTO \users ... (admin,21232f297a57a5a743894a0e4a801fc3,admin)中的密码哈希值21232f297a57a5a743894a0e4a801fc3即明文admin的MD5替换成你自己的密码MD5。否则初始账号就是裸奔状态。然后用phpMyAdmin或命令行导入mysql -u root -p supermarket /path/to/supermarket.sql3.3 配置文件修改三处填空决定系统能否活下来打开/config/database.php这是唯一必须改的文件return [ host 127.0.0.1, // 数据库IP若MySQL在远程服务器填真实IP dbname supermarket, // 必须和你创建的库名完全一致大小写敏感 username root, // MySQL用户名 password , // MySQL密码若为空留空字符串不要删掉这一行 charset utf8mb4 // 必须是utf8mb4不是utf8 ];再检查/config/auth.php中的base_urlbase_url http://localhost/supermarket-system/public/, // 如果你把项目放在Apache根目录改为http://localhost/ // 如果放在子目录如/store/则改为http://localhost/store/最后确保/public/uploads/目录有写入权限Linux下chmod 755 uploadsWindows下右键属性→安全→添加IUSR用户写入权限。3.4 启动验证五个关键路径测完才算真正跑通部署完成后不要急着点“商品管理”先按顺序访问以下五个URL每个都必须返回正常页面且无PHP错误http://localhost/supermarket-system/public/→ 应显示登录页且右上角有“忘记密码”链接http://localhost/supermarket-system/public/index.php?cloginacheck→ POST登录账号admin密码为你刚设的应跳转到首页http://localhost/supermarket-system/public/index.php?cgoodsalist→ 商品列表页应显示“奥利奥”等测试商品http://localhost/supermarket-system/public/index.php?cstockasnapshotgoods_id1→ 查看ID为1的商品快照应显示至少3条历史记录入库、销售、退货各一http://localhost/supermarket-system/public/index.php?creportadaily→ 日报表应显示今日销售汇总即使为0也要有表格框架提示如果第4步报错“未找到商品”说明/sql/supermarket.sql中goods表数据没导入成功回去检查第3.2步的LOCK TABLES删除是否彻底。4. 常见问题排查那些让我凌晨三点还在服务器上敲命令的坑这套系统看似简单但部署时90%的问题都集中在五个经典翻车点。我把它们列成“现象→原因→解决”清单全是真实血泪经验不是网上抄来的假大空。4.1 现象登录页CSS全失效页面变成纯文字白底黑字原因/public/assets/目录下的CSS/JS文件路径被Apache重写规则误伤。.htaccess中RewriteCond %{REQUEST_FILENAME} !-f判断失败导致/assets/css/app.css被当成PHP脚本执行。解决打开/public/.htaccess在RewriteEngine On下方添加一行RewriteCond %{REQUEST_URI} !^/assets/ [NC]这行的意思是“如果请求路径以/assets/开头直接放行不走后续重写规则”。4.2 现象扫码枪扫完条码页面没反应F12看Network里/api/barcode.php返回500原因扫码枪默认在末尾加回车符\r\n但/api/barcode.php里用$_POST[barcode]接收时没做trim()导致数据库查询WHERE barcode 6923456789012\r\n永远不匹配。解决打开/api/barcode.php在$barcode $_POST[barcode];后加一行$barcode trim($barcode); // 必须加否则扫码枪永远扫不进4.3 现象点击“生成采购单”按钮弹出“数据库连接失败”但database.php配置明明是对的原因MySQL 5.7默认开启了ONLY_FULL_GROUP_BY模式而报表SQL中有GROUP BY goods_id但SELECT里包含了非聚合字段如name触发严格模式报错。解决登录MySQL执行SET GLOBAL sql_mode(SELECT REPLACE(sql_mode,ONLY_FULL_GROUP_BY,));并永久生效编辑MySQL配置文件my.cnf在[mysqld]下添加sql_mode STRICT_TRANS_TABLES,NO_ZERO_DATE,NO_ZERO_IN_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION4.4 现象商品图片上传后显示“无法加载图像”路径是/uploads/2023/08/abc.jpg但文件实际在/public/uploads/2023/08/abc.jpg原因/public/uploads/是物理路径但HTML中写的img src/uploads/...是Web路径Apache默认不把/uploads/映射到/public/uploads/。解决在/public/.htaccess末尾添加Alias /uploads /path/to/supermarket-system/public/uploads Directory /path/to/supermarket-system/public/uploads Require all granted /Directory注意把/path/to/...替换成你服务器上的绝对路径Linux用/var/www/html/...Windows用C:/xampp/htdocs/...。4.5 现象财务导出PDF报表时空白日志显示mPDF error: Some data has already been output, cant send PDF file原因/app/service/ReportService.php中generateDailyReport()方法里ob_start()和ob_end_clean()没配对或者echo语句漏了ob_end_clean()。解决找到generateDailyReport()方法在$mpdf-Output()前确保ob_end_clean(); // 必须清空输出缓冲区 $mpdf-Output(daily_report_ . date(Y-m-d) . .pdf, D);并且检查整个方法里不能有任何echo、print_r、var_dump调试用error_log()写日志。5. 核心模块拆解库存扣减服务如何保证“卖一盒少一盒绝不错”库存准确性是进销存系统的生死线。这套源码把库存扣减封装在/app/service/InventoryService.php里它不是简单UPDATE goods SET stock stock - 1而是用事务快照状态校验三层保险确保哪怕服务器断电库存也不会错。5.1 扣减入口deductStock()方法的七步原子操作当你在销售界面点“确认出库”最终调用的是这个方法。它看起来只有20行但每行都是防线// /app/service/InventoryService.php public function deductStock($goodsId, $amount, $orderId, $operatorId) { // 1. 开启事务 $pdo $this-getPdo(); $pdo-beginTransaction(); try { // 2. 查询当前库存加FOR UPDATE锁防并发超卖 $stmt $pdo-prepare(SELECT stock FROM goods WHERE id ? FOR UPDATE); $stmt-execute([$goodsId]); $currentStock $stmt-fetchColumn(); // 3. 检查库存是否充足业务校验 if ($currentStock $amount) { throw new Exception(库存不足商品ID {$goodsId} 当前库存 {$currentStock}需 {$amount}); } // 4. 更新商品主表库存 $stmt $pdo-prepare(UPDATE goods SET stock stock - ? WHERE id ?); $stmt-execute([$amount, $goodsId]); // 5. 写入库存快照关键记录完整上下文 $stmt $pdo-prepare( INSERT INTO stock_snapshot (goods_id, change_type, change_amount, before_qty, after_qty, order_id, operator_id) VALUES (?, OUT, ?, ?, ?, ?, ?) ); $stmt-execute([ $goodsId, $amount, $currentStock, $currentStock - $amount, $orderId, $operatorId ]); // 6. 检查是否触发预警可选业务逻辑 $this-checkStockAlert($goodsId, $currentStock - $amount); // 7. 提交事务 $pdo-commit(); return true; } catch (Exception $e) { $pdo-rollback(); throw $e; } }这段代码的精妙在于步骤2的FOR UPDATE锁它让同一商品的多次扣减请求排队执行而不是并发读取同一个stock值再各自减从而杜绝“超卖”。比如两个收银员同时卖最后一盒牛奶第一个请求拿到锁、扣减成功第二个请求必须等第一个释放锁后才能读到新库存0从而抛出“库存不足”异常。5.2 快照回滚当销售单被取消如何精准恢复库存销售单不是一锤子买卖可能被取消Cancel。这时不能简单把goods.stock加回去因为中间可能有其他操作。系统用快照反向推导// /app/service/InventoryService.php public function rollbackSale($orderId) { $pdo $this-getPdo(); $pdo-beginTransaction(); try { // 1. 查找该订单对应的所有OUT快照 $stmt $pdo-prepare( SELECT * FROM stock_snapshot WHERE order_id ? AND change_type OUT ); $stmt-execute([$orderId]); $snapshots $stmt-fetchAll(PDO::FETCH_ASSOC); foreach ($snapshots as $snap) { // 2. 每条OUT快照生成一条ADJUST快照类型为ADJUST金额为正数 $stmt $pdo-prepare( INSERT INTO stock_snapshot (goods_id, change_type, change_amount, before_qty, after_qty, order_id, operator_id) VALUES (?, ADJUST, ?, ?, ?, ?, ?) ); $stmt-execute([ $snap[goods_id], $snap[change_amount], // 正数表示补回 $snap[after_qty], // 补回前的库存即扣减后的值 $snap[before_qty], // 补回后的库存即扣减前的值 $orderId, $_SESSION[user_id] ]); // 3. 更新商品主表加回库存 $stmt $pdo-prepare(UPDATE goods SET stock stock ? WHERE id ?); $stmt-execute([$snap[change_amount], $snap[goods_id]]); } $pdo-commit(); } catch (Exception $e) { $pdo-rollback(); throw $e; } }这里的关键是不信任goods.stock当前值而是用快照里的before_qty和after_qty做精确计算。即使有人手动改过goods.stock快照里的原始数据依然完整回滚时不会放大错误。5.3 预警触发库存低于阈值时不只是弹窗还要留痕库存预警不是前端JS弹个alert就完事。checkStockAlert()方法会做三件事插入一条alert_log记录表结构在/sql/alert_log.sql中给指定角色如仓管发送站内信写入notifications表在首页顶部横幅显示“【预警】奥利奥库存仅剩5盒低于安全线10盒”。private function checkStockAlert($goodsId, $currentStock) { // 查询商品安全库存线 $stmt $this-pdo-prepare(SELECT min_stock, name FROM goods WHERE id ?); $stmt-execute([$goodsId]); $goods $stmt-fetch(PDO::FETCH_ASSOC); if ($currentStock $goods[min_stock] $goods[min_stock] 0) { // 1. 记录预警日志 $this-pdo-prepare( INSERT INTO alert_log (goods_id, current_stock, min_stock, triggered_at, status) VALUES (?, ?, ?, NOW(), PENDING) )-execute([$goodsId, $currentStock, $goods[min_stock]]); // 2. 发送站内信给仓管角色用户 $this-pdo-prepare( INSERT INTO notifications (user_id, title, content, created_at, is_read) SELECT id, 库存预警, CONCAT(商品【, ?, 】库存仅剩, ?, 低于安全线, ?, 请立即补货), NOW(), 0 FROM users WHERE role warehouse )-execute([$goods[name], $currentStock, $goods[min_stock]]); } }提示alert_log表设计时加了status字段PENDING/RESOLVED/IGNORED方便后续统计“预警响应时效”。这才是真正在管库存而不是假装在管。6. 二次开发实战给系统加个“扫码枪自动识别商品类型”功能上周帮一家母婴店改造他们想让扫码枪扫完奶粉条码系统自动弹出“是否录入批次号和有效期”而扫纸尿裤就跳过。这需求看似小但涉及三个模块联动扫码接口、商品扩展属性、前端动态表单。我用不到2小时就完成了过程值得复刻。6.1 数据库扩展给goods表加两个可空字段母婴店需要批次和效期但普通零食不需要。所以不新建表而在goods表上加字段ALTER TABLE goods ADD COLUMN need_batch TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否需要批次号1是0否, ADD COLUMN need_expiry TINYINT(1) NOT NULL DEFAULT 0 COMMENT 是否需要有效期1是0否;然后在/sql/supermarket.sql的goods建表语句里把这两列加到CREATE TABLE中确保新部署也自带。6.2 扫码接口增强/api/barcode.php返回商品元信息原接口只返回商品基础信息现在要带上need_batch和need_expiry// /api/barcode.php 原有代码基础上修改 $stmt $pdo-prepare(SELECT id, name, sell_price, need_batch, need_expiry FROM goods WHERE barcode ?); $stmt-execute([$barcode]); $goods $stmt-fetch(PDO::FETCH_ASSOC); if ($goods) { // 返回JSON前端根据need_batch/need_expiry决定是否显示输入框 echo json_encode([ success true, data $goods ]); } else { echo json_encode([success false, message 未找到商品]); }6.3 前端动态表单用jQuery监听扫码返回条件渲染DOM在销售页面/public/views/sale/create.php里原有扫码逻辑是// 原有代码 $.post(/api/barcode.php, {barcode: code}, function(res) { if (res.success) { $(#goods_name).val(res.data.name); $(#sell_price).val(res.data.sell_price); } });现在增强为$.post(/api/barcode.php, {barcode: code}, function(res) { if (res.success) { // 1. 填充基础字段 $(#goods_name).val(res.data.name); $(#sell_price).val(res.data.sell_price); // 2. 动态显示/隐藏批次和效期输入框 if (res.data.need_batch 1) { $(#batch_group).show(); // HTML中已有div idbatch_group styledisplay:none; } else { $(#batch_group).hide(); $(#batch_no).val(); // 清空已输入内容 } if (res.data.need_expiry 1) { $(#expiry_group).show(); } else { $(#expiry_group).hide(); $(#expiry_date).val(); } // 3. 聚焦到第一个需要输入的框提升体验 if (res.data.need_batch 1) { $(#batch_no).focus(); } else if (res.data.need_expiry 1) { $(#expiry_date).focus(); } else { $(#quantity).focus(); // 默认聚焦数量 } } });对应的HTML结构在create.php中!-- 批次号输入组 -- div idbatch_group styledisplay:none; label批次号/label input typetext idbatch_no namebatch_no /div !-- 有效期输入组 -- div idexpiry_group styledisplay:none; label有效期至/label input typedate idexpiry_date nameexpiry_date /div6.4 后端保存把批次和效期存进扩展表而非污染主表母婴店要求批次和效期要能单独查询比如查“飞鹤奶粉20230801批次还剩多少”所以新建goods_batch表CREATE TABLE goods_batch ( id int(11) NOT NULL AUTO_INCREMENT, goods_id int(11) NOT NULL, batch_no varchar(50) NOT NULL, expiry_date date DEFAULT NULL, stock decimal(10,2) NOT NULL DEFAULT 0.00, created_at datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_goods_batch (goods_id,batch_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;然后在/app/service/InventoryService.php的deductStock()方法里当检测到need_batch1时额外插入或更新goods_batch表// 在deductStock()方法中更新goods主表后加入 if ($goods[need_batch] 1 !empty($_POST[batch_no])) { $batchNo $_POST[batch_no]; $expiryDate $_POST[expiry_date] ?? null; // 先查是否存在该批次 $stmt $pdo-prepare(SELECT id, stock FROM goods_batch WHERE goods_id ? AND batch_no ?); $stmt-execute([$goodsId, $batchNo]); $batch $stmt-fetch(PDO::FETCH_ASSOC); if ($batch) { // 存在则更新库存 $stmt $pdo-prepare(UPDATE goods_batch SET stock stock - ? WHERE id ?); $stmt-execute([$amount, $batch[id]]); } else { // 不存在则新建 $stmt $pdo-prepare(INSERT INTO goods_batch (goods_id, batch_no, expiry_date, stock) VALUES (?, ?, ?, ?)); $stmt-execute([$goodsId, $batchNo, $expiryDate, $amount]); } }这样主goods表保持干净扩展属性由独立表承载既满足母婴店需求又不影响普通超市使用。从那以后我每次接到“加个XX功能”的需求都强制走一遍“数据库改不改→接口返不返→前端显不显→后端存不存”四步验证。少走一步上线后就要多熬一夜。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询