ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java课程设计实战:超市管理系统数据库设计与JDBC事务详解

Java课程设计实战:超市管理系统数据库设计与JDBC事务详解 简介一份面向Java初学者的课程设计资源以超市后台管理为业务场景重点演示Java与MySQL的交互实现。项目不依赖复杂前端通过命令行或文本方式操作便于集中理解面向对象、JDBC编程、SQL增删改查、异常处理等后端核心知识。压缩包共49个文件包含22个Java源码、22个编译后的class文件、MySQL驱动jar包和1个sql数据库脚本整体仅783KBjava源码用于查看业务实现class文件对应编译产物sql脚本提供建表与初始化数据jar包解决JDBC连接依赖目录按dao、db、po、view、main等模块分层结构清晰适合直接导入IDE学习或作为课设参考。已有1379人学习下载说明其对同类课设题目有较好的借鉴价值。通过阅读源码与导入SQL脚本可掌握从数据表设计、数据库连接到业务逻辑实现的完整流程还能了解多线程、日志记录、设计模式等进阶话题。1. 超市管理系统这门JAVA课设到底在考你什么超市管理系统是Java课程设计里出场率最高、也最容易做成“空壳”的题目功能清单看起来只是对一张商品表做增删改查但真动手就会发现它同时覆盖了面向对象设计、JDBC数据库操作、图形界面事件监听、事务和SQL组织几乎把Java SE的核心考点一次串完。这个题目适合两类人刚学完Java基础、想用一个完整项目打通知识点的初学者以及想把这门课设做成简历项目的应届生。它的难点不在界面多华丽而在进销存业务有严格的先后顺序——进货要先改库存销售要先判断库存够不够退货要回滚库存。把业务顺序理清楚了再动键盘系统才不是一堆按钮的堆砌。2. 12张表拆出超市的进销存底账进货单为什么必须拆成主表和明细课设翻车最常见的原因不是代码写不出来而是数据库表设计得不对。我看到很多同学把商品的名称、库存、供应商、进价全塞进一张表结果每次进货都要 UPDATE 商品表的库存字段历史进价丢失退货时算不清成本。这里我先给出一个可以直接抄的12张表设计再重点讲清楚最容易错的两处。2.1 主数据表商品、库存、分类、供应商、员工与账号主数据表负责回答“店里有什么东西”建议拆成六张互相之间用外键关联表名作用关键字段t_category商品分类category_id, category_namet_goods商品主数据goods_id, goods_code, goods_name, category_id, sale_price, purchase_price, statust_stock当前库存stock_id, goods_id, quantity, warn_linet_supplier供应商supplier_id, supplier_name, contact_phonet_employee员工与登录账号employee_id, employee_name, role, login_name, passwordt_member会员member_id, member_name, phone, points三个设计点值得写进课设报告。第一商品和库存分开。商品表管“是什么”库存表管“有多少”以后门店扩张、加仓库只需在库存表加一个 warehouse_id 字段商品表完全不动。第二金额用 DECIMAL 不用 double。float/double 是二进制浮点0.1 加 0.2 会得到 0.30000000000000004金额算错在答辩现场非常尴尬。Java 里对应 BigDecimal数据库里对应 DECIMAL(10,2)。第三密码字段不要用明文课设阶段用 MD5 存一个摘要就能体现你的安全考虑。建库和商品侧表脚本如下MySQL 5.7 以上通用CREATE DATABASE IF NOT EXISTS supermarket_db DEFAULT CHARACTER SET utf8mb4; USE supermarket_db; CREATE TABLE t_category ( category_id INT NOT NULL AUTO_INCREMENT COMMENT 分类主键, category_name VARCHAR(32) NOT NULL COMMENT 分类名称, PRIMARY KEY (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品分类; CREATE TABLE t_goods ( goods_id INT NOT NULL AUTO_INCREMENT COMMENT 商品主键, goods_code VARCHAR(20) NOT NULL UNIQUE COMMENT 商品条码扫码枪录入, goods_name VARCHAR(64) NOT NULL COMMENT 商品名称, category_id INT NOT NULL COMMENT 分类外键, spec VARCHAR(32) DEFAULT COMMENT 规格如500ml, purchase_price DECIMAL(10,2) NOT NULL COMMENT 进价, sale_price DECIMAL(10,2) NOT NULL COMMENT 售价, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (goods_id), KEY idx_category (category_id), CONSTRAINT fk_goods_category FOREIGN KEY (category_id) REFERENCES t_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品主数据; CREATE TABLE t_stock ( stock_id INT NOT NULL AUTO_INCREMENT, goods_id INT NOT NULL UNIQUE COMMENT 一个商品一条库存记录, quantity INT NOT NULL DEFAULT 0 COMMENT 当前库存数量, warn_line INT NOT NULL DEFAULT 10 COMMENT 低于此值预警, PRIMARY KEY (stock_id), CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES t_goods (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT实时库存;为什么商品主键选自增 INT而不是直接用条码当主键条码虽然是唯一业务键但超市里无码商品、生鲜称重商品很多条码可能为空或一码多品自增主键让业务键和物理主键分离以后改条码不影响外键关系。UNIQUE 约束仍然保证了条码不重复。2.2 流水表进货、销售、退换货主表加明细两张表第二类表是流水表记录“发生了什么事”。以销售为例销售单是“某时刻某收银员卖了一批货”这是主表这批货里有三种商品、每种几件、各卖多少钱这是明细表。一张主表对应多张明细表这种结构叫主从表是进销存系统的标准建模方式。CREATE TABLE t_sale ( sale_id BIGINT NOT NULL AUTO_INCREMENT COMMENT 销售单主键, sale_no VARCHAR(24) NOT NULL UNIQUE COMMENT 单号程序生成, employee_id INT NOT NULL COMMENT 收银员, member_id INT DEFAULT NULL COMMENT 会员可空, total_amount DECIMAL(12,2) NOT NULL COMMENT 应收总额, sale_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (sale_id), KEY idx_sale_time (sale_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售单主表; CREATE TABLE t_sale_item ( item_id BIGINT NOT NULL AUTO_INCREMENT, sale_id BIGINT NOT NULL COMMENT 所属销售单, goods_id INT NOT NULL COMMENT 商品, quantity INT NOT NULL COMMENT 数量, price DECIMAL(10,2) NOT NULL COMMENT 成交单价快照价格, subtotal DECIMAL(10,2) NOT NULL COMMENT 小计数量*单价, PRIMARY KEY (item_id), KEY idx_sale_item_sale (sale_id), CONSTRAINT fk_sale_item_sale FOREIGN KEY (sale_id) REFERENCES t_sale (sale_id), CONSTRAINT fk_sale_item_goods FOREIGN KEY (goods_id) REFERENCES t_goods (goods_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT销售单明细;这里要理解三个容易被问倒的点。第一明细表里冗余了 price 和 subtotal 两个快照字段。商品售价以后会调但已成交单据不能跟着商品表变否则历史对不上账退货时也要按成交价退不是按最新售价退。第二主表主键用 BIGINT。流水表几十万行在真实系统里不算多但 INT 上限约 21 亿对流水表来说不值得赌BIGINT 几乎不增加成本。第三sale_no 不是自增主键而是程序生成的“日期序号”比如 S202506121001。自增主键给内部关联用单号给人看、给售后核对用语义要稳定。进货表 t_purchase / t_purchase_item 和退货表结构完全对称主表有 purchase_no、supplier_id、employee_id、total_amount、purchase_time明细表有 purchase_id、goods_id、purchase_price、quantity、subtotal。不建议把退货做成“负数销售”那样销售报表的 SUM 统计会变得很难解释维护成本反而更高。2.3 初始化数据登录账号、分类和库存预警值一次配齐表建好后先写初始化脚本否则界面起来一片空边调试边插数据很费劲。分类和商品之间有外键插入顺序必须是先分类后商品。INSERT INTO t_category (category_name) VALUES (饮料), (零食), (生鲜), (日用品); INSERT INTO t_goods (goods_code, goods_name, category_id, purchase_price, sale_price) VALUES (6901001000011, 矿泉水500ml, 1, 0.80, 1.50), (6901001000028, 薯片原味, 2, 3.20, 4.50), (6901001000035, 苹果称重, 3, 4.00, 5.98); INSERT INTO t_stock (goods_id, quantity, warn_line) VALUES (1, 120, 20), (2, 88, 15), (3, 60, 10);warn_line 默认 10但不同品类应该不同生鲜损耗大、补货周期短建议调到 20 到 30日用品周转慢10 就够。用商品表的 category_id 来批量调整比一条一条改更省事。登录账号这步建议也放进初始化脚本直接用 MD5 摘要存密码别把明文丢进数据库。3. JDBC连接与DAO层让Swing界面和MySQL不再互相折腾3.1 用DBUtil统一管理连接驱动加载、连接获取、资源关闭先交代选型理由课设的 Swing JDBC 路线重点在于打通“界面 → DAO → 数据库”这条链路不要一上来就引 HikariCP 或 Druid 连接池。原因有三一是课设源码就是给老师看的纯 JDBC 每一行都敢拿出来讲二是连接池的配置项最大连接数、超时时间、隔离级别对你现在是个黑匣子被追问反而答不上三是等答辩完要升级简历项目时把 DBUtil 换成 Spring 配置是最自然的一步。我一般按“先用 DriverManager 跑通再讲连接池为什么不省心”的顺序来写。环境方面确认 JDK 8 或 11 已装好JAVA_HOME 和 PATH 配完先跑java -version避免后面报错分不清是代码问题还是环境问题。项目里新建 lib 目录放 MySQL 驱动 jar8.x 版本IDEA 里右键 Add as Library。配置文件放 src 根目录jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456DBUtil 工具类import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.ResultSet; import java.sql.SQLException; import java.sql.Statement; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try { // 从 classpath 根目录读配置打包成 jar 后一样能读到 InputStream in DBUtil.class.getClassLoader() .getResourceAsStream(db.properties); Properties props new Properties(); props.load(in); Class.forName(props.getProperty(jdbc.driver)); url props.getProperty(jdbc.url); username props.getProperty(jdbc.username); password props.getProperty(jdbc.password); } catch (Exception e) { // 静态初始化失败要立刻暴露不要等到点按钮才报错 throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } public static void close(Connection conn, Statement stmt) { close(conn, stmt, null); } public static void close(Connection conn, Statement stmt, ResultSet rs) { if (rs ! null) { try { rs.close(); } catch (SQLException e) { } } if (stmt ! null) { try { stmt.close(); } catch (SQLException e) { } } if (conn ! null) { try { conn.close(); } catch (SQLException e) { } } } }代码逻辑说明静态代码块在类第一次加载时执行一次驱动注册、配置读取都放这里。如果 db.properties 找不到getResourceAsStream 返回 nullprops.load 抛 NPE外层包成 ExceptionInInitializerError程序启动就报错而不是运行到登录按钮才炸出来排错成本低很多。用 classloader 读配置而不是new FileInputStream(db.properties)是因为课设最后常要导出可执行 jar相对路径在 IDE 里正常、在 jar 里会失效classpath 读取两种场景都兼容。close 三个重载是给调用方省事的查询走三参增删改走两参关闭顺序必须是 ResultSet → Statement → Connection倒过来可能把还没取完的结果集提前关掉。每次调 getConnection 都会新建物理连接这是公认的性能短板但课设规模完全够用如果你做的是 Web 版课设且要写进简历第 6 章再换连接池。3.2 DAO层的查询封装PreparedStatement参数绑定与ResultSet映射DAO 层建议按“一个实体类对应一张表”的约定组织。下面是一个按商品名模糊查询并分页的方法放在 GoodsDao 里public ListGoods findByName(String keyword, int pageNum, int pageSize) { String sql SELECT g.goods_id, g.goods_code, g.goods_name, c.category_name, g.sale_price, s.quantity FROM t_goods g JOIN t_category c ON g.category_id c.category_id JOIN t_stock s ON g.goods_id s.goods_id WHERE g.goods_name LIKE ? ORDER BY g.goods_id LIMIT ?, ?; ListGoods list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (pageNum - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setGoodsId(rs.getInt(goods_id)); g.setGoodsCode(rs.getString(goods_code)); g.setGoodsName(rs.getString(goods_name)); g.setCategoryName(rs.getString(category_name)); g.setSalePrice(rs.getBigDecimal(sale_price)); g.setQuantity(rs.getInt(quantity)); list.add(g); } } } catch (SQLException e) { // 记录日志后返回空列表或包装异常不要静默吞掉 return Collections.emptyList(); } return list; }参数说明LIMIT ?, ?第一个问号是偏移量(pageNum - 1) * pageSize第二个是每页条数。第 2 页每页 20 条偏移量就是 20。LIKE ?的 % 通配符拼在参数里而不是 SQL 里这样关键词中出现引号也不会破坏 SQL 结构。rs.getBigDecimal 对应 DECIMAL 列rs.getInt 对应 INT 列类型对不上时 getXxx 会尝试兼容转换但可读性会变差。这段用了 try-with-resources连接、语句、结果集全部自动关闭JDK 7 之后的课设可以放心用。3.3 登录模块一条参数绑定SQL演示如何防注入登录是几乎所有课设的第一关也是最能暴露基本功的地方。常见错误写法是把用户输入直接拼进 SQLString sql SELECT * FROM t_employee WHERE login_name name AND password pwd ;这就是典型的拼接 SQL。如果输入 OR 11整个条件恒真不需要密码就能进系统这就是 SQL 注入。正确写法是用占位符public Employee login(String loginName, String password) { String sql SELECT employee_id, employee_name, role FROM t_employee WHERE login_name ? AND password ?; try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, loginName); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Employee e new Employee(); e.setEmployeeId(rs.getInt(employee_id)); e.setEmployeeName(rs.getString(employee_name)); e.setRole(rs.getString(role)); return e; } } } catch (SQLException e) { // 记录日志 } return null; }PreparedStatement 的占位符 ? 只占一个参数位setString 会把输入当成普通字符串 OR 11只是对比值里的普通文本不会改变 SQL 结构这是它与 Statement 最核心的差别。课设报告里写一句“用参数绑定替代字符串拼接避免 SQL 注入”会比你写十个功能点更有说服力。4. 销售收银与进货入库事务边界、库存联动的完整代码4.1 销售收银判断库存、写入明细、扣减库存必须在一个事务里核心业务里最容易翻车的是把“改库存”和“插流水”拆成两次独立操作。给老师演示时单用户操作看不出问题一旦两台机器同时收银库存就会算错。正确做法是一条销售 主表 多条明细 多次库存扣减三个动作在同一事务里完成public boolean createSale(Sale sale, ListSaleItem items) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 事务开始 // 1. 插入销售主表拿回自增主键 String saleSql INSERT INTO t_sale(sale_no, employee_id, member_id, total_amount) VALUES(?, ?, ?, ?); PreparedStatement psSale conn.prepareStatement(saleSql, Statement.RETURN_GENERATED_KEYS); psSale.setString(1, sale.getSaleNo()); psSale.setInt(2, sale.getEmployeeId()); if (sale.getMemberId() ! null) { psSale.setInt(3, sale.getMemberId()); } else { psSale.setNull(3, Types.INTEGER); } psSale.setBigDecimal(4, sale.getTotalAmount()); psSale.executeUpdate(); ResultSet keys psSale.getGeneratedKeys(); long saleId 0; if (keys.next()) { saleId keys.getLong(1); } keys.close(); psSale.close(); // 2. 逐条插入销售明细同时扣减库存 String itemSql INSERT INTO t_sale_item(sale_id, goods_id, quantity, price, subtotal) VALUES(?, ?, ?, ?, ?); PreparedStatement psItem conn.prepareStatement(itemSql); String stockSql UPDATE t_stock SET quantity quantity - ? WHERE goods_id ? AND quantity ?; PreparedStatement psStock conn.prepareStatement(stockSql); for (SaleItem item : items) { psItem.setLong(1, saleId); psItem.setLong(2, item.getGoodsId()); psItem.setInt(3, item.getQuantity()); psItem.setBigDecimal(4, item.getPrice()); BigDecimal subtotal item.getPrice().multiply( BigDecimal.valueOf(item.getQuantity())); psItem.setBigDecimal(5, subtotal); psItem.addBatch(); psStock.setInt(1, item.getQuantity()); psStock.setLong(2, item.getGoodsId()); psStock.setInt(3, item.getQuantity()); int rows psStock.executeUpdate(); if (rows 0) { throw new SQLException(库存不足或商品已下架goodsId item.getGoodsId()); } } psItem.executeBatch(); // 批量提交明细 conn.commit(); // 全部成功才提交 return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); // 任一步失败回滚所有操作 } catch (SQLException ex) { // 回滚失败也要记录 } } return false; } finally { DBUtil.close(conn, null, null); } }三个决策讲清楚。第一conn.setAutoCommit(false)之后所有 SQL 都归入一个事务直到 commit 或 rollback。这是 Java 里保证数据一致性的最底层手段如果每句 SQL 各自自动提交明细写完、库存没扣成流水和库存就对不上。第二扣库存的 SQL 把判断写进 WHEREquantity ?这个条件让数据库在行锁层面保证同一时刻只有一个事务能成功扣这一行受影响行数为 0 说明库存不够直接抛异常回滚。比“先 SELECT 查库存够再 UPDATE”的两步走稳得多能避免超卖和库存负数。第三RETURN_GENERATED_KEYS配合getGeneratedKeys()取自增主键注意 sale_id 必须从当前连接上取事务还没提交别另开连接去查last_insert_id()那可能拿到别的连接上的脏值。还有一个细节member_id 为空时用psSale.setNull(3, Types.INTEGER)而不是setInt(3, 0)。0 会被当成会员主键去关联外键约束直接报错就算没有外键也会产生一条“会员ID0”的假记录。4.2 进货入库库存累加与平账校验进货流程和销售对称插进货主表、插进货明细、把库存累加。唯一不同是库存累加的 SQL 不能带quantity ?条件因为没有“库存够不够”的问题库存为 0 的第一次补货也不能被拒public boolean createPurchase(Purchase purchase, ListPurchaseItem items) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); String headerSql INSERT INTO t_purchase(purchase_no, supplier_id, employee_id, total_amount) VALUES(?, ?, ?, ?); PreparedStatement psHeader conn.prepareStatement(headerSql, Statement.RETURN_GENERATED_KEYS); psHeader.setString(1, purchase.getPurchaseNo()); psHeader.setInt(2, purchase.getSupplierId()); psHeader.setInt(3, purchase.getEmployeeId()); psHeader.setBigDecimal(4, purchase.getTotalAmount()); psHeader.executeUpdate(); long purchaseId 0; ResultSet keys psHeader.getGeneratedKeys(); if (keys.next()) { purchaseId keys.getLong(1); } keys.close(); psHeader.close(); BigDecimal sum BigDecimal.ZERO; String itemSql INSERT INTO t_purchase_item(purchase_id, goods_id, purchase_price, quantity, subtotal) VALUES(?, ?, ?, ?, ?); String stockSql UPDATE t_stock SET quantity quantity ? WHERE goods_id ?; PreparedStatement psItem conn.prepareStatement(itemSql); PreparedStatement psStock conn.prepareStatement(stockSql); for (PurchaseItem item : items) { BigDecimal subtotal item.getPurchasePrice().multiply( BigDecimal.valueOf(item.getQuantity())); sum sum.add(subtotal); psItem.setLong(1, purchaseId); psItem.setLong(2, item.getGoodsId()); psItem.setBigDecimal(3, item.getPurchasePrice()); psItem.setInt(4, item.getQuantity()); psItem.setBigDecimal(5, subtotal); psItem.addBatch(); psStock.setInt(1, item.getQuantity()); psStock.setLong(2, item.getGoodsId()); psStock.executeUpdate(); } psItem.executeBatch(); // 平账校验单据总金额必须和明细合计一致 if (sum.compareTo(purchase.getTotalAmount()) ! 0) { throw new SQLException(进货单金额不平明细合计 sum 单据金额 purchase.getTotalAmount()); } conn.commit(); return true; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { // 记录日志 } } return false; } finally { DBUtil.close(conn, null, null); } }平账校验用的是 BigDecimal 的 compareTo 而不是 equals。equals 同时比较数值和精度1.0与1.00在 equals 下不相等compareTo 只比数值大小正好是金额比较需要的语义。这个细节能在答辩时说明你踩过坑或提前做过功课。进货的 total_amount 由界面录入明细的 subtotal 由程序累加两者不一致说明录入出错必须中断整个事务。4.3 商品列表分页与模糊搜索JTable刷新与三个调参位置收银界面不能一打开就把上万行商品全部塞进 JTable分页是必须做的基础能力。配合 3.2 节的 GoodsDao 分页方法Swing 侧刷新逻辑如下private void refreshTable(int pageNum) { int pageSize 20; String keyword searchField.getText().trim(); ListGoods data goodsDao.findByName(keyword, pageNum, pageSize); // 总页数统计依赖另一个 count 查询避免每页都全表扫描 int total goodsDao.countByKeyword(keyword); int totalPage total % pageSize 0 ? total / pageSize : total / pageSize 1; final int currentPage pageNum; SwingUtilities.invokeLater(() - { tableModel.setDataVector( convertToTableData(data), new String[]{条码, 名称, 分类, 售价, 库存}); pageLabel.setText(第 currentPage / totalPage 页); }); }三个参数最容易调错。第一pageSize 取 20 还是 50取决于界面高度和单机收银量课设取 20 稳妥。第二(pageNum - 1) * pageSize作为 SQL 的 offset前端传入页码必须先做正整数校验页码小于等于 0 时默认回第 1 页否则 offset 变成负数直接报 SQL 异常。第三翻页和搜索必须共用同一个 refreshTable点搜索后要把 pageNum 重置为 1否则在第 5 页搜索“矿泉水”结果只有两页界面会停在空白页这是演示时最容易暴露的低级 bug。另外注意用 SwingUtilities.invokeLater 包了一层。数据库查询在后台线程执行数据回来后不要在查询线程里直接操作 Swing 组件否则刷新 JTable 时可能闪烁甚至抛并发修改异常。课设规模下很难复现但写成这样能在答辩现场说清 Swing 的单线程模型。5. 课设避坑与排查乱码、ClassNotFound与库存负数5.1 控制台正常但Swing界面全是问号三处字符集设置现象在 IDEA 里跑控制台打印中文正常JTable、JLabel 上一片问号或者打开数据库表中文全变成???。原因三处编码不一致的组合问题。第一MySQL 建库建表没指定DEFAULT CHARSETutf8mb4用了默认 latin1中文存进去就是乱码第二JDBC URL 没带characterEncodingutf8驱动用平台默认编码和 MySQL 通信Windows 下默认 GBK第三Java 源文件本身被 IDE 存成了 GBK 或带 BOM 的 UTF-8。三处只要有两处不一致某个环节必然出现乱码。解决建库脚本统一用 utf8mb4注意不是 utf8MySQL 的 utf8 是 3 字节存不了 emoji 和生僻字JDBC URL 固定写?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiIDEA 里确认源文件编码是 UTF-8。三处对齐后中文乱码基本绝迹。这个排查思路同样适用于 Web 版课设的 JSP、HTML 编码设置。5.2 编译通过但运行报ClassNotFoundException驱动类名与classpath现象javac编译正常一运行就抛java.lang.ClassNotFoundException: com.mysql.jdbc.Driver或者抛No suitable driver found for jdbc:mysql://...。原因驱动 jar 没进运行时 classpath或驱动类名写错。MySQL 8.x 驱动类名是com.mysql.cj.jdbc.Driver老教程里写的com.mysql.jdbc.Driver只存在于 5.x8.x 已经移除。另一个常见场景是 IDEA 里依赖没加到模块编译期靠 IDE 自动补全能过运行期找不到类。解决先把驱动 jar 放到项目根目录lib/下IDEA 里右键 Add as Library再确认驱动类名和 jar 内容一致最后检查java -version的版本与 IDEA 项目编译级别是否一致混用 JDK 8 和 JDK 17 会引发一些匪夷所思的 ClassNotFound 和版本报错。5.3 库存变负数并发场景下的数据一致性现象演示时单机操作一切正常两台电脑同时登录收银卖同一个商品库存从 10 变成 -2或者盘点时发现库存和账面对不上。原因代码里用的是“先 SELECT 查库存够用再 UPDATE”的两步走。这在单用户时没问题但两个事务同时读到库存 10各自通过校验各自扣 5最终库存是 0 而不是期望的 5极端情况就是负数。SELECT 和 UPDATE 之间没有加锁或条件约束本质是竞态条件。解决把判断收进 UPDATE 的 WHERE用 4.1 节的UPDATE t_stock SET quantity quantity - ? WHERE goods_id ? AND quantity ?。数据库在行锁层面保证同一时刻只有一个事务能成功更新这行受影响行数为 0 就说明不够直接抛异常回滚整个销售单。这才是 Java 里保证数据一致性的可行方案课设报告里写一句“利用受影响行数代替先查后改”老师会认可。5.4 外键约束导致删不掉商品从主数据表下手现象商品管理里点删除报Cannot delete or update a parent row: a foreign key constraint fails删分类也报同样的错。原因t_sale_item、t_stock 都通过外键引用 t_goodsMySQL 默认外键动作是 RESTRICT只要有子表记录父表就不允许删。物理删除一个销售流水里出现过的商品等于破坏了历史单据的参照完整性。解决课设范围里别纠缠物理删除改用逻辑删除。删除按钮执行UPDATE t_goods SET status 0 WHERE goods_id ?列表查询只显示status 1的商品。对外键保留 RESTRICT 反而是安全的答辩时可以这样讲历史单据不能因为商品下架就消失所以用状态字段保留商品主数据。若确实要物理删除得先删 t_sale_item 和 t_stock 里关联的行但那会造成销售明细丢失、报表统计缺数据不建议。6. 用Spring Boot和MyBatis重写课设四个验证点与第一行配置如果想在答辩后把它写进简历“Swing JDBC”这个组合很难通过技术初筛最快的升级路线是 Spring Boot MyBatis。表结构一行不用改DAO 里的 SQL 几乎原样搬进 Mapper XML改造工作量集中在三层pom.xml 引入 spring-boot-starter-web、mybatis-spring-boot-starter、mysql-connector-japplication.yml 把 db.properties 的四行配置平移进去DBUtil 删掉换成 MyBatis 自动配置的数据源。第一步验证登录接口。写 LoginController、LoginService、LoginMapper跑通后说清 Mapper XML 的命名空间与接口方法名一一对应。第二步验证事务把 4.1 节的 createSale 逻辑复制到 Service方法上加 Transactional人为让某条明细插入失败验证销售主表也被回滚。第三步验证分页先在 XML 里直接写LIMIT #{offset}, #{pageSize}保持 SQL 可见不要一上来就用 PageHelper 插件课设阶段先手写 LIMIT 更好讲。第四步验证连接池参数Spring Boot 默认 HikariCP只需关注connection-timeout和maximum-pool-size两个配置分别设 30000 和 10 就够。一个容易踩的细节是自增主键回填MyBatis 的 insert 标签必须写useGeneratedKeystrue keyPropertysaleId否则 Sale 对象的 saleId 永远是 null明细表的外键就断了。这个坑不换框架也躲不掉建议升级时先写一条最小插入用例验证主键回填。我自己的课设当年就吃过这个亏“改库存”和“插流水”分开两次提交答辩演示现场库存变负数老师让当场解释。那次以后我养成一个习惯凡是动账又动存量的方法第一行就关自动提交再写后面任何 SQL。这套习惯带到 Spring Boot 里同样成立Transactional 不是后悔药事务边界才是。希望帮到你。本文还有配套的精品资源点击获取
返回列表