news 2026/9/29 4:42:56

化妆品仓储库存管理系统:JSP毕设从建表到WAR部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
化妆品仓储库存管理系统:JSP毕设从建表到WAR部署全流程

简介:这份资源是一套面向高校计算机相关专业毕业设计的完整项目资料,主题为基于JSP的化妆品仓储库存管理系统,适合正在准备毕设、需要参考B/S架构与Java Web开发流程的学生及初学者。项目采用MyEclipse开发平台,以Java为开发语言、MySQL为后台数据库,通过用户与系统管理员两种角色实现化妆品店各项信息的登记、修改与查询等库存管理功能,覆盖了从需求分析到功能落地的典型开发链路。压缩包共164个文件,约5.25MB,其中60个jsp页面文件承担前端交互与业务展示,7个java源文件与7个class文件对应核心逻辑,另有15个jar包、7个js脚本、3个css样式及若干xml、properties等配置与图片资源,结构较为完整。资源内含项目完整代码及配套报告,读者可据此理清分层设计思路、数据库表结构与角色权限控制方式,并对照报告理解系统实现过程,为毕设撰写与答辩提供可参考的范例。目前已有123人学习下载。

1. 化妆品仓储库存管理系统:从JSP毕设选题到能跑起来的WAR包

每年毕业季,计算机毕业设计选题里总有一批人盯着“基于JSP的XX管理系统”。化妆品仓储库存管理系统就是其中典型:业务场景具体、数据表结构清晰、功能模块好拆分,答辩时老师问“为什么选这个题”也能答得上来。但真正动手才发现,JSP这门老技术在新版IDEA里新建项目就卡住一批人,Tomcat版本对不上又卡一批,最后打包WAR丢到服务器上跑不起来再卡一批。这篇笔记不讲空泛的选题意义,只讲怎么从零把这个系统做出来、跑通、打包、部署,以及那些只有踩过才知道的坑。适合正在做JSP毕设、需要一套可复现路径的本科生,也适合想快速回顾传统JavaWeb开发流程的从业者。

2. 化妆品仓储库存管理系统到底要管什么:业务建模与数据表设计

2.1 化妆品仓储的特殊性决定了表结构不能照抄普通进销存

普通进销存系统的商品表往往只有名称、规格、单价、数量几个字段,但化妆品仓储有几个绕不开的业务约束。第一是批号管理,同一款口红不同生产批号的效期不同,出库时必须按先进先出原则扣减对应批号库存。第二是效期预警,化妆品保质期通常三年,但仓储环节要求剩余保质期低于六个月的批次自动进入预警列表。第三是品类属性,化妆品分护肤、彩妆、香水、洗护等大类,每类下面又有品牌、系列、色号/规格等多级属性,如果全部塞进一张商品表,查询和统计会非常难受。

我一般会把核心表拆成七张:商品分类表、品牌表、商品主表、商品SKU表、批次库存表、入库单表、出库单表。商品主表存公共属性(名称、品牌、分类、单位),SKU表存具体规格和条码,批次库存表存每个批号的当前数量和生产/过期日期。入库和出库单表各自再挂明细表,记录每次操作的SKU和数量。这样拆的好处是,查某个商品的总库存只需要对批次库存表按SKU分组求和,查某个批号的流向则直接关联出入库明细。

2.2 建表SQL与关键字段说明

下面这段SQL可以直接在MySQL 5.7或8.0里执行,字符集用utf8mb4以支持化妆品名称里的特殊符号和emoji。

