☰
JSP+MySQL进销存系统源码详解:三层架构、部署配置与库存对账
2026/10/11 21:46:52 网站建设 项目流程

简介:一套基于JSP与MySQL的进销存管理系统项目,适合Java Web开发者、相关专业学生及需要快速实现进销存业务的中小企业技术人员。系统包含商品管理、订单管理、库存管理和报表分析四大核心模块,采用MVC分层架构,通过JDBC访问MySQL,并涉及标签库、连接池、权限控制等关键实现技术,可作为课程设计、毕业设计或业务系统开发参考。压缩包约630KB,标题注明含数据库文件,具体文件目录与类型明细需下载后查看。目前已有199人学习或下载,适合希望理解进销存场景下数据建模、订单流转和库存联动机制的读者。下载后可获得系统设计思路、功能模块划分、数据库组织方式及前后端交互流程,有助于快速梳理JSP+MySQL项目的完整开发路径。

1. 为什么现在还会有人下载 JSP+MySQL 的进销存系统

如果单纯按技术新旧来评判,JSP+MySQL 的组合确实不算时髦,但这份源码包放在今天的真实场景里依然有实用价值:某高校的 Java Web 课设要求交一个完整项目、小门店需要一个能记录进货出货的本地台账、刚转行的开发者想找一份能看懂全部代码的练手工程。这套系统正好踩在所有需求点上——它用最传统的 JSP+Servlet+JDBC 分层把进销存的核心业务讲清楚了,还附带可直接导入的数据库脚本,省去从零建表的时间。适合的人也很明确:手里有课设或小项目要交差、想研究老 Java Web 项目是怎么处理库存事务、或者准备把这种经典结构改造成 Spring Boot 版本的人。下载之前,先搞懂它的架构和表设计,后面跑起来才不抓瞎。

2. 先看架构再看库:JSP+Servlet+JDBC 的三层结构与账本数据模型

2.1 这种“老三层”为什么还没被淘汰

进销存系统的核心是数据流动:商品进来、商品出去、库存要随时对得上。这份源码用的是典型的三层结构,JSP 只负责页面展示,Servlet 接收请求并调用业务方法,DAO 层用 JDBC 访问 MySQL。你在包里看到的源码目录大致长这样:

src/ ├── controller/ # Servlet,处理登录、增删改查请求 ├── service/ # 业务逻辑,库存加减和单据校验 ├── dao/ # JDBC 数据访问,SQL 语句集中在这里 ├── entity/ # 商品、供应商、订单等实体类 └── util/ # DBUtil 数据库连接工具 WebContent/ ├── admin/ # 后台管理 JSP 页面 ├── css/ js/ images/ # 静态资源 └── WEB-INF/ ├── web.xml # Servlet 映射与欢迎页配置 └── lib/ # mysql-connector-java 驱动包

controller 里每类操作对应一个 Servlet,例如 GoodsServlet、StockInServlet、StockOutServlet,避免把大量逻辑堆在 JSP 里;DAO 层每个方法只做一件事,比如按 ID 查商品、插入进货单。这样做的直接好处是新手能顺着一行行代码搞清楚从前端到数据库的完整链路,不需要理解 Spring 的 IOC 和 AOP,也不用猜测 MyBatis 的代理机制。相比前后端分离项目,这套系统没有跨域、没有 Token 鉴权,学习成本低得多,所以很多课程的期末设计要求里仍然把 JSP 列为可选方案。

这套结构的劣势也明显:JSP 页面里往往混着 Java 片段和 HTML,改动界面时必须小心不要破坏业务代码;没有框架的事务管理,库存扣减要手动控制 Connection 提交或回滚。后文会专门讲库存事务的写法,这部分是进销存系统最容易出 bug 的地方。

2.2 数据库脚本里藏着哪些核心表

数据库脚本一般放在源码包根目录的 sql 或 db 文件夹里,文件名类似 init.sql 或 db_erp.sql。导入后主要能看到下面几张表。

