☰
Java超市管理系统:Servlet+JDBC+MySQL完整MVC实战
2026/10/4 5:22:59 网站建设 项目流程

简介:本资源是一套基于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元数据,直接导入即可。

  1. 解压下载包,确认根目录下存在WebContent/、src/、build/、.project四个关键目录
  2. Eclipse菜单栏:File → Import → General → Existing Projects into Workspace
  3. Select root directory选解压后的文件夹,勾选项目名(通常为SupermarketSystem)
  4. 关键一步:点击Next后,在下方Projects列表中右键项目 →Properties→Project Facets→ 确保Dynamic Web Module版本为3.1,Java版本为1.8(本项目编译目标为Java 8)
  5. 若提示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 NULL

2.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&amp;serverTimezone=Asia/Shanghai&amp;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()仅用于SELECT
  • WHERE 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依赖与配置

  1. 下载log4j-api-2.20.0.jar和log4j-core-2.20.0.jar,放入WebContent/WEB-INF/lib/
  2. 在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: 12ms

6.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表主键,立刻定位是网络超时还是事务回滚。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询