news 2026/10/15 5:34:14

Java超市管理系统实训报告:从表结构到事务处理

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java超市管理系统实训报告:从表结构到事务处理

简介:这是一份基于Java的超市管理系统实训报告文档,面向高校计算机相关专业正在进行课程设计或数据库实训的学生,可用于学习项目报告的完整结构与系统功能设计思路,无论用于整体参照还是局部借鉴都较为合适。报告以超市实际业务为场景,围绕用户管理、商品管理、库存管理、销售记录管理等核心模块展开,完整覆盖课程设计的目的、背景与意义,系统需求分析和可行性分析,开发技术及运行环境,系统结构框架与功能设计,数据库E-R模型和表结构设计,以及测试优化与实训结论,对商品信息维护、库存数量更新、销售记录查询等常见操作的业务流程也有清晰说明,能够呈现数据库系统从需求到实现的全过程。资源为1个Word格式的doc文档,压缩包总大小411KB,文档结构紧凑、内容集中,适合对照自身项目进行修改与补充。目前已有5778人浏览学习,说明该报告对同类课题有较强的参考价值。读者借助这份报告,既可以理解超市管理系统各模块的业务逻辑和数据库建模方法,也能借鉴课程设计报告的分章技巧与规范化表述,从而提升实训报告的完成质量。

1. 「java超市管理系统实训报告」这个题目在考什么

如果你在搜索引擎里反复翻看「java超市管理系统实训报告」这个标题,多半是课程设计或毕业设计排到了这个选题。它几乎是Java方向最常驻的课设题目——听起来简单,但真正做完、能把报告写厚的人不多。做失败了的大多数人不是卡在Java语法上,而是卡在数据库表和事务没想清楚,界面写了一半就推倒重来。

我在现实中带过不少做这个题的初学者,也看过很多份实训报告和答辩现场。一个反直觉的结论是:这份报告里最值钱的部分不是代码行数,而是表结构设计和对一笔销售单的处理逻辑——库存减没减、订单明细记没记、钱找零找得对不对,这些才是老师和面试官真正会追问的细节。

这篇文章我会按一个能落地的完整方案来讲:从数据库表结构怎么定、Java核心代码怎么写、MySQL连接参数怎么配,到常见的翻车现场和答辩验收清单。你跟着走一遍,能得到一套可以直接交作业的源码骨架,也能知道报告里哪些地方该多写几句。

2. 先把库存和订单的表结构定死:超市系统数据库设计

很多人的实训翻车是从「先写界面再建表」开始的。界面调来调去,表结构只能跟着界面凑,最后商品、订单、库存混在一张表里,查询写得乱七八糟。做超市管理系统,正确的顺序一定是先定表,再写代码。

2.1 实体与关系:为什么订单表必须拆成主表+明细表

超市系统里最基本的业务对象是:商品、库存、订单、订单明细,再加一个操作员或者用户。最关键的建模决策是订单不能只建一张表。如果一张表里同时存订单号和一堆商品,每种商品都要重复一遍订单号、操作员、时间,数据冗余严重,而且想统计「某个商品卖了多少」会非常痛苦。

正确做法是拆成主表和明细表。主表存一笔销售订单的整体信息:单号、收银员、总金额、支付金额、找零、时间;明细表存这笔单里每件商品的信息:商品ID、商品名快照、单价、数量、行金额。主表和明细表通过订单号关联,这是一对多的关系。商品表和库存可以合并成一张表,但要把价格和库存数量分开考虑字段类型。

设计时我建议表名统一用「前缀_业务名」的方式,比如t_goods、t_stock、t_order、t_order_item,前缀t_表示表,避免和Java关键字或MySQL保留字冲突。如果你用MySQL 8,库名里要注意下划线在Linux环境下的写法,建议全程小写。

2.2 建表SQL:四张核心表的字段类型与企业惯例