表名用途关键字段
sys_user系统用户表id, username, password, role
base_goods商品信息表id, goods_no, goods_name, spec, unit, price, stock_qty
base_supplier供应商表id, supplier_no, supplier_name, contact, phone
bill_stock_in进货单表id, bill_no, supplier_id, goods_id, qty, price, total, create_time
bill_stock_out销售单表id, bill_no, goods_id, qty, price, total, customer_name, create_time
stock库存表id, goods_id, current_qty, warn_qty, update_time

注意进货单、销售单和库存表是分开的,而不是在商品表里直接改库存字段。这种设计叫流水账模式,好处是每笔进出都有据可查:要核对库存,只要把 bill_stock_in 的总量减去 bill_stock_out 的总量,再和 stock 表的 current_qty 对比就能发现有没有错误。如果你拿到手的脚本里没有 stock 表,而是直接在 base_goods 里存库存数量,说明原项目偷懒了,后续对账会非常痛苦。

字段设计上有几个细节值得留意。goods_no 和 bill_no 都有唯一索引,单据号通常按日期加序号生成,例如 RK20250115001;price 字段保留两位小数;qty 字段默认不能为负。这些约束保证了基础数据不容易脏,但并不会真正阻止两个用户同时操作同一件商品的库存,所以源码里必须在业务代码层面解决并发问题,后面第 4 章会展开。

2.3 数据库连接参数在什么地方改

老 JSP 项目一般不会用配置文件管理数据库连接,而是直接写死在一个 DBUtil 类里。你打开 util 包下的 DBUtil.java,会看到类似代码:

public class DBUtil { private static final String DRIVER = "com.mysql.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/db_erp?useUnicode=true&characterEncoding=utf-8"; private static final String USERNAME = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USERNAME, PASSWORD); } }

这段代码的常规坑点有两个。第一,MySQL 8.0 版本下驱动类名要改成 com.mysql.cj.jdbc.Driver,否则启动时直接报找不到驱动;第二,URL 里如果没有 serverTimezone 参数,MySQL 8.0 连接时会报时区错误,需要追加 serverTimezone=Asia/Shanghai。更稳妥的做法是增加一个 db.properties 文件,把 URL、账号、密码放在外面,这样换数据库环境时不用重新编译,后面部署章节会给出具体示例。

3. 把源码跑起来:JDK/Tomcat/MySQL 环境搭配与数据库导入

3.1 版本组合怎么选才不打架

拿到源码包别急着双击,先把环境对齐。这套系统的标准运行组合是 JDK 1.8 + Tomcat 8.5/9.0 + MySQL 5.7,如果本机装的是 MySQL 8.0 或 JDK 17,也能跑通,但要注意几个调整点。

环境项推荐版本不兼容时的典型报错
JDK1.8JDK 17 下部分老 Tomcat 无法启动
Tomcat8.5 / 9.0Tomcat 10 的 javax.servlet 包名已变
MySQL5.7MySQL 8 需换驱动类和 URL 参数
连接驱动mysql-connector-java-5.1.49连 MySQL 8 报 Public Key Retrieval 错误
IDEEclipse / IDEA注意 WebContent 目录导入方式不同

Tomcat 10 的坑最隐蔽:它把 javax.servlet 更改为 jakarta.servlet,老项目 WEB-INF/lib 里的包和源码引用都会失效,所以建议直接用 Tomcat 9。如果你习惯了 IDEA,按“导入项目 → 选择 Web”的方式打开,IDEA 会自动识别为 Web 项目;如果用 Eclipse,导入时选 General → Existing Projects into Workspace,然后右键项目配置 Target Runtime。

3.2 先把数据库脚本灌进去再谈其他

数据库脚本是这份资源最值钱的部分之一,包含建库建表语句和初始数据。导入时建议先看清楚脚本开头的字符集声明,再用命令行或图形化工具执行。这里给出命令行方式:

mysql -u root -p # 输入密码后创建数据库 CREATE DATABASE IF NOT EXISTS db_erp DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; USE db_erp; SOURCE /path/to/db_erp.sql;

