尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaSwing超市收银系统课设全解析:JDBC事务与分层架构实践

JavaSwing超市收银系统课设全解析:JDBC事务与分层架构实践 简介基于Java Swing和MySQL数据库实现的超市收银系统课程设计资源面向Java初学者、在校学生及需要完成面向对象课程设计的开发者。系统涵盖收银员登录、收银台结算、会员管理、商品管理、消费记录查询等核心模块支持会员手机号唯一性校验、商品库存自动扣减、会员积分累计等业务逻辑可直接运行或二次开发。资源包共23个文件包含17个Java源码文件、MySQL建表及数据脚本.sql、mysql-connector-java 5.1.25和8.0.23两个驱动Jar包、1个运行说明文本以及2张界面图片压缩包整体仅4.29MB结构清晰便于学习和部署。目前已有193人学习下载。借助附带教程和项目介绍读者可快速理解Swing界面与数据库交互的实现思路掌握面向对象设计在实际收银场景中的应用适用于课程设计答辩和Java GUI开发入门。1. 课程设计最怕代码全堆在 JFrame 里JavaSwing 超市收银系统的正确打开方式做课程设计最怕的不是写不出代码而是代码全堆在一个类里JFrame 事件里直接写 JDBC 连接、SQL 拼字符串、金额用 double 算。演示的时候能跑老师一问“把商品查询改成按分类查”当场卡住。JavaSwing 超市收银系统看起来是个 GUI 练习真正的训练点是 JDBC 和面向对象分层MySQL 负责数据落盘Swing 只做输入输出和事件响应中间的业务规则、事务控制、数据访问必须拆开否则标题里“面向对象课程设计”就是空话。下面按一条完整链路走从 MySQL 建库、连接驱动、分层代码到一次收银的事务一样样过一遍每一步都给出能直接粘进自己工程的东西。2. 起步配置JDK、MySQL 版本与 JDBC 连接串先把环境一次调对调环境最耗时间而课设环境其实不需要追新。JDK 8 到 11 都合适Swing 在这两个版本里是内置模块双击就能跑。MySQL 选 5.7 或 8.0 都行真正影响你的是配套的驱动 jar。驱动类名在中版本里改过一次5.1.x 用的是com.mysql.jdbc.Driver8.0 之后必须写com.mysql.cj.jdbc.Driver写错就报ClassNotFoundException。建议直接用 8.0.x 驱动配 MySQL 8.0类名和连接参数都按新写法来网上教程虽然多但一半以上还在教老类名照抄容易踩坑。组件推荐版本说明JDK8 或 11Swing 内置javax.swing 包无需额外引入MySQL5.7 或 8.08.0 注意认证插件和时区参数5.7 更省心mysql-connector-j8.0.x配 MySQL 8.0驱动类名com.mysql.cj.jdbc.Driver5.1.49配 MySQL 5.7驱动类名com.mysql.jdbc.Driver两套写法别混如果你下的是 MySQL 8.0.46 压缩版初始化流程和安装版不一样解压后先写一个my.ini指定basedir和datadir然后用管理员权限执行mysqld --initialize-insecure最后net start mysql。初始化时用--initialize-insecure会生成一个空密码的 root 账号比--initialize随机密码好处理适合本地课设。初始化完第一步就是把端口确认一下默认 3306如果你本机装过老版本 MySQL端口冲突会导致net start失败去my.ini里改端口之后JDBC 的 URL 也要同步改。2.1 建库命令一条 SQL 建好 database 和字符集库名、表名、字符集要在连数据库之前就定好后面改会牵连 DAO 层所有 SQL。收银系统的库建议就叫supermarket字符集用utf8mb4它比utf8多支持 emoji 和生僻字MySQL 8.0 的默认字符集本身就是 utf8mb45.7 需要显式指定。用命令行或者 mysql workbench 都行执行这条mysql -uroot -p -e CREATE DATABASE supermarket DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci;-e参数表示执行完 SQL 就退出适合脚本化操作。COLLATE utf8mb4_general_ci指定排序规则_ci后缀表示大小写不敏感这样WHERE usernameadmin和WHERE usernameAdmin查出来的结果一致登录时不会因为大小写分歧被老师挑刺。建完验证一下mysql -uroot -p -e SHOW DATABASES LIKE supermarket;建表语句我会直接粘进 mysql workbench 执行因为 workbench 有语法高亮和错误提示比命令行好排查而且 create 语句里的关键字会被自动标颜色order、user这种保留字一眼就能看出来。2.2 JDBC 连接串参数逐个拆useSSL、serverTimezone、characterEncoding 缺一个都报错连接串是课设里报错最多的地方而且报错信息在中文搜索引擎里能搜出一大堆互相矛盾的答案。直接给一份能用的写法import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/supermarket ?useSSLfalse serverTimezoneAsia/Shanghai characterEncodingutf8 useUnicodetrue allowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }useSSLfalse是让本地连接不走 SSL 加密省去证书配置serverTimezoneAsia/Shanghai解决 8.0 的时区报错不写这个会抛The server time zone value Öйú±ê׼ʱ¼ä is unrecognizedcharacterEncodingutf8配合useUnicodetrue保证中文不乱码入库出库都是 UTF-8allowPublicKeyRetrievaltrue专门解决 MySQL 8.0 默认的caching_sha2_password认证插件报的Public Key Retrieval is not allowed。2.3 验证连接的最小 Java 程序写一个main方法测连接别一上来就写界面连接不通后面全是白搭public class ConnectionTest { public static void main(String[] args) { try (Connection conn DBUtil.getConnection()) { System.out.println(连接成功: conn.getCatalog()); } catch (SQLException e) { e.printStackTrace(); } } }try-with-resources会自动关闭连接不用手写 finally。conn.getCatalog()返回当前连接的数据库名打印出来是supermarket就说明 URL、账号密码全对。这里有个容易忽略的点getConnection()每次调用都会新建一个物理连接课设里无所谓但后面做事务的时候必须让多个 DAO 方法共用同一个Connection对象否则commit和rollback各管各的事务根本控制不住这一点到第 5 章结账流程里细说。3. 数据库设计先行四张表、唯一索引与一次收银的事务边界收银系统的表拆法有个固定套路订单头 订单明细。订单头存“谁在什么时间买了一单一共多少钱”订单明细存“这一单里具体有哪些商品、每个买了几个、成交单价是多少”。拆成两张表之后才能回答“昨天卖了几个可乐”这种问题在明细表里按商品分组求和就行。剩下的商品表、用户表是支撑数据。整个系统四张表就够别为了凑功能多加日志表课设的表结构越精简答辩时越容易讲清楚关系。表名职责关键字段注意点user登录账号与角色id, username, password, rolerole 区分 CASHIER / ADMINgoods商品档案与库存id, barcode, name, price, stockbarcode 唯一检索入口sale_order订单头id, order_no, total_amount, create_time, user_id一次收银一条记录sale_order_item订单明细id, order_id, goods_id, quantity, priceprice 存快照价冗余设计3.1 建表 SQL金额用 DECIMAL 而不用 DOUBLE库存用 INT 加上无符号约束下面是整套建表语句直接粘进 mysql workbench 执行CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, role VARCHAR(20) NOT NULL DEFAULT CASHIER ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(30) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT NOT NULL DEFAULT 0 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sale_order ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL UNIQUE, total_amount DECIMAL(10,2) NOT NULL, pay_type TINYINT NOT NULL DEFAULT 1, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, user_id INT NOT NULL, KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE sale_order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL, price DECIMAL(10,2) NOT NULL ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE INDEX idx_item_order ON sale_order_item(order_id); CREATE INDEX idx_item_goods ON sale_order_item(goods_id);user加反引号是因为它在 MySQL 的语境里算敏感词不加引号可能在某些工具里报语法错误。金额必须用DECIMAL(10,2)不能图省事用DOUBLE二进制浮点存 0.1 有误差连加十次就出现 9.999999这在金额计算里是硬伤。sale_order_item.price存的是成交时的商品单价不是去关联 goods 表现在价格这叫快照意义在于商品以后涨价了历史订单的金额还是当时算出来的数。3.2 索引取舍唯一索引防重复下单普通索引撑报表查询这段专门说索引因为它是答辩加分点。barcode和order_no都加的是UNIQUE唯一索引。barcode唯一是业务硬约束同一个商品不能有两个条码order_no唯一是防重复提交生成订单号时用时间戳加随机数极小概率撞号撞了直接插入报错就是天然提醒。create_time加的是普通索引用途是日结报表收银员下班要按天查自己收了多少笔WHERE create_time BETWEEN ... AND ...走这个索引才快单机课设数据量小看不太出差别但索引设计意图要能讲得出来。3.3 外键加还是不加两种做法课程设计里都成立外键看你的课时和答辩风格。加物理外键的写法是在建表后面补FOREIGN KEY (user_id) REFERENCES user(id)好处是数据库层面保证了不能插入不存在的用户缺点是你往别的环境搬数据或者删数据的时候会被依赖关系绊住。我一般建议课设不加物理外键用应用层维护引用关系Java 代码在插入订单之前先查一次 user 和 goods确保主键存在插入明细的时候保证 order_id 是刚生成的。这种做法更接近真实项目的做法而且答辩被问“为什么没加外键”答案是现成的——业务规则放应用层控制更灵活这一点讲到第 4 章事务时会体现。3.4 事务边界一次收银 1 张订单头 N 条明细 1 次库存扣减收银的事务边界是整篇代码设计的核心。一次结账要往数据库写三批数据往sale_order插一条订单往sale_order_item插若干明细把goods.stock扣掉。这三件事必须同时成功或同时失败。比如收银员点了结账订单插进去了但扣库存的网络断了结果就是账对不上库存虚高。解决办法是把三组 SQL 放进同一个事务conn.setAutoCommit(false)之后执行全部成功再commit任何一步抛异常就rollback回到结账前的状态。这也是第 5 章收银代码最值得讲给老师听的地方。4. 面向对象分层把 Swing、业务与 JDBC 拆成四层再串起来先说结论Swing 项目最容易犯的错误是“在事件监听器里写 SQL”。面向对象在课设里的落地方式不是把类写得多抽象而是把职责边界划清楚。整个工程按这种包结构组织类名按职责命名一眼看出谁负责什么层包名典型类职责实体entityGoods, User, SaleOrder纯数据载体字段私有提供构造和 getter数据访问daoGoodsDAO, UserDAO只写 JDBC 和 SQL不写业务判断业务serviceGoodsService, SaleOrderService参数校验、事务控制、并发判断UIuiLoginFrame, MainFrame收集输入、刷新界面不写 SQL调用方向是单向的UI 层调 ServiceService 层调 DAODAO 层调 JDBC。哪一层都不能反向依赖反向依赖就说明职责串了。4.1 实体类与 BaseDAO封装、继承落地的两个位置面向对象的封装体现在实体类里所有字段private只通过 getter/setter 访问。继承体现在 DAO 层的公共基类把获取连接、释放资源这些重复代码收上去public class BaseDAO { protected Connection getConnection() throws SQLException { return DBUtil.getConnection(); } protected void close(ResultSet rs, Statement stmt, Connection conn) { if (rs ! null) { try { rs.close(); } catch (SQLException ignored) {} } if (stmt ! null) { try { stmt.close(); } catch (SQLException ignored) {} } if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }子类继承这个基类只写自己关心的查询逻辑这是继承最朴素的用法。close方法里的ignored参数名是刻意写的关闭资源时如果本身已经关了再调一次会抛异常但这里用忽略比往上抛更合理因为关闭阶段的异常不影响业务结果。这段代码的另一个作用是给 DAO 层一个统一出口其它类看不到 Connection 的创建过程。4.2 entity 与 DAO商品查询的完整链路PreparedStatement 参数化是底线商品按条码查找是收银系统最高频的操作扫码枪扫一下就要查一次。写成 DAO 方法public class GoodsDAO extends BaseDAO { public Goods findByBarcode(String barcode) throws SQLException { String sql SELECT id, barcode, name, price, stock FROM goods WHERE barcode ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, barcode); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { Goods g new Goods(); g.setId(rs.getInt(id)); g.setBarcode(rs.getString(barcode)); g.setName(rs.getString(name)); g.setPrice(rs.getBigDecimal(price)); g.setStock(rs.getInt(stock)); return g; } return null; } } } }这里用PreparedStatement的?占位符替代字符串拼接是最低要求。条码是收银员可输入的字段用字符串拼 SQL 等于把注入漏洞摆给老师看ps.setString(1, barcode)是参数化绑定特殊字符会被转义这才是正常的 JDBC 写法。rs.getBigDecimal(price)从结果集取 DECIMAL 字段对应实体类里的BigDecimal price不能用getDouble再转一遍一跳过类型就会丢精度。4.3 Service 层兜住业务规则库存不足的检查必须放在这里DAO 层只做“查出来、写进去”库存到底够不够判断放 Service 层public class GoodsService { private GoodsDAO goodsDAO new GoodsDAO(); public void checkStock(String barcode, int quantity) throws Exception { if (quantity 0) { throw new IllegalArgumentException(数量必须大于 0); } Goods goods goodsDAO.findByBarcode(barcode); if (goods null) { throw new RuntimeException(商品不存在: barcode); } if (goods.getStock() quantity) { throw new RuntimeException(库存不足: goods.getName()); } } }Service 的存在价值是把“能不能结账”这个判断从 UI 里挪出来。UI 层拿到异常只负责弹窗提示不负责决策。有的课设会把这段判断直接写在按钮点击事件里功能上没错但被问“业务规则放哪了”就答不上来。IllegalArgumentException表示参数本身非法RuntimeException表示运行期业务失败两类异常在 Service 的边界上抛给 UI 层统一处理。4.4 多态的位置定义 SalesmanService 接口UI 只依赖接口不依赖实现多态在这类课设里最容易讲的位置是接口。定义一个结账服务的接口让 UI 依赖接口而不是具体类public interface SaleService { int checkout(ListCartItem cart, int cashierId) throws Exception; } public class SaleServiceImpl implements SaleService { // 第 5 章实现 }MainFrame里持有的字段类型写SaleService真正 new 出来的对象是SaleServiceImpl。好处是以后想换成另一种结账策略比如先锁库存再算金额只需要新增一个实现类UI 一行不用改。这就是面向对象里的“面向接口编程”答辩时比背定义有用得多。为了把这段代码讲全补上CartItem这个临时行对象它不落库只存在于一次结账的过程里public class CartItem { private String barcode; private int quantity; public CartItem(String barcode, int quantity) { this.barcode barcode; this.quantity quantity; } public String getBarcode() { return barcode; } public int getQuantity() { return quantity; } }5. 收银主流程的代码复现登录、结账、扣库存前面几张表和一个空壳的分层到这里全部串起来。收银主流程包三块登录窗口查用户并记录角色主界面管理临时购物车结账时按事务提交订单和库存。每块都是课程设计答辩必问的环节。5.1 登录窗口参数化查询用户把角色带进主界面登录窗口的逻辑很简单文本框拿到用户名密码查 user 表有记录就进主界面没有就弹窗。查用户和判断密码分开写这里只贴 DAO 的核心public User findByUsernameAndPassword(String username, String password) throws SQLException { String sql SELECT id, username, role FROM user WHERE username ? AND password ?; try (Connection conn getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, password); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new User(rs.getInt(id), rs.getString(username), rs.getString(role)); } return null; } } }password用明文存储是课程设计的常见妥协因为整篇代码的重心在流程和分层上。想加分就改成 MD5 加盐存储把注册或初始化用户的数据写成MD5(密码)存进库登录时对输入做同样的摘要再比对。角色的作用在于权限控制CASHIER只能进收银界面ADMIN能额外看到商品管理按钮这个判断写一个if (ADMIN.equals(user.getRole()))就行。5.2 收银台布局JTable 临时购物车与扫码输入主界面左边是商品列表或购物车明细推荐用JTable配DefaultTableModel这是 Swing 里最常用的数据表格组合。购物车不建表结账前的数据全在内存里每次扫码回车就把一条CartItem塞进DefaultTableModelDefaultTableModel cartModel new DefaultTableModel( new Object[]{条码, 名称, 单价, 数量}, 0 ); private void addToCart(String barcode) { try { Goods goods goodsService.findByBarcode(barcode); cartModel.addRow(new Object[]{ goods.getBarcode(), goods.getName(), goods.getPrice(), 1 }); calculateTotal(); } catch (Exception e) { JOptionPane.showMessageDialog(this, e.getMessage()); } }扫码输入框注册键盘事件回车就调用addToCart。DefaultTableModel构造参数里的0表示初始行数之后每 addRow 一次界面上就多一行JTable 会自动重绘。calculateTotal遍历cartModel里的“单价”和“数量”两列累加总额显示在底部的 JLabel 上。注意这里累加用的是BigDecimal不能直接调doubleValue()否则前面数据库精度白守了。5.3 结账方法三条写入、一个事务、一段回滚结账是整个系统最重的代码。几个 DAO 必须共用同一个Connection所以 Service 方法里显示管理连接的生命周期DAO 方法调整为接收 Connection 作为参数public int checkout(ListCartItem cart, int cashierId) throws Exception { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); BigDecimal total BigDecimal.ZERO; for (CartItem item : cart) { Goods goods goodsDAO.findByBarcodeForUpdate(conn, item.getBarcode()); if (goods.getStock() item.getQuantity()) { throw new RuntimeException(库存不足: goods.getName()); } total total.add(goods.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } String orderNo SO System.currentTimeMillis(); int orderId saleOrderDAO.insert(conn, orderNo, total, cashierId); for (CartItem item : cart) { saleOrderItemDAO.insert(conn, orderId, item.getBarcode(), item.getQuantity()); goodsDAO.reduceStock(conn, item.getBarcode(), item.getQuantity()); } conn.commit(); return orderId; } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); } }这个方法的调用链值得给老师讲两分钟。第一层循环只做两件事用悲观锁锁住商品行、校验库存、累加总金额。锁库存的 SQL 是SELECT ... FOR UPDATE作用是这一行数据在你提交之前别人改不了这在单个收银员本地运行时体会不到但并发情况下能防止超卖。第二层循环才真正落库先插订单明细再扣库存。两段循环分开写是有意的先在内存里把整单金额算出来再开始写数据库避免算到一半发现库存不足还要回滚。orderNo用System.currentTimeMillis()生成时间戳毫秒级碰撞概率极低课设足够。finally里恢复setAutoCommit(true)是为了连接归还连接池时状态干净虽然课设没用连接池但这个习惯得留。5.4 商品管理JTable 刷新数据与删除前检查引用商品管理的增删改查是 ADMIN 的操作台原理比收银简单点“查询”按钮就调goodsDAO.findAll()把结果整个塞进DefaultTableModelpublic void refreshGoodsTable() { goodsModel.setRowCount(0); for (Goods g : goodsDAO.findAll()) { goodsModel.addRow(new Object[]{g.getId(), g.getBarcode(), g.getName(), g.getPrice(), g.getStock()}); } }setRowCount(0)是清空旧数据的关键方法不清直接 addRow 会导致重复显示。删除商品前要查sale_order_item有没有引用这个商品的记录有就提示“该商品已被订单引用不能删除”SELECT COUNT(*) FROM sale_order_item WHERE goods_id ?这个检查容易被忽略但不做就会出现订单明细指向不存在的商品报表按商品分组时会查出一堆空行。更稳的做法是给 goods 表加一个status字段删除时只把 status 置 0查询默认只查 status1这就是软删除。商品表几千条数据时硬删除的连锁问题不明显但课设做完之后你可以跟老师提一句“我做的是软删除”级别立刻不一样。6. 答辩前把这两处调稳并发扣库存的锁与连接参数自查6.1 悲观锁与乐观锁课设讲清一种另一种一句话带过并发扣库存是答辩老师最常追问的点。本系统在第 5 章用的SELECT ... FOR UPDATE是悲观锁它在 MySQL 里锁住查询到的行事务提交前其它事务的更新会阻塞对比项悲观锁乐观锁实现方式SELECT ... FOR UPDATE数据表加 version 字段适用场景高冲突业务写多于读低冲突业务读多于写课程设计建议推荐能讲清楚锁的粒度代码复杂一点适合加分乐观锁的做法是每条商品记录带一个version字段更新时UPDATE goods SET stock stock - ?, version version 1 WHERE id ? AND version ?更新受影响行数为 0 就说明版本变了重新读取再试一次。课设里把悲观锁讲解清楚就达标了乐观锁作为“另一种思路”提一句即可别为了炫技堆两套方案答辩容易把自己绕进去。6.2 中文乱码、时区、驱动类名三分钟排错顺序结账跑通了剩下的坑大概率在环境上。按这个顺序查每次都有明确目标报ClassNotFoundException驱动 jar 没引进项目或者类名写成了旧的com.mysql.jdbc.Driver8.0 驱动必须用com.mysql.cj.jdbc.Driver。报时区错URL 里没有serverTimezoneAsia/Shanghai补上。中文乱码URL 缺characterEncodingutf8或者 MySQL 库、表建的时候字符集不是 utf8mb4。报Public Key Retrieval is not allowedURL 加allowPublicKeyRetrievaltrue和useSSLfalse两个一起加。连接超时MySQL 端口不是 3306或你的 URL 里端口和my.ini配置不一致。这五条按顺序过一遍能覆盖 JavaSwing MySQL 课程设计百分之九十的连接问题。最后留一个检查项给验证结账完成后手动去 mysql workbench 执行SELECT SUM(total_amount) FROM sale_order WHERE create_time CURDATE()能看到今天的营业额和界面合计对得上就说明事务、类型、精度整条链都是通的。本文还有配套的精品资源点击获取
返回列表