
简介这份资源是面向Java Web初学者与课程设计开发者的JSP进销存管理系统完整项目包针对商品种类繁多、业务量大、库存管理复杂等场景用计算机全程管理进货、出货与销售流程替代手工操作带来的高工作量与不确定因素帮助读者理解进销存业务从商品、供应商、客户到库存、销售各环节的完整实现思路。压缩包共183个文件约6.68MB以77个jsp页面为主体配合12个java源文件、12个class编译文件、6个jar依赖包、4个xml配置以及gif、jpg、png、bmp等界面素材另含txt说明、html页面、css样式、数据库mdf与ldf文件及docx、pdf文档覆盖源码、数据库与配套文档三类内容。目前已有245人学习下载适合作为毕业设计、课程设计或Java Web入门练手项目读者可据此梳理JSPJavaBean的分层结构、数据库连接与业务表设计并对照文档快速完成环境搭建与功能调试。1. 拿到「jsp进销存管理系统源码数据库文档.rar」先别急着解压它到底能帮你解决什么如果你手里正躺着一个名为「jsp进销存管理系统源码数据库文档.rar」的压缩包或者你正在为 Java Web 课程设计、毕业设计找一套能跑起来、能讲清楚、能改得动的进销存项目那这篇就是写给你的。进销存管理系统本质上是把「采购入库、销售出库、库存盘点、往来账款」这几件事用软件串起来而 JSP 版本的价值在于它足够传统、足够典型几乎把 Java Web 的请求响应、会话管理、JDBC 操作、页面渲染全流程都暴露在你面前。对新手来说它是理解 MVC 分层和数据库增删改查的活教材对熟手来说它是二次开发或给客户做轻量级内部工具的低成本起点。但我要先泼一盆冷水这类压缩包里的代码质量参差不齐直接双击运行大概率翻车真正值钱的是你拆解它的方法。2. 拆包先看结构JSP 进销存项目的目录里藏着哪些必改项2.1 从压缩包到可导入工程识别源码、数据库脚本和文档的边界拿到一个 .rar 包第一步不是解压完就点运行而是先做「资产盘点」。常见的 JSP 进销存项目结构大致长这样根目录下有一个src文件夹放 Java 源码一个WebContent或web文件夹放 JSP 页面、CSS、JS 和WEB-INF一个sql或db文件夹放数据库脚本外加一份 Word 或 PDF 格式的文档。你要先确认三件事数据库脚本是完整建表语句还是只有零散 insertJSP 页面里有没有硬编码的绝对路径WEB-INF/lib下的 jar 包是否齐全。很多压缩包解压后中文文件名乱码是因为打包时用了 GBK 而你的系统是 UTF-8这时候不要急着改代码先用支持编码切换的解压工具重新解压。# 查看压缩包内文件列表不解压先摸清结构 unrar l jsp进销存管理系统源码数据库文档.rar # 如果 unrar 不可用用 7z 列出内容 7z l jsp进销存管理系统源码数据库文档.rar上面两条命令的作用是「只读侦察」。unrar l和7z l都会输出文件名、大小、修改时间你能据此判断源码目录名、数据库脚本文件名以及文档格式。参数上l表示 list不会产生任何解压文件。如果列表里出现Thumbs.db、.DS_Store或__MACOSX说明打包者用的是 Windows 或 Mac 的普通压缩这些是系统垃圾文件解压后可以直接删。2.2 数据库脚本先跑通建库、建表、插初始数据的顺序不能乱进销存系统的数据库通常包含商品表、供应商表、客户表、采购单表、销售单表、库存表、用户表这几张核心表。压缩包里的.sql文件往往把建库、建表、插入初始数据混在一起直接全选执行容易报错。稳妥的做法是分段执行先建库并指定字符集再建表最后插数据。-- 第一步建库字符集用 utf8mb4避免中文乱码 CREATE DATABASE IF NOT EXISTS jsp_inventory DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE jsp_inventory; -- 第二步建商品表注意库存字段用 int 还是 decimal 取决于业务 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称, spec VARCHAR(50) DEFAULT NULL COMMENT 规格, unit VARCHAR(20) DEFAULT NULL COMMENT 单位, purchase_price DECIMAL(10,2) DEFAULT 0.00 COMMENT 采购价, sale_price DECIMAL(10,2) DEFAULT 0.00 COMMENT 销售价, stock INT DEFAULT 0 COMMENT 当前库存, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 第三步插入一条测试数据验证表结构可用 INSERT INTO product (name, spec, unit, purchase_price, sale_price, stock) VALUES (测试商品, 500ml, 瓶, 2.50, 4.00, 100);这段 SQL 的逻辑说明utf8mb4是必须的因为进销存里商品名、客户名经常出现生僻字或 emoji 类字符utf8三字节版本会丢数据。DECIMAL(10,2)用于金额绝不能用FLOAT否则对账时会出现 0.01 的误差累积。stock用INT是因为大多数中小进销存按整数件管理如果你的业务涉及称重商品要改成DECIMAL(10,3)。执行完这三步后用SELECT * FROM product;确认能查到数据再继续导入其他表。2.3 用 IDEA 新建 JSP 项目并导入源码别被「没有 web.xml」吓到现在 IDEA 新建 JSP 项目已经比 Eclipse 时代方便很多但导入老项目时最常见的坑是项目没有web.xml或者web.xml的 servlet 版本太老。Servlet 3.0 以后可以用注解替代web.xml配置但老项目往往还是 XML 驱动。导入步骤是File → New → Project from Existing Sources选中解压后的源码根目录IDEA 会识别出它是 Eclipse 或 MyEclipse 项目你需要在 Project Structure 里手动指定WebContent为 Web 资源目录src为源码目录然后把WEB-INF/lib下的 jar 全部加入 Libraries。!-- 如果项目缺少 web.xml在 WebContent/WEB-INF 下补一个最小可用的 -- ?xml version1.0 encodingUTF-8? web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee xmlns:xsihttp://www.w3.org/2001/XMLSchema-instance xsi:schemaLocationhttp://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd version4.0 welcome-file-list welcome-filelogin.jsp/welcome-file /welcome-file-list /web-app这个web.xml只做一件事指定默认首页为login.jsp。version4.0对应 Servlet 4.0Tomcat 9 及以上支持。如果你的 Tomcat 是 8.5把 version 改成 3.1xsd 地址同步改。注意welcome-file里的文件名必须和实际 JSP 文件名大小写完全一致Linux 服务器上大小写敏感Windows 上不敏感这是本地能跑、服务器 404 的经典原因。3. 让登录和库存查询先跑起来JSP 进销存最小闭环的四个关键动作3.1 数据库连接池配置别再用 DriverManager 裸连了压缩包里十有八九是用DriverManager.getConnection()在每个 Servlet 里现连现关这种写法在并发稍微高一点的时候就会把数据库连接数打满。正确的做法是配一个连接池。老项目常用 DBCP 或 C3P0新一点用 Druid 或 HikariCP。你不需要大改业务代码只需要把获取连接的方式从DriverManager换成DataSource.getConnection()。// db.properties 放在 src 根目录下 // drivercom.mysql.cj.jdbc.Driver // urljdbc:mysql://localhost:3306/jsp_inventory?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai // usernameroot // password你的密码 import com.alibaba.druid.pool.DruidDataSource; import java.io.InputStream; import java.util.Properties; public class DBUtil { private static DruidDataSource dataSource new DruidDataSource(); static { try (InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties)) { Properties props new Properties(); props.load(in); dataSource.setDriverClassName(props.getProperty(driver)); dataSource.setUrl(props.getProperty(url)); dataSource.setUsername(props.getProperty(username)); dataSource.setPassword(props.getProperty(password)); // 连接池关键参数初始 5最大 20空闲检测 60 秒 dataSource.setInitialSize(5); dataSource.setMaxActive(20); dataSource.setMinIdle(5); dataSource.setMaxWait(3000); dataSource.setValidationQuery(SELECT 1); dataSource.setTestWhileIdle(true); dataSource.setTimeBetweenEvictionRunsMillis(60000); } catch (Exception e) { throw new ExceptionInInitializerError(数据库连接池初始化失败: e.getMessage()); } } public static DruidDataSource getDataSource() { return dataSource; } }逻辑说明静态代码块保证连接池只初始化一次db.properties从 classpath 读取避免硬编码。参数上initialSize5是启动时预建连接数maxActive20是并发上限maxWait3000表示拿不到连接最多等 3 秒就抛异常防止请求线程无限阻塞。validationQuery用SELECT 1是各数据库通用的心跳检测语句。testWhileIdle配合timeBetweenEvictionRunsMillis实现空闲连接保活避免 MySQL 8 小时自动断连后第一次请求报错。3.2 登录 Servlet 的会话管理session 存什么、不存什么进销存系统的登录逻辑看似简单但 session 里放什么直接决定后续权限控制好不好做。常见错误是把整个用户对象序列化进 session或者只存一个用户名。稳妥的做法是存用户 ID、用户名、角色三个字段角色用于后续菜单过滤。WebServlet(/login) public class LoginServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 实际项目里密码要加盐哈希这里为演示保留明文比对 String sql SELECT id, username, role FROM sys_user WHERE username? AND password?; try (Connection conn DBUtil.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { HttpSession session req.getSession(); session.setAttribute(userId, rs.getInt(id)); session.setAttribute(userName, rs.getString(username)); session.setAttribute(role, rs.getString(role)); session.setMaxInactiveInterval(30 * 60); // 30 分钟无操作失效 resp.sendRedirect(req.getContextPath() /main.jsp); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } } catch (SQLException e) { throw new ServletException(登录查询失败, e); } } }这段代码的关键点用PreparedStatement而不是Statement拼接杜绝 SQL 注入。session.setMaxInactiveInterval(30 * 60)设置 30 分钟超时进销存系统通常内网使用这个值可以适当放宽到 60 分钟。登录失败用forward而不是sendRedirect是为了保留req.setAttribute(msg)的错误提示。注意sendRedirect后要return否则后续代码可能继续执行导致重复响应。3.3 库存查询分页JSP 页面渲染和 SQL LIMIT 的配合库存列表是进销存里访问最频繁的页面数据量上千条后必须分页。分页的核心是两条 SQL一条查总数一条查当前页数据。JSP 只负责渲染不要在 JSP 里写 SQL。// 分页查询商品库存 public ListProduct queryByPage(int pageNo, int pageSize) throws SQLException { int offset (pageNo - 1) * pageSize; String sql SELECT id, name, spec, unit, stock, sale_price FROM product ORDER BY id DESC LIMIT ? OFFSET ?; ListProduct list new ArrayList(); try (Connection conn DBUtil.getDataSource().getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, pageSize); ps.setInt(2, offset); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setId(rs.getInt(id)); p.setName(rs.getString(name)); p.setSpec(rs.getString(spec)); p.setUnit(rs.getString(unit)); p.setStock(rs.getInt(stock)); p.setSalePrice(rs.getBigDecimal(sale_price)); list.add(p); } } } return list; }参数说明pageNo从 1 开始pageSize建议 10 到 20太大页面加载慢太小翻页频繁。LIMIT ? OFFSET ?是 MySQL 的写法PostgreSQL 也支持Oracle 需要改用ROWNUM嵌套。ORDER BY id DESC让最新入库的商品排在前面符合操作习惯。JSP 页面里用 JSTL 的c:forEach遍历list不要用脚本片段% %否则后期维护会非常痛苦。3.4 采购入库和销售出库的事务边界库存增减必须原子进销存最核心的业务逻辑是采购入库时增加库存并写采购记录销售出库时减少库存并写销售记录。这两步必须在同一个事务里否则会出现「采购单写了但库存没加」或者「销售单写了但库存没减」的脏数据。public boolean purchaseIn(int productId, int qty, BigDecimal price) throws SQLException { String updateStock UPDATE product SET stock stock ? WHERE id ?; String insertRecord INSERT INTO purchase_record (product_id, qty, price, create_time) VALUES (?, ?, ?, NOW()); Connection conn null; try { conn DBUtil.getDataSource().getConnection(); conn.setAutoCommit(false); // 开启事务 try (PreparedStatement ps1 conn.prepareStatement(updateStock); PreparedStatement ps2 conn.prepareStatement(insertRecord)) { ps1.setInt(1, qty); ps1.setInt(2, productId); if (ps1.executeUpdate() ! 1) { conn.rollback(); return false; } ps2.setInt(1, productId); ps2.setInt(2, qty); ps2.setBigDecimal(3, price); ps2.executeUpdate(); conn.commit(); return true; } } catch (SQLException e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } } }逻辑说明setAutoCommit(false)开启事务两条 SQL 都成功才commit任何一步失败就rollback。ps1.executeUpdate() ! 1的判断是为了确认商品 ID 存在如果更新行数为 0 说明商品不存在直接回滚。finally里恢复自动提交并关闭连接这是连接池环境下必须做的否则连接归还后事务状态会污染下一个使用者。注意NOW()是 MySQL 函数其他数据库用CURRENT_TIMESTAMP。4. 避坑与排查JSP 进销存项目从本地到服务器最容易翻车的五个地方4.1 中文乱码页面、请求、数据库三处编码要统一现象登录页输入中文商品名保存后数据库里显示问号或者 JSP 页面显示乱码。原因JSP 文件本身的编码、pageEncoding声明、请求体编码、数据库连接编码、表字符集五处只要有一处不一致就会乱。解决JSP 文件头写% page contentTypetext/html;charsetUTF-8 pageEncodingUTF-8 %在web.xml或过滤器里设置request.setCharacterEncoding(UTF-8)JDBC URL 加characterEncodingutf8mb4建库建表用utf8mb4。五处对齐后乱码基本消失。4.2 ClassNotFoundException: com.mysql.jdbc.Driver现象启动 Tomcat 后访问登录页后台报找不到 MySQL 驱动。原因WEB-INF/lib下没有放mysql-connector-java的 jar 包或者放了但版本和代码里的驱动类名不匹配。MySQL 5.x 用com.mysql.jdbc.DriverMySQL 8.x 用com.mysql.cj.jdbc.Driver。解决确认 jar 包在WEB-INF/lib下而不是只在 IDEA 的 Libraries 里因为 IDEA 的 Libraries 只在 IDE 内生效打包成 war 后不会带进去。同时检查驱动类名和数据库版本是否对应。4.3 连接池报 Too many connections现象系统用一段时间后所有数据库操作都超时MySQL 端SHOW PROCESSLIST看到大量 Sleep 连接。原因代码里拿了连接没关或者连接池maxActive设得太大而 MySQL 的max_connections太小。解决用 try-with-resources 确保Connection、PreparedStatement、ResultSet都在作用域结束时关闭连接池maxActive设为 MySQLmax_connections的 60% 左右开启removeAbandoned并设置removeAbandonedTimeout让池自动回收泄漏连接。4.4 JSP 页面修改后不生效现象改了 JSP 文件刷新浏览器还是旧内容。原因Tomcat 默认会编译 JSP 成 Servlet有时候工作目录缓存没清。解决在 Tomcat 的conf/server.xml里把Host标签的autoDeploy和reloadable设为 true或者直接删除work/Catalina目录下的缓存。IDEA 里则检查 On Update Action 是否设为 Update classes and resources。生产环境不建议开reloadable会影响性能。4.5 打包 war 后部署到 Linux 服务器 404现象本地 Windows 跑得好好的传到 Linux 服务器上 Tomcat 后访问 404。原因Windows 文件系统大小写不敏感Linux 敏感。JSP 里login.jsp写成了Login.jsp或者web.xml里welcome-file大小写和实际文件不一致。解决用find . -name *.jsp列出所有 JSP 文件逐一核对引用处的大小写。另外检查 war 包解压后的目录名是否和contextPath一致。5. 把进销存改成你自己的三个可落地的二次开发切入点5.1 给库存表加乐观锁防止并发超卖进销存系统在多个人同时出库同一商品时会出现库存扣成负数的情况。最轻量的改法是给product表加一个version字段每次更新时带上版本号比对。ALTER TABLE product ADD COLUMN version INT DEFAULT 0; -- 出库时用版本号做乐观锁 UPDATE product SET stock stock - ?, version version 1 WHERE id ? AND stock ? AND version ?;执行后检查executeUpdate()返回值如果为 0 说明库存不足或版本冲突提示用户重试。这个方案不用改表结构太多也不用引入分布式锁适合中小规模进销存。参数上stock ?是防止扣成负数version ?是防止并发覆盖。每次读取商品时把version一起查出来更新时传回去。5.2 用过滤器做统一权限校验别在每个 JSP 里写 if老项目常见做法是在每个 JSP 顶部写% if(session.getAttribute(user)null) response.sendRedirect(login.jsp); %几十个页面就要写几十遍。改成 Filter 后只写一次。WebFilter(/*) public class AuthFilter implements Filter { private static final ListString WHITE_LIST Arrays.asList( /login.jsp, /login, /css/, /js/, /images/); public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; String uri request.getRequestURI(); String path uri.substring(request.getContextPath().length()); for (String white : WHITE_LIST) { if (path.startsWith(white)) { chain.doFilter(req, resp); return; } } if (request.getSession().getAttribute(userId) null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } chain.doFilter(req, resp); } }白名单里的路径直接放行其余请求检查 session 里有没有userId。注意path.startsWith(white)对/css/这类目录前缀有效对/login这种精确路径也有效但不要把/login写成/login.jsp之外的形式否则登录请求本身会被拦截。这个过滤器注册在/*上静态资源也在白名单里避免登录页样式丢失。5.3 把 JSP 里的 Java 代码抽到 Servlet 或 Tag 文件JSP 里混大量% %脚本是这类项目的通病后期改一个字段要翻好几个文件。渐进式重构的做法是先把数据库查询逻辑抽到 DAO 类再把 JSP 里的循环改成 JSTL 标签最后把公共头部、尾部抽成.tag文件或% include %。不需要一次改完每次改一个模块保证系统始终可运行。我自己的习惯是新加的功能一律不写脚本片段老功能改到哪个模块就顺手把那个模块的脚本清掉半年下来整个项目就干净了。最后说一个我踩过的坑曾经为了省事直接把压缩包里的WEB-INF/lib下所有 jar 复制到 Tomcat 的lib目录结果两个版本的servlet-api.jar冲突Tomcat 启动直接报ClassCastException。后来我养成习惯项目依赖只放WEB-INF/libTomcat 的lib只保留容器自身需要的包。这个习惯帮我省下了至少三次通宵排查的时间。希望帮到你。本文还有配套的精品资源点击获取