SOURCE 后面写脚本文件的绝对路径,MySQL 会逐行执行脚本里的建表和插入语句。如果看到 ERROR 1064 语法错误,优先怀疑是脚本里有中文注释而当前连接字符集不对,可以在执行前先执行 SET NAMES utf8mb4; 再试一次。图形化工具导入更直观,Navicat 或类似工具里右键数据库 → 运行 SQL 文件,导入前同样确认目标数据库名正确。

登录账号一般也由脚本初始化,最常见的是用户名 admin、密码 admin123 或 123456,但不要凭猜,直接去 sys_user 表查:SELECT username, password FROM sys_user;。注意很多课设项目的密码是明文存储的,那是因为演示需要;真实生产环境至少要用 MD5 或 BCrypt,这一条后面避坑章节会再提。

3.3 配置 db.properties 并部署到 Tomcat

如果你决定把 DBUtil 改成读配置文件,在 src 目录新建 db.properties,这样以后只需要改文件内容,不用改 Java 代码:

db.driver=com.mysql.jdbc.Driver db.url=jdbc:mysql://localhost:3306/db_erp?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai db.username=root db.password=123456

DBUtil 里对应改成 Properties 读取方式。要点是 driver 和 url 必须和 MySQL 环境匹配:MySQL 5.7 用 com.mysql.jdbc.Driver 没问题,MySQL 8.0 必须换成 com.mysql.cj.jdbc.Driver,同时在 url 中追加 useSSL=false 和 serverTimezone。密码不要有特殊字符,如果有 @ 或 & 记得转义。

配置完成后,把项目打成 war 包放到 Tomcat 的 webapps 目录,或者直接在 IDE 里配置 Tomcat Server 并 Deploy。启动后访问 http://localhost:8080/项目名/index.jsp,能看到登录页就说明 Web 层没问题。若启动时 Tomcat 报端口占用,改 conf/server.xml 里的 8080 端口即可;若页面能打开但点登录时报数据库错误,按第 5 章排查清单逐项核对。

4. 核心功能模块拆解:登录鉴权、商品管理与进销存流水

4.1 登录鉴权:Session 控制页面的访问边界

登录功能看似简单,但老项目里经常能看到两种极端:一种是不做任何拦截,直接在 JSP 里写业务代码;另一种是全部页面都检查 Session,把所有页面变成一张登录跳转表。这份源码里采用的是最基础的 Session 拦截方式,核心逻辑如下:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username = request.getParameter("username"); String password = request.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, password); if (user != null) { request.getSession().setAttribute("loginUser", user); response.sendRedirect("admin/index.jsp"); } else { request.setAttribute("errorMsg", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); } }

逻辑不复杂,但有两个参数层面必须注意:第一,密码比较发生在 DAO 层,username 和 password 以字符串拼进 SQL,如果源码里用的是加号拼接而不是 PreparedStatement,就存在 SQL 注入风险;第二,Session 超时时间可以在 web.xml 中调整,配置为 30 分钟比较合理。对于课设或内部小系统,这种 Session 方案足够用;如果你要改造成更安全的版本,至少要把密码加密和登录失败次数限制加上。

4.2 商品管理:分页与条件查询的 SQL 套路

商品列表页是进销存系统最常打开的一页,通常支持按商品名称、编号模糊查询并分页展示。源码里对应的 DAO 方法一般长这样:

public List<Goods> findGoodsByPage(String keyword, int pageNo, int pageSize) { String sql = "SELECT * FROM base_goods WHERE goods_name LIKE ? OR goods_no LIKE ? LIMIT ?, ?"; try (PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); ps.setInt(3, (pageNo - 1) * pageSize); ps.setInt(4, pageSize); ... } }

这里最容易踩坑的是 LIMIT 起始值换算。pageNo 从 1 开始,所以偏移量是 (pageNo - 1) * pageSize,如果写成 pageNo * pageSize,第二页会跳掉第一页。另一个常见问题是在 MySQL 中使用中文列名做模糊查询时,条件字段和数据库字符集不一致会导致查不到结果,所以页面和数据库的 UTF-8 必须串起来统一。商品编号一般有唯一索引,修改/删除时按 id 操作,同时在 product 变动时要同步更新库存表,否则会出现一张商品有两个库存数量的尴尬数据。