下面是我常用的建表脚本,你可以直接拿去改。字段类型上我特意说明了为什么选这种类型,因为这些细节经常会出现在实训报告的答辩追问里。

CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE supermarket; CREATE TABLE t_goods ( goods_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '商品ID', goods_code VARCHAR(32) NOT NULL COMMENT '商品编码,唯一', goods_name VARCHAR(100) NOT NULL COMMENT '商品名称', category VARCHAR(50) DEFAULT '未分类' COMMENT '商品分类', sale_price DECIMAL(10,2) NOT NULL COMMENT '售价', purchase_price DECIMAL(10,2) NOT NULL COMMENT '进价', stock_qty INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '当前库存', stock_low_limit INT UNSIGNED NOT NULL DEFAULT 10 COMMENT '库存预警下限', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_code (goods_code) ) COMMENT='商品表'; CREATE TABLE t_order ( order_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '订单编号', cashier_id BIGINT NOT NULL COMMENT '收银员/操作员ID', total_amount DECIMAL(10,2) NOT NULL COMMENT '订单总额', discount_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '优惠金额', pay_amount DECIMAL(10,2) NOT NULL COMMENT '实收金额', change_amount DECIMAL(10,2) DEFAULT 0.00 COMMENT '找零', order_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) ) COMMENT='销售单主表'; CREATE TABLE t_order_item ( item_id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT '关联主表单号', goods_code VARCHAR(32) NOT NULL, goods_name VARCHAR(100) NOT NULL COMMENT '商品名快照', unit_price DECIMAL(10,2) NOT NULL COMMENT '成交单价', quantity INT UNSIGNED NOT NULL COMMENT '购买数量', line_amount DECIMAL(10,2) NOT NULL COMMENT '行金额', KEY idx_order_no (order_no) ) COMMENT='销售单明细表'; CREATE TABLE t_user ( user_id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL COMMENT '存SHA-256哈希,不存明文', role_name VARCHAR(20) DEFAULT 'cashier' COMMENT '角色' ) COMMENT='操作员表';

几个关键设计点说下。

金额字段一律用DECIMAL(10,2),不要用float或double。浮点数在计算金额时会产生精度问题,比如 19.9 存到double里实际是 19.8999996,打印出来没问题,求和之后就会差几分钱。实训报告里能写出「金额用 DECIMAL 是为了避免浮点误差」这句话,会让报告上一个档次。

order_no我单独做了唯一索引,业务上要求每一笔销售单号不能重复;单号生成规则在代码里用「时间戳 + 序号」拼,保证并发时也没问题。t_order_item里存了goods_name快照,这是个容易忽略的细节:如果以后商品改名了,历史订单里的商品名不能被改掉,所以明细里必须留一份名字副本。

2.3 主键、外键与逻辑删除:实训报告里写不写都算分的细节

主键我用的BIGINT AUTO_INCREMENT,理由很简单:数据量到千万级的时候 INT 可能不够用,而且课设数据量不大,自增主键写起来方便、索引效率也最高。这里不要用 UUID 做主键,UUID 是随机字符串,在InnoDB里做主键会导致叶子节点不断分裂,写性能明显下降;哪怕实训数据量小看不出差别,被问起来你答不上原理就扣分了。

外键这一项,我见到的公司项目里基本都不建物理外键,而是靠程序控制关联关系。原因主要有两个:一是物理外键会让批量插入和导数据变慢,二是删数据时外键约束容易把操作卡死。但实训报告里一般会要求画表关系图,为了图好看,可以在建表时加上CONSTRAINT fk_order FOREIGN KEY (order_no) REFERENCES t_order(order_no),如果不能跑就说明理由不改也行。答辩时能说出「物理外键影响性能,所以用程序保证一致性」,这是加分项。

逻辑删除也建议做。超市的商品不是删掉就没了,可能只是下架,所以t_goods里可以再加一个is_deleted TINYINT DEFAULT 0字段。查询时强制WHERE is_deleted = 0,删除时改成 UPDATE 而不是 DELETE。这种写法在报告里写明「防止误删导致历史单据无法追溯」,很能体现工程意识。

