MySQL原生ERP源码:事务/触发器/存储过程实现进销存闭环

发布时间:2026/10/31 5:41:49
MySQL原生ERP源码:事务/触发器/存储过程实现进销存闭环 简介这是一套面向中小企业开发者与Java后端学习者的ERP进销存系统开源源码聚焦采购、销售、库存及基础财务管理四大核心场景助力初学者理解企业级业务系统设计逻辑也支持中高级开发者进行二次开发与架构实践。资源包共1448个文件涵盖213个Java后端业务类、172个HTML与182个JS/CSS前端页面、74个JSP视图组件、417个PNG图标与界面素材以及关键的MySQL建库脚本sql、配置文件properties和权限控制相关XML定义整体压缩包仅14.26MB轻量易部署。已有771人下载学习适合通过真实项目掌握MVC分层结构、数据库事务处理、前后端交互及RBAC权限模型等关键技术点。源码结构清晰含完整easyui前端框架集成、WeatherProxy等典型代理模块及多套样式资源便于快速启动与功能拓展。1. 这不是又一个“Demo级ERP”一套能跑通采购入库→销售出库→库存实时扣减→财务凭证自动生成的MySQL原生ERP源码你见过多少标着“ERP源码”的压缩包解压进去index.php跳转到登录页账号密码写死在 config 文件里点开“库存查询”返回SELECT * FROM goods—— 没分页、没搜索、没状态过滤更别提“采购单审核后自动更新库存”这种基础闭环。这套 ERP 进销存系统源码不是那样。它用纯 MySQL 5.7 原生特性存储过程、触发器、事务隔离级别控制把进、销、存三环节拧成一个可验证的数据流一张采购入库单提交库存表stock_detail实时增stock_summary汇总值同步刷新一张销售出库单审核通过库存扣减走的是带FOR UPDATE的行锁事务同时自动生成一条account_voucher记账凭证科目、金额、摘要全部由业务动作驱动。它不依赖 ORM 层做逻辑兜底所有关键状态变更都压在数据库层完成。适合正在从 Excel 管理转向轻量级数字化的中小制造/贸易团队也适合想搞懂“真实业务系统里事务怎么落地”的后端开发者——不是学概念是看proc_update_stock_on_sale这个存储过程里怎么用DECLARE CONTINUE HANDLER捕获库存不足异常并回滚。2. 为什么选 MySQL 原生方案而不是 Laravel Eloquent三处硬核设计告诉你边界在哪2.1 库存扣减必须原子化为什么不用应用层加锁而用SELECT ... FOR UPDATE很多团队第一反应是“我在 PHP 里先查库存够了再 update 扣减”。这在并发场景下必然翻车。比如两个销售单几乎同时查到某商品有 100 件库存都判定“足够”然后都执行UPDATE stock SET qty qty - 10 WHERE id 123—— 最终库存变成 80但实际应为 90。本系统直接放弃应用层判断所有出库动作统一走存储过程DELIMITER $$ CREATE PROCEDURE proc_deduct_stock(IN p_goods_id INT, IN p_qty DECIMAL(10,2)) BEGIN DECLARE v_current_qty DECIMAL(10,2) DEFAULT 0; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; RESIGNAL; END; START TRANSACTION; -- 关键加行锁阻塞其他会话对同一商品的读写 SELECT qty INTO v_current_qty FROM stock_summary WHERE goods_id p_goods_id FOR UPDATE; IF v_current_qty p_qty THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 库存不足; END IF; UPDATE stock_summary SET qty qty - p_qty WHERE goods_id p_goods_id; INSERT INTO stock_log (goods_id, change_type, change_qty, operator) VALUES (p_goods_id, OUT, p_qty, USER()); COMMIT; END$$ DELIMITER ;参数说明p_goods_id是商品主键p_qty是本次扣减数量FOR UPDATE在SELECT时即锁定该行后续UPDATE不会脏写SIGNAL主动抛出业务异常由调用方PHP 层捕获并提示用户。这不是“炫技”是当你的仓库每天处理 200 出库单时避免月底盘亏的后悔药。2.2 凭证生成不靠“事后补录”销售单审核触发器如何联动财务模块财务凭证不是等月底汇总再手工填而是每张销售单状态变为approved时由数据库触发器自动落库。这样保证业务流和财务流时间戳严格对齐审计时能直接追溯“这笔应收账款是哪张销售单产生的”。DELIMITER $$ CREATE TRIGGER tr_after_sale_approved AFTER UPDATE ON sale_order FOR EACH ROW BEGIN IF OLD.status ! approved AND NEW.status approved THEN -- 插入主凭证应收账款 主营业务收入 INSERT INTO account_voucher (voucher_no, voucher_date, description, status) VALUES ( CONCAT(VCH-, DATE_FORMAT(NOW(), %Y%m%d), LPAD(FLOOR(RAND()*90001000), 4, 0)), CURDATE(), CONCAT(销售单 , NEW.order_no, 审核通过), posted ); -- 获取刚插入的 voucher_id SET voucher_id LAST_INSERT_ID(); -- 插入明细借应收账款贷主营业务收入 应交税费 INSERT INTO account_voucher_line (voucher_id, account_code, debit, credit, remark) VALUES (voucher_id, 1122, NEW.total_amount, 0.00, 客户 NEW.customer_name), (voucher_id, 6001, 0.00, ROUND(NEW.total_amount / 1.13, 2), 销售收入), (voucher_id, 22210101, 0.00, ROUND(NEW.total_amount * 0.13 / 1.13, 2), 销项税额); END IF; END$$ DELIMITER ;注意触发器中account_code使用标准会计科目编码如1122应收账款6001主营业务收入非业务字段ROUND(..., 2)强制保留两位小数避免浮点误差导致凭证借贷不平衡voucher_no采用日期随机4位生成杜绝并发重复。这套联动机制让财务人员无需再问业务“这张凭证对应哪张单”——数据库已用外键sale_order_id在account_voucher_line表里埋好线索。2.3 为什么坚持不用视图做“实时库存汇总”而用物化汇总表触发器维护有人会说“用视图CREATE VIEW v_stock_summary AS SELECT goods_id, SUM(qty) FROM stock_detail GROUP BY goods_id不就完了”不行。视图每次查询都要全表扫描stock_detail可能百万级记录且无法建立高效索引。本系统采用“物化汇总表 触发器维护”策略stock_summary表结构极简goods_idPK、qty当前可用库存、updated_at所有入库/出库/调拨操作均通过INSERT/UPDATE/DELETE触发器实时更新该表查询库存时SELECT qty FROM stock_summary WHERE goods_id ?毫秒级响应-- 入库触发器示例简化 CREATE TRIGGER tr_after_purchase_in AFTER INSERT ON purchase_in_detail FOR EACH ROW BEGIN INSERT INTO stock_summary (goods_id, qty, updated_at) VALUES (NEW.goods_id, NEW.qty, NOW()) ON DUPLICATE KEY UPDATE qty qty NEW.qty, updated_at NOW(); END;关键点ON DUPLICATE KEY UPDATE替代INSERT ... SELECT ... UNION避免因并发插入相同goods_id导致主键冲突updated_at字段为后续“按最后更新时间筛选滞销品”提供依据。这是用空间换时间的务实选择——当你的stock_detail日增 5000 条记录时视图性能衰减是线性的而物化表查询是 O(1)。3. 部署前必做的五项校验从 MySQL 配置到权限模型漏一项就卡在登录页3.1 MySQL 5.7 必须启用的三项配置否则存储过程报错系统大量依赖存储过程与触发器若 MySQL 未开启对应权限或配置PHP 连接后执行CALL proc_login_check(...)会直接返回空结果或ERROR 1418。需在my.cnf中确认以下配置已生效# my.cnf 或 my.ini [mysqld] log_bin_trust_function_creators 1 sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION max_connections 500解释log_bin_trust_function_creators 1允许创建存储过程否则主从复制环境下被拒绝sql_mode中STRICT_TRANS_TABLES强制事务表报错而非静默截断max_connections设为 500 是为应对高峰期 20 并发单据提交。改完必须重启 MySQL 服务用SHOW VARIABLES LIKE log_bin_trust_function_creators;验证是否为ON。3.2 数据库用户权限不能只给ALL PRIVILEGES要精确到对象级很多部署失败源于权限过大或过小。系统要求数据库用户具备对erp_*所有表的SELECT, INSERT, UPDATE, DELETE对所有存储过程的EXECUTE对所有触发器的TRIGGER但禁止DROP,CREATE,ALTER权限防误删表执行授权语句替换erp_user和erp_db为你的实际用户名/库名-- 创建用户MySQL 5.7 CREATE USER erp_userlocalhost IDENTIFIED BY StrongPass123!; -- 授予表级权限 GRANT SELECT, INSERT, UPDATE, DELETE ON erp_db.* TO erp_userlocalhost; -- 授予存储过程执行权 GRANT EXECUTE ON PROCEDURE erp_db.proc_deduct_stock TO erp_userlocalhost; GRANT EXECUTE ON PROCEDURE erp_db.proc_create_purchase TO erp_userlocalhost; -- 授予触发器权限MySQL 5.7 需显式授予 GRANT TRIGGER ON erp_db.* TO erp_userlocalhost; -- 刷新权限 FLUSH PRIVILEGES;血泪经验曾有某公司 DBA 为省事直接GRANT ALL ON erp_db.* TO ...结果开发误执行DROP TABLE stock_summary整个库存模块瘫痪 4 小时。精确授权不是麻烦是给系统加一道保险丝。3.3 PHP 环境必须关闭magic_quotes_gpc并启用mysqli扩展虽然已是老配置但仍有老旧服务器残留。若phpinfo()中显示magic_quotes_gpc On会导致所有 POST 参数被自动加反斜杠例如输入密码ab变成a\b登录永远失败。检查并修改php.ini; php.ini magic_quotes_gpc Off extensionmysqli验证方法新建test_db.php内容为?php $link mysqli_connect(localhost, erp_user, StrongPass123!, erp_db); if (!$link) die(连接失败: . mysqli_connect_error()); echo MySQL 连接成功版本 . mysqli_get_server_info($link); ?浏览器访问该文件应输出版本号而非空白页或错误。4. 避坑上线前必须排查的四个高频故障点附现象、原因、解决4.1 现象登录成功后跳转到空白页浏览器控制台无 JS 报错Network 显示index.php返回 200 但 HTML 内容为空原因PHPshort_open_tag未开启导致?开头的模板代码如?php echo $user_name; ?被当作纯文本输出PHP 解析器未执行。解决编辑php.ini将short_open_tag Off改为On重启 Web 服务。验证新建test_short.php内容? echo OK; ?访问应输出 OK。4.2 现象点击“新增采购单”弹窗提示“数据库执行失败”日志中出现SQLSTATE[HY000]: General error: 1305 PROCEDURE erp_db.proc_create_purchase does not exist原因导入 SQL 初始化脚本时未按顺序执行——必须先执行01_create_tables.sql建表再执行02_create_procedures.sql建存储过程否则过程依赖的表不存在。解决删除当前数据库重新按文件名序号依次执行mysql -u erp_user -p erp_db 01_create_tables.sql→mysql -u erp_user -p erp_db 02_create_procedures.sql。切勿用 phpMyAdmin “批量导入”功能它不保证执行顺序。4.3 现象销售单审核后库存扣减成功但account_voucher表无新记录account_voucher_line为空原因MySQL 未启用event_scheduler而部分凭证生成逻辑依赖事件调度器如定时结转成本。但本系统核心凭证由触发器驱动此问题实为触发器未生效——常见于sql_mode中包含NO_AUTO_CREATE_USER且用户创建语句不规范导致触发器创建失败。解决登录 MySQL执行SHOW TRIGGERS LIKE tr_after_sale_approved;若无结果说明触发器未创建。重新执行03_create_triggers.sql并确保执行前sql_mode已设为前述严格模式。4.4 现象多用户同时提交采购入库单部分单据状态卡在pendingstock_summary库存增量比实际入库总数少原因purchase_in_header表的status字段更新未加WHERE id ? AND status pending条件导致 A 用户更新时把 B 用户刚提交、尚未处理的单据状态也覆盖为completedB 用户的后续库存更新被跳过。解决检查proc_create_purchase存储过程中更新单据状态的 SQL必须形如UPDATE purchase_in_header SET status completed, updated_at NOW() WHERE id p_header_id AND status pending; -- 检查影响行数 IF ROW_COUNT() 0 THEN SIGNAL SQLSTATE 45000 SET MESSAGE_TEXT 单据状态异常可能已被他人处理; END IF;5. 进阶技巧用三张表 一个函数实现“动态库存预警”并邮件通知非轮询真正的业务价值不在“能查库存”而在“库存快没了我马上知道”。本系统不依赖 Crontab 每分钟扫表而是用 MySQL 事件 自定义函数 状态标记实现低开销预警。5.1 构建预警规则表支持按商品分类、安全库存阈值、通知方式灵活配置CREATE TABLE stock_alert_rule ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT COMMENT 商品分类ID0表示全部, min_qty DECIMAL(10,2) NOT NULL COMMENT 安全库存下限, notify_method ENUM(email,sms) DEFAULT email, is_active TINYINT(1) DEFAULT 1, created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 示例所有电子类商品库存低于50台时邮件提醒 INSERT INTO stock_alert_rule (category_id, min_qty, notify_method) VALUES (101, 50.00, email);5.2 创建预警检查函数返回需预警的商品ID列表供事件调用DELIMITER $$ CREATE FUNCTION fn_get_low_stock_goods() RETURNS TEXT READS SQL DATA DETERMINISTIC BEGIN DECLARE result TEXT DEFAULT ; DECLARE done INT DEFAULT FALSE; DECLARE v_goods_id INT; DECLARE v_qty DECIMAL(10,2); DECLARE v_category_id INT; -- 游标遍历所有激活的规则 DECLARE cur_rules CURSOR FOR SELECT s.goods_id, s.qty, r.category_id FROM stock_summary s INNER JOIN goods g ON s.goods_id g.id INNER JOIN stock_alert_rule r ON (r.category_id 0 OR g.category_id r.category_id) WHERE r.is_active 1 AND s.qty r.min_qty; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done TRUE; OPEN cur_rules; read_loop: LOOP FETCH cur_rules INTO v_goods_id, v_qty, v_category_id; IF done THEN LEAVE read_loop; END IF; IF result THEN SET result CONCAT(v_goods_id); ELSE SET result CONCAT(result, ,, v_goods_id); END IF; END LOOP; CLOSE cur_rules; RETURN result; END$$ DELIMITER ;说明函数返回逗号分隔的goods_id字符串如123,456,789供后续事件解析。DETERMINISTIC声明确保函数结果仅依赖输入可被 MySQL 优化器缓存。5.3 创建每日一次的预警事件只在业务低峰期运行避免干扰交易DELIMITER $$ CREATE EVENT ev_daily_stock_alert ON SCHEDULE EVERY 1 DAY STARTS 2024-01-01 02:00:00 DO BEGIN DECLARE v_goods_list TEXT; DECLARE v_email_body TEXT DEFAULT ; SET v_goods_list fn_get_low_stock_goods(); IF v_goods_list ! THEN -- 构造邮件正文此处简化实际调用外部邮件函数 SET v_email_body CONCAT(【库存预警】以下商品低于安全库存\n, v_goods_list); -- 关键写入通知日志表由独立脚本非MySQL读取并发送邮件 INSERT INTO system_notification (type, content, status, created_at) VALUES (stock_alert, v_email_body, pending, NOW()); END IF; END$$ DELIMITER ;为什么不用 MySQL 直接发邮件因为SMTP发送涉及网络 I/O会阻塞事件调度器导致后续事件堆积。正确做法是MySQL 只负责“记一笔待办”由一台专用通知服务器Python 脚本每 5 分钟轮询system_notification表取status pending的记录调用企业邮箱 SMTP 发送成功后更新status sent。这样数据库永远轻量。从那以后我每次部署新环境都会在初始化数据库后立刻执行SELECT fn_get_low_stock_goods();手动验证函数是否返回预期结果并用SHOW EVENTS;确认ev_daily_stock_alert状态为ENABLED。这三步花不了两分钟却能避开 80% 的预警失效问题。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

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

尧图内容编辑团队

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

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

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

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

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

网站改版的5个关键决策

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

获取专属建站方案

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

立即免费咨询