
简介这是一份面向Java初学者与毕业设计学生的B/S架构仓储管理系统实战项目聚焦化妆品行业库存管理场景帮助学习者掌握JSPServletMySQL的Web开发全流程。资源包含164个文件主体为60个JSP页面实现前后端交互、30个Java类文件含ComServlet、AdminServlet、ComBean等核心业务逻辑及配套的CSS、JS、GIF/JPG资源辅以15个Jar包和7个Class文件支撑运行整体压缩包仅5.25MB轻量易部署。已有123人下载学习适合课程设计、毕设参考或Java Web技术栈入门实践。用户可直接导入MyEclipse运行获得完整可执行代码、数据库脚本、系统管理员与普通用户双角色权限控制逻辑以及配套设计报告涵盖需求分析、ER图、功能模块划分与测试说明具备清晰的MVC分层结构与典型Filter过滤器、DBO数据库连接封装等工程化实践细节。1. 为什么一个“基于JSP的化妆品仓储库存管理系统”毕业设计至今仍是企业级小系统开发的硬核练手项目你可能在答辩PPT里见过它蓝色背景、带购物车图标、左侧树形菜单展开“入库管理/出库管理/库存查询”右上角写着“管理员张三”。表面看是套模板但真跑起来——当某款玻尿酸精华液批次号重复入库、当保质期倒计时触发预警却没弹窗、当Excel导入2000条SKU后页面卡死3秒——你就掉进了真实业务逻辑的毛细血管里。这不是写个Hello World就能糊弄过去的项目它强制你把JDBC连接池怎么配、Servlet如何拦截非法参数、JSP EL表达式怎么防XSS、MySQL事务怎么锁住库存字段这些散装知识点焊成一条能扛住并发查改的流水线。适合大三下刚学完Java Web基础、手头只有Tomcat 8.5 MySQL 5.7 Eclipse的老实人也适合想用最小技术栈验证“业务闭环”是否成立的准工程师——毕竟化妆品行业对批次、效期、货位的强约束比图书借阅系统更能照出你代码里的裂缝。2. 搭建可运行环境从零配置TomcatMySQLJSP工程的最小可行路径2.1 下载与安装避开JDK版本陷阱的三步确认法化妆品仓储系统对时间精度敏感比如效期计算依赖java.time.LocalDate必须用JDK 8u202以上版本。我踩过最深的坑是Eclipse默认用JDK 11新建Dynamic Web Project结果JSP里% page importjava.time.* %直接报错——因为Tomcat 8.5官方只兼容JDK 8~10。确认步骤java -version输出必须含1.8.0_开头Tomcat官网下载页选Apache Tomcat 8.5.x (tar.gz or zip)别碰9.xServlet 4.0需JDK 8但部分JSP标签库不兼容MySQL选5.7.32避免8.0默认strict mode导致INSERT忽略0值而化妆品批次号常含前导零。提示解压Tomcat后务必修改conf/server.xml中Connector port8080为port8081——因为Windows 10后台常驻Skype占8080端口不改就启动失败还找不到原因。2.2 创建Web工程用Eclipse生成标准目录结构而非手动建文件夹新手常犯错误在WebContent下直接建index.jsp结果部署后404。正确路径是Eclipse → New → Dynamic Web Project → 命名cosmetic-warehouse在向导中勾选Generate web.xml deployment descriptor关键否则Filter无法注册完成后自动创建src/放Java类、WebContent/放JSP/HTML/CSS、WebContent/WEB-INF/web.xml配置入口。此时WebContent/index.jsp才是合法首页。若想快速验证往里面写% page contentTypetext/html;charsetUTF-8 % html headtitle化妆品仓储系统/title/head body h2系统已启动当前时间% new java.util.Date() %/h2 /body /html右键项目 → Run As → Run on Server → 选择Tomcat 8.5 → 浏览器打开http://localhost:8081/cosmetic-warehouse/看到时间戳即成功。2.3 数据库建模按化妆品行业特性设计的5张核心表化妆品仓储的核心矛盾是同品名不同批次、同批次不同效期、同效期不同货位。我放弃ER图直接给出MySQL建表语句含注释说明业务逻辑-- 商品主表记录SKU基础信息注意barcode唯一且非空扫码入库必需 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL COMMENT 商品名称如修丽可CE精华, barcode VARCHAR(50) UNIQUE NOT NULL COMMENT 条形码用于扫码识别, category VARCHAR(30) NOT NULL COMMENT 分类如精华/面膜/防晒, unit VARCHAR(10) DEFAULT 瓶 COMMENT 计量单位 ); -- 批次表同一SKU可有多个批次每批次独立效期和库存 CREATE TABLE batch ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, batch_no VARCHAR(50) NOT NULL COMMENT 批次号如202310A001, expire_date DATE NOT NULL COMMENT 到期日用于效期预警, stock_quantity INT DEFAULT 0 COMMENT 当前库存数量, FOREIGN KEY (product_id) REFERENCES product(id) ); -- 货位表仓库物理位置支持多级编码如A-01-03表示A区1排3列 CREATE TABLE location ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(20) UNIQUE NOT NULL COMMENT 货位编码如A0103, description VARCHAR(100) COMMENT 描述如A区冷藏柜第3层 ); -- 库存明细表关联批次与货位解决同一批次分存多货位问题 CREATE TABLE inventory ( id INT PRIMARY KEY AUTO_INCREMENT, batch_id INT NOT NULL, location_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0 COMMENT 该货位存放数量, last_update TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (batch_id) REFERENCES batch(id), FOREIGN KEY (location_id) REFERENCES location(id), UNIQUE KEY uk_batch_loc (batch_id, location_id) ); -- 操作日志表记录每次出入库动作审计必备 CREATE TABLE operation_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, operator VARCHAR(20) NOT NULL COMMENT 操作人姓名, operation_type ENUM(IN,OUT,ADJUST) NOT NULL COMMENT 入库/出库/调拨, batch_id INT NOT NULL, quantity INT NOT NULL, remark TEXT, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (batch_id) REFERENCES batch(id) );执行后在product表插入测试数据INSERT INTO product (name, barcode, category, unit) VALUES (雅诗敦烟酰胺精华, 6970123456789, 精华, 瓶);注意inventory表的UNIQUE KEY uk_batch_loc是防止同一批次被重复分配到同一货位的关键约束漏掉会导致库存统计翻倍。3. 实现库存核心业务用ServletJSP完成“扫码入库→效期校验→货位分配→库存更新”闭环3.1 入库流程的Servlet控制器用doPost接收表单并校验业务规则用户扫描条形码后前端提交/servlet/StockInServlet后端需完成四件事根据条形码查商品是否存在检查批次号是否重复同一SKU不允许相同批次号验证效期是否晚于当前日期杜绝过期品入库将新批次写入batch表并在inventory表中分配默认货位如A0001。关键代码StockInServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { // 1. 获取参数注意前端form中name属性必须匹配 String barcode request.getParameter(barcode).trim(); String batchNo request.getParameter(batchNo).trim(); String expireDateStr request.getParameter(expireDate); // 格式2025-12-31 // 2. 查询商品是否存在 ProductDAO productDAO new ProductDAO(); Product product productDAO.findByBarcode(barcode); if (product null) { request.setAttribute(error, 条形码不存在请检查商品是否已录入); request.getRequestDispatcher(/stockIn.jsp).forward(request, response); return; } // 3. 效期校验转为LocalDate比较 LocalDate expireDate LocalDate.parse(expireDateStr); if (expireDate.isBefore(LocalDate.now())) { request.setAttribute(error, 效期不能早于今天); request.getRequestDispatcher(/stockIn.jsp).forward(request, response); return; } // 4. 检查批次号是否重复同一商品下 BatchDAO batchDAO new BatchDAO(); if (batchDAO.existsByProductAndBatch(product.getId(), batchNo)) { request.setAttribute(error, 该商品下已存在相同批次号); request.getRequestDispatcher(/stockIn.jsp).forward(request, response); return; } // 5. 插入批次记录 Batch batch new Batch(); batch.setProductId(product.getId()); batch.setBatchNo(batchNo); batch.setExpireDate(expireDate); batch.setStockQuantity(0); // 初始库存为0后续通过inventory表累加 int batchId batchDAO.insert(batch); // 6. 分配默认货位实际项目应调用货位算法此处简化 LocationDAO locationDAO new LocationDAO(); Location defaultLoc locationDAO.findByCode(A0001); if (defaultLoc null) { // 若默认货位不存在则创建 defaultLoc new Location(A0001, 默认入库货位); locationDAO.insert(defaultLoc); } // 7. 写入库存明细quantity0等待后续扫码补录实际数量 Inventory inventory new Inventory(); inventory.setBatchId(batchId); inventory.setLocationId(defaultLoc.getId()); inventory.setQuantity(0); new InventoryDAO().insert(inventory); request.setAttribute(success, 入库登记成功请扫码补录数量。); request.getRequestDispatcher(/stockIn.jsp).forward(request, response); }参数说明request.getParameter(barcode)对应HTML中input namebarcode命名必须一致LocalDate.parse()要求expireDateStr格式严格为yyyy-MM-dd前端用input typedate可保证batchDAO.existsByProductAndBatch()需在DAO层实现SQLSELECT COUNT(*) FROM batch WHERE product_id? AND batch_no?。3.2 JSP页面交互用EL表达式动态渲染错误提示与成功消息stockIn.jsp不是静态页面而是用JSP标签动态响应后端传来的状态% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle扫码入库/title/head body h2化妆品扫码入库/h2 !-- 显示后端传来的错误或成功消息 -- c:if test${not empty error} div stylecolor:red;❌ ${error}/div /c:if c:if test${not empty success} div stylecolor:green;✅ ${success}/div /c:if form action/servlet/StockInServlet methodpost label条形码input typetext namebarcode required autofocus/labelbr label批次号input typetext namebatchNo required/labelbr label效期input typedate nameexpireDate required/labelbr button typesubmit登记批次/button /form !-- 补录数量的独立表单实际项目应合并此处为教学拆分 -- h3补录数量扫码后填写/h3 form action/servlet/UpdateInventoryServlet methodpost input typehidden namebatchId value${batchId} !-- 从上一步传递 -- label数量input typenumber namequantity min1 required/labelbr button typesubmit确认入库/button /form /body /html关键点c:if标签需引入JSTL库WEB-INF/lib/jstl-1.2.jar否则报错value${batchId}依赖上一步Servlet中request.setAttribute(batchId, batchId)但当前代码未设置——这是典型坑点需在StockInServlet成功后添加request.setAttribute(batchId, batchId);autofocus让光标默认落在条形码输入框提升扫码体验。3.3 库存更新的原子性保障用MySQL事务锁住批次记录补录数量时必须确保batch.stock_quantity和inventory.quantity同步更新否则出现“库存表显示100瓶明细表合计只有99瓶”的脏数据。UpdateInventoryServlet核心逻辑// 开启事务 Connection conn dataSource.getConnection(); conn.setAutoCommit(false); try { // 1. 锁定batch记录防止并发修改 PreparedStatement ps1 conn.prepareStatement( UPDATE batch SET stock_quantity stock_quantity ? WHERE id ?); ps1.setInt(1, quantity); // 补录数量 ps1.setInt(2, batchId); ps1.executeUpdate(); // 2. 更新inventory明细 PreparedStatement ps2 conn.prepareStatement( UPDATE inventory SET quantity quantity ?, last_update NOW() WHERE batch_id ?); ps2.setInt(1, quantity); ps2.setInt(2, batchId); ps2.executeUpdate(); conn.commit(); // 两步都成功才提交 } catch (SQLException e) { conn.rollback(); // 任一步失败则回滚 throw new RuntimeException(库存更新失败, e); } finally { conn.close(); }为什么不用JDBC默认自动提交因为ps1和ps2是两个独立SQL若中间网络断开ps1成功而ps2失败就会产生库存不一致。显式事务是唯一可靠方案。4. 避坑指南这5个血泪经验让答辩老师当场点头4.1 现象JSP页面中文乱码地址栏显示%E5%85%A5%E5%BA%93原因Tomcat 8.5默认用ISO-8859-1解码URL参数而浏览器用UTF-8编码中文。解决在conf/server.xml的Connector标签内加URIEncodingUTF-8Connector port8081 protocolHTTP/1.1 connectionTimeout20000 redirectPort8443 URIEncodingUTF-8 /同时JSP顶部加% page contentTypetext/html;charsetUTF-8 %缺一不可。4.2 现象MySQL插入含前导零的批次号如00123变成123原因VARCHAR字段虽声明为字符串但若建表时误用INT类型MySQL会自动截断前导零。解决检查batch.batch_no字段类型必须是VARCHAR执行DESCRIBE batch;确认。若已建错用ALTER TABLE batch MODIFY batch_no VARCHAR(50);修正。4.3 现象点击“库存查询”按钮后页面空白控制台无报错原因web.xml中Servlet映射路径与实际访问URL不匹配。例如Servlet类名为StockQueryServlet但web.xml写成servlet-mapping servlet-nameStockQueryServlet/servlet-name url-pattern/query/url-pattern !-- 前端form action/query -- /servlet-mapping而实际Servlet类名是StockQueryServlet但servlet-name写成了StockQuery少了个Servlet后缀。解决严格比对servlet-name在servlet和servlet-mapping中是否完全一致大小写敏感。4.4 现象效期预警功能失效所有商品都显示“已过期”原因MySQL中expire_date字段类型为DATETIME而非DATE导致CURDATE()比较时隐式转换出错。解决执行ALTER TABLE batch MODIFY expire_date DATE;并确认插入数据时用2025-12-31而非2025-12-31 00:00:00。4.5 现象Excel导入2000条数据后Tomcat内存溢出OutOfMemoryError原因用Apache POI的HSSFWorkbook.xls或XSSFWorkbook.xlsx一次性读取全部行占用堆内存过大。解决改用SXSSFWorkbook流式写入或OPCPackage.open()配合XSSFReader逐行解析。毕业设计阶段可先限制导入量// 在导入Servlet中加校验 if (rows.size() 500) { request.setAttribute(error, 单次导入不得超过500条请分批上传); request.getRequestDispatcher(/import.jsp).forward(request, response); return; }5. 效期预警与库存盘点用定时任务JSP报表实现真正的业务价值5.1 用ServletContextListener启动后台预警线程毕业设计常被质疑“只是增删改查”加一个每天凌晨2点扫描近效期商品的定时任务立刻体现工程思维。不用Quartz太重用ServletContextListener启动守护线程public class ExpireWarningListener implements ServletContextListener { private Timer timer; Override public void contextInitialized(ServletContextEvent sce) { timer new Timer(ExpireWarningTimer, true); // 每天凌晨2点执行 Calendar cal Calendar.getInstance(); cal.set(Calendar.HOUR_OF_DAY, 2); cal.set(Calendar.MINUTE, 0); cal.set(Calendar.SECOND, 0); Date firstTime cal.getTime(); if (firstTime.before(new Date())) { cal.add(Calendar.DATE, 1); firstTime cal.getTime(); } timer.scheduleAtFixedRate(new ExpireWarningTask(), firstTime, 24 * 60 * 60 * 1000); } Override public void contextDestroyed(ServletContextEvent sce) { if (timer ! null) timer.cancel(); } } // 任务类查询未来30天内到期的商品 class ExpireWarningTask extends TimerTask { Override public void run() { try { ListBatch expiringBatches new BatchDAO() .findExpiringBatches(30); // SQL: WHERE expire_date BETWEEN CURDATE() AND DATE_ADD(CURDATE(), INTERVAL 30 DAY) // 发送邮件或写入预警表毕业设计可简化为写log System.out.println(【效期预警】发现 expiringBatches.size() 个临近到期批次); for (Batch b : expiringBatches) { System.out.println( - b.getProductName() 批次 b.getBatchNo() 到期日 b.getExpireDate()); } } catch (Exception e) { e.printStackTrace(); } } }在web.xml中注册监听器listener listener-classcom.cosmetic.listener.ExpireWarningListener/listener-class /listener5.2 库存盘点报表用JSPJSTL生成可导出的HTML表格盘点不是简单查总数要对比系统库存与实物差异。inventory_report.jsp核心逻辑% page contentTypetext/html;charsetUTF-8 % % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt % html headtitle库存盘点报表/title/head body h2截至 fmt:formatDate value${now} patternyyyy-MM-dd HH:mm:ss//h2 table border1 tr th商品名称/th th批次号/th th效期/th th货位/th th系统库存/th th实物盘点/th th差异/th /tr c:forEach items${inventoryList} varinv tr td${inv.productName}/td td${inv.batchNo}/td tdfmt:formatDate value${inv.expireDate} patternyyyy-MM-dd//td td${inv.locationCode}/td td${inv.systemQty}/td tdinput typenumber namephysical_${inv.id} value${inv.physicalQty} min0/td td${inv.systemQty - inv.physicalQty}/td /tr /c:forEach /table form action/servlet/SubmitInventoryCountServlet methodpost c:forEach items${inventoryList} varinv input typehidden nameinvId value${inv.id} input typehidden namesystemQty value${inv.systemQty} /c:forEach button typesubmit提交盘点结果/button /form /body /html关键技巧fmt:formatDate确保日期格式统一避免toString()输出Wed Oct 18 00:00:00 CST 2023physical_${inv.id}命名方式让后端能用request.getParameterMap()批量获取所有盘点数差异列实时计算${inv.systemQty - inv.physicalQty}无需JavaScriptJSP原生支持。5.3 导出Excel的终极妥协方案用CSV替代POI答辩演示时需要“导出报表”但POI依赖复杂且易出错。我教学生用纯文本CSV// SubmitInventoryCountServlet中处理导出请求 response.setContentType(text/csv;charsetUTF-8); response.setHeader(Content-Disposition, attachment; filenameinventory_report_ new SimpleDateFormat(yyyyMMdd_HHmmss).format(new Date()) .csv); PrintWriter out response.getWriter(); out.println(商品名称,批次号,效期,货位,系统库存,实物盘点,差异); for (InventoryReportItem item : reportList) { out.print(item.getProductName() ,); out.print(item.getBatchNo() ,); out.print(new SimpleDateFormat(yyyy-MM-dd).format(item.getExpireDate()) ,); out.print(item.getLocationCode() ,); out.print(item.getSystemQty() ,); out.print(item.getPhysicalQty() ,); out.println(item.getSystemQty() - item.getPhysicalQty()); }为什么CSV够用Excel双击即可打开格式兼容性100%无jar包冲突Tomcat重启不报错中文用UTF-8编码加BOM头response.getOutputStream().write(new byte[]{(byte)0xEF, (byte)0xBB, (byte)0xBF});可防乱码。6. 让答辩老师眼前一亮的3个细节优化从“能跑”到“像生产系统”6.1 用CSS Grid重构左侧菜单告别Table布局的玄学对齐老式JSP常用table做导航菜单结果在Chrome/Firefox下错位。改用CSS Grid只需10行代码style .menu-grid { display: grid; grid-template-rows: repeat(5, 40px); gap: 5px; } .menu-item { background: #f0f8ff; padding: 8px; text-align: center; cursor: pointer; } .menu-item:hover { background: #4682b4; color: white; } /style div classmenu-grid div classmenu-item onclicklocation.hrefstockIn.jsp扫码入库/div div classmenu-item onclicklocation.hrefstockOut.jsp扫码出库/div div classmenu-item onclicklocation.hrefinventoryReport.jsp库存盘点/div div classmenu-item onclicklocation.hrefexpireWarning.jsp效期预警/div div classmenu-item onclicklocation.hrefadmin.jsp系统管理/div /div效果菜单高度固定、悬停变色、点击跳转比Bootstrap更轻量且所有现代浏览器兼容。6.2 为批次号添加二维码用ZXing生成图片并嵌入JSP化妆品仓储必须支持扫码但毕业设计常只做“输入条形码”。用ZXing库生成批次号二维码下载zxing-core-3.4.1.jar和zxing-javase-3.4.1.jar放入WEB-INF/lib在BatchDetailServlet中生成二维码String batchNo request.getParameter(batchNo); BitMatrix matrix new MultiFormatWriter().encode(batchNo, BarcodeFormat.QR_CODE, 200, 200); ByteArrayOutputStream bos new ByteArrayOutputStream(); MatrixToImageWriter.writeToStream(matrix, PNG, bos); byte[] imageBytes bos.toByteArray(); response.setContentType(image/png); response.getOutputStream().write(imageBytes);JSP中引用img srcservlet/BatchQrCodeServlet?batchNo${batch.batchNo}价值点评委看到“扫码入库→生成二维码→贴箱单”闭环立刻理解你懂业务场景。6.3 日志分级用Log4j2记录操作日志而非System.out.printlnSystem.out.println(用户张三入库了10瓶)在生产环境等于裸奔。替换为Log4j2WEB-INF/classes/log4j2.xml配置?xml version1.0 encodingUTF-8? Configuration statusWARN Appenders File nameFileAppender fileNamelogs/warehouse.log PatternLayout pattern%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n/ /File /Appenders Loggers Root levelinfo AppenderRef refFileAppender/ /Root /Loggers /ConfigurationServlet中调用private static final Logger logger LogManager.getLogger(StockInServlet.class); // 替换所有System.out.println为 logger.info(用户{}扫码入库商品{}批次{}数量{}, operator, productName, batchNo, quantity);为什么重要日志文件自动按天滚动加RollingFile配置避免磁盘爆满logger.info()可被ELK等工具采集体现工程规范意识答辩时打开logs/warehouse.log展示真实操作记录比PPT更有说服力。我带过12届毕业设计最常听到的反馈是“这个系统看起来简单但每个按钮背后都有业务逻辑在咬人。”化妆品仓储的批次、效期、货位三重约束逼你写出带事务、带校验、带日志的真实代码而不是拼凑几个CRUD。现在你手里有可运行的Tomcat工程、5张表的SQL、带事务的入库Servlet、效期预警线程、CSV导出方案——剩下的就是把stockOut.jsp复制粘贴改个名再把operation_log表连到报表里。别追求炫技把这根业务主线焊死答辩时指着日志文件说“这里记录了每一次库存变动”老师自然点头。希望帮到你。本文还有配套的精品资源点击获取