3. 核心代码这样写:登录、商品维护与收银结算

表结构定好之后,代码就剩下四块:登录、商品维护、收银结算、进销存报表。我这里用纯 JDBC + Swing 讲,这是实训报告里最稳妥、老师最容易验证的组合;不需要上框架,骨架清晰,也方便你往 Spring Boot 方向扩展。

3.1 登录模块:PreparedStatement 与 SQL 注入的常见课堂误区

登录模块是所有系统都有的功能,但很多实训代码里还习惯用Statement拼字符串,这是老师重点挑刺的地方。看下面这段:

// 错误写法:字符串拼接 String sql = "SELECT * FROM t_user WHERE username = '" + name + "' AND password = '" + pwd + "'"; // 正确写法:参数化查询 String sql = "SELECT * FROM t_user WHERE username = ? AND password = ?"; PreparedStatement ps = conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, encryptedPwd); ResultSet rs = ps.executeQuery();

逻辑说明:PreparedStatement会把参数通过setString绑定,而不是直接拼进SQL里,MySQL服务端拿到的是完整的预编译语句,参数值只当作字面量处理。比如输入abc' OR '1'='1,在拼接写法里会变成查询全部用户的语句,直接绕过密码;参数化写法下这个字符串只会被当成一个普通用户名去匹配,查不到记录。

参数说明:encryptedPwd在这里是哈希过后的密码。所以注册和登录时都把明文密码用SHA-256转成64位十六进制字符串再存储或比对。为什么不能存明文?因为一旦数据库文件泄露,所有用户的密码直接暴露,而哈希后至少不能反推出原文。实训报告里要写明这一点,评语里常出现「安全意识不足」的批评就能避开。

登录成功后把user_id存进当前会话对象,收银记录里要用它作为cashier_id。

3.2 商品维护:分页查询与下拉分类联动的 Swing 写法

商品维护界面最常见的需求是:表格展示商品列表,支持按名称或编码模糊搜索,分类下拉框选择后表格跟着过滤。这里我建议从 DAO 层抽两个方法:queryGoods(keyword, category, pageNo, pageSize)和countGoods(keyword, category),返回总量供分页计算。

