简介:一款基于Eclipse与MySQL实现的Java超市管理系统,面向正在做课程设计或毕业设计的Java学习者,也适合希望了解零售业进销存流程与权限控制的开发人员。压缩包共378个文件、约10.37MB,包含49个Java源文件、98个class编译文件与56个JSP页面,另配SQL脚本、MySQL数据库文件、XML配置以及CSS、JS等前端资源,目录结构按功能模块划分,便于导入Eclipse后直接查看和运行。系统围绕商品销售、商品采购与商品数据统计三大模块展开,提供管理员和收银员两种操作界面,可覆盖商品增删改查、结账收银、供应商管理、采购订单跟踪,以及销售排行、库存预警、毛利润分析等常用业务场景。目前已有2384人学习下载,对需要搭建完整Java Web项目或学习Servlet、JSP与MySQL整合开发的读者来说,这是一套代码结构完整、业务逻辑清晰的可运行参考。
1. 为什么这套 Java 超市管理系统值得你动手跑一遍
java超市管理系统(Eclipse+MySQL)是课设素材里被复跑次数最多的一类:商品进销存完整闭环、管理员与收银员双角色页面、销售排行与库存预警报表,恰好构成一个能讲清楚的 javaweb 项目完整案例。对准备 java 基础面试、期末赶课设、或者第一次想把 Servlet+JDBC+JSP 串起来跑通的人来说,它比零散的登录 demo 有价值得多——你能看到商品、会员、供应商、销售记录这几张业务表怎么设计,也能看到收银台结账时库存如何联动扣减。适合三类人:需要交课设的学生、想理解分层思想的 java 初学者、想拿真实业务场景练手的从业者。这套源码最实在的地方在于:它不炫技,每一行代码都在解决超市运营里的具体问题。
2. 项目骨架与数据模型:五个 DAO 类先把分层撑起来
2.1 从 ProductDaoImpl 到 CheckoutServlet:每个类负责哪一段业务
拿到这套源码,先别急着点运行,去编译输出目录看一眼。里面躺着 ProductDaoImpl.class、VipDaoImpl.class、ProviderDaoImpl.class、UserDaoImpl.class 和 CheckoutServlet.class,类名已经把架构交代了一半:这是一个典型的 JSP + Servlet + DAO 三层结构。JSP 页面只负责展示,Servlet 负责接收请求、调用业务逻辑,DAO 负责用 JDBC 跟 MySQL 打交道。分层的好处是你可以单独改某一段而不影响其他层,比如把数据库从 MySQL 换掉,只需动 DAO 层,页面一行不用改。
五个类对应的职责很清晰:ProductDaoImpl 管商品表的增删改查,UserDaoImpl 管登录用户(员工账号),VipDaoImpl 管会员信息和折扣,ProviderDaoImpl 管供应商档案,CheckoutServlet 则负责收银台结账这个核心动作。注意 CheckoutServlet 是 servlet 而不是 dao,它要同时调用商品和销售两张表的 DAO,是典型的控制层逻辑编排。我一般会把这个对应关系写在 PPT 里,答辩时直接讲“类名即职责”,比背十页概念有效得多。
下面是商品 DAO 里最典型的一个查询方法,这套源码里的增删改都长这样:
public class ProductDaoImpl implements ProductDao { @Override public Product findById(int id) { String sql = "SELECT id, name, price, stock, provider_id FROM product WHERE id = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setName(rs.getString("name")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); p.setProviderId(rs.getInt("provider_id")); return p; } } } catch (SQLException e) { e.printStackTrace(); } return null; } }代码里值得注意的有三处。第一,用了 PreparedStatement 而不是 Statement,占位符?先编译再传参,能挡住最基础的 SQL 注入,这是 java 面试里大概率被追问的点。第二,价格字段用 rs.getBigDecimal 拿,而不是 getDouble,金额用浮点数在累加时会出精度尾巴,后面统计报表一章会看到后果。第三,try-with-resources 语法让 Connection、PreparedStatement、ResultSet 自动关闭,这比你手写 finally 关连接干净得多。
这套源码里每个 DAO 基本上保持了同样的模式:一个接口定义方法,一个 Impl 类写 JDBC 实现。接口存在的意义不是多敲几个字,而是以后想换成 MyBatis 或 Spring JdbcTemplate 时,业务层调的还是同一个接口,这是分层设计的关键收益。
2.2 MySQL 表结构:商品、供应商、会员、销售记录怎么贴在一起
数据库是整个系统的地基,表没设计好,后面所有代码都在打补丁。这套源码对应的建表语句大概是下面这几张核心表,字段名和课设常规做法一致:
CREATE TABLE provider ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, contact VARCHAR(50), phone VARCHAR(20), address VARCHAR(200) ); CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, cost_price DECIMAL(10,2), stock INT NOT NULL DEFAULT 0, provider_id INT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_product_provider FOREIGN KEY (provider_id) REFERENCES provider(id) ); CREATE TABLE vip ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, phone VARCHAR(20) UNIQUE, discount DECIMAL(3,2) DEFAULT 1.00, points INT DEFAULT 0 ); CREATE TABLE sale_record ( id INT PRIMARY KEY AUTO_INCREMENT, product_id INT NOT NULL, product_name VARCHAR(100), count INT NOT NULL, unit_price DECIMAL(10,2), total DECIMAL(10,2), operator VARCHAR(50), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_product (product_id), KEY idx_create_time (create_time) ); CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(50) NOT NULL, role VARCHAR(10) NOT NULL DEFAULT 'cashier' );重点说三个设计决策。第一,金额字段全部用 DECIMAL(10,2),不是 FLOAT 也不是 DOUBLE,浮点数二进制表示无法精确表达 0.1,多行累计下来会差出一分钱,这在零售系统里是不能忍的。第二,sale_record 里冗余了 product_name 和 unit_price 两个字段,这是刻意的:历史销售记录不能跟着商品表走,商品改价甚至删除后,老订单仍然能还原出当时成交的单价和品名。第三,role 字段直接存在 sys_user 表里,'admin' 和 'cashier' 两个值决定了登录后进哪个界面,这是整权限控制的最小实现,简单但够用。
外键这里提醒一句:product 表通过 provider_id 关联 provider,删除供应商前必须先清理该供应商下的商品,否则 MySQL 会甩一个外键约束错误出来。常见做法是加一个删除前的引用检查,或者在供应商表上加is_deleted逻辑删除标志,不让物理删除发生。
2.3 Eclipse 环境准备:版本、驱动 jar、连接串一处都不能差
这套源码的复现环境不算苛刻,但版本没对齐会浪费时间。我一般建议:JDK 8,Tomcat 8.5,MySQL 5.7 或 8.0,Eclipse 必须用 Eclipse IDE for Enterprise Java 版本,普通 Java 版没有 Servers 视图,后面配 Tomcat 会非常别扭。MySQL 是开源免费的,官网下载社区版装好,把自带的 SQL 脚本导入即可,导入命令参考下面:
# 用命令行导入建表脚本(或直接用 Navicat 导入) mysql -uroot -p < supermarket.sql # 验证表是否建成功 mysql -uroot -p -e "USE supermarket; SHOW TABLES;"代码层面需要确认两件事。第一,把 mysql-connector-java.jar 加到项目的 Build Path 上,右键项目 → Build Path → Configure Build Path → Add JARs,这一步漏掉的话运行时报 ClassNotFoundException 是跑不掉的。第二,检查 DBUtil 里的连接串,MySQL 8.0 和 5.7 的连接串写法有差异:
// 适用于 MySQL 8.0,注意驱动类名是 com.mysql.cj.jdbc.Driver String url = "jdbc:mysql://localhost:3306/supermarket?useSSL=false" + "&characterEncoding=utf8&serverTimezone=Asia/Shanghai";连接串里三个参数缺一个就可能踩坑:useSSL=false 关掉 SSL 握手,本地开发不需要加密链路;characterEncoding=utf8 保证中文明细不乱码;serverTimezone 是 MySQL 8.0 的时区要求,不写可能直接报错。驱动类方面,5.7 用com.mysql.jdbc.Driver,8.0 用com.mysql.cj.jdbc.Driver,旧类名在 8.0 下只会打警告但还能跑,别被警告带偏去改代码。
环境装好后,按 Eclipse 使用教程里的标准流程:File → Import → Existing Projects into Workspace 选项目根目录,然后在 Servers 视图新建 Tomcat 8.5 实例,右键项目 Run As → Run on Server。不要用 Run As Java Application 去跑,Web 项目必须有容器支撑,这一步跑偏的话后面全乱。
3. 核心业务拆解:结账、采购入库与统计报表怎么联动
3.1 CheckoutServlet:收银台一次结账的完整动作
超市管理系统最核心的页面是收银台,而收银台背后真正干活的类就是 CheckoutServlet。它的完整动作链是这样的:收银员提交购物车里的商品和数量,Servlet 收到请求后先逐条查商品当前库存,足够才继续;如果有会员,按会员折扣重新计算总价;接着写销售记录、扣减库存,最后返回一张结账成功的小票信息。这个过程里最怕的是做到一半出错,比如库存刚扣成功但销售记录没写进去,顾客走了账对不上。
下面是按照常见课设做法还原的结账核心逻辑,重点看参数解析和库存校验那段:
@WebServlet("/checkout") public class CheckoutServlet extends HttpServlet { protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { request.setCharacterEncoding("UTF-8"); String[] productIds = request.getParameterValues("productId"); String[] counts = request.getParameterValues("count"); ProductDao productDao = new ProductDaoImpl(); SaleDao saleDao = new SaleDaoImpl(); // 会员折扣:页面上带了 vipId 就按折扣算,没有则按原价 double discount = 1.0; String vipIdParam = request.getParameter("vipId"); if (vipIdParam != null && !vipIdParam.isEmpty()) { Vip vip = new VipDaoImpl().findById(Integer.parseInt(vipIdParam)); if (vip != null) { discount = vip.getDiscount(); } } // 逐条校验库存并结账 try { for (int i = 0; i < productIds.length; i++) { int pid = Integer.parseInt(productIds[i]); int count = Integer.parseInt(counts[i]); Product p = productDao.findById(pid); if (p.getStock() < count) { request.setAttribute("msg", "商品「" + p.getName() + "」库存不足"); request.getRequestDispatcher("pos.jsp").forward(request, response); return; } double total = p.getPrice().doubleValue() * count * discount; // 注意:这里简化了事务,实际应包在同一个 Connection 里 productDao.updateStock(pid, p.getStock() - count); saleDao.insert(new SaleRecord(pid, p.getName(), count, p.getPrice(), total, request.getSession().getAttribute("username"))); } response.sendRedirect("checkoutSuccess.jsp"); } catch (Exception e) { e.printStackTrace(); request.setAttribute("msg", "结账失败,请重试"); request.getRequestDispatcher("pos.jsp").forward(request, response); } } }这段代码要读明白的关键点有三个。第一个是参数用getParameterValues拿数组,因为购物车是一次性提交多件商品,页面上 checkbox 或隐藏域按这个 name 命名。第二个是库存校验必须在写库之前做,而且校验和写库之间理论上还有并发窗口,课设阶段这样做够用,代码注释里也标了事务没包,这是后面进阶章节要补的坑。第三个是接口上的@WebServlet("/checkout"),这是 Servlet 3.0 以后的注解写法,不用再去 web.xml 里手动登记映射,项目里如果看到 web.xml,说明还保留了老式写法,两种方式效果一样,别重复配置。
转发和重定向在这里有讲究:结账失败要回填错误消息,用 forward 把请求转发回 pos.jsp,request 里的 msg 属性还在;结账成功用 sendRedirect,防止用户按 F5 重复提交把同一笔订单再写一遍。这是 java 面试里常被问的 forward 与 redirect 区别,在这个场景里体现得很具体。
3.2 采购入库与库存联动:进销存的“进”怎么落库
采购模块是这套源码里最容易轻视、但数据联动最明显的一块。管理员在页面上选供应商、选商品、填采购数量提交后,系统要做两件事:往采购流水表里插一条记录,同时把对应商品的库存数量累加上去。很多初版代码会把这两个动作做成先后两步,先插流水再更新库存,中间任何一步失败都会导致账实不符。
正确的做法是把两步放进同一个事务,或者至少在 SQL 层面保证第二步的原子性。库存累加最稳的写法是直接在当前值上做增量:
-- 采购入库:库存累加,避免先查后改 UPDATE product SET stock = stock + ? WHERE id = ?; -- 采购流水 INSERT INTO purchase_record (provider_id, product_id, count, cost_price, operator) VALUES (?, ?, ?, ?, ?);为什么用stock = stock + ?而不是先 SELECT 再 UPDATE?因为两个操作之间如果有另一个收银台同时结账扣库存,你读到的旧值会把别人扣掉的库存加回来。直接在 SQL 里做增量运算,数据库行锁会保证这一步只有一个事务在改,这是解决并发库存错乱最基本的手段。理解了这个,后面看数据库连接池和事务章节会更有感觉。
3.3 数据统计:销售排行、库存预警、毛利润报表
数据统计是这个系统的亮点,摘要里提到的销售排行、库存预警、毛利润分析,本质上都是对 sale_record 和 product 两张表做分组聚合。这三条 SQL 是整套统计模块的地基,值得反复看:
-- 1. 销售排行:按商品分组求和,取销量前 10 SELECT product_id, product_name, SUM(count) AS total_count FROM sale_record GROUP BY product_id, product_name ORDER BY total_count DESC LIMIT 10; -- 2. 库存预警:低于阈值直接列出来 SELECT name, stock FROM product WHERE stock < 20; -- 3. 毛利润:零售总额 - 按进价计算的成本 SELECT p.name, SUM(s.total) AS sales_amount, SUM(s.count * p.cost_price) AS cost_amount, SUM(s.total - s.count * p.cost_price) AS gross_profit FROM sale_record s JOIN product p ON s.product_id = p.id GROUP BY p.id ORDER BY gross_profit DESC;销售排行这条,GROUP BY 里同时放了 product_id 和 product_name,是因为 sale_record 里冗余存了 product_name,按两个字段分组不会互相干扰。LIMIT 10 只是一个演示值,页面上一般会做成可选的排名条数,或者在 JSP 里接收一个 limit 参数。
毛利润这条藏着一个常见的翻车点:如果商品表里没有 cost_price 字段,或者这个字段全是 NULL,毛利润算出来就是空的。很多课设源码只存零售价不存进价,统计模块的毛利润分析就成了摆设。我一般会强烈建议保留 cost_price,哪怕录入时麻烦一点,报表的价值会完全不一样。库存预警的阈值 20 是硬编码的示例,真要上线应该做成配置项或者按商品类目区分,收银员和管理员看到的是同一个数字,但只有管理员能改阈值。
三条 SQL 都有一个共同的边界问题:跨月统计。正确写法是用半开区间WHERE create_time >= '2024-01-01' AND create_time < '2024-02-01',而不是BETWEEN '2024-01-01' AND '2024-01-31'。BETWEEN 会漏掉 1 月 31 日 23 点之后的记录,因为 DATETIME 精度到秒,这个坑实测踩过,报表差一天的数据在月结时非常难看。
4. 双角色权限落地:管理员与收银员的隔离边界在哪
4.1 登录校验与 Session 里的角色标记
这套系统的权限模型是“管理员页面 + 收银员页面”双界面,底层靠的是登录时把角色写进 Session。登录 Servlet 拿到用户名密码后去 sys_user 表查,查到了就把整个 user 对象和 role 字段分别放进 Session,然后按角色跳转不同首页。代码大概是这样的结构:
String username = request.getParameter("username"); String password = request.getParameter("password"); User user = new UserDaoImpl().findByUsernameAndPassword(username, password); if (user != null) { HttpSession session = request.getSession(); session.setAttribute("loginUser", user); session.setAttribute("role", user.getRole()); // 'admin' 或 'cashier' if ("admin".equals(user.getRole())) { response.sendRedirect("admin/index.jsp"); } else { response.sendRedirect("cashier/index.jsp"); } } else { request.setAttribute("error", "用户名或密码错误"); request.getRequestDispatcher("login.jsp").forward(request, response); }把 role 单独存一份而不是每次从 loginUser 里取,是图个方便:JSP 页面用${sessionScope.role}直接判空就行,不用解对象。这里顺带提一句,课设源码里密码大多是明文存库,你心里要清楚这是隐患,至少也该用 MD5 加盐。面试时如果被问到,答“我知道这里能改进”比假装不存在好得多。
Session 角色只是第一道门,真正兜底的是 Filter。直接访问受保护页面的 URL 是绕过登录的常见路径,必须有一个过滤器统一拦:
@WebFilter("/*") public class LoginFilter implements Filter { 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); String uri = request.getRequestURI(); if (uri.endsWith("login.jsp") || uri.endsWith("/login") || (session != null && session.getAttribute("role") != null)) { chain.doFilter(req, resp); } else { response.sendRedirect("login.jsp"); } } }过滤器的逻辑只有三条:登录页和登录接口放行,Session 里有角色的放行,其余一律踢回登录页。注意request.getSession(false)带了参数,没有 Session 时返回 null 而不是新建,避免给每个请求都强行造一个会话。这种白名单式的过滤器是 Servlet 权限控制的标准兜底,也是面试里“你项目权限怎么做”的标准答案。
4.2 页面菜单按角色渲染:收银员看不到的东西别让它出现
双界面的体验层面靠 JSP 条件渲染实现。菜单条用 JSTL 标签按 role 判断,收银员登录后看到的菜单里压根就没有供应商管理、采购入库和销售统计这几个入口,管理员登录则能看到全部。菜单是这样处理的:
<c:if test="${sessionScope.role == 'admin'}"> <li><a href="provider/list.jsp">供应商管理</a></li> <li><a href="purchase/list.jsp">采购入库</a></li> <li><a href="stat/saleRank.jsp">销售统计</a></li> <li><a href="user/list.jsp">员工管理</a></li> </c:if> <c:if test="${sessionScope.role == 'cashier'}"> <li><a href="pos.jsp">收银台</a></li> </c:if>页面隐藏只是体验隔离,真正的数据隔离要下沉到 DAO。最典型的是进价字段:收银员能看到商品零售价和库存,但绝不能看到 cost_price。如果所有角色共用同一个findAll()方法,返回值里带着进价,前端不显示也只是“看不见”,数据已经传到了浏览器。更稳妥的做法是给查询方法做角色区分,收银员走的查询 SQL 压根不 select cost_price 字段,数据层面的隔离才是真隔离。
4.3 权限设计的三个常见误用
这个系统双角色的权限矩阵在答辩时可以直接画出来讲,功能、管理员、收银员三列一摆就非常清楚:
| 功能 | 管理员 | 收银员 |
|---|---|---|
| 商品增删改与进价维护 | 可以 | 不可以 |
| 收银结账 | 可以 | 可以 |
| 采购入库与供应商管理 | 可以 | 不可以 |
| 会员管理 | 可以 | 只读 |
| 销售统计与报表 | 可以 | 不可以 |
| 员工账号管理 | 可以 | 不可以 |
围绕这个矩阵,最常见的三个误用得重点避掉。第一个是在 JSP 里用c:if把管理员按钮藏掉就以为安全了,直接敲 URL 照样能访问管理页,必须靠 Servlet 入口和 Filter 兜底。第二个是权限判断散落在各个 Servlet 里,每个方法开头都复制一遍if(!role.equals("admin")),改需求时容易漏改,应该统一收进 Filter 或者一个权限工具类。第三个是 session 过期后页面还在,点按钮发 AJAX 请求返回的是登录页而不是数据,前端拿到一堆 HTML 也不知道怎么处理,稳妥做法是过期后前端判断响应类型,重定向去登录页。
权限这套东西,课设评分时往往是个拔高点。把 4.1 的 Session 登录、4.2 的菜单渲染、加上这个权限矩阵讲明白,比你多做五个页面更让老师记住。
5. 部署避坑:连接失败、乱码、ClassNotFound 的四个现场
5.1 现场一:eclipse 找不到或无法加载主类 org.apache.catalina.startup.Bootstrap
现象是点 Run As → Run on Server 后,页面白屏,控制台直接报“找不到或无法加载主类 org.apache.catalina.startup.bootstrap”,看起来像是 Tomcat 坏了。
原因基本是两类:一是导入项目后没把 Tomcat 运行环境关联到项目的 Build Path,servlet-api.jar 这些根本没进编译路径;二是用了普通 Eclipse Java 版,压根没有 Server Runtime 的配置入口。
解决是在 Window → Preferences → Server → Runtime Environments 里手动 Add 一个 Tomcat 8.5,指定 Tomcat 安装目录;然后项目右键 Properties → Build Path → Libraries,确认列表里有 Server Runtime [Tomcat 8.5]。最后打开 Servers 视图,把项目 Add 到 Tomcat 实例的发布列表,再重启一次。这套顺序走完,Bootstrap 的报错基本消失。
5.2 现场二:MySQL SSL 连接错误与 Public Key Retrieval is not allowed
现象是项目启动后一查数据库就报 SSLHandshakeException,或者干脆报“Public Key Retrieval is not allowed for user”,Log 里还夹着成片的 WARN,提示 Establishing SSL connection 没开安全认证。
原因是 MySQL 8.0 默认认证插件是 caching_sha2_password,驱动侧的 JDBC 默认要求 SSL 加密和公钥检索,本地开发的连接串没给够参数就握手失败。
解决是连接串补齐三个参数,一行搞定:
jdbc:mysql://localhost:3306/supermarket?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=Asia/ShanghaiuseSSL=false 表示本地链路不加密,allowPublicKeyRetrieval=true 允许驱动向服务端请求公钥,serverTimezone 保证日期时间字段不出差。这是 java 项目连 MySQL 8.0 最典型的玄学报错,九成都是连接串少参数,不是代码逻辑问题。MySQL 5.7 不会有这个错,所以换数据库版本时记得回查连接串。
5.3 现场三:error 2002 (HY000): Can't connect to local MySQL server through socket
现象是 Linux 下命令行mysql -uroot -p都进不去,报 error 2002 (HY000): Can't connect to local MySQL server through socket '/tmp/mysql.sock',项目里当然也连不上。
原因十有八九是 MySQL 服务没启动,或者装完以后服务挂了。连接串写的是 localhost,JDBC 在 Linux 上优先走 unix socket,服务不活着 socket 文件就不存在。
解决先确认服务再谈别的:
systemctl status mysqld # 没起来则启动 systemctl start mysqld # 确认 3306 在监听 ss -lnt | grep 3306这个坑的排查顺序是固定的:服务状态 → 端口监听 → 驱动 jar → 连接串 → SQL 权限。任何一步过了再走下一步,不要上来就怀疑代码。很多新人把半小时耗在改连接串上,最后发现 mysqld 根本没启动,教训很直接。
5.4 现场四:页面、数据库、控制台来回中文乱码
现象是页面上提交中文商品名,存进数据库变成一串???;或者数据库里好好的,页面一查出来乱码。
原因是编码链断裂。中文从 JSP 页面到数据库要过四道关:JSP 文件本身的编码、Tomcat 接收请求的编码、JDBC 传输编码、MySQL 表字符集,四者必须一致,断一处就乱。
解决按一条链从前往后排:JSP 顶部写pageEncoding="UTF-8",文件另存为 UTF-8;Servlet 入口先request.setCharacterEncoding("UTF-8");JDBC 连接串加characterEncoding=utf8;建表时明确CHARACTER SET utf8mb4。Tomcat 8.5 默认 URIEncoding 已经是 UTF-8,老版本 Tomcat 需要在 server.xml 的 Connector 上加URIEncoding="UTF-8"。
血泪经验:建库时别偷懒不写字符集,等数据都进去了再改字符集,数据库里已存的中文可能已经不可逆。第一次建表就统一 utf8mb4,后面能少掉一半乱码问题。
5.5 一个固定排查顺序
这四个现场其实可以压缩成一套动作:先看 MySQL 服务起来没有、端口 3306 通不通,再看驱动 jar 在不在 Build Path,然后查连接串参数全不全,接着确认 SQL 里表名库名对不对,最后才是编码。顺序不要乱。服务、端口、驱动、连接串、SQL、编码,六步按序走,九成报错都能在十分钟内定位。踩坑踩多了你会发现,大部分“环境问题”最终都落在连接串和 jar 这两处。
6. 进阶改造:连接池、事务与答辩前必过的验证清单
6.1 用数据库连接池替换裸 JDBC
裸 JDBC 每次 getConnection 都要新建物理连接,频繁结账时性能会很差。最省事的改造是上 Druid 连接池,原理是启动时预创建一批连接反复复用。改法很简单,写一个 druid.properties 放进 src 目录,然后替换 DBUtil 的实现:
url=jdbc:mysql://localhost:3306/supermarket?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=utf8 username=root password=123456 initialSize=5 maxActive=20 maxWait=10000public class DBUtil { private static DataSource dataSource; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new RuntimeException("初始化连接池失败", e); } } public static Connection getConnection() throws SQLException { return dataSource.getConnection(); } }所有 DAO 的调用方只认 DBUtil.getConnection(),所以改完这一处,全项目无感切换。initialSize 是初始连接数,maxActive 是上限,maxWait 是拿不到连接时的等待毫秒数,这三个参数在演示时拿出来说,比一句“连接池更快”有说服力。
6.2 把结账事务包进同一个 Connection
3.1 里的结账代码注释写了“简化了事务”,进阶就该把它补上。扣库存和写销售记录必须同生共死,用 setAutoCommit(false) 手动控制:
Connection conn = DBUtil.getConnection(); try { conn.setAutoCommit(false); productDao.updateStockWithConn(conn, pid, count); // 扣库存 saleDao.insertWithConn(conn, record); // 写销售记录 conn.commit(); } catch (Exception e) { conn.rollback(); throw new RuntimeException("结账失败,已回滚", e); } finally { conn.setAutoCommit(true); conn.close(); }注意原来的 DAO 方法内部自己开连接,这里要改成把 conn 传进去的重载方法,事务才能共享同一个连接。这是“事务收拢”的标准做法,答到这一步,评委就知道你不只是把代码跑通了。
6.3 答辩前必过的验证清单
| 检查项 | 做法 |
|---|---|
| 换库能跑 | 另一台机器导入 SQL、改连接串,全流程重走一遍 |
| 金额精度 | 结账多件商品,确认无浮点尾差 |
| 权限越权 | 用收银员账号直接敲 admin 页面 URL,应被拦回登录页 |
| 库存边界 | 结账数量超过库存时,有提示且不写销售记录 |
| 中文编码 | 中文商品名、会员名、供应商名增删改查全部走一遍 |
从那以后我每次接手 Java 课设源码,都强制先按这张清单把全流程跑一遍再动代码——百分之八十的“玄学报错”,最后都落在这五个固定点上,跑通了才敢说这套源码是真正可用的。希望帮到你。
本文还有配套的精品资源,点击获取