news 2026/10/26 3:21:55

Swing+SQL Server宾馆管理系统:开退房事务与房态看板

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Swing+SQL Server宾馆管理系统:开退房事务与房态看板

简介:这是一套面向高校数据库课程设计场景的宾馆管理系统完整项目,采用 SQL Server 作为数据存储、Java Swing 构建桌面端界面,适合正在完成数据库课设、课程作业,或需要参考 CRUD 与界面联动实现的学生对照学习。压缩包内共 40 个文件,约 2.02MB,包含 SQL 建库脚本、MDF/LDF 数据库文件、Java 源码、编译后的 class、可运行 jar 包以及使用说明文档,从建库到启动界面形成较完整的交付链路。已有 379 人学习,可作为同类选题的参考样本。项目覆盖客房、入住、结账等典型业务模块,读者可据此理解数据库表结构设计、SQL 语句编写、Swing 窗体布局与事件处理之间的配合方式,也能借助说明文档快速还原运行环境。源码完成度偏课程作业水平,胜在结构直白、依赖较少,便于拆解改造和二次练习,对刚接触数据库课设、需要一份可运行范例的人来说比较实用。

1. 开房三秒、退房自动结算:这套 Swing + SQL Server 宾馆管理系统到底能干什么

宾馆管理系统这几个字在数据库课设里出现频率极高,但多数交上去的版本只能查询,不能干活。这套基于 SQL Server + Swing 的宾馆管理项目不一样:前台登录后能看到实时房态表,选中空房点开房,身份证、押金、入住时间一次录完,房态立刻变红;退房时按天数自动算房费、扣押金、出结算单,房态转待打扫。它适合两类人——课程设计既要交差又要当场演示的学生,以及想找一个结构干净、带真实事务处理、能往里加模块的 Java 桌面练习项目的人。技术栈是 JDBC 直连,没有 Spring 那套重框架,代码量可控,改起来不迷路。

2. 技术栈与目录结构:Swing 桌面端 + SQL Server 五张表怎么分层

2.1 为什么选 Swing + SQL Server,而不是 JavaFX + MySQL

先说说为什么最后选了 Swing + SQL Server,而不是 JavaFX + MySQL。这是有两方面考量。第一是环境成本:课设机器上 JDK 装好就能跑 Swing,JavaFX 从 JDK 11 起要单独配 SDK 和模块路径,答辩现场换个电脑就容易起不来,这种意外一旦发生,演示效果会大打折扣。第二是数据库的可见性:SQL Server 配好 SSMS 之后,建表、看数据、跑查询都是图形化的,改一个字段不用背命令行;MySQL 也能做,但学校实验环境里 SQL Server 的现成实例更多,连接参数一问就有。

从工程角度看,Swing 的好处是「所见即所得」——一个JFrame里塞JTabbedPane,每个 Tab 一个业务面板,改一个模块不影响别的模块。坏处也很明确:Swing 没有数据绑定,表格刷新、状态变色全要自己写,这部分后面第 4 章会展开。

SQL Server 这边,用到的能力其实不多:自增主键IDENTITY、GETDATE()取服务器时间、WITH (UPDLOCK, ROWLOCK)做行级锁、SCOPE_IDENTITY()拿刚插入的主键。这四样撑起了整个项目的业务正确性,尤其是行锁,它是「两个人同时给同一间房开房」这种并发问题的唯一防线。

JDBC 驱动只加一个依赖,Maven 里写:

<!-- pom.xml 只加这一个,版本按本机 JDK 选 jre8 / jre11 后缀那个包 --> <dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.4.2.jre8</version> </dependency>

参数说明:mssql-jdbc是微软官方驱动,jre8后缀表示适配 JDK 8 及以上,如果你的环境是 JDK 17,换成对应的jre11包即可,纯 JDBC 用法两者没差别。不加这个依赖,运行时第一行就会抛ClassNotFoundException,这是新手最常见的第一个错误。

2.2 项目分层与目录:从 main 到 DAO 的一条直线

这个项目没有用任何 MVC 框架,但分层是清楚的,一共五层,调用方向单向向下,不允许 UI 直接拼 SQL。目录清单如下:

目录 / 文件职责改动频率
src/main/java/com/hotel/App.java程序入口,设置外观、拉起登录窗极低
ui/LoginFrame.java登录窗口,校验账号密码低
ui/MainFrame.java主窗口,Tab 容器,房态看板中
ui/panel/CheckInPanel.java开房面板,表单 + 校验高
ui/panel/CheckOutPanel.java退房结算面板高
ui/panel/RoomPanel.java房态表格、筛选、刷新高
service/RoomService.java业务编排,事务边界在这一层高
dao/RoomDao.java、dao/CheckInDao.java、dao/GuestDao.java纯 SQL,只做增删改查中
entity/Room.java、entity/Guest.java、entity/CheckIn.java与表结构一一对应的 POJO低
util/DBUtil.java连接获取与资源关闭极低
sql/init.sql建库建表 + 初始化数据一次性

这张表本身就是改造地图:想加功能,一般是「加一个 entity → 加一个 dao 方法 → 在 service 里组织事务 → 加一个 panel」。四步走完,功能就完整了。反过来说,如果看到有人直接在CheckInPanel的按钮监听里写DriverManager.getConnection,那这份代码基本没法维护——连接泄漏、事务缺失、SQL 注入三个问题会一起出现。

父容器MainFrame用的是JTabbedPane,每个 Tab 一个JPanel子类,构造时把service传进去:

// MainFrame 构造里的关键几行:Tab 之间不互相引用,只共享 service JTabbedPane tabs = new JTabbedPane(); RoomService roomService = new RoomService(); // 无状态,可安全共享 tabs.addTab("房态看板", new RoomPanel(roomService)); tabs.addTab("入住登记", new CheckInPanel(roomService, this::refreshAll)); tabs.addTab("退房结算", new CheckOutPanel(roomService, this::refreshAll)); tabs.addTab("客人查询", new GuestPanel(roomService)); setContentPane(tabs);

这里this::refreshAll是个方法引用,作用是:开房或退房成功后,通知主窗口刷新房态表。参数说明:RoomService内部只持有 DAO,不持有任何 UI 对象,所以多个 Tab 共用一个实例不会互相影响。这种「面板之间不直接调用、通过回调通知父容器」的写法,是我在这个规模的项目里最推荐的做法——它避免了A 面板持有 B 面板引用的死结,也方便你以后把某个 Tab 换成对话框。

2.3 五张核心表:房态、入住单、客人信息怎么关联

数据库设计是这个项目的重点。五张表,关系不复杂但要经得起推敲:

-- 房型:价格从这张表来,不写死在代码里 CREATE TABLE RoomType ( TypeId INT PRIMARY KEY IDENTITY(1,1), TypeName NVARCHAR(20) NOT NULL, -- 标准间 / 大床房 / 商务套房 Price DECIMAL(10,2) NOT NULL -- 每晚单价 ); -- 客房:Status 是整个系统的核心状态位 CREATE TABLE Room ( RoomNo VARCHAR(10) PRIMARY KEY, -- 房号用字符串,301、A102 都能存 TypeId INT NOT NULL REFERENCES RoomType(TypeId), Floor INT NOT NULL, Status TINYINT NOT NULL DEFAULT 0 -- 0空闲 1已入住 2待打扫 3维修 ); -- 入住单:一张单子对应一次住宿,退房后 Status 改 1,数据永久保留 CREATE TABLE CheckIn ( OrderNo VARCHAR(20) PRIMARY KEY, RoomNo VARCHAR(10) NOT NULL REFERENCES Room(RoomNo), GuestId INT NOT NULL REFERENCES Guest(GuestId), CheckInTime DATETIME NOT NULL, CheckOutTime DATETIME NULL, -- 未退房时为 NULL Deposit DECIMAL(10,2) NOT NULL DEFAULT 0, Status TINYINT NOT NULL DEFAULT 0 -- 0在住 1已退房 );

关键在于Room.Status和CheckIn.Status是两个独立的字段,这是有意为之。房态回答「这间房现在能不能住」,入住单状态回答「这笔交易结没结账」。两者混用一个字段,退房之后你就再也查不到历史账目了。旁边还有Guest表(身份证唯一索引)和SysUser表(账号、密码哈希、角色),这两张是配套的支撑表。

注意:Room.Price不要在Room表里冗余,价格统一从RoomType取。否则调价那天你会发现三张表的价格对不上,改完 301 忘了改 302,这种数据不一致比代码 bug 更难查。

3. 环境搭建与首次跑通:从装 JDBC 驱动到登录窗口弹出