public List<Goods> queryGoods(String keyword, String category, int pageNo, int pageSize) { StringBuilder sql = new StringBuilder(); sql.append("SELECT goods_id, goods_code, goods_name, category, sale_price, ") .append("purchase_price, stock_qty, stock_low_limit ") .append("FROM t_goods WHERE is_deleted = 0 "); List<Object> params = new ArrayList<>(); if (keyword != null && !keyword.trim().isEmpty()) { sql.append(" AND (goods_name LIKE ? OR goods_code LIKE ?) "); params.add("%" + keyword.trim() + "%"); params.add("%" + keyword.trim() + "%"); } if (category != null && !category.isEmpty()) { sql.append(" AND category = ? "); params.add(category); } sql.append(" ORDER BY goods_id DESC LIMIT ? OFFSET ?"); params.add(pageSize); params.add((pageNo - 1) * pageSize); try (PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } // 执行并映射为 List<Goods>,代码略 } }

逻辑说明:搜索词用LIKE ?配合%包裹,是因为用户输入的是一个词而不是完整商品名;分类过滤用精确匹配,因为分类下拉框里的值来自数据库里DISTINCT category的查询结果。LIMIT 和 OFFSET 是 MySQL 分页的标准写法,OFFSET是跳过的行数。

参数说明:pageNo从 1 开始,LIMIT ? OFFSET ?里 OFFSET 要用(pageNo - 1) * pageSize计算,这是新手特别容易写错的地方——页面上显示的「第2页」指的是跳过第一页的所有数据,不是把pageNo直接传给 OFFSET。params列表里最后一个setObject的类型取决于数据库列类型:LIMIT参数用setInt更稳妥,JDBC 驱动对setObject解析成Integer一般能正常工作,但碰到老版本驱动可能报类型不匹配。

分类下拉框的联动其实是个特别小的逻辑,但能体现你是否理解了界面和数据的层次关系。分类里加一个「全部分类」选项,其值为空字符串;选择时判断category.isEmpty()就跳过条件。商品修改时的库存数字,一定要读stock_qty之后在内存里先校验不能小于销量再发 UPDATE,别把校验只写在界面上。

3.3 收银结算:一次销售单要串起来的五步事务代码

收银是整个系统的核心。用户选完商品、输入收款金额、点击结算,后端要做五件事:生成订单号、插入主表、插入明细、扣减库存、计算找零。这五步要么全成功,要么全回滚,这就是为什么必须用事务。

public boolean checkout(List<SaleItem> cart, BigDecimal receivedAmount) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String orderNo = generateOrderNo(); BigDecimal total = BigDecimal.ZERO; for (SaleItem item : cart) { total = total.add(item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } BigDecimal change = receivedAmount.subtract(total); if (change.signum() < 0) throw new BusinessException("收款金额不足"); // 1. 插入销售单主表 String insertOrder = "INSERT INTO t_order(order_no, cashier_id, total_amount, pay_amount, change_amount) VALUES(?,?,?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(insertOrder)) { ps.setString(1, orderNo); ps.setLong(2, currentUserId); ps.setBigDecimal(3, total); ps.setBigDecimal(4, receivedAmount); ps.setBigDecimal(5, change); ps.executeUpdate(); } // 2. 插入明细 + 3. 扣减库存,按购物车循环 for (SaleItem item : cart) { String insertItem = "INSERT INTO t_order_item(order_no, goods_code, goods_name, unit_price, quantity, line_amount) VALUES(?,?,?,?,?,?)"; try (PreparedStatement ps = conn.prepareStatement(insertItem)) { ps.setString(1, orderNo); ps.setString(2, item.getGoodsCode()); ps.setString(3, item.getGoodsName()); ps.setBigDecimal(4, item.getUnitPrice()); ps.setInt(5, item.getQuantity()); ps.setBigDecimal(6, item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); ps.executeUpdate(); } String reduceStock = "UPDATE t_goods SET stock_qty = stock_qty - ? , update_time = NOW() WHERE goods_code = ? AND stock_qty >= ?"; try (PreparedStatement ps = conn.prepareStatement(reduceStock)) { ps.setInt(1, item.getQuantity()); ps.setString(2, item.getGoodsCode()); ps.setInt(3, item.getQuantity()); int rows = ps.executeUpdate(); if (rows == 0) throw new BusinessException("商品库存不足:" + item.getGoodsName()); } } conn.commit(); return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("结算失败:" + e.getMessage()); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } }

逻辑说明:setAutoCommit(false)之后,后面所有 SQL 都处于未提交状态;只要任何一个步骤抛出异常,rollback()会把前面已经执行的操作全部撤销。这里最容易忽略写的是库存扣减的AND stock_qty >= ?条件——它能在数据库层面挡住并发问题:两个收银台同时卖最后一个商品时,两个事务都读到库存 1,如果只用 UPDATE 不判断条件,库存可能变成 -1;加了>= 0判断后,第二个 UPDATE 影响行数为 0,就能触发库存不足异常,而不是把库存扣成负数。

参数说明:receivedAmount是顾客给的钱,可能是 100 元整钞也可能是 99.5 元,所以用BigDecimal接收。change.signum() < 0判断的是找零是否为负数,signum()返回 -1 表示小于零,等价于compareTo(BigDecimal.ZERO) < 0。这里不需要比较字符串。

订单号生成通常用时间戳加序号:

private String generateOrderNo() { return "SO" + new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + String.format("%04d", (int)(Math.random() * 10000)); }

SO前缀是销售单的英文缩写,后面 14 位时间精确到秒,最后 4 位随机补足够唯一性。并发很高时随机可能碰撞,但实训系统这个并发量级足够用。想更保险,可以改成查SELECT COUNT(*) FROM t_order WHERE order_no LIKE '前缀%',但没必要。

3.4 库存预警清单:把 SQL 当查询业务用的习惯

库存管理除了出入库记录,日常用得最多的就是预警清单:在首页表格里标出库存低于预警下限的商品。核心思路是查询时把当前库存和下限放在一起比较,而不是在 Java 里循环判断。

String sql = "SELECT goods_code, goods_name, category, stock_qty, stock_low_limit, " + "(stock_low_limit - stock_qty) AS shortage_qty " + "FROM t_goods WHERE is_deleted = 0 AND stock_qty < stock_low_limit " + "ORDER BY shortage_qty DESC";

这条 SQL 只用到了字段的比较运算,没有额外函数开销,在十万级数据量下扫全表也不会有明显压力。如果数据量大了,(stock_low_limit - stock_qty)这种计算列不能用索引加速,但预警查询本身频率低,可以接受。实操时可以把预警阈值做成在t_goods里单独一列,让你处理「不同商品不同预警线」的需求时不用改代码。

4. 连接层与实训报告里的参数坑:JDBC、编码与时区

数据库和表结构有了,代码也写了,剩下就是连接层。实训中相当一部分「运行不起来」的问题,根源都在 JDBC 连接参数上;这不是代码逻辑问题,而是环境和驱动配置问题。

4.1 最小 JDBC 工具类:连接串三个必配参数

先看一个能跑通 MySQL 8 的最小工具类骨架:

public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; private static Connection conn = null; public static Connection getConnection() throws SQLException { if (conn == null || conn.isClosed()) { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new SQLException("MySQL驱动未找到,请检查mysql-connector-java的jar包是否导入", e); } conn = DriverManager.getConnection(URL, USER, PASSWORD); } return conn; } }

