简介这份资源是一套面向高校计算机相关专业毕业设计的完整项目资料主题为基于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 父分类ID0为顶级, sort_order INT DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 商品主表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 批次库存表核心表 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;注意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项目的正确姿势新版IDEA2021.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 contentTypetext/html;charsetUTF-8 languagejava % 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 /htmlcontentTypetext/html;charsetUTF-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.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/cosmetic_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.passwordyour_password注意serverTimezoneAsia/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 typedate控件传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; // 还需出库的数量 Listint[] 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, sizeoffset (page - 1) * size。count查询单独写一条SELECT COUNT(*)条件与数据查询一致。这里注意如果筛选条件里有expireStatuscount查询也要带上同样的条件否则总页数会算错。效期状态的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没有配URIEncodingUTF-8。解决每个JSP文件头部统一写% page contentTypetext/html;charsetUTF-8 languagejava pageEncodingUTF-8 %。然后在Tomcat的conf/server.xml里找到Connector port8080 ...加上URIEncodingUTF-8。如果是IDEA内置Tomcat在Run Configuration的VM options里加-Dfile.encodingUTF-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是不是providedprovided的依赖不会被打进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 srchttps://cdn.jsdelivr.net/npm/echarts5/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包的类加载路径有差异早发现早解决别等到答辩前一天才发现部署有问题。希望帮到你。本文还有配套的精品资源点击获取