JavaWeb超市管理系统:真实业务闭环与工程实践指南 简介本资源是一套完整的JavaWeb超市管理系统毕业设计项目面向计算机专业本科生及Java初学者解决课程设计、毕设选题与实战能力提升需求。压缩包共68个文件含19个核心Java源码、37个编译后Class文件、4个MySQL建表与数据脚本涵盖用户、商品、进货、销售等关键业务表以及2个JAR依赖库、2个JPG系统界面图和Eclipse工程配置文件.project/.classpath/.prefs整体3.5MB结构清晰开箱即用。已有796人学习下载说明其在实践教学中具备较高参考价值。读者可直接导入EclipseTomcat环境运行完整掌握B/S架构下员工与管理员双角色权限体系深入理解JSPServletJDBC三层开发模式并通过真实业务模块如货架管理、销售统计、进货台账巩固数据库设计与CRUD操作能力。1. 这不是又一个“登录增删改查”的JavaWeb模板而是一套能跑通完整业务闭环的超市管理系统你可能已经见过太多标着“超市管理系统”的JavaWeb项目——点开后发现只有用户表和商品表连库存扣减逻辑都写在JSP里更别提进货、销售、货架、类型之间的关联约束。但这个源码包不同它用tb_stock.sql、tb_purchase.sql、tb_sell.sql三张核心表构建了真实业务链路进货单生成后自动更新库存销售单提交时校验库存余量并触发负数拦截货架信息与商品绑定后支持按区域快速盘点。它不是为演示MVC分层而分层而是把dao包里每个DAO类都对应到具体业务动作比如PurchaseDao.java里有insertWithStockUpdate()方法view下的JSP页面也按角色权限做了硬隔离——员工看不到系统管理模块管理员进不去销售单审核页。适合两类人一是正在赶毕设 deadline 的本科生它已通过EclipseTomcat 8.5MySQL 5.7实测可部署二是想补全JavaWeb工程化能力的转行者你能从中看到如何用utils包封装DBUtils连接池、如何用pojo类字段命名映射SQL列、甚至test目录下还留着JUnit 4写的DAO层单元测试桩。2. 从数据库建模到Servlet路由理解这套系统为什么能支撑真实业务流转2.1 数据库设计不是ER图堆砌而是围绕“货流”构建的三层关系模型该系统数据库脚本包含4个SQL文件但真正驱动业务的是tb_stock、tb_purchase、tb_sell三张表的联动设计。tb_stock表不仅存商品ID和当前库存量还包含shelf_id外键指向货架表这使得“某品牌矿泉水在A区3号货架剩余12箱”这类查询成为可能tb_purchase表中purchase_date和supplier_name字段被PurchaseServlet用于生成采购报表而tb_sell表的sell_time和employee_id则支撑销售业绩统计。关键约束在于tb_purchase插入时触发库存增加tb_sell插入时触发库存减少且sell_amount stock_amount校验在SellDao.updateStockAfterSell()方法中强制执行。提示不要直接运行所有SQL脚本。先执行tb_user.sql创建管理员账号默认账号admin/123456再依次执行tb_stock.sql→tb_purchase.sql→tb_sell.sql否则外键约束会报错。2.1.1 核心表字段语义与业务强绑定表名关键字段业务含义在代码中的使用位置tb_stockstock_amount,shelf_id,goods_id当前库存数量、所属货架、商品IDStockDao.getStockByGoodsId()用于销售前校验tb_purchasepurchase_amount,purchase_price,goods_id采购数量、单价、商品IDPurchaseService.calculateTotalCost()计算采购总金额tb_sellsell_amount,sell_price,employee_id销售数量、售价、操作员工IDSellServlet中调用SellService.checkStockAndSell()做原子性操作这种设计避免了常见毕设项目中“库存字段在商品表里硬编码”的反模式。例如当同一商品在多个货架存放时tb_stock的复合主键(goods_id, shelf_id)天然支持分区库存管理而tb_goods表只保留基础属性名称、类型、单位符合第三范式。2.2 Servlet层不是HTTP请求转发器而是业务编排中心系统未使用Spring MVC而是用原生Servlet实现控制流。以销售功能为例SellServlet.java接收POST请求后并非简单调用DAO插入记录而是执行以下原子操作链// SellServlet.java 片段 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String goodsId request.getParameter(goodsId); int sellAmount Integer.parseInt(request.getParameter(sellAmount)); // 1. 查询当前库存 Stock stock stockDao.getStockByGoodsId(goodsId); if (stock null || stock.getStockAmount() sellAmount) { request.setAttribute(error, 库存不足无法销售); request.getRequestDispatcher(/view/sell/error.jsp).forward(request, response); return; } // 2. 扣减库存并插入销售记录事务保障 try { connection.setAutoCommit(false); stockDao.updateStockAmount(goodsId, -sellAmount); // 库存减少 sellDao.insert(new Sell(null, goodsId, sellAmount, ...)); // 销售记录入库 connection.commit(); } catch (SQLException e) { connection.rollback(); request.setAttribute(error, 销售失败 e.getMessage()); request.getRequestDispatcher(/view/sell/error.jsp).forward(request, response); return; } }这段代码揭示了三个关键实践事务边界明确setAutoCommit(false)到commit()之间包裹库存更新和销售插入避免出现“销售成功但库存没扣”的数据不一致空值防御前置getStockByGoodsId()返回null时直接拦截而非让后续updateStockAmount()抛NPE错误路径完整rollback()后仍设置error属性并跳转至错误页保证用户感知。注意connection对象来自DBUtil.getConnection()该工具类在utils包中实现了基于DriverManager的连接池简易封装最大连接数5超时30秒比每次new Connection更贴近生产环境。2.3 JSP视图层隐藏了权限控制细节但逻辑藏在request.getAttribute()里view目录下的JSP并非纯展示层。以main.jsp为例它根据session中存储的userRole决定渲染哪些菜单!-- view/main.jsp 片段 -- % String role (String) session.getAttribute(userRole); % div classmenu a hrefsystem/login.jsp系统管理/a % if (admin.equals(role)) { % a hrefemployee/list.jsp员工管理/a a hrefgoods/list.jsp商品管理/a % } % a hrefsell/add.jsp销售录入/a % if (admin.equals(role)) { % a hrefpurchase/list.jsp进货查询/a % } % /div这种基于字符串的角色判断虽简单却规避了“员工也能访问/employee/list.jsp”的URL越权风险——因为EmployeeServlet在处理请求前会再次校验session.getAttribute(userRole)是否为admin形成双重防护。而img目录下的图标资源被img srcimg/icon_user.png直接引用说明静态资源路径已配置为Tomcat的默认webapps结构无需额外配置web.xml的servlet-mapping。3. 部署调试避坑指南EclipseTomcat环境下90%失败原因都在这三步3.1 数据库连接配置必须修改且不能只改jdbc.properties项目src/utils/jdbc.properties中默认配置为drivercom.mysql.jdbc.Driver urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingUTF-8 usernameroot password123456但实际部署时需检查三点MySQL版本兼容性若使用MySQL 8.0driver必须改为com.mysql.cj.jdbc.Driver并在URL末尾添加serverTimezoneGMT%2B8否则报The server time zone value XXX is unrecognized数据库名存在性先手动在MySQL中执行CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;再导入SQL脚本避免因库不存在导致SQLException: Unknown database supermarket密码强度策略MySQL 5.7默认启用validate_password插件若密码123456被拒绝需执行SET GLOBAL validate_password.length 4;临时降低要求。3.1.1 Eclipse中Tomcat启动失败的典型日志与修复当控制台输出SEVERE: Error filterStart或java.lang.ClassNotFoundException: org.apache.commons.dbcp.BasicDataSource时说明lib目录缺少依赖JAR。该项目依赖如下JAR包名作用是否必须mysql-connector-java-5.1.47.jarMySQL驱动必须commons-dbutils-1.7.jarDBUtils工具类必须DAO层大量使用commons-dbcp-1.4.jar数据库连接池必须DBUtil.java中BasicDataSource实例化jstl-1.2.jarJSP标签库必须view中c:if等标签提示若Eclipse提示The project was not built since its build path is incomplete右键项目→Properties→Java Build Path→Libraries→Add External JARs将lib下全部JAR加入然后Clean Project。3.2 登录页面404检查web.xml中servlet-mapping的URL-patternweb.xml中定义了LoginServlet的映射servlet servlet-nameLoginServlet/servlet-name servlet-classcom.supermarket.servlet.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping这意味着访问http://localhost:8080/SuperMarketSystem-main/login才能触发登录逻辑。但初学者常犯两个错误直接访问http://localhost:8080/SuperMarketSystem-main/login.jsp绕过Servlet导致session未初始化将项目部署名为SuperMarketSystem-main但浏览器输入http://localhost:8080/login缺少上下文路径。正确流程是启动Tomcat后在浏览器打开http://localhost:8080/SuperMarketSystem-main/首页会自动重定向至/view/login.jsp该页面的form表单action为/login由web.xml中配置的servlet接收。3.3 商品查询返回空列表排查DAO层SQL拼接与参数绑定当点击“商品信息查询”按钮后页面显示“暂无数据”但数据库中已有记录问题往往出在GoodsDao.java的listAll()方法public ListGoods listAll() throws SQLException { String sql SELECT * FROM tb_goods WHERE 11; QueryRunner qr new QueryRunner(DBUtil.getDataSource()); return qr.query(sql, new BeanListHandler(Goods.class)); }这段代码看似正确但QueryRunner.query()在无参数时可能因驱动差异返回空集合。更健壮的写法是显式传入空数组return qr.query(sql, new BeanListHandler(Goods.class), new Object[]{}); // 添加空参数数组同理带条件查询的方法如findByType(String type)中若type为null应避免SQL注入风险// 错误写法拼接字符串 String sql SELECT * FROM tb_goods WHERE goods_type type ; // 正确写法预编译 String sql SELECT * FROM tb_goods WHERE goods_type ?; return qr.query(sql, new BeanListHandler(Goods.class), type);4. 进阶技巧用现有代码快速扩展“销售退货”功能模块4.1 复用库存扣减逻辑但方向相反销售退货本质是“反向销售”即库存增加、销售记录标记为已退货。无需新建DAO直接复用StockDao.updateStockAmount()方法传入正数增量// ReturnServlet.java 新增方法 protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String sellId request.getParameter(sellId); // 原销售单ID Sell sell sellDao.findById(sellId); // 1. 恢复库存原销售数量 stockDao.updateStockAmount(sell.getGoodsId(), sell.getSellAmount()); // 2. 更新销售记录状态 sell.setReturnStatus(1); // 1表示已退货 sellDao.updateReturnStatus(sell); }关键点在于sellDao.updateReturnStatus()需在tb_sell表中新增return_status字段TINYINT类型默认0并在Sell.java中添加对应属性及getter/setter。这样既保持原有销售数据可追溯又区分正常销售与退货。4.2 在销售查询页嵌入退货操作按钮修改view/sell/list.jsp在每条销售记录后添加退货链接c:forEach items${sellList} varsell tr td${sell.sellId}/td td${sell.goodsName}/td td${sell.sellAmount}/td td c:if test${sell.returnStatus 0} a hrefreturn?sellId${sell.sellId}退货/a /c:if c:if test${sell.returnStatus 1} span stylecolor:red已退货/span /c:if /td /tr /c:forEach此处利用JSTL的c:if标签根据returnStatus值动态渲染避免前端JavaScript操作DOM带来的权限绕过风险。同时hrefreturn?sellId...指向新编写的ReturnServlet其url-pattern需在web.xml中注册为/return。4.3 数据库层面添加退货时间戳与操作员审计为满足毕设答辩中“数据完整性”要求在tb_sell表中追加两个字段ALTER TABLE tb_sell ADD COLUMN return_time DATETIME NULL COMMENT 退货时间, ADD COLUMN return_employee_id VARCHAR(20) NULL COMMENT 退货操作员ID;ReturnServlet中更新时同步写入sell.setReturnTime(new Timestamp(System.currentTimeMillis())); sell.setReturnEmployeeId((String) session.getAttribute(userId)); sellDao.updateReturnInfo(sell); // 新增方法更新return_time和return_employee_id这样当导师问“如何证明退货操作真实发生”时你可以直接导出tb_sell表中return_time非空的记录并指出return_employee_id与tb_user表关联验证操作者身份——这比单纯说“我加了退货功能”更有说服力。本文还有配套的精品资源点击获取