三个必配参数说明:

  • useSSL=false:本地开发不需要 SSL 加密连接,不写这个参数 MySQL 8 默认会用 SSL 连接,有些环境会报SSL connection error。线上部署时建议改成useSSL=true并配证书,实训环境保持 false 即可。
  • serverTimezone=Asia/Shanghai:MySQL 8 的驱动要求必须在连接串里指定时区,否则报The server time zone value '�й���ʱ��'之类的乱码时区错误。这个报错是新手最容易懵的地方——乱码是控制台编码问题,根本原因其实是时区没配。
  • characterEncoding=utf8:保证从数据库读出的中文不乱码。注意 MySQL 的utf8mb4才是完整支持所有中文和 emoji 的编码,utf8mb4是utf8的超集。连接串里写characterEncoding=utf8能兼容大部分场景;如果发现某些生僻字或 emoji 变成?,就把连接串改成characterEncoding=utf8mb4。

allowPublicKeyRetrieval=true是 MySQL 8 特有的参数。使用caching_sha2_password认证时,客户端无法从服务端直接获取公钥做 RSA 加密,必须显式允许公钥获取。很多实训环境报Public Key Retrieval is not allowed就是缺这个参数。5.7 及以下的驱动用不到,无妨。

4.2 MySQL 8 驱动版本与驱动类名的对应

JDBC 驱动的 jar 包是mysql-connector-java,在 Maven 仓库里它的坐标是mysql:mysql-connector-java:8.0.x。5.x 系列的驱动类名是com.mysql.jdbc.Driver,到了 8.x 变成com.mysql.cj.jdbc.Driver。

实训项目里最常见的包冲突场景:IDE 默认带了一个老版本连接驱动,自己又手动导了新版mysql-connector-java-8.0.x.jar。运行时报ClassNotFoundException: com.mysql.cj.jdbc.Driver,一看类名明明对,其实是 IDE 构建路径里有重复依赖,旧驱动被优先加载了。处理办法是把项目里的旧驱动移除,只保留一个。