4.3 进货和销售:事务边界决定库存对不对

进销存的核心不在页面,而在进货单和销售单提交时对库存的影响。一份正确实现进货的代码应该是这样的:

public boolean stockIn(StockInBill bill) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 1. 写入进货单 stockInDao.insertBill(conn, bill); // 2. 检查商品是否已有库存记录 Stock stock = stockDao.findByGoodsId(conn, bill.getGoodsId()); if (stock == null) { stockDao.insertStock(conn, bill.getGoodsId(), bill.getQty()); } else { stockDao.addQty(conn, bill.getGoodsId(), bill.getQty()); } // 3. 提交事务 conn.commit(); return true; } catch (SQLException e) { conn.rollback(); e.printStackTrace(); return false; } finally { DBUtil.close(conn); } }

这段代码的核心是手动控制事务边界:进货单写入和库存增加必须作为一个整体,要么都成功,要么都回滚。很多老项目翻车就翻在步骤 2 查不到库存记录时直接报空指针,或者步骤 3 之前没有关闭自动提交,导致单据写入了但库存没变。参数方面要注意重复提交流程:如果前端页面多次点击提交按钮,同一条进货单会被插入两次,常见做法是在生成订单号时加入时间戳到毫秒或使用唯一索引强行防止重复。销售单的逻辑同理,只是增加库存变为减少库存,并且要检查商品库存是否充足,不充足时应直接抛异常回滚。

4.4 统计报表:一条 SQL 看出利润趋势

进销存项目的最后一定有一个统计报表。报表功能通常不是靠 Java 代码计算,而是用 SQL 聚合一把梭。例如查询每个月的进货总额:

SELECT DATE_FORMAT(create_time, '%Y-%m') AS bill_month, SUM(total) AS total_amount FROM bill_stock_in GROUP BY bill_month ORDER BY bill_month DESC;

DATE_FORMAT 是关键函数,它把 datetime 类型的 create_time 格式化成月份字符串,GROUP BY 按月汇总。销售报表用同样的思路,把表换成 bill_stock_out。如果要算毛利,把两张表的按月汇总通过 JOIN 关联商品维度,减掉进货成本再除以销售额就是毛利率。报表页的常见问题集中在日期格式和数据口径上,比如有的项目把 create_time 存成字符串导致无法用日期函数,看到这种数据只能先靠代码清洗。

5. 常见问题排查:从乱码到事务失败的核心翻车点

5.1 Tomcat 启动报 ClassNotFoundException: com.mysql.jdbc.Driver

现象:启动 Tomcat 后,页面能打开,但一访问数据库相关页面就报找不到驱动类。

原因:WEB-INF/lib 下缺 mysql-connector-java 驱动包,或者驱动版本与 MySQL 不匹配。MySQL 8.0 环境不能再用 com.mysql.jdbc.Driver。

解决:确认驱动 jar 已放在 WebContent/WEB-INF/lib 目录并重新部署。若使用 MySQL 8.0,把 DBUtil 和 db.properties 里的驱动类改为 com.mysql.cj.jdbc.Driver,同时在 URL 中加入 useSSL=false 和 serverTimezone=Asia/Shanghai。改完必须重启 Tomcat,光刷新页面不会生效。

5.2 数据库导入成功但登录时中文全变问号

现象:sys_user 表里能查到 admin 用户,但登录后页面显示用户名或商品名全是??。

原因:MySQL 客户端连接字符集不是 UTF-8,或建库语句用了 latin1。源码包里的 SQL 脚本开头可能写的是 SET NAMES utf8,但数据库实例的默认字符集是 latin1。

解决:在 SQL 脚本最前面加上 SET NAMES utf8mb4;,并把表字段显式设置为 utf8mb4。如果数据已经导入,执行 ALTER DATABASE db_erp CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;,再对每张表执行 ALTER TABLE 表名 CONVERT TO CHARACTER SET utf8mb4;。统一以后,连接 URL 里的 characterEncoding=utf-8 也要保留。

5.3 提交进货单成功,但库存没有增加

