简介:本资源是一套完整的JavaWeb超市管理系统毕业设计项目,面向计算机专业本科生及Java初学者,解决课程设计、毕设选题与实战能力提升需求。压缩包共68个文件,含19个核心Java源码、37个编译后Class文件、4个MySQL建表与数据脚本(涵盖用户、商品、进货、销售等关键业务表),以及2个JAR依赖库、2个JPG系统界面图和Eclipse工程配置文件(.project/.classpath/.prefs),整体3.5MB,结构清晰,开箱即用。已有796人学习下载,说明其在实践教学中具备较高参考价值。读者可直接导入Eclipse+Tomcat环境运行,完整掌握B/S架构下员工与管理员双角色权限体系,深入理解JSP+Servlet+JDBC三层开发模式,并通过真实业务模块(如货架管理、销售统计、进货台账)巩固数据库设计与CRUD操作能力。
1. 这不是又一个“登录+增删改查”的JavaWeb模板,而是一套能跑通完整业务闭环的超市管理系统
你可能已经见过太多标着“超市管理系统”的JavaWeb项目——点开后发现只有用户表和商品表,连库存扣减逻辑都写在JSP里,更别提进货、销售、货架、类型之间的关联约束。但这个源码包不同:它用tb_stock.sql、tb_purchase.sql、tb_sell.sql三张核心表构建了真实业务链路,进货单生成后自动更新库存,销售单提交时校验库存余量并触发负数拦截,货架信息与商品绑定后支持按区域快速盘点。它不是为演示MVC分层而分层,而是把dao包里每个DAO类都对应到具体业务动作(比如PurchaseDao.java里有insertWithStockUpdate()方法),view下的JSP页面也按角色权限做了硬隔离——员工看不到系统管理模块,管理员进不去销售单审核页。适合两类人:一是正在赶毕设 deadline 的本科生,它已通过Eclipse+Tomcat 8.5+MySQL 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_stock | stock_amount,shelf_id,goods_id | 当前库存数量、所属货架、商品ID | StockDao.getStockByGoodsId()用于销售前校验 |
tb_purchase | purchase_amount,purchase_price,goods_id | 采购数量、单价、商品ID | PurchaseService.calculateTotalCost()计算采购总金额 |
tb_sell | sell_amount,sell_price,employee_id | 销售数量、售价、操作员工ID | SellServlet中调用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 class="menu"> <a href="system/login.jsp">系统管理</a> <% if ("admin".equals(role)) { %> <a href="employee/list.jsp">员工管理</a> <a href="goods/list.jsp">商品管理</a> <% } %> <a href="sell/add.jsp">销售录入</a> <% if ("admin".equals(role)) { %> <a href="purchase/list.jsp">进货查询</a> <% } %> </div>这种基于字符串的角色判断虽简单,却规避了“员工也能访问/employee/list.jsp”的URL越权风险——因为EmployeeServlet在处理请求前会再次校验session.getAttribute("userRole")是否为admin,形成双重防护。而img目录下的图标资源被<img src="img/icon_user.png">直接引用,说明静态资源路径已配置为Tomcat的默认webapps结构,无需额外配置web.xml的servlet-mapping。
3. 部署调试避坑指南:Eclipse+Tomcat环境下90%失败原因都在这三步
3.1 数据库连接配置必须修改,且不能只改jdbc.properties
项目src/utils/jdbc.properties中默认配置为:
driver=com.mysql.jdbc.Driver url=jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=UTF-8 username=root password=123456但实际部署时需检查三点:
- MySQL版本兼容性:若使用MySQL 8.0+,
driver必须改为com.mysql.cj.jdbc.Driver,并在URL末尾添加&serverTimezone=GMT%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.jar | MySQL驱动 | 必须 |
commons-dbutils-1.7.jar | DBUtils工具类 | 必须(DAO层大量使用) |
commons-dbcp-1.4.jar | 数据库连接池 | 必须(DBUtil.java中BasicDataSource实例化) |
jstl-1.2.jar | JSP标签库 | 必须(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-pattern
web.xml中定义了LoginServlet的映射:
<servlet> <servlet-name>LoginServlet</servlet-name> <servlet-class>com.supermarket.servlet.LoginServlet</servlet-class> </servlet> <servlet-mapping> <servlet-name>LoginServlet</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 List<Goods> listAll() throws SQLException { String sql = "SELECT * FROM tb_goods WHERE 1=1"; 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}" var="sell"> <tr> <td>${sell.sellId}</td> <td>${sell.goodsName}</td> <td>${sell.sellAmount}</td> <td> <c:if test="${sell.returnStatus == '0'}"> <a href="return?sellId=${sell.sellId}">退货</a> </c:if> <c:if test="${sell.returnStatus == '1'}"> <span style="color:red">已退货</span> </c:if> </td> </tr> </c:forEach>此处利用JSTL的<c:if>标签根据returnStatus值动态渲染,避免前端JavaScript操作DOM带来的权限绕过风险。同时href="return?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表关联验证操作者身份——这比单纯说“我加了退货功能”更有说服力。
本文还有配套的精品资源,点击获取