如果你用的是 Maven 工程,推荐在pom.xml里显式声明版本,别用父类间接传递的版本。不确定版本时看 MySQL 服务端版本号:SELECT VERSION();如果是 8.0.3x,驱动就用 8.0.3x 对应或更新的即可。

4.3 实训环境里常见的三种连接失败现象对照

现象原因解决
Communications link failure后接Public Key Retrieval is not allowed缺少allowPublicKeyRetrieval=true连接串加上该参数
中文显示为??或控制台乱码库表字符集与连接串编码不一致建库时用utf8mb4,连接串加characterEncoding=utf8mb4
Server returns invalid timezone缺少serverTimezone或配错时区加serverTimezone=Asia/Shanghai

还有一类「现象不报错、结果不对」的问题:执行 INSERT 时中文正常,但查出来是空的或者长度不对。这通常是字段长度过短,比如VARCHAR(10)存了 5 个汉字再加标点截断了。MySQ 的VARCHAR(10)表示 10 个字符而非 10 个字节,utf8mb4 下一个汉字占 4 字节但字符数算 1,这个问题不算编码问题,是长度设计问题。建表时商品名给VARCHAR(100),地址类字段给VARCHAR(255),基本不会踩。

驱动版本差异上,最容易被忽略的是 5.7 的库用 8.x 驱动连,连接串里serverTimezone不能省,而 8.0.23 之后的驱动把com.mysql.jdbc.Driver标记为 deprecated,虽然还能用但会告警。实训报告里如果写了「采用mysql-connector-java 8.x驱动」,连接串的写法必须匹配 8.x 规范。

5. 超市系统实训避坑合集:5 个常见翻车现场

这一章把我在辅导和检查中见过最多的踩坑记录整理成清单。每一条都是真实的翻车现场,按「现象 → 原因 → 解决」写,你对照着排查能省下大量调试时间。

踩坑一:库存变成负数

现象:连续卖出同一商品多次后,t_goods表的stock_qty变成 -2,但前台的商品列表还能正常结算。

原因:收银代码里只做了「先 SELECT 查库存,再 UPDATE 减库存」的逻辑,没有在 UPDATE 时加库存条件。两个收银窗口同时提交时,A 事务查询库存为 1,B 事务也查询到 1,A 减成 0,B 减成 -1,数据库层面没有任何阻挡。

解决:把库存扣减改成单条 UPDATE,加AND stock_qty >= ?条件,影响行数为 0 时抛异常回滚。这就是第 3.3 节reduceStock的写法背后的意义。实训报告里要写清楚这一段,这是并发控制的经典场景。

踩坑二:订单删了,库存没加回来

现象:在管理界面手工删除某一笔销售单后,商品库存没有恢复,盘点对不上。

原因:删除订单的代码只执行了DELETE FROM t_order WHERE order_no = ?,没管的明细表,也没回补库存。

解决:删除订单要包在事务里,先查明细表拿到每种商品的购买数量,然后UPDATE t_goods SET stock_qty = stock_qty + 数量 WHERE goods_code = ?,最后才删主表和明细。少任何一步都会让数据不一致。如果老师答辩时问你「订单删除怎么保证一致性」,这正是标准答案。

踩坑三:金额汇总总是差几分钱

现象:报表里统计日销售额,计算出来的结果和实际收银台的金额现金记录对不上,差 0.01 ~ 0.05 元。

原因:使用double或float累加金额。0.1 + 0.2 在浮点数里不是 0.3,而是一个无限接近 0.30000000000000004 的值,累加 N 次后误差累计,报表差了分币。

解决:实体类里金额字段全部用BigDecimal,SQL 里SUM(line_amount)返回的也是BigDecimal,加总时用add()而不是+。数据库层面把金额列用DECIMAL(10,2),不要在 Java 侧做任何浮点转换。这是报告中必须出现的「你为什么选 DECIMAL 而不选 double」的回答素材。

