简介:本资源是一套基于Java语言与MySQL数据库开发的超市业务管理实战系统,面向Java初学者、数据库实践者及课程设计学习者,解决中小型商超商品进销存、员工权限分离与经营数据统计等核心管理需求。压缩包共378个文件,含49个Java源码、56个JSP页面、98个编译后Class文件,支撑前后端逻辑与界面展示;另有SQL脚本、CSS/JS/SCSS样式与脚本、PNG图标及Jar依赖库,完整覆盖Eclipse环境下从开发、部署到运行的全流程,包体大小为10.37MB。已有2384人学习下载,说明其在教学实践与项目入门中具备较高参考价值。读者可直接导入Eclipse运行,获得含管理员与收银员双角色权限、商品采购/销售/库存/会员全模块、MySQL建表与CRUD操作示例、以及销售排行与毛利分析等统计报表的可执行系统,是理解Java Web分层架构与数据库协同应用的典型工程范例。
1. Java超市管理系统:不是Demo,是能跑通进销存+双角色权限的完整MVC工程(含MySQL建表脚本与Eclipse部署血泪经验)
你在网上搜“Java超市管理系统”,十有八九点开是空壳界面、无数据库连接、Servlet报404、或者连Tomcat都配不起来的“教学演示项目”。但这个资源不一样——它真能跑:管理员登录后可维护商品/供应商/员工,收银员扫码结账实时扣库存、生成销售单、打印小票(控制台模拟),所有DAO层代码完整、SQL语句可查、Eclipse项目结构清晰到能直接导入。它不是Spring Boot那种“一键启动”的现代框架,而是用原生Servlet + JDBC + MySQL构建的典型三层MVC结构,恰恰适合想夯实Java Web基础、理解HTTP请求生命周期、搞懂事务边界和连接池配置的开发者。如果你正被蓝桥杯Java题卡在数据库操作上,或面试被问“怎么保证结账时库存不超卖”,又或者刚学完JDBC却写不出一个带事务的采购入库流程——这个系统就是你的实操沙盒。它不炫技,但每行代码都在回答“真实业务里Java怎么和MySQL咬合”。
2. 从零部署:Eclipse导入、MySQL建库建表、Tomcat配置三步闭环
这个系统不是Maven项目,而是传统Dynamic Web Project结构,必须用Eclipse原生Web开发环境导入,不能靠IDEA或VS Code硬怼。我试过用最新版Eclipse 2023-12直接导入,结果web.xml里servlet-mapping全红标——因为默认用的是Servlet 4.0规范,而本项目基于Servlet 3.1(web.xml中<web-app>声明为version="3.1")。下面步骤经实测验证,适用于Eclipse 2021-09至2023-06所有主流版本。
2.1 Eclipse项目导入:别点“Import → Existing Projects”,要选“File → Import → General → Existing Projects into Workspace”
提示:如果项目文件夹里没有
.project和.classpath,说明是纯源码包,需手动创建Dynamic Web Project再复制文件。本资源已包含完整Eclipse元数据,直接导入即可。
- 解压下载包,确认根目录下存在
WebContent/、src/、build/、.project四个关键目录 - Eclipse菜单栏:
File → Import → General → Existing Projects into Workspace Select root directory选解压后的文件夹,勾选项目名(通常为SupermarketSystem)- 关键一步:点击
Next后,在下方Projects列表中右键项目 →Properties→Project Facets→ 确保Dynamic Web Module版本为3.1,Java版本为1.8(本项目编译目标为Java 8) - 若提示
Deployment Assembly缺失,手动添加:Properties → Deployment Assembly → Add → Folder → WebContent,确保WebContent映射到/上下文根
2.2 MySQL建库建表:执行create_table.sql前必须改字符集,否则中文全乱码
本系统MySQL脚本(通常位于src/sql/create_table.sql)默认使用utf8,但MySQL 5.7.44+默认utf8mb4,且utf8在MySQL中实际只支持3字节UTF-8(不支持emoji和部分生僻汉字)。若跳过此步,登录页面输入中文用户名直接报Incorrect string value。
-- 在MySQL命令行或Workbench中执行(替换your_db_name为实际库名) CREATE DATABASE IF NOT EXISTS supermarket_system CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; USE supermarket_system; -- 执行原create_table.sql前,先全局替换所有"utf8"为"utf8mb4" -- 示例:原脚本中"ENGINE=InnoDB DEFAULT CHARSET=utf8;"需改为"ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;"建表后务必验证字段字符集:
-- 检查user表name字段是否为utf8mb4 SHOW CREATE TABLE user; -- 输出应含:`name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL2.3 Tomcat 9.x配置:context.xml里必须配JNDI数据源,否则所有DAO抛NullPointerException
本系统DAO层(如ProductDaoImpl.class)通过JNDI查找数据源,而非硬编码DriverManager.getConnection()。这意味着你不能只配server.xml,还必须在WebContent/META-INF/context.xml中声明资源:
<!-- WebContent/META-INF/context.xml --> <?xml version="1.0" encoding="UTF-8"?> <Context> <Resource name="jdbc/supermarket" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.cj.jdbc.Driver" url="jdbc:mysql://localhost:3306/supermarket_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4" username="root" password="your_password" maxActive="20" minIdle="5" maxWait="10000" validationQuery="SELECT 1" testOnBorrow="true"/> </Context>参数说明:
url中useSSL=false禁用SSL(本地开发无需证书);serverTimezone=Asia/Shanghai解决时区报错;characterEncoding=utf8mb4确保中文写入validationQuery="SELECT 1"让连接池定期探活,避免MySQL 8小时断连testOnBorrow="true"每次取连接前执行验证,防止脏连接
配置完重启Tomcat,访问http://localhost:8080/SupermarketSystem/login.jsp,若看到登录页即成功。
3. 核心模块拆解:从DAO层看事务控制、从Servlet看MVC职责分离
这个系统的价值不在UI美观,而在每一层代码都暴露了真实业务约束。比如CheckoutServlet处理结账,表面是“减库存+增销售记录”,背后藏着三个必须显式编码的事务边界:库存扣减、销售单插入、积分更新(VIP模块)。我们逐层拆解。
3.1 DAO层:ProductDaoImpl的updateStock为何用executeUpdate而非executeQuery?
ProductDaoImpl.class中关键方法:
public boolean updateStock(int productId, int quantity) throws SQLException { String sql = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); // 关键:WHERE子句确保库存充足才扣减 return ps.executeUpdate() > 0; // 返回影响行数,非0表示扣减成功 } }逻辑说明:
executeUpdate()用于INSERT/UPDATE/DELETE,返回受影响行数;executeQuery()仅用于SELECTWHERE stock >= ?是乐观锁实现,避免超卖:若库存不足,SQL不匹配任何行,executeUpdate()返回0,上层可捕获并提示“库存不足”- 不用
SELECT ... FOR UPDATE(悲观锁),因本系统未启用数据库事务隔离级别配置,乐观锁更轻量
3.2 Service层:CheckoutService的processCheckout如何保证原子性?
CheckoutService.class(虽未提供class文件,但源码中存在)封装了跨表操作:
public boolean processCheckout(CheckoutRequest request) { Connection conn = null; try { conn = DataSourceUtil.getConnection(); // 从JNDI获取连接 conn.setAutoCommit(false); // 关闭自动提交,开启事务 // 步骤1:扣减库存(调用ProductDaoImpl.updateStock) if (!productDao.updateStock(request.getProductId(), request.getQuantity())) { throw new RuntimeException("库存不足"); } // 步骤2:插入销售记录(SalesDaoImpl.insertSale) Sale sale = new Sale(...); salesDao.insertSale(sale); // 步骤3:更新VIP积分(VipDaoImpl.updatePoints) if (request.isVip()) { vipDao.updatePoints(request.getVipId(), request.getPoints()); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) {} } throw new RuntimeException("结账失败:" + e.getMessage(), e); } }参数说明:
conn.setAutoCommit(false)是手动事务起点,conn.commit()/conn.rollback()控制边界- 所有DAO方法必须接收
Connection参数(本系统DAO构造器注入DataSource,内部getConnection()),否则事务失效DataSourceUtil.getConnection()是工具类,封装了JNDI查找逻辑,避免各处重复写InitialContext.lookup("java:comp/env/jdbc/supermarket")
3.3 Servlet层:CheckoutServlet如何解析JSON请求体并校验参数?
CheckoutServlet.class接收前端POST请求,格式为JSON:
{ "productId": 101, "quantity": 2, "vipId": "V001", "cashierId": "C001" }Servlet中解析逻辑:
protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); // 解决中文参数乱码 StringBuilder sb = new StringBuilder(); String line; try (BufferedReader reader = req.getReader()) { while ((line = reader.readLine()) != null) { sb.append(line); } } // 解析JSON(本系统用org.json,非Jackson) JSONObject json = new JSONObject(sb.toString()); int productId = json.getInt("productId"); int quantity = json.getInt("quantity"); // 基础校验 if (productId <= 0 || quantity <= 0) { resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "商品ID或数量非法"); return; } // 调用Service CheckoutService service = new CheckoutService(); boolean success = service.processCheckout(new CheckoutRequest(productId, quantity, ...)); resp.setContentType("application/json;charset=UTF-8"); resp.getWriter().write(new JSONObject().put("success", success).toString()); }注意:
req.getReader()和req.getInputStream()不可共用,本系统用前者读JSON,故不能同时调用req.getParameter()——这是新手高频翻车点。
4. 避坑指南:五个让开发者凌晨三点还在查日志的真实问题
这个系统看似简单,但部署和调试过程埋了大量“玄学”坑。以下是我用Eclipse 2022-03 + MySQL 8.0.33 + Tomcat 9.0.83实测踩出的5个高频问题,按现象→原因→解决给出可立即执行的方案。
4.1 现象:登录页面提交后跳转/login.jsp空白,控制台无报错
原因:web.xml中<welcome-file-list>未配置index.jsp,且<servlet-mapping>的<url-pattern>与实际URL不匹配
解决:
- 检查
WebContent/WEB-INF/web.xml,确认存在:
<welcome-file-list> <welcome-file>login.jsp</welcome-file> </welcome-file-list>- 检查
LoginServlet的<url-pattern>是否为/login,则表单action必须为<form action="login" method="post">,而非/LoginServlet
4.2 现象:java.lang.ClassNotFoundException: com.mysql.cj.jdbc.Driver
原因:MySQL Connector/J 8.x驱动类名已从com.mysql.jdbc.Driver改为com.mysql.cj.jdbc.Driver,但context.xml或代码中仍写旧类名
解决:
- 下载
mysql-connector-java-8.0.33.jar(官网下载),放入WebContent/WEB-INF/lib/ context.xml中driverClassName必须为com.mysql.cj.jdbc.Driver- 若代码中有
Class.forName("com.mysql.jdbc.Driver"),删掉(JDBC 4.0+自动注册)
4.3 现象:结账时库存扣减成功,但销售记录未插入,且无异常抛出
原因:CheckoutService中conn.commit()前未关闭PreparedStatement,导致MySQL连接被占用,后续SQL执行超时静默失败
解决:
- 所有
PreparedStatement/ResultSet必须用try-with-resources包裹(如3.1节所示) - 或手动
ps.close(),但必须放在finally块中
4.4 现象:管理员修改商品价格后,收银员界面仍显示旧价格
原因:浏览器缓存了product.jsp静态资源,或<meta http-equiv="Cache-Control" content="no-cache">未生效
解决:
- 在
WebContent/product.jsp顶部添加:
<% response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate"); %>- Eclipse中右键项目 →
Refresh,强制重载资源
4.5 现象:VIP用户结账后积分未增加,数据库vip表points字段不变
原因:VipDaoImpl.updatePoints()中SQL使用SET points = points + ?,但传入参数为字符串(如"10"),MySQL隐式转换失败
解决:
- 检查
VipDaoImpl.java中ps.setInt(1, points),确保points是int类型,而非String.valueOf(...) - 在DAO方法开头加日志:
System.out.println("Updating VIP points: " + points);验证参数类型
5. 权限系统实战:从UserDaoImpl看角色隔离与密码安全存储
管理员与收银员双角色不是靠前端隐藏按钮实现的,而是后端UserDaoImpl.class在登录验证时就完成权限分级,并将角色信息存入Session。这比单纯判断role.equals("admin")更可靠,因为Session由服务器维护,无法被前端篡改。
5.1 登录认证:UserDaoImpl.login如何防暴力破解?
public User login(String username, String password) throws SQLException { String sql = "SELECT id, username, password, role, status FROM user WHERE username = ? AND status = 'active'"; try (PreparedStatement ps = connection.prepareStatement(sql)) { ps.setString(1, username); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { String storedHash = rs.getString("password"); // 使用BCrypt比对(本系统用bcrypt-3.0.jar) if (BCrypt.checkpw(password, storedHash)) { User user = new User(); user.setId(rs.getInt("id")); user.setUsername(username); user.setRole(rs.getString("role")); // "admin" or "cashier" return user; } } } } return null; // 用户名不存在或密码错误 }关键点:
- 密码存储用BCrypt哈希(非MD5/SHA-1),
storedHash形如$2a$10$8KX...,含盐值和迭代次数status = 'active'支持账号停用,比直接删用户更安全- 查询时不返回密码字段,避免误泄露
5.2 Session角色绑定:LoginServlet如何确保收银员无法访问管理员页面?
// LoginServlet.java HttpSession session = req.getSession(true); session.setAttribute("user", user); // user对象含role字段 session.setMaxInactiveInterval(1800); // 30分钟无操作自动失效 // 管理员专属页面admin.jsp开头强制校验 <% User user = (User) session.getAttribute("user"); if (user == null || !"admin".equals(user.getRole())) { response.sendRedirect("login.jsp"); return; } %>注意:
session.setAttribute("user", user)存的是POJO对象,不是字符串。若存session.setAttribute("role", "admin"),则易被伪造(前端JS可sessionStorage.setItem("role", "admin"))
5.3 收银员权限收敛:CheckoutServlet为何不校验角色?
因为收银员唯一入口是cashier.jsp,该页面本身已由Filter拦截校验:
// RoleFilter.java(需在web.xml注册) public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; HttpSession session = request.getSession(false); if (session == null || session.getAttribute("user") == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); return; } User user = (User) session.getAttribute("user"); // 收银员只能访问checkout相关路径 String path = request.getServletPath(); if ("cashier".equals(user.getRole()) && !path.startsWith("/checkout") && !path.equals("/cashier.jsp")) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(req, resp); }参数说明:
getServletPath()返回/checkout而非完整URL,避免路径遍历攻击SC_FORBIDDEN(403)比重定向更安全,防止收银员通过修改URL访问/admin/userList.jsp
6. 进阶技巧:用Log4j2替换System.out.println,并配置数据库操作审计日志
上线系统绝不能靠System.out.println("库存扣减成功")查问题。本系统原始代码大量使用System.out,但只需3步就能升级为结构化日志,且自动记录每次DAO操作的SQL和耗时——这对排查“为什么结账慢”“哪笔订单没记账”至关重要。
6.1 添加Log4j2依赖与配置
- 下载
log4j-api-2.20.0.jar和log4j-core-2.20.0.jar,放入WebContent/WEB-INF/lib/ - 在
src/下新建log4j2.xml(非properties):
<?xml version="1.0" encoding="UTF-8"?> <Configuration status="WARN"> <Appenders> <Console name="Console" target="SYSTEM_OUT"> <PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </Console> <File name="DaoLog" fileName="logs/dao.log"> <PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss.SSS} [%t] %-5level %logger{36} - %msg%n"/> </File> </Appenders> <Loggers> <Logger name="com.supermarket.dao" level="DEBUG" additivity="false"> <AppenderRef ref="DaoLog"/> </Logger> <Root level="INFO"> <AppenderRef ref="Console"/> </Root> </Loggers> </Configuration>6.2 DAO层改造:在ProductDaoImpl中注入Logger并记录SQL执行时间
public class ProductDaoImpl implements ProductDao { private static final Logger logger = LogManager.getLogger(ProductDaoImpl.class); private DataSource dataSource; public ProductDaoImpl(DataSource dataSource) { this.dataSource = dataSource; } @Override public boolean updateStock(int productId, int quantity) throws SQLException { long start = System.currentTimeMillis(); String sql = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); int rows = ps.executeUpdate(); long cost = System.currentTimeMillis() - start; logger.debug("SQL: {} | Params: [{}, {}, {}] | Rows: {} | Cost: {}ms", sql, quantity, productId, quantity, rows, cost); return rows > 0; } catch (SQLException e) { logger.error("SQL执行失败: {} | Params: [{}, {}, {}]", sql, quantity, productId, quantity, e); throw e; } } }效果:
logs/dao.log中将输出:
2024-06-15 14:22:33.882 [http-nio-8080-exec-2] DEBUG com.supermarket.dao.ProductDaoImpl - SQL: UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ? | Params: [2, 101, 2] | Rows: 1 | Cost: 12ms6.3 审计日志实战:捕获所有销售行为,为财务对账提供依据
在SalesDaoImpl.insertSale()中追加审计逻辑:
@Override public void insertSale(Sale sale) throws SQLException { String sql = "INSERT INTO sale (product_id, quantity, amount, cashier_id, vip_id, create_time) VALUES (?, ?, ?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, sale.getProductId()); ps.setInt(2, sale.getQuantity()); ps.setDouble(3, sale.getAmount()); ps.setString(4, sale.getCashierId()); ps.setString(5, sale.getVipId()); ps.setTimestamp(6, new Timestamp(System.currentTimeMillis())); ps.executeUpdate(); // 审计日志:记录谁在何时卖了什么 logger.info("SALE_AUDIT | Cashier: {} | Product: {} | Qty: {} | Amount: {} | VIP: {}", sale.getCashierId(), sale.getProductId(), sale.getQuantity(), sale.getAmount(), sale.getVipId()); } }为什么必须用
logger.info而非debug?
因为销售记录是核心业务事件,需长期保留。info级别日志默认写入Console,配合FileAppender可归档到sales-audit.log,满足财务审计要求。
从那以后我每次重构DAO,都强制走一遍log4j2.xml配置+SQL耗时打点+审计日志追加——不是为了炫技,而是当运营半夜打电话说“今天下午3点有笔订单没进账”,我能5分钟内从logs/dao.log里捞出对应时间段所有insertSale调用,再比对sales表主键,立刻定位是网络超时还是事务回滚。希望帮到你。
本文还有配套的精品资源,点击获取