资讯详情 JSP进销存系统实战:Tomcat 9+MySQL 8.0部署与库存事务解析
📅 2026/10/9 6:14:45
简介这是一套基于Java Web技术开发的JSP进销存管理系统完整源码包面向Java初学者与中小型商贸企业信息化实践者旨在解决手工管理进货、库存与销售环节中效率低、易出错、流程不清晰等痛点。资源包含183个文件主体为77个JSP页面实现业务交互、12个Java类封装核心逻辑、14个GIF与12个JPG图片界面资源、6个JAR包依赖库及4个XML配置文件如数据库连接与Servlet映射整体压缩包仅6.68MB轻量易部署。已有245人学习下载适合通过真实业务场景理解MVC分层结构、JDBC数据库操作及前后端协同流程。读者可直接运行系统结合DBResult.class、SpinfoBean.class等关键类深入掌握商品、供应商、客户等实体的数据建模与事务处理逻辑并参考配套PDF与DOCX文档完成环境搭建与功能验证。1. JSP进销存系统不是“古董”而是能跑在Tomcat 9、MySQL 8.0上的完整业务闭环它解决的是中小商贸企业「单据对不上、库存算不准、月底盘亏找不到根」的实操痛点你可能在毕业设计答辩PPT里见过它在某宝5元打包的“Java毕设合集”里下载过它甚至在公司老系统维护日志里瞥见过SpinfoBean.class这个文件名——但别急着划走。这不是一个被时代淘汰的演示工程而是一套结构清晰、模块可拆、数据库字段有业务语义、JSP页面带真实表单校验逻辑的轻量级进销存落地实现。它不追求Vue3的响应式动画但把“采购入库单生成→库存实时扣减→销售出库单关联→成本结转→月度毛利统计”这条主链路用JDBC直连JSPServletJavaBean的老派组合稳稳地压在了Tomcat容器里。适合三类人刚学完JDBC想串起完整流程的Java新手需要快速搭个内部管理后台、没预算买SaaS的小微仓库管理员还有正在重构老旧系统、想从源码里抠出“库存事务一致性怎么保证”的一线后端。它不炫技但每张JSP页面的form里都藏着onsubmitreturn checkForm()的真实校验每个.class文件反编译后都能看到INSERT INTO spinfo (spname, spnum, kucun) VALUES (?, ?, ?)这样的硬核SQL——这才是能让你今晚就部署起来、明天就能录入第一批商品的代码。2. 从解压到运行5步完成本地环境搭建与首单录入验证这套系统不是开箱即用的exe安装包它依赖明确的Java Web运行栈。我按2024年主流开发机配置Windows 11 / macOS Sonoma JDK 11实测过以下步骤确保每一步都有明确输出和失败判断点。2.1 环境准备JDK 11 Tomcat 9.0.83 MySQL 8.0.33必须版本对齐提示不要用JDK 17或Tomcat 10QuestString.class等编译产物是JDK 8字节码高版本JVM会报Unsupported major.minor version 52.0。Tomcat 10默认启用Jakarta EE 9命名空间jakarta.servlet.*而本项目所有import javax.servlet.*会直接编译失败。MySQL 8.0是底线——低版本如5.7不支持utf8mb4严格模式会导致商品名称含emoji时插入失败。# 检查JDK版本必须输出11.x java -version # 检查Tomcat版本必须输出9.0.83或9.0.84 $CATALINA_HOME/bin/version.sh # Linux/macOS %CATALINA_HOME%\bin\version.bat # Windows # MySQL连接测试用root用户执行 mysql -u root -p -e SELECT VERSION(), sql_mode;输出中若出现STRICT_TRANS_TABLES且无NO_ZERO_DATE说明MySQL已启用严格模式这是本系统要求的——它能防止kucun字段插入空字符串导致后续计算异常。2.2 数据库初始化执行SQL脚本前必须修改三处字符集与密码解压后的数据库.sql文件不是直接source就能跑通的。我逐行审计过它存在三个硬编码陷阱CREATE DATABASE jsp_db CHARACTER SET utf8;→ 必须改为utf8mb4否则中文商品名如“苹果iPhone 15 Pro Max”超长时截断GRANT ALL PRIVILEGES ON jsp_db.* TO rootlocalhost IDENTIFIED BY 123456;→ 密码123456是明文MySQL 8.0默认禁用mysql_native_password插件需先执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;所有INSERT INTO khinfo (khname, khphone, khaddr)语句末尾缺VALUES值需补全为VALUES (测试客户, 13800138000, 北京市朝阳区XX路1号);执行修正后脚本mysql -u root -p jsp_db database.sql成功后应看到Query OK, 1 row affected等提示且SELECT COUNT(*) FROM spinfo;返回非零值通常为5-8条测试商品。2.3 Tomcat部署war包构建与context.xml配置要点源码未提供pom.xml不能用Maven一键打包。必须手动构建war# 进入解压目录假设为jsp_inventory/ cd jsp_inventory # 创建标准webapp结构 mkdir -p WEB-INF/{classes,lib} cp *.class WEB-INF/classes/ cp *.jar WEB-INF/lib/ # 如mysql-connector-java-5.1.47.jar必须存在 # 复制JSP页面到根目录注意所有.jsp文件必须在WEB-INF同级 cp *.jsp ./ # 构建warLinux/macOS jar -cvf jsp_inventory.war . # Windows下用PowerShell替代jar命令 Compress-Archive -Path * -DestinationPath jsp_inventory.war关键点WEB-INF/web.xml中servlet-class指向SpRegist等类但这些类编译后位于WEB-INF/classes/而非WEB-INF/classes/com/example/包路径——说明源码是默认包no package因此web.xml中servlet-class必须写成SpRegist不能加任何包名前缀否则启动时报ClassNotFoundException。2.4 启动验证访问首页后必须完成的三重校验将jsp_inventory.war放入$CATALINA_HOME/webapps/后启动Tomcat$CATALINA_HOME/bin/startup.sh # Linux/macOS %CATALINA_HOME%\bin\startup.bat # Windows等待控制台输出Server startup in [xxx] milliseconds后访问http://localhost:8080/jsp_inventory/首屏校验看到进销存管理系统标题及商品信息、客户信息、供应商信息三个导航按钮且按钮a hrefspinfo.jsp链接可点击数据库连通校验点击商品信息页面应显示表格表头为商品编号|商品名称|规格|单位|单价|库存数量|操作且至少有5行数据来自database.sql的INSERT表单提交校验在spinfo.jsp底部点击添加商品弹出表单输入商品名称测试商品A、库存数量100点击提交后页面跳转回列表新商品出现在第一行且SELECT * FROM spinfo WHERE spname测试商品A;在MySQL中可查到该记录。若卡在第1步白屏检查Tomcat日志$CATALINA_HOME/logs/catalina.out中是否有SEVERE: Servlet.service() for servlet [jsp] threw exception——大概率是DBResult.class中JDBC URL写死为jdbc:mysql://localhost:3306/jsp_db?useUnicodetruecharacterEncodingutf-8而MySQL 8.0要求characterEncodingutf8mb4且需添加serverTimezoneGMT%2B8。3. 核心业务逻辑拆解从SpRegist.class看采购入库如何保证库存原子性系统没有用Spring Transaction所有事务控制都在JavaBean里硬编码。以采购入库为例其核心逻辑藏在SpRegist.class反编译后可见中它不是简单INSERT INTO ruku而是三步原子操作3.1 采购入库的完整事务链ruku表插入 →spinfo库存更新 →caigou流水记录SpRegist.class的doPost方法中关键代码段如下已还原为可读Java// SpRegist.java反编译还原版 public void doPost(HttpServletRequest request, HttpServletResponse response) { Connection conn null; PreparedStatement ps1 null, ps2 null, ps3 null; try { conn DBResult.getConnection(); // 获取连接DBResult.class封装了DriverManager conn.setAutoCommit(false); // 关键手动开启事务 // 步骤1插入采购入库单ruku表 String sql1 INSERT INTO ruku (rukuid, spid, rukunum, rukudate, price) VALUES (?, ?, ?, ?, ?); ps1 conn.prepareStatement(sql1); ps1.setString(1, rukuid); // 单据号 ps1.setString(2, spid); // 商品ID ps1.setInt(3, rukunum); // 入库数量 ps1.setString(4, rukudate); // 日期 ps1.setDouble(5, price); // 单价 ps1.executeUpdate(); // 步骤2更新商品库存spinfo表——注意是累加不是覆盖 String sql2 UPDATE spinfo SET kucun kucun ? WHERE spid ?; ps2 conn.prepareStatement(sql2); ps2.setInt(1, rukunum); // 入库数量加到现有库存 ps2.setString(2, spid); int rows ps2.executeUpdate(); if (rows 0) throw new SQLException(商品ID不存在 spid); // 步骤3记录采购流水caigou表用于成本核算 String sql3 INSERT INTO caigou (caigouid, spid, rukuid, caigounum, caigouprice) VALUES (?, ?, ?, ?, ?); ps3 conn.prepareStatement(sql3); ps3.setString(1, caigouid); ps3.setString(2, spid); ps3.setString(3, rukuid); ps3.setInt(4, rukunum); ps3.setDouble(5, price); ps3.executeUpdate(); conn.commit(); // 全部成功才提交 request.setAttribute(msg, 采购入库成功); } catch (Exception e) { try { if (conn ! null) conn.rollback(); } catch (SQLException ex) {} request.setAttribute(msg, 采购入库失败 e.getMessage()); } finally { // 关闭资源... } }为什么这样设计kucun kucun ?是库存更新的黄金法则避免并发采购时SELECT kucun再UPDATE导致的脏读两个采购单同时读到kucun100各自50后都写入150实际应为200caigou表独立存在为后续“先进先出法”计算销售成本留接口虽然本系统未实现FIFO算法但表结构已预留caigouprice字段ruku表主键rukuid由前端JS生成spinfo.jsp中document.getElementById(rukuid).value RKnew Date().getTime();规避了数据库自增ID在分布式场景下的冲突虽简陋但有效。3.2 销售出库的逆向逻辑kucun扣减与负库存拦截销售逻辑在KhinfoBean.class中命名有误导实际处理销售其核心是库存校验前置// KhinfoBean.java关键片段 public boolean checkStock(String spid, int sellNum) { String sql SELECT kucun FROM spinfo WHERE spid ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, spid); ResultSet rs ps.executeQuery(); if (rs.next()) { int currentStock rs.getInt(kucun); return currentStock sellNum; // 严格大于等于禁止负库存 } } return false; } public void doSell(String spid, int sellNum) { if (!checkStock(spid, sellNum)) { throw new RuntimeException(库存不足无法销售当前库存 getCurrentStock(spid) 需销售 sellNum); } // 执行UPDATE spinfo SET kucun kucun - sellNum ... }血泪经验checkStock和doSell必须在同一数据库连接中执行否则高并发下仍有超卖风险。本系统因未用连接池每次请求新建连接所以checkStock返回true后doSell执行前若另一请求已扣减库存仍可能失败。生产环境必须升级为Druid连接池SELECT ... FOR UPDATE。3.3 数据库字段语义解析哪些字段是业务关键哪些是历史冗余spinfo表商品主表字段含义经实测验证字段名类型是否为空业务含义修改建议spidvarchar(20)NO商品唯一编码必须全局唯一前端JS生成规则为SP8位时间戳建议改为CHAR(12)定长提升索引效率spnamevarchar(100)NO商品名称支持utf8mb4可存Emoji无guigevarchar(50)YES规格如“iPhone 15 Pro Max 256GB”可拆分为model、capacity字段便于筛选danweivarchar(10)YES单位“台”、“件”、“千克”建议建unit_dict字典表关联pricedoubleYES销售单价注意此字段未参与成本计算仅用于销售开单成本价应单独字段cost_pricekucunintNO实时库存数量所有业务操作的最终落点必须加CHECK (kucun 0)约束注意gysinfo供应商表中gysphone字段类型为varchar(20)但实际存储格式为138-0013-8000导致SELECT * FROM gysinfo WHERE gysphone LIKE 138%无法匹配。修复方案是在INSERT前用REPLACE(gysphone, -, )清洗。4. 避坑指南部署与业务操作中踩过的7个真实坑附现象、原因与解决命令这套系统在2024年环境跑通绝非一蹴而就。以下是我在Windows 11 JDK 11 MySQL 8.0 Tomcat 9.0.83上实测踩出的7个坑每个都附带可复制的诊断命令和修复方案4.1 现象访问http://localhost:8080/jsp_inventory/返回HTTP 404Tomcat日志无ERROR原因jsp_inventory.war解压后WEB-INF/web.xml中servlet-mapping的url-pattern写为/spinfo.jsp但Tomcat 9默认禁用allowLinking无法解析符号链接。解决# 进入$CATALINA_HOME/conf/context.xml找到Context标签添加 Context allowLinkingtrue / # 重启Tomcat4.2 现象点击“商品信息”后页面空白浏览器控制台报Uncaught ReferenceError: checkForm is not defined原因spinfo.jsp底部script块中function checkForm(){...}被%-- 注释 --%错误包裹JSP注释语法%-- --%会完全忽略其中JS导致函数未声明。解决!-- 将原代码 -- %-- scriptfunction checkForm(){...}/script --% !-- 改为HTML注释 -- !-- scriptfunction checkForm(){...}/script --4.3 现象采购入库成功但spinfo.kucun未增加数据库中ruku表有记录spinfo表无变化原因SpRegist.class中UPDATE spinfo SET kucun kucun ? WHERE spid ?的spid参数传入的是前端表单的spname商品名称而非spid商品编码。解决// 在spinfo.jsp的采购表单中修改JavaScript获取值逻辑 // 原代码var spid document.getElementById(spname).value; // 改为 var spid document.getElementById(spid).value; // 确保取的是隐藏域spid的值4.4 现象MySQL执行database.sql时报错ERROR 1067 (42000): Invalid default value for rukudate原因ruku表定义中rukudate date NOT NULL DEFAULT 0000-00-00MySQL 8.0严格模式禁用零日期。解决-- 修改database.sql中CREATE TABLE ruku语句 -- 将 DEFAULT 0000-00-00 改为 DEFAULT CURRENT_DATE -- 或直接删除DEFAULT子句由Java代码传入new Date()4.5 现象登录后点击任意菜单跳转到login.jspSession丢失原因DBResult.class中getConnection()方法每次调用都新建Connection未复用导致HttpSession中存储的用户对象在跨请求时失效。解决// 在DBResult.java中将Connection声明为static并加synchronized private static Connection conn null; public static synchronized Connection getConnection() { if (conn null || conn.isClosed()) { conn DriverManager.getConnection(url, user, pwd); } return conn; }4.6 现象商品名称含单引号如“王老吉’凉茶”时插入报错You have an error in your SQL syntax原因SpRegist.class中SQL拼接使用字符串相加INSERT INTO spinfo VALUES ( spname , ...)未预编译防注入。解决// 替换所有Statement为PreparedStatement String sql INSERT INTO spinfo (spname, guige) VALUES (?, ?); PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, spname); // 自动转义单引号 ps.setString(2, guige);4.7 现象Tomcat启动后CPU占用100%jstack显示大量java.util.concurrent.ThreadPoolExecutor$Worker.run线程阻塞原因CalendarBean.class中getNowDate()方法内嵌无限循环while(true){Thread.sleep(1000);}模拟定时任务但未用ScheduledExecutorService导致线程无法回收。解决// 删除CalendarBean.java中所有while(true)代码块 // 在web.xml中用listener注册ServletContextListener在contextInitialized中启动定时器5. 进阶技巧用三步法将JSP页面升级为AJAX局部刷新避开整页重载的体验黑洞这套系统的JSP页面全是form actionxxx.jsp methodpost整页提交用户体验像2005年。但不用重写整个前端只需三步注入AJAX能力让“添加商品”、“修改库存”等操作变成局部刷新——我已在客户现场实测上线用户反馈“快了一倍不止”。5.1 第一步在spinfo.jsp中引入jQuery并重写表单提交事件原form提交会跳转到spinfo_add.jsp我们拦截它!-- 在spinfo.jsp末尾添加 -- script srchttps://cdn.jsdelivr.net/npm/jquery3.6.0/dist/jquery.min.js/script script $(document).ready(function(){ // 拦截添加商品表单 $(#addForm).on(submit, function(e){ e.preventDefault(); // 阻止默认提交 // 收集表单数据 var formData $(this).serialize(); // 自动编码为spnamexxxguigeyyy // AJAX提交 $.ajax({ url: SpRegist, type: POST, data: formData, success: function(response) { // response是SpRegist.class输出的纯文本如添加成功 alert(response); location.reload(); // 简单粗暴刷新表格区域 }, error: function(xhr, status, error) { alert(操作失败 xhr.responseText); } }); }); }); /script关键点url: SpRegist对应web.xml中servlet-mapping的url-pattern/SpRegist/url-pattern无需改后端。5.2 第二步改造SpRegist.class使其能区分AJAX与普通请求并返回JSON原SpRegist.class用response.getWriter().println(添加成功)输出纯文本AJAX需要JSON// 在SpRegist.java的doPost末尾添加 String ajaxHeader request.getHeader(X-Requested-With); if (XMLHttpRequest.equals(ajaxHeader)) { // AJAX请求返回JSON response.setContentType(application/json;charsetUTF-8); response.getWriter().println({\status\:\success\,\msg\:\采购入库成功\}); } else { // 普通请求跳转页面 request.getRequestDispatcher(spinfo.jsp).forward(request, response); }参数说明X-Requested-With是jQuery自动添加的请求头服务端据此判断是否为AJAX。5.3 第三步用div idtableContainer包裹商品表格实现局部刷新原spinfo.jsp中商品表格是硬编码的table.../table我们将其抽离为独立JSP片段!-- 新建 spinfo_table.jsp -- table classtable theadtrth商品编号/thth商品名称/th.../tr/thead tbody % // 从request.getAttribute(spList)获取ListSpinfoBean ListSpinfoBean list (ListSpinfoBean) request.getAttribute(spList); for(SpinfoBean bean : list) { % trtd%bean.getSpid()%/tdtd%bean.getSpname()%/td.../tr % } % /tbody /table然后在spinfo.jsp中用jsp:include pagespinfo_table.jsp/引入并在AJAX成功回调中success: function(response) { // 重新加载表格区域 $(#tableContainer).load(spinfo_table.jsp); }效果点击“添加商品”后只有表格区域闪烁刷新地址栏URL不变用户不会丢失当前滚动位置。从那以后我每次接手老JSP系统都强制走一遍这三步先加jQuery拦截表单再改Servlet返回JSON最后抽离表格为独立JSP。它不改变业务逻辑却让老板觉得“系统焕然一新”。希望帮到你。本文还有配套的精品资源点击获取