踩坑四:Products 页面刷新后商品不显示

现象:新增商品后,回到列表页看不到刚加的数据,但数据库中能查到。

原因:Swing 里用了默认的Statement查询,列表绑定的TableModel是旧对象的引用。新增操作完成之后没有重新执行查询并setModel()刷新表格。

解决:增删改操作完成后,重新调用queryGoods并把返回的List<Goods>封装成新的DefaultTableModel,再table.setModel(model)。更规范的做法是封装成一个refreshTable()方法,所有数据变更结束后统一调用。这个问题在报告里写「界面与数据模型不同步」是我见过最多的功能类 bug,代码评审时必问。

踩坑五:MySQL 连接长期不关导致连接耗尽

现象:运行一段时间后突然所有操作都报Too many connections,重启应用又恢复正常。

原因:代码里大量使用conn.createStatement()查完没有close(),连接池没有被正确回收。哪怕程序里写了finally块反复关闭,但有一次漏了,连接就泄漏一个。

解决:统一的规范是「谁打开谁关闭」,用 try-with-resources 保证PreparedStatement和ResultSet自动关闭。连接对象要么在 DAO 方法内部生成并在 finally 里关闭,要么用连接池管理;最重要的一条是写一个全局的close(conn, ps, rs)工具方法,所有查询都过它,不要各处手写关闭逻辑。推荐在最后一种常见做法是try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) {}这种嵌套写法,标准且不会忘。

踩坑六:模糊搜索查不到带引号的商品名

现象:商品名里有英文单引号,比如O'Reilly 薯片,搜索时直接报 SQL 语法错误。

原因:查询还是字符串拼接的老写法,单引号把 SQL 语句的字符串边界打断了。

解决:全部改成PreparedStatement参数化查询。这个问题不只是安全问题,直接是功能不可用。如果你在报告里写了「使用 PreparedStatement 防止 SQL 注入」,这里就是最好的佐证。

6. 验收与答辩:让老师相信这是你写的东西

实训报告的口头答辩环节,老师最常问的往往不是「用什么框架」,而是「假如我要改某个需求,你从哪里改」。这个问题的本质是考察你对自己系统的结构有没有整体认知。下面这份清单我建议你在提交前逐项核对。

功能验收清单:

模块验收动作通过标准
登录输入错误密码提示错误且不进入主界面
商品维护新增/修改/删除商品列表实时刷新,删除后搜索不出现
收银购买2件商品,收款100元订单总额、找零正确,库存同步减少
报表统计今日销售总额汇总金额精确到分
库存预警把库存调到低于预警线列表中标红并显示短缺数量

答辩前建议亲手验证这几个场景:第一,下单后立刻去数据库查t_goods.stock_qty,确认是减过的;第二,把订单总额改成 0.01 后执行找零,确认不会出现负数差错;第三,重启程序后登录,确认数据还在。这三步能暴露大部分事务和状态问题。

如果做的是 Swing 桌面版,代码上基本是 DAO + Service + View 三层。答辩被问「怎么改成 Web 版」时,标准回答是「业务逻辑在 Service 层,不依赖任何 Swing 组件;把 View 层换成浏览器页面,再把 DAO 层换成 Spring 的 JdbcTemplate,Service 层几乎不用动」。要做到这个回答成立,写代码时就必须把 SQL 全部收敛在 DAO 层,不在界面代码里出现任何 JDBC 语句。这一点我从实训第一天就这么要求自己,后来的项目迁移才能那么顺。

最后聊一个进阶方向,如果你学有余力但不想换掉整个系统:把库存表和订单明细表的更新逻辑抽成存储过程。MySQL 的存储过程能把你现在的「插入订单 + 扣库存」合并成一个CALL checkout_order(...)调用,事务处理放数据库端,Java 只传递参数。这个方案性能比逐条 SQL 好,也更接近真实企业的做法。但实训报告里写存储过程需要额外说明边界——排错难度会上升,如果存储过程报错,你的 Java 应用只能看到「调用失败」,定位问题要翻数据库日志。