3.1 环境清单与 SQL Server 必开的三项配置

先把环境摆清楚,缺一样都跑不起来:

组件要求检查方式
JDK8 及以上java -version
SQL Server2016 及以上(Express 版够用)SSMS 能连上实例
JDBC 驱动mssql-jdbcjar项目依赖里有
IDEIntelliJ IDEA / Eclipse 均可能识别 Maven 工程

SQL Server 安装完,有三项配置必须手动确认,具体位置在 SQL Server 配置管理器里。第一项是TCP/IP 协议:展开「SQL Server 网络配置 → 实例的协议」,把TCP/IP状态改成「已启用」。第二项是端口:右键 TCP/IP 属性,在IPAll里看「TCP 端口」是不是 1433,如果是动态端口,把它清空并填 1433。第三项是身份验证模式:在 SSMS 里右键实例 → 属性 → 安全性,选「SQL Server 和 Windows 身份验证模式」,然后重启 SQL Server 服务。

这三步做完,去 SSMS 里执行一句SELECT @@VERSION,能出版本号,说明实例是通的。再用sa账号在 SSMS 里连一次,能进,说明混合验证生效了。很多人卡在这里,现象是通过 Windows 身份能连、Java 里就是连不上,几乎全是这三项里漏了一项。

提示:sa是超管账号,写进代码里当连接账号在真实项目里是大忌。课设环境自己电脑上问题不大,但如果你打算把这个项目放到能公网访问的机器上演示,一定要新建一个只有HotelDB权限的账号,别图省事。

3.2 连接工具类 DBUtil:一处配置,全局复用

连接串只写一个地方,这是整个项目的纪律。所有 DAO 都走DBUtil.getConn(),绝不自己DriverManager.getConnection:

package com.hotel.util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public final class DBUtil { // 连接串四个要点:地址、库名、关加密、信任证书 private static final String URL = "jdbc:sqlserver://127.0.0.1:1433;" + "DatabaseName=HotelDB;" + "encrypt=false;" + "trustServerCertificate=true;" + "loginTimeout=5"; private static final String USER = "sa"; private static final String PWD = "Your@Strong123"; static { try { // 新版驱动可以省略这行,但显式加载能第一时间暴露 classpath 问题 Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError("JDBC 驱动不在 classpath:" + e.getMessage()); } } private DBUtil() { } // 工具类不允许 new public static Connection getConn() throws SQLException { Connection conn = DriverManager.getConnection(URL, USER, PWD); conn.setAutoCommit(true); // 默认自动提交,需要事务的地方自己关 return conn; } public static void close(AutoCloseable... res) { for (AutoCloseable r : res) { if (r != null) { try { r.close(); } catch (Exception ignored) { } } } } }

逐段说明。encrypt=false和trustServerCertificate=true是给新版驱动用的——驱动 10 以后默认要求加密连接,而本地实例通常没配证书,不加这两个参数会直接报 SSL 握手失败。loginTimeout=5把默认的 30 秒等待压到 5 秒,连不上时能立刻看到错误,不用在登录窗前面干等半分钟,调试期这个参数能省下大量时间。

static代码块里加载驱动,作用是让错误提前暴露:如果你忘了加依赖,程序启动的第一秒就崩,而不是等用户点了「登录」才崩。close方法用可变参数把「关几个都行」这件事统一了,调用侧写成DBUtil.close(rs, ps, conn)一行搞定,避免三个嵌套 try 的丑陋写法。

3.3 建库建表脚本与初始化数据

建库脚本放在sql/init.sql,SSMS 里整段执行一次就行。注意GO是 SSMS 的批次分隔符,不是 SQL 语句,用 JDBC 执行时要把它删掉:

CREATE DATABASE HotelDB; GO USE HotelDB; GO -- 客人表:身份证唯一,同一客人多次入住只留一条记录 CREATE TABLE Guest ( GuestId INT PRIMARY KEY IDENTITY(1,1), IdCard VARCHAR(18) NOT NULL UNIQUE, Name NVARCHAR(20) NOT NULL, Phone VARCHAR(20) ); -- 系统用户:Role 1 前台,2 管理员 CREATE TABLE SysUser ( UserId INT PRIMARY KEY IDENTITY(1,1), UserName VARCHAR(20) NOT NULL UNIQUE, PwdHash VARCHAR(64) NOT NULL, Role TINYINT NOT NULL DEFAULT 1 ); -- 初始化:房型三条、房间十二间、一个管理员账号 INSERT INTO RoomType(TypeName, Price) VALUES (N'标准间', 199.00), (N'大床房', 269.00), (N'商务套房', 459.00); INSERT INTO Room(RoomNo, TypeId, Floor, Status) VALUES ('301',1,3,0),('302',1,3,0),('303',2,3,0),('305',3,3,0), ('401',1,4,0),('402',1,4,0),('403',2,4,0),('405',3,4,0);

参数说明:NVARCHAR用于存中文,VARCHAR存纯数字或英文,两者别混用——Name用了VARCHAR的话,写入中文可能变成问号,这个错误现象后面第 5 章还会提到。IDENTITY(1,1)是自增主键,从 1 开始步长 1。

3.4 启动入口与登录校验

入口类干三件事:设外观、拉登录窗、登录成功后开主窗。登录校验的关键点是用 PreparedStatement 而不是拼字符串:

// LoginFrame 里「登录」按钮的事件 private void doLogin() { String sql = "SELECT UserId, Role FROM SysUser WHERE UserName = ? AND PwdHash = ?"; try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, userField.getText().trim()); ps.setString(2, sha256(new String(pwdField.getPassword()))); // 库里存哈希 try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { int role = rs.getInt("Role"); dispose(); // 关登录窗 SwingUtilities.invokeLater(() -> new MainFrame(role).setVisible(true)); } else { JOptionPane.showMessageDialog(this, "账号或密码不正确"); } } } catch (SQLException e) { // 这里不要把 e.getMessage() 原样弹给用户,日志里打全栈就够了 JOptionPane.showMessageDialog(this, "数据库连接失败,请检查服务是否启动"); } }

逻辑说明:setString会把用户输入当纯值处理,输入' OR 1=1 --也只会被当成一整串用户名去比对,注入无从下手。密码比对用 SHA-256 哈希,PwdHash字段长度 64 刚好够(32 字节十六进制)。dispose()之后再invokeLater开主窗,是为了确保登录窗先彻底销毁,避免任务栏里残留一个看不见的窗口。

参数说明:sha256是自己写的小工具方法,用MessageDigest.getInstance("SHA-256")实现,输出小写十六进制。课设里常见直接存明文密码,能跑但演示时被老师问一句「密码为什么不加密」就很尴尬,加这十行代码成本极低。

4. 核心业务链路拆解:入住、换房、退房三笔事务怎么写才不出脏数据

4.1 开房:一条 SQL 锁不住的并发,得靠事务

开房是整个系统的核心,它要同时改三张表:查房态、写客人、写入住单、改房态。任何一步失败,前面做的都得撤回来,所以必须放在一个事务里:

public String checkIn(String roomNo, Guest guest, BigDecimal deposit) throws SQLException { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 关掉自动提交,事务从这里开始 // 1. 加行锁查房态:UPDLOCK 让别的事务在这行上排队 try (PreparedStatement ps = conn.prepareStatement( "SELECT Status FROM Room WITH (UPDLOCK, ROWLOCK) WHERE RoomNo = ?")) { ps.setString(1, roomNo); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) throw new SQLException("房号不存在:" + roomNo); if (rs.getInt(1) != 0) throw new SQLException("该房间不是空闲状态"); } } // 2. 客人:身份证已存在就更新联系方式,否则插入新记录 int guestId; try (PreparedStatement ps = conn.prepareStatement( "SELECT GuestId FROM Guest WHERE IdCard = ?")) { ps.setString(1, guest.getIdCard()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { guestId = rs.getInt(1); try (PreparedStatement up = conn.prepareStatement( "UPDATE Guest SET Name = ?, Phone = ? WHERE GuestId = ?")) { up.setString(1, guest.getName()); up.setString(2, guest.getPhone()); up.setInt(3, guestId); up.executeUpdate(); } } else { try (PreparedStatement ins = conn.prepareStatement( "INSERT INTO Guest(IdCard, Name, Phone) VALUES (?, ?, ?)", Statement.RETURN_GENERATED_KEYS)) { ins.setString(1, guest.getIdCard()); ins.setString(2, guest.getName()); ins.setString(3, guest.getPhone()); ins.executeUpdate(); try (ResultSet keys = ins.getGeneratedKeys()) { keys.next(); guestId = keys.getInt(1); } } } } } // 3. 写入住单,入住时间取数据库服务器时间,避免客户端时钟不准 String orderNo = "CI" + LocalDateTime.now() .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")) + String.format("%02d", ThreadLocalRandom.current().nextInt(100)); try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO CheckIn(OrderNo, RoomNo, GuestId, CheckInTime, Deposit, Status) " + "VALUES (?, ?, ?, GETDATE(), ?, 0)")) { ps.setString(1, orderNo); ps.setString(2, roomNo); ps.setInt(3, guestId); ps.setBigDecimal(4, deposit); ps.executeUpdate(); } // 4. 改房态:条件里再判一次 Status = 0,双保险 try (PreparedStatement ps = conn.prepareStatement( "UPDATE Room SET Status = 1 WHERE RoomNo = ? AND Status = 0")) { ps.setString(1, roomNo); if (ps.executeUpdate() != 1) throw new SQLException("房态已被其他操作修改"); } conn.commit(); return orderNo; } catch (SQLException e) { if (conn != null) conn.rollback(); // 任何一步失败,全部回滚 throw e; } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) { } DBUtil.close(conn); // 连接一定要还回去 } } }

逻辑说明,按四步走。第一步的WITH (UPDLOCK, ROWLOCK)是整个方法的安全基础:UPDLOCK在读取时就对行加更新锁并持有到事务结束,另一个事务在同一房号上执行到这里会阻塞等待,直到前一个事务提交或回滚。没有它,两个前台同时点开 301,都能读到Status = 0,都能写入住单,最后两笔订单指向同一间房——这就是最典型的脏写。第四步UPDATE的WHERE Status = 0是双保险,万一锁没起作用,这里的executeUpdate()返回 0,照样抛异常回滚。

参数说明:deposit是押金,用BigDecimal不用double,金额计算一旦用浮点,退房时算出来199.00000000000003这种数就很难看。orderNo的构造成CI + 时间戳 + 两位随机数,一天内重复概率极低,VARCHAR(20)刚好放得下。

4.2 退房结算:按天计费与押金差额

退房的核心是计费和状态流转,计费规则要写清楚——不足一天按一天算,这是行业惯例,也是答辩时容易被问的点:

public BigDecimal checkOut(String roomNo) throws SQLException { Connection conn = null; try { conn = DBUtil.getConn(); conn.setAutoCommit(false); // 1. 取出在住单和房型单价 String orderNo; BigDecimal deposit, price; LocalDateTime inTime; try (PreparedStatement ps = conn.prepareStatement( "SELECT c.OrderNo, c.Deposit, c.CheckInTime, t.Price " + "FROM CheckIn c " + "JOIN Room r ON r.RoomNo = c.RoomNo " + "JOIN RoomType t ON t.TypeId = r.TypeId " + "WHERE c.RoomNo = ? AND c.Status = 0")) { ps.setString(1, roomNo); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) throw new SQLException("该房间没有在住订单"); orderNo = rs.getString("OrderNo"); deposit = rs.getBigDecimal("Deposit"); price = rs.getBigDecimal("Price"); inTime = rs.getTimestamp("CheckInTime").toLocalDateTime(); } } // 2. 计费:按自然天向上取整,不足一天按一天 long minutes = Duration.between(inTime, LocalDateTime.now()).toMinutes(); long days = Math.max(1, (minutes + 1439) / 1440); BigDecimal roomFee = price.multiply(BigDecimal.valueOf(days)); BigDecimal refund = deposit.subtract(roomFee); // 负数表示还要补钱 // 3. 结单:改单状态 + 写退房时间 try (PreparedStatement ps = conn.prepareStatement( "UPDATE CheckIn SET Status = 1, CheckOutTime = GETDATE() " + "WHERE OrderNo = ? AND Status = 0")) { ps.setString(1, orderNo); if (ps.executeUpdate() != 1) throw new SQLException("订单状态异常,可能已退房"); } // 4. 房态转为待打扫,而不是直接空闲 try (PreparedStatement ps = conn.prepareStatement( "UPDATE Room SET Status = 2 WHERE RoomNo = ?")) { ps.setString(1, roomNo); ps.executeUpdate(); } conn.commit(); return refund; } catch (SQLException e) { if (conn != null) conn.rollback(); throw e; } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) { } DBUtil.close(conn); } } }

逻辑说明:(minutes + 1439) / 1440是向上取整算天数,入住 25 小时算 2 天,入住 2 小时也算 1 天;Math.max(1, ...)兜住「刚开房就退」的 0 分钟情况。返回值refund可能为负,UI 层拿到负数就该显示「需补交 XX 元」,这个判断放在界面而不是 DAO 里,职责更清楚。

第 4 步把房态设成 2(待打扫)而不是 0(空闲),是这个系统里容易被忽略的一处细节。退房后房间还没打扫,直接标空闲,下一个客人开进去就是脏房,这种问题在演示时被追问会很难解释。房态从 2 回到 0 需要一个「打扫完成」的操作,通常放在客房管理模块里,一个按钮搞定。

4.3 房态看板:JTable 模型与定时刷新

房态表是整个界面的门面,用JTable+DefaultTableModel实现,数据源是一条带三次左连接的查询:

private void refreshRoomTable() { String sql = "SELECT r.RoomNo, t.TypeName, t.Price, r.Floor, " + " CASE r.Status WHEN 0 THEN N'空闲' WHEN 1 THEN N'已入住' " + " WHEN 2 THEN N'待打扫' ELSE N'维修' END AS StatusText, " + " ISNULL(g.Name, '') AS GuestName " + "FROM Room r " + "JOIN RoomType t ON t.TypeId = r.TypeId " + "LEFT JOIN CheckIn c ON c.RoomNo = r.RoomNo AND c.Status = 0 " + "LEFT JOIN Guest g ON g.GuestId = c.GuestId " + "ORDER BY r.Floor, r.RoomNo"; DefaultTableModel model = (DefaultTableModel) roomTable.getModel(); model.setRowCount(0); // 先清空,避免重复追加 try (Connection conn = DBUtil.getConn(); PreparedStatement ps = conn.prepareStatement(sql); ResultSet rs = ps.executeQuery()) { while (rs.next()) { model.addRow(new Object[]{ rs.getString("RoomNo"), rs.getString("TypeName"), rs.getBigDecimal("Price"), rs.getString("StatusText"), rs.getString("GuestName") }); } } catch (SQLException e) { JOptionPane.showMessageDialog(this, "加载房态失败:" + e.getMessage()); } }

逻辑说明:两个LEFT JOIN是必要的,因为空闲房间没有入住单,用INNER JOIN会把空房全部过滤掉,表格里只剩已入住的行——这个 bug 我在早期版本里遇到过,排查了半天才反应过来是连接类型写错。ISNULL(g.Name, '')把 NULL 转成空串,否则表格里会显示成null,很显眼。

配合一个javax.swing.Timer,每 30 秒自动刷新一次,就实现了房态看板的「准实时」:

// 构造里启动,注意 30_000 是毫秒 new javax.swing.Timer(30_000, e -> refreshRoomTable()).start();

5. 踩坑排查:Swing 连 SQL Server 最常见的五类翻车现场

5.1 连接类故障:驱动、端口、身份验证

现象一:启动直接抛ExceptionInInitializerError,堆栈里写着「JDBC 驱动不在 classpath」。原因:Maven 依赖加了但没刷新,或者手动导 jar 时只加到了 IDE 的模块路径、没进运行时 classpath。 解决:IDEA 里点 Maven 面板的刷新按钮,然后在Project Structure → Artifacts里确认mssql-jdbc在WEB-INF/lib或输出目录下。判断依据很简单——在DBUtil的静态块里打一行System.out.println,能看到这行输出,说明类加载成功。

现象二:报The TCP/IP connection to the host 127.0.0.1, port 1433 has failed。原因:SQL Server 的 TCP/IP 协议没启用,或者端口不是 1433(Express 版默认用动态端口)。 解决:配置管理器里启用 TCP/IP,在IPAll的TCP 端口填 1433,把TCP 动态端口清空,然后重启 SQL Server 服务。验证方法是在命令行敲telnet 127.0.0.1 1433,能连上就说明端口通了。

现象三:报Login failed for user 'sa',但 SSMS 里用 sa 明明能登。原因:实例只开了 Windows 身份验证模式,或者 sa 账号被禁用。 解决:SSMS 里右键实例 → 属性 → 安全性,改成混合模式;再到「安全性 → 登录名 → sa」里确认「登录」状态是启用。改完必须重启服务,否则配置不生效。这个配置很多人改完不重启,白折腾半小时,也算是常见问题之一。

5.2 数据与事务类故障:乱码、脏读、主键

现象四:插进去的中文在表里显示成问号,但英文数字正常。原因:字段类型用了VARCHAR而不是NVARCHAR,或者 JDBC 连接串没指定字符集。 解决:建表时所有存中文的列一律用NVARCHAR(n),Java 侧用setString而非setNString也能正常写入(驱动会自动转)。已经建好的表用ALTER TABLE ... ALTER COLUMN Name NVARCHAR(20)改,但要注意改之前先备份数据,VARCHAR里已经变成问号的内容是救不回来的。

现象五:两个人同时开同一间房,两笔订单都成功,房态却只变了一次。原因:只在 Java 里判断了if (status == 0),没有在事务里对行加锁,两个线程都读到了 0。 解决:按 4.1 的写法,查询时加WITH (UPDLOCK, ROWLOCK),更新时WHERE Status = 0并检查影响行数。这类并发问题在单机演示时基本复现不了,但答辩老师真让你两个客户端一起点,当场就能看到后果——这种意外很伤。

5.3 界面类故障:EDT 卡死与表格不刷新

现象六:点「查询」按钮后窗口白屏几秒,拖都拖不动。原因:数据库查询直接在事件分发线程(EDT)上执行,Swing 的 UI 重绘被阻塞了。 解决:耗时操作放SwingWorker的doInBackground,结果回done()里更新表格。数据量小的时候不觉得,一旦客人表上千行,这个毛病就藏不住了,属于典型的「不测不知道,一测就出问题」。

// 用 SwingWorker 把查询挪出 EDT,界面上还能显示个忙等光标 new SwingWorker<List<Object[]>, Void>() { @Override protected List<Object[]> doInBackground() throws Exception { return roomDao.queryAll(); // 这里跑在后台线程 } @Override protected void done() { try { fillTable(get()); // 这里回到 EDT,可以安全操作组件 } catch (Exception e) { JOptionPane.showMessageDialog(null, "查询失败:" + e.getMessage()); } } }.execute();

现象七:开房成功了,房态表格却还是老样子。原因:DefaultTableModel加了行但没触发重绘,或者根本没调用刷新方法。 解决:确认调用了model.setRowCount(0)再重新灌数据,或者用model.fireTableDataChanged()。更省事的做法是在MainFrame里留一个refreshAll(),所有会改数据的操作完成后都回调它一次,把「改完数据忘了刷界面」这个错误从流程上消除。

6. 二次开发:把课设改成能演示、能加分的作品

基础版本跑通之后,往上加功能其实很快,因为分层清楚,加东西有固定路径。下面三个方向性价比最高,按投入产出排序。

第一个是预订模块。加一张Reserve表(预订号、房号、客人、预计到店时间、状态),在房态表上加一列显示「今日预订」,退房逻辑里加一条「有预订则优先分配给预订客人」。这条改造会逼着你处理「预留房不能被开」的判断,业务味道立刻就出来了。

第二个是营收统计。不改表结构,只加一个查询面板,用一条 SQL 按天、按房型汇总:

-- 近 30 天营收:退房单按房型分组,用于统计面板的柱状图 SELECT t.TypeName, COUNT(*) AS 单数, SUM(DATEDIFF(DAY, c.CheckInTime, c.CheckOutTime) + 1) AS 间夜数, SUM(t.Price * (DATEDIFF(DAY, c.CheckInTime, c.CheckOutTime) + 1)) AS 营收 FROM CheckIn c JOIN Room r ON r.RoomNo = c.RoomNo JOIN RoomType t ON t.TypeId = r.TypeId WHERE c.Status = 1 AND c.CheckOutTime >= DATEADD(DAY, -30, GETDATE()) GROUP BY t.TypeName ORDER BY 营收 DESC;

参数说明:DATEDIFF(DAY, ...) + 1是自然日计费口径,和 4.2 里 Java 侧的向上取整不完全等价,跨零点入住会有半天差异。我一般会在注释里写清楚「统计口径按自然日,与结算按 24 小时向上取整略有出入」,避免以后自己对不上账。这种地方不写注释,三个月后连自己都得重新推一遍。

第三个是权限分级。SysUser.Role已经有 1 和 2 两个值了,把管理员才可见的「房价调整」「用户管理」两个 Tab 在MainFrame构造时按角色动态加:

// 按角色决定加不加管理 Tab,比在每个面板里判角色干净得多 if (role == 2) { tabs.addTab("房价调整", new PricePanel(roomService)); tabs.addTab("用户管理", new UserPanel(userService)); }

改完之后的自测清单,我按顺序走一遍,基本能覆盖大部分回归问题:

检查项预期结果容易漏的点
空库启动提示初始化脚本未执行别直接崩出堆栈
开房后立即退房计费 1 天,房态转待打扫0 分钟时除以 1440 得 0
同一身份证二次入住不新增客人记录唯一索引会报错
关闭窗口连接全部释放任务管理器看 javaw 进程
两台客户端同时开同一房一台成功一台提示占用行锁是否生效

改这个项目最深的体会是:当年我第一次写退房结算,图省事没开事务,先改房态再写订单,结果中间抛了个空指针,房变成空闲了订单还挂着,第二天早上对着两张矛盾的表发了半天呆,没有任何回滚的余地。从那以后我每次写这种「多表联动」的方法,第一步都是先把setAutoCommit(false)敲上去,再把commit和rollback的骨架搭好,最后才往里填业务 SQL——先搭好回滚机制这个习惯,比多写十个功能都值。希望帮到你。

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

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

技能管理实战:从零散技能点到结构化技能组合的完整指南

1. 从“skills”这个词说起&#xff1a;为什么它突然成了高频搜索最近后台收到不少留言&#xff0c;问的都是同一个方向的问题&#xff1a;技能到底该怎么学、怎么攒、怎么用。有人问“skills”这个词为什么突然被反复搜索&#xff0c;有人问自己手里那点本事到底算不算核心竞争…

作者头像 李华
网站建设 2026/10/26 3:21:42

ChatGLM3云服务器私有化部署全流程实战指南

大模型私有化部署这件事&#xff0c;从去年火到现在&#xff0c;问的人一点没少。我前前后后帮几个团队在云上落地过不同规模的对话模型&#xff0c;踩的坑从显卡驱动版本对不上&#xff0c;到显存算错导致 OOM&#xff0c;再到端口安全组没开白折腾一下午&#xff0c;基本都经…

作者头像 李华
网站建设 2026/10/26 3:21:37

微网协同优化与需求响应的双层模型实践

1. 微网系统面临的现实挑战与解决思路去年夏天参与某工业园区微电网改造项目时&#xff0c;我亲眼目睹了这样一个场景&#xff1a;光伏阵列在正午阳光最强烈时被迫降额运行&#xff0c;而相邻的微电网却因空调负荷激增正在高价购入市电。这种资源错配现象正是当前分布式能源发展…

作者头像 李华
网站建设 2026/10/26 3:21:36

深入解析HashMap:从数据结构到JDK优化演进

1. HashMap的前世今生&#xff1a;从数据结构说起第一次接触HashMap是在2013年&#xff0c;当时还在用JDK1.6开发一个电商系统。某个深夜排查OOM问题时&#xff0c;我盯着MAT工具里那个占满内存的HashMap实例&#xff0c;第一次意识到这个看似简单的数据结构背后竟藏着如此多的…

作者头像 李华
网站建设 2026/10/26 3:19:48

C#水晶报表内置图表:四类图表绑定、打印导出与避坑

简介&#xff1a;面向C#开发者与报表设计初学者的水晶报表图表实例&#xff0c;围绕在Crystal Reports中呈现柱形图、线形图、面积图和饼图展开&#xff0c;帮助解决数据可视化报表从数据绑定到图表样式定制的实际问题。压缩包共28个文件、约377KB&#xff0c;包含7个cs源码、3…

作者头像 李华
网站建设 2026/10/26 3:17:50

YOLOv12在植物病害检测中的应用与优化

1. 项目概述&#xff1a;当YOLOv12遇上植物病理学去年夏天&#xff0c;我在某农业科技园区亲眼目睹了技术员们拿着放大镜逐片检查番茄叶片的场景。这种传统病害检测方式不仅效率低下&#xff0c;而且对经验依赖极高。这正是我们开发基于YOLOv12的叶片病害识别系统的初衷——用A…

作者头像 李华