-- 商品分类表 CREATE TABLE category ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT '分类名称,如护肤、彩妆', parent_id INT DEFAULT 0 COMMENT '父分类ID,0为顶级', sort_order INT DEFAULT 0 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 商品主表 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, category_id INT NOT NULL, brand_id INT NOT NULL, name VARCHAR(100) NOT NULL COMMENT '商品名称', unit VARCHAR(10) DEFAULT '件', shelf_life_months INT DEFAULT 36 COMMENT '保质期月数', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_category (category_id), INDEX idx_brand (brand_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 批次库存表,核心表 CREATE TABLE stock_batch ( id INT PRIMARY KEY AUTO_INCREMENT, sku_id INT NOT NULL, batch_no VARCHAR(50) NOT NULL COMMENT '生产批号', produce_date DATE NOT NULL, expire_date DATE NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT '当前库存数量', warehouse_id INT DEFAULT 1, UNIQUE KEY uk_sku_batch (sku_id, batch_no), INDEX idx_expire (expire_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

注意stock_batch表上的uk_sku_batch唯一索引,它保证同一个SKU的同一个批号只有一条库存记录,入库时用INSERT ... ON DUPLICATE KEY UPDATE quantity = quantity + ?就能实现累加,避免先查后插的并发问题。idx_expire索引则是给效期预警查询用的,后面会讲到怎么用。

2.3 效期预警的查询逻辑与索引命中

效期预警不是简单查expire_date < NOW(),而是要查“剩余保质期低于阈值”的批次。假设阈值是六个月,查询语句如下:

SELECT sb.batch_no, p.name, sb.quantity, sb.expire_date, DATEDIFF(sb.expire_date, CURDATE()) AS remain_days FROM stock_batch sb JOIN product_sku ps ON sb.sku_id = ps.id JOIN product p ON ps.product_id = p.id WHERE sb.quantity > 0 AND sb.expire_date <= DATE_ADD(CURDATE(), INTERVAL 6 MONTH) ORDER BY sb.expire_date ASC;

这里WHERE条件里对expire_date做了范围比较,能命中idx_expire索引。如果把DATEDIFF函数写在WHERE里,索引就会失效,这是很多人写SQL时容易翻车的地方。另外quantity > 0的条件不能省,否则会把已经出完的批次也列出来,预警列表会变得很长。

3. 用IDEA新建JSP项目并跑通第一个页面

3.1 IDEA新建JSP项目的正确姿势

新版IDEA(2021.3之后)的“New Project”里已经没有直接的“Java Enterprise”选项了,很多人在这里就卡住。正确路径是:先选“Java”建一个普通Java项目,然后右键项目 → Add Framework Support → 勾选“Web Application”。这一步做完,项目里会出现web目录和WEB-INF/web.xml。

接下来配置Tomcat。Run → Edit Configurations → 左上角+ → Tomcat Server → Local。在“Deployment”标签页点+ → Artifact → 选“xxx:war exploded”。Application context建议设成/cosmetic,这样访问路径就是http://localhost:8080/cosmetic/。这里有个细节:如果Artifact里没有war exploded选项,说明上一步Add Framework Support没做成功,回去检查。

3.2 第一个JSP页面与中文乱码处理

在web目录下新建index.jsp,内容如下:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>化妆品仓储库存管理系统</title> </head> <body> <h2>系统首页</h2> <p>当前时间:<%= new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new java.util.Date()) %></p> </body> </html>

contentType="text/html;charset=UTF-8"这行必须写,否则中文会显示成问号。另外在web.xml里建议加上请求编码过滤器,或者在每个Servlet里手动request.setCharacterEncoding("UTF-8")。我一般会在web.xml里配一个CharacterEncodingFilter,一劳永逸。

启动Tomcat后访问http://localhost:8080/cosmetic/index.jsp,如果能看到中文标题和当前时间,说明JSP环境通了。如果报404,检查Application context是否配错;如果报500,看Tomcat日志里具体的异常堆栈。

3.3 连接MySQL的JDBC配置与连接池

在WEB-INF/lib下放入mysql-connector-java-8.0.28.jar(版本号按你实际下载的来),然后在src下新建db.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cosmetic_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=your_password

注意serverTimezone=Asia/Shanghai这个参数,MySQL 8.0的驱动如果不加会报时区错误。useUnicode和characterEncoding保证中文存取不乱码。

连接池用Druid或者HikariCP都行,毕设项目用Druid更常见,因为监控页面方便调试。在web.xml里配Druid的Servlet和Filter,然后写一个DBUtil类读取db.properties并初始化数据源。这里不展开完整代码,核心是DruidDataSource的setUrl、setUsername、setPassword、setInitialSize(5)、setMaxActive(20)这几个参数。初始连接数5对于毕设演示足够,最大20防止并发测试时连接耗尽。

4. 入库、出库、库存查询三个核心模块的实现

4.1 入库操作:批号累加与事务边界

入库的Servlet核心逻辑如下:

protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); int skuId = Integer.parseInt(request.getParameter("skuId")); String batchNo = request.getParameter("batchNo"); int quantity = Integer.parseInt(request.getParameter("quantity")); String produceDate = request.getParameter("produceDate"); String expireDate = request.getParameter("expireDate"); Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入入库单主表 String insertOrder = "INSERT INTO inbound_order (order_no, create_time, operator) VALUES (?, NOW(), ?)"; PreparedStatement ps1 = conn.prepareStatement(insertOrder, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, "IN" + System.currentTimeMillis()); ps1.setString(2, "admin"); ps1.executeUpdate(); ResultSet rs = ps1.getGeneratedKeys(); rs.next(); int orderId = rs.getInt(1); // 2. 插入入库明细 String insertDetail = "INSERT INTO inbound_detail (order_id, sku_id, batch_no, quantity) VALUES (?,?,?,?)"; PreparedStatement ps2 = conn.prepareStatement(insertDetail); ps2.setInt(1, orderId); ps2.setInt(2, skuId); ps2.setString(3, batchNo); ps2.setInt(4, quantity); ps2.executeUpdate(); // 3. 批次库存累加,利用唯一索引 String upsertStock = "INSERT INTO stock_batch (sku_id, batch_no, produce_date, expire_date, quantity) " + "VALUES (?,?,?,?,?) ON DUPLICATE KEY UPDATE quantity = quantity + ?"; PreparedStatement ps3 = conn.prepareStatement(upsertStock); ps3.setInt(1, skuId); ps3.setString(2, batchNo); ps3.setString(3, produceDate); ps3.setString(4, expireDate); ps3.setInt(5, quantity); ps3.setInt(6, quantity); ps3.executeUpdate(); conn.commit(); response.sendRedirect("inbound_list.jsp"); } catch (Exception e) { if (conn != null) try { conn.rollback(); } catch (SQLException ex) {} e.printStackTrace(); response.sendError(500, "入库失败:" + e.getMessage()); } finally { if (conn != null) try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ex) {} } }

这段代码的关键点有三个。第一,conn.setAutoCommit(false)开启事务,保证入库单、明细、库存三张表的写入要么全成功要么全回滚。第二,ON DUPLICATE KEY UPDATE依赖stock_batch表上的唯一索引,如果索引没建对,这里会插入重复记录而不是累加。第三,Statement.RETURN_GENERATED_KEYS用来拿自增主键,MySQL驱动必须支持这个特性,8.0版本没问题。

参数说明:skuId从前端下拉框传过来,batchNo是手工输入的批号,quantity是入库数量,produceDate和expireDate用<input type="date">控件传yyyy-MM-dd格式字符串,JDBC会自动转成DATE类型。

4.2 出库操作:先进先出扣减与库存不足判断

出库比入库复杂,因为要按批号先进先出扣减。核心逻辑是:先查该SKU所有quantity > 0的批次,按expire_date升序排列,然后依次扣减直到满足出库数量。

// 查询可用批次,按效期升序 String queryBatch = "SELECT id, batch_no, quantity FROM stock_batch " + "WHERE sku_id = ? AND quantity > 0 ORDER BY expire_date ASC"; PreparedStatement ps = conn.prepareStatement(queryBatch); ps.setInt(1, skuId); ResultSet rs = ps.executeQuery(); int remaining = outQuantity; // 还需出库的数量 List<int[]> deductList = new ArrayList<>(); // 记录 [batchId, deductQty] while (rs.next() && remaining > 0) { int batchId = rs.getInt("id"); int batchQty = rs.getInt("quantity"); int deduct = Math.min(batchQty, remaining); deductList.add(new int[]{batchId, deduct}); remaining -= deduct; } if (remaining > 0) { throw new RuntimeException("库存不足,缺少 " + remaining + " 件"); } // 执行扣减 String updateStock = "UPDATE stock_batch SET quantity = quantity - ? WHERE id = ?"; PreparedStatement psUpdate = conn.prepareStatement(updateStock); for (int[] item : deductList) { psUpdate.setInt(1, item[1]); psUpdate.setInt(2, item[0]); psUpdate.addBatch(); } psUpdate.executeBatch();

这里有个容易忽略的点:remaining > 0的判断必须在扣减之前做,否则会出现部分扣减后才发现库存不足,事务回滚虽然能恢复数据,但用户体验很差。另外ORDER BY expire_date ASC保证了先进先出,如果按produce_date排序,遇到生产日期早但效期长的批次会先出,不符合化妆品仓储的实际要求。

4.3 库存查询页面的分页与多条件筛选

库存查询页面需要支持按商品名称模糊查、按分类筛选、按效期状态筛选(正常/临期/过期),还要分页。JSP页面用JSTL的<c:forEach>渲染表格,后端Servlet接收page、keyword、categoryId、expireStatus四个参数。

分页SQL用LIMIT offset, size,offset = (page - 1) * size。count查询单独写一条SELECT COUNT(*),条件与数据查询一致。这里注意:如果筛选条件里有expireStatus,count查询也要带上同样的条件,否则总页数会算错。

效期状态的SQL条件如下:

-- 正常:剩余天数 > 180 AND DATEDIFF(sb.expire_date, CURDATE()) > 180 -- 临期:0 <= 剩余天数 <= 180 AND DATEDIFF(sb.expire_date, CURDATE()) BETWEEN 0 AND 180 -- 过期:剩余天数 < 0 AND DATEDIFF(sb.expire_date, CURDATE()) < 0

注意这里DATEDIFF写在WHERE里会导致索引失效,但库存查询页面数据量通常不大(毕设演示级别几千条),全表扫描也能接受。如果数据量上万,建议把效期状态做成冗余字段,入库时计算好存进去。

5. 避坑与排查:JSP毕设最容易翻车的五个地方

5.1 现象:Tomcat启动报“ClassNotFoundException: com.mysql.cj.jdbc.Driver”

原因:MySQL驱动jar包没有放到WEB-INF/lib下,或者放到了项目根目录但没加入Artifact。IDEA的Artifact配置里,WEB-INF/lib下的jar才会被打包到war里。

解决:把mysql-connector-java-8.0.28.jar复制到web/WEB-INF/lib目录,然后在Project Structure → Artifacts → 选中war exploded → 右侧Available Elements里确认jar在WEB-INF/lib下。如果不在,双击把它加到左边。

5.2 现象:JSP页面中文显示为乱码,但数据库里存的中文是正常的

原因:JSP页面的pageEncoding和contentType不一致,或者Tomcat的server.xml里Connector没有配URIEncoding="UTF-8"。

解决:每个JSP文件头部统一写<%@ page contentType="text/html;charset=UTF-8" language="java" pageEncoding="UTF-8" %>。然后在Tomcat的conf/server.xml里找到<Connector port="8080" ...>,加上URIEncoding="UTF-8"。如果是IDEA内置Tomcat,在Run Configuration的VM options里加-Dfile.encoding=UTF-8。

5.3 现象:入库时同一批号插入了两条记录,库存数量没有累加

原因:stock_batch表的唯一索引uk_sku_batch没建,或者建在了错误的列上。ON DUPLICATE KEY UPDATE依赖唯一索引或主键冲突来触发更新,没有唯一索引就会直接插入新行。

解决:执行SHOW INDEX FROM stock_batch;确认uk_sku_batch存在且列顺序是(sku_id, batch_no)。如果不存在,用ALTER TABLE stock_batch ADD UNIQUE KEY uk_sku_batch (sku_id, batch_no);补上。注意如果表里已有重复数据,加索引会失败,先清理重复行。

5.4 现象:出库时库存扣成了负数

原因:并发情况下两个请求同时查到同一批次有库存,都执行了扣减,导致超卖。毕设演示通常单用户不会遇到,但答辩时老师可能问。

解决:在UPDATE stock_batch SET quantity = quantity - ? WHERE id = ? AND quantity >= ?的WHERE里加上quantity >= ?条件,然后检查executeUpdate()的返回值,如果返回0说明扣减失败,抛异常回滚事务。这是乐观锁的思路,不需要额外加锁。

5.5 现象:打包war后丢到独立Tomcat跑,报“NoClassDefFoundError”但IDEA里正常

原因:IDEA里跑的是war exploded,依赖jar直接从项目目录加载;打包成war后,jar必须在WEB-INF/lib下才会被Tomcat的类加载器加载。如果依赖是通过Maven引入的,检查pom.xml里scope是不是provided,provided的依赖不会被打进war。

解决:把scope改成compile或者删掉scope标签。然后mvn clean package重新打包,用jar tf target/xxx.war | grep lib确认jar在war包里。部署时把war丢到Tomcat的webapps目录,启动后访问http://服务器IP:8080/war包名/。

6. 从能跑到能答辩:三个让老师眼前一亮的进阶技巧

第一个技巧是给库存查询页面加一个ECharts饼图,展示各分类的库存占比。数据从后端SELECT c.name, SUM(sb.quantity) FROM stock_batch sb JOIN product_sku ps ON sb.sku_id = ps.id JOIN product p ON ps.product_id = p.id JOIN category c ON p.category_id = c.id WHERE sb.quantity > 0 GROUP BY c.id查出来,转成JSON传给前端。ECharts的引入只需要在JSP里加一个<script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script>,然后初始化一个饼图。这个改动工作量不到半小时,但答辩演示时视觉效果提升明显。

第二个技巧是给效期预警加一个定时任务。用ServletContextListener在项目启动时注册一个ScheduledExecutorService,每天凌晨跑一次效期扫描,把临期和过期的批次数量写入一张alert_log表。答辩时可以说“系统具备自动预警能力”,比手动点查询按钮显得完整。代码核心是contextInitialized方法里Executors.newSingleThreadScheduledExecutor().scheduleAtFixedRate(task, 0, 24, TimeUnit.HOURS)。

第三个技巧是准备一份数据初始化脚本。答辩前老师可能会让你现场演示,如果数据库是空的就很尴尬。写一个init_data.sql,插入几十条商品、SKU和批次数据,覆盖正常、临期、过期三种状态。演示时先跑一遍脚本,然后依次展示入库、出库、预警、图表四个功能,流程顺畅。

我自己的习惯是,每次做完一个模块就立刻打包war丢到本地独立Tomcat跑一遍,而不是只在IDEA里点运行。因为IDEA的war exploded和真实war包的类加载路径有差异,早发现早解决,别等到答辩前一天才发现部署有问题。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/29 4:41:27

网神SecGate3600防火墙快速上手:从Console连接到策略放行

简介&#xff1a;网神SecGate3600防火墙快速指南面向网络安全运维人员、系统集成工程师及刚接触该型号设备的技术人员&#xff0c;用于解决设备开箱上架、初始化配置与日常管理入门问题。资源包共1个PDF文件&#xff0c;大小约1.49MB&#xff0c;内容为官方V8.1版快速指南&…

作者头像 李华
网站建设 2026/9/29 4:40:57

MTK平台充电调试全解析:从硬件通路到快充协议

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 4:40:25

Flask+OpenCV+YOLO实现低延迟RTSP视频流实时检测

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 4:40:22

操作系统存储器管理核心:地址翻译、虚拟内存与页面置换算法

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 4:39:07

汽车电子环境可靠性测试全解析:从温度循环到盐雾振动

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/29 4:38:17

云克隆助力顶刊:从“削峰填谷”到精准检测的科研征程

2026年7月30日&#xff0c;华东理工大学刘昌胜院士团队在《Nature Biomedical Engineering》&#xff08;IF23.6&#xff09;发表了一项突破性研究。 他们设计的一种名为26SCS的工程化硫酸化多糖&#xff0c;通过精准靶向RANK蛋白的K97位点&#xff0c;实现了“削峰填谷”式的骨…

作者头像 李华