现象:页面提示进货单保存成功,查看进货单列表有记录,但商品详情页库存数不变。

原因:进货方法中的数据库操作没有处于同一事务内,或者库存表更新语句执行失败但进货单已经提交。常见的失败原因是卖的商品 id 在库存表里没有初始记录,代码里直接 UPDATE stock SET current_qty = current_qty + ?,结果影响行数为 0,但程序没有检查更新结果。

解决:按第 4 章的方式重写该业务方法:先查库存记录,不存在则 INSERT,存在则 UPDATE,整个过程使用同一个 Connection 并关闭自动提交,任何一步异常都回滚。完成后手动核对流水记录与库存差值,可以用下一章的 SQL 对账法验证。

5.4 JSP 页面上出现 ${goods.name} 原样文本

现象:列表页不显示数据,页面上直接输出 ${...} 这种字样。

原因:项目的 web.xml 对应 Servlet 版本较低,默认不启用 EL 表达式;或者 JSP 头部的 page 指令里设置了 isELIgnored="true"。

解决:打开出问题的 JSP,确认文件开头是 <%@ page language="java" contentType="text/html; charset=UTF-8"%>,并显式追加 isELIgnored="false"。如果整站都这样,检查 WEB-INF/web.xml 中 web-app 的版本声明,至少要改成 2.5 以上,推荐 3.1,这样 Tomcat 9 才按新版规则解析。

5.5 部署时发现 8080 端口被占用

现象:Tomcat 启动后立刻弹出端口被占用,或浏览器访问 localhost:8080 打开的是其他页面。

原因:本机已有其他程序占用 8080 端口,常见的是其他 Web 服务的默认端口冲突。

解决:找到 Tomcat 安装目录 conf/server.xml,修改 <Connector port="8080" 为其他空闲端口,例如 8090。改完后重启 Tomcat 再访问时用 http://localhost:8090/项目名/。如果修改后仍然启动失败,用命令行检查占用进程再杀掉,但注意线程级进程不要乱杀。

6. 进阶验证方法:用 SQL 自检库存一致性

拿到这份源码后,第一步建议先做一次全量对账。把库存表的数据和进货销售流水放一起算,能快速确认系统是否自洽。下面是三条可以直接用的核对 SQL。

-- 按商品对比流水数据和库存表差异 SELECT g.goods_id, g.goods_name, s.current_qty AS stock_qty, IFNULL(in_sum.total_in, 0) - IFNULL(out_sum.total_out, 0) AS calc_qty FROM base_goods g LEFT JOIN stock s ON g.goods_id = s.goods_id LEFT JOIN (SELECT goods_id, SUM(qty) AS total_in FROM bill_stock_in GROUP BY goods_id) in_sum ON g.goods_id = in_sum.goods_id LEFT JOIN (SELECT goods_id, SUM(qty) AS total_out FROM bill_stock_out GROUP BY goods_id) out_sum ON g.goods_id = out_sum.goods_id HAVING stock_qty != calc_qty;

如果这条查询返回多行,说明库存表与流水账对不上,要优先排查是不是事务回滚没做好。第二条检查重复单据:

-- 按订单号分组,找出重复生成的进货单 SELECT bill_no, COUNT(*) AS cnt FROM bill_stock_in GROUP BY bill_no HAVING cnt > 1;

第三条验证销售单里的价格没有异常,商品售价为负数这种数据大概率是表单校验缺失。把这三条 SQL 命名为 check_erp.sql 存档,每次改完代码或导入新数据后执行一遍,比手工点页面核对快得多。

老项目的通病是看页面觉得一切正常,但账是花的。我一般拿到任何进销存源码,第一件事不是打开页面,而是先执行对账 SQL,如果流水和库存对不上,后面看再多功能都是白搭。从那以后,我每次部署这类系统都会强制走一遍“导数据 → 对账 → 模拟进货 → 再对账”的流程,确认无误才交给使用者。这套系统的价值就在于它能完整演示进货、销售、库存三者的联动,把对账 SQL 吃透,你也就真正理解了进销存的业务核心。把它下载下来照着改一遍,比空看十篇教程都管用,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询