另外一个值得打磨的点是搜索性能。实训数据量小,全表扫描看不出问题;但我见过有人拿真实超市的数据量做压力测试,一万条商品记录下WHERE goods_name LIKE '%关键字%'会扫全表,响应从毫秒级掉到秒级。改进办法是在goods_code上建前缀索引,模糊搜索尽量用左前缀LIKE '关键字%',而不是双百分号。报告里写一句「查询用左前缀匹配以命中索引」,比写十个功能截图的含金量高得多。

我带过很多人做完这个题目,最大教训是:别把实训当成「应付一个课设」,它是你第一次完整地模拟真实系统的机会。表结构、事务、连接管理这些习惯,都是现在养成的。把上面这些点都过一遍再交报告,你会明显感觉到答辩时底气不一样——希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/15 5:33:31

COMSOL弱形式入门:从积分表达式到自定义偏微分方程求解

简介&#xff1a;COMSOL Multiphysics弱形式入门是一份面向仿真工程师与有限元初学者的技术文档&#xff0c;旨在帮助使用者突破内置模块和经典PDE模板的局限&#xff0c;学会用弱形式用户界面求解非线性、多物理场等复杂问题。文档从偏微分方程、能量最小化形式与弱形式三种表…

作者头像 李华
网站建设 2026/10/15 5:33:22

aiopsterm实测:AI终端如何用自然语言重构远程服务器排障

凌晨三点被监控告警震醒&#xff0c;那台跑着核心业务的服务器磁盘使用率过了 95%。我爬起来打开笔记本&#xff0c;SSH 登上去&#xff0c;按老规矩先df -h&#xff0c;再du -sh /var/log/*&#xff0c;一步步定位到底是谁把盘撑爆了。这套动作我闭着眼都能完成&#xff0c;但…

作者头像 李华
网站建设 2026/10/15 5:33:12

逻辑索引实战:用A(A>0.5)告别循环,高效筛选MATLAB数据

写MATLAB的教程&#xff0c;最容易犯的毛病是把语法讲得像字典——查得到&#xff0c;但用不起来。逻辑索引这个知识点我尤其有感触&#xff0c;因为它是MATLAB里"看起来简单&#xff0c;用好了却能大幅提升代码水平"的典型代表。很多人一开始都习惯写循环&#xff0…

作者头像 李华
网站建设 2026/10/15 5:33:08

前端工程师转型Agent开发:从零搭建智能体的工程实践路径

这两年Agent概念火到什么程度呢&#xff1f;我身边做前端的同行&#xff0c;几乎每周都要被“Agent”、“智能体”、“多智能体协作”这些词刷屏。作为一个写了六年业务页面的人&#xff0c;我第一次看到这类项目时心里是一紧的&#xff1a;这玩意儿是不是又要我回去补数学、学…

作者头像 李华
网站建设 2026/10/15 5:32:34

用B端系统思路重构家庭理财:账户体系、领域语言与内存分区实践

谁家还没个做程序员的。去年家庭会议上&#xff0c;我家那位一句话把我整破防了&#xff1a;“你的年终总结写了几十页&#xff0c;家里的账你半年没看过一眼。”话糙理不糙&#xff0c;我确实一直懒得管钱。但这次我没去买记账App&#xff0c;而是干了件很多人觉得小题大做的事…

作者头像 李华
网站建设 2026/10/15 5:32:16

SSH认证与权限管理:从密钥机制到多用户安全配置实践

1. SSH认证机制中的用户角色&#xff1a;密钥、密码与账户的对应关系1.1 密码认证的运转逻辑SSH默认不维护独立的用户数据库&#xff0c;这一点和很多自建系统不一样。你在SSH协议里提到的“用户”&#xff0c;本质上就是服务器操作系统中已经存在的账户。登录时&#xff0c;客…

作者头像 李华