首页 / 资讯中心 / 文章详情

Java Swing+SQL Server宾馆管理系统:事务链路与房态联动

Java Swing+SQL Server宾馆管理系统:事务链路与房态联动 ★ FEATURED ARTICLE
简介这份宾馆管理系统课程设计资料面向高校计算机、软件相关专业正在准备数据库课程设计的学生以 SQL Server 作为后台数据库、Java Swing 搭建桌面交互界面完整呈现从建库建表、数据维护到前台业务操作的实现链路可用于课设选题参考、功能改造与答辩演示准备。压缩包共 40 个文件约 2.02MB含建库 SQL 脚本与 mdf、ldf 数据库文件、8 个 Java 源文件、若干编译后的 class 文件、可直接运行的 jar 包以及一份使用说明文档另附带界面布局依赖库代码、编译输出与工程配置分区存放便于按需检索。已有 379 人学习在同类课设资料中具备一定参考价值。读者可从中拿到较完整的表结构设计与建库脚本、Swing 界面布局与事件处理写法、工程配置文件以及可运行的成品程序对照说明文档即可理清各模块职责再按需替换数据表、增补业务或改写界面较快形成自己的课程设计版本。1. 前台说 803 早就退房了系统里还挂着“在住”凌晨一点客房的电话打过来803 的客人半小时前就退了房系统里房态还是“在住”下一拨客人已经站在大堂等房卡。这种账实不符的场面在还在用纸质登记本加 Excel 排房的宾馆里几乎每周都会出现一次——房态在人脑里账单在另一个表里押金记在便利贴上。这套用 SQL Server Swing 写的宾馆管理系统核心价值就是一件事把房态、入住单、账单这三样东西绑定在同一条事务链上。前台点下“退房”按钮的瞬间房间从“在住”变“脏房”、消费明细停止计时、应收金额结出数字三个动作要么一起成功要么一起回滚。它适合两类人一类是要交数据库课设、需要把建表、视图、触发器、事务都跑一遍的学生另一类是想给自家小宾馆做个单机前台程序的开发者——不用服务器、不用联网一台装了 JDK 的电脑加一个数据库实例就能开张。2. 技术选型与工程骨架三层结构怎么分包名怎么起2.1 为什么是 Swing 而不是 JavaFX 或 Web 前端课设和单机前台这个场景最现实的三条约束摆在那儿答辩教室的电脑装不了新版 JDK、现场不一定有网、评审老师最好双击就能跑起来。Swing 是 JDK 自带的一个可执行 jar 加一条java -jar命令就能起来不用装 Node、不用等 Maven 拉一堆依赖、也不用配 Web 容器。JavaFX 从 JDK 11 起被移出标准库要额外带一堆模块文件至于做成 Web 系统前端工程、后端容器、跨域配置全都要背上出问题的面一下就大了。所以这里选 Swing 是因为它没有额外运行时依赖并非因为它技术先进。代价也得说清楚。Swing 默认的 Metal 外观确实不好看表格和按钮一股十几年前的味道复杂布局靠手写GridBagLayout又很啰嗦。我的做法是引一个 FlatLaf 的 jar单文件、无传递依赖在main里加一行UIManager.setLookAndFeel(new FlatLightLaf())界面立刻能拉到能看的水平这是整套工程里性价比最高的一步改造。字体也要顺手指定一下UIManager.put(defaultFont, new Font(Microsoft YaHei, Font.PLAIN, 13))否则中文在小字号下会发虚。数据层选 SQL Server 的理由类似教学机和不少门店电脑上本来就装了用图形化客户端建表直观T-SQL 写存储过程和触发器门槛低答辩时能演示“约束写在数据库端”这一点很加分。驱动要单独下载 mssql-jdbc 的 jarJDK 8 配 8.x 版本JDK 11 及以上用 10 以后的版本版本错配时报的错往往很难往驱动上想这是后面要专门说的一类坑。2.2 分包结构从 entity 到 ui 的一条直线工程按经典三层切包名直接体现职责不做过度抽象。课设代码最怕的是“为了设计模式而设计模式”一个 DAO 接口配三个实现类最后谁也说不清哪个在跑。这里每个领域对象就一个 DAO接口保留是为了后面换实现比如换成本地缓存或者换库但不强求。hotel-system/ ├── src/main/java/com/example/hotel/ │ ├── App.java # 启动入口设置外观、字体、打开登录窗 │ ├── common/ │ │ ├── DBUtil.java # 读配置、拿连接、关资源 │ │ ├── TxTemplate.java # 事务模板commit/rollback 只写一遍 │ │ ├── ServiceException.java # 业务异常message 直接给前台看 │ │ └── DateUtil.java # 天数与房费计算 │ ├── entity/ # Room / Guest / CheckIn / Bill / User ... │ ├── dao/ # RoomDao / CheckInDao / BillDao / UserDao │ ├── service/ # CheckInService / BillService / RoomService │ └── ui/ │ ├── LoginFrame.java │ ├── MainFrame.java # CardLayout 装各个业务面板 │ ├── panel/RoomPanel.java # 房态看板 │ ├── panel/CheckInPanel.java │ └── model/RoomTableModel.java └── src/main/resources/ ├── db.properties └── sql/schema.sql # 建表、视图、触发器、初始化数据分层的边界我是这么卡的ui层只做两件事——收集输入、渲染结果绝不出现java.sql的包名service层管事务和业务规则房间能不能开、押金够不够扣一个方法对应一个完整业务动作dao层只做 SQL 执行和结果集到实体的映射不判断业务。这条线守住之后后面加“钟点房”“会员折扣”这类需求改动基本只落在 service 一个文件里。2.3 驱动与连接db.properties 与 DBUtil 的写法连接信息不写死在代码里扔进resources/db.properties改数据库地址不用重新编译。配置里最容易翻车的是 JDBC URL 后面的参数新版驱动默认要求加密本地实例没配证书就会连不上所以要显式关掉。# db.properties jdbc.drivercom.microsoft.sqlserver.jdbc.SQLServerDriver # encryptfalse 用于本地无证书实例连接超时设短一点连不上时别让界面干等 jdbc.urljdbc:sqlserver://127.0.0.1:1433;databaseNameHotelDB;encryptfalse;trustServerCertificatetrue;loginTimeout5 jdbc.usersa jdbc.passwordChangeMe_2024DBUtil用静态块在类加载时把驱动和配置一次性读完之后只管发连接。这里没有上连接池——单机前台同一时刻最多三五个并发DriverManager足够如果后面要改成多终端联网再把getConnection()换成连接池的dataSource.getConnection()即可调用方一行都不用动。public final class DBUtil { private static String url; private static String user; private static String password; static { // 用 try-with-resources 读配置文件缺失时给出能看懂的报错而不是 NPE try (InputStream in DBUtil.class.getClassLoader().getResourceAsStream(db.properties)) { if (in null) throw new IllegalStateException(classpath 下找不到 db.properties); Properties p new Properties(); p.load(in); Class.forName(p.getProperty(jdbc.driver)); url p.getProperty(jdbc.url); user p.getProperty(jdbc.user); password p.getProperty(jdbc.password); } catch (Exception e) { // 静态块里抛异常会被包成 ExceptionInInitializerError这里先把原因带出来 throw new ExceptionInInitializerError(数据库配置加载失败 e.getMessage()); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } /** 统一关闭顺序反着来任何一个为 null 都安全跳过 */ public static void close(AutoCloseable... resources) { for (int i resources.length - 1; i 0; i--) { if (resources[i] ! null) { try { resources[i].close(); } catch (Exception ignored) { } } } } private DBUtil() { } }参数说明loginTimeout5是血泪经验默认超时太长数据库没启动时登录窗会白屏卡十几秒用户以为程序坏了直接去点叉。encryptfalse只适合本机或内网可信环境公网部署必须换成encrypttrue并配好证书这一点别图省事。3. 数据库设计八张表、两个视图和一个触发器的落地细节3.1 主干表客房、客人、入住单、账单怎么拆整套库围绕四个主干对象转t_room物理房间、t_room_type房型与价格、t_guest客人档案、t_checkin入住单。账单和消费明细挂在入住单下面一次入住对应一条账单、多条消费。这样拆的好处是价格跟着房型走而不是跟着房间走调价时只改t_room_type一行房间表不用动。表名作用关键字段说明t_room_type房型与基准价type_id、type_name、price价格放在这里房间表只引用t_room房间实体与房态room_no、type_id、statusroom_no唯一房态用 TINYINTt_guest客人档案id_card、name、phone证件号唯一回头客直接复用t_checkin入住单room_id、guest_id、in_time、status一条入住单对应一次住店t_bill账单checkin_id、room_fee、total、pay_type一次入住一条退房时生成t_consume消费明细checkin_id、item_name、amount押金、迷你吧、洗衣都走这里t_user操作员username、pwd_hash、salt、role角色区分前台与管理员t_log操作日志user_id、action、create_time谁在什么时候退了哪间房房态是整个系统的状态机核心用数字编码比用中文字符串稳避免“在住”和“在 住”这种空格事故状态码含义允许的下一步0空闲开房 → 2预订 → 1维修 → 31已预订客人到店 → 2取消 → 02在住退房 → 43维修修好 → 04脏房保洁完成 → 0状态机的规则写在 service 层同时用数据库的CHECK约束兜底保证不会出现“状态码 9”这种脏数据。这是我在数据库课设里最推荐的做法业务规则用代码保证可读性用约束保证底线。-- 房型 CREATE TABLE t_room_type ( type_id INT IDENTITY(1,1) PRIMARY KEY, type_name NVARCHAR(30) NOT NULL, price DECIMAL(10,2) NOT NULL CHECK (price 0), bed_count TINYINT NOT NULL DEFAULT 1, remark NVARCHAR(200) NULL ); -- 房间房号唯一状态只能是 0~4 CREATE TABLE t_room ( room_id INT IDENTITY(1,1) PRIMARY KEY, room_no VARCHAR(10) NOT NULL, type_id INT NOT NULL, floor_no INT NOT NULL, status TINYINT NOT NULL DEFAULT 0, remark NVARCHAR(200) NULL, CONSTRAINT uq_room_no UNIQUE (room_no), CONSTRAINT ck_room_status CHECK (status BETWEEN 0 AND 4), CONSTRAINT fk_room_type FOREIGN KEY (type_id) REFERENCES t_room_type(type_id) ); -- 入住单一条记录一次住店退房后 status 置 1 CREATE TABLE t_checkin ( checkin_id INT IDENTITY(1,1) PRIMARY KEY, room_id INT NOT NULL, guest_id INT NOT NULL, in_time DATETIME NOT NULL DEFAULT GETDATE(), expect_out DATETIME NULL, out_time DATETIME NULL, deposit DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0, -- 0 在住 1 已退 2 已取消 operator NVARCHAR(30) NOT NULL, CONSTRAINT fk_ci_room FOREIGN KEY (room_id) REFERENCES t_room(room_id), CONSTRAINT fk_ci_guest FOREIGN KEY (guest_id) REFERENCES t_guest(guest_id) );参数说明金额一律用DECIMAL(10,2)绝不用FLOAT浮点算房费会出现399.99999999这种账单客人当场就要跟你理论。时间用DATETIME而不是DATE因为计费需要精确到小时。room_no用VARCHAR而非NVARCHAR房号里不会有中文省一半存储。3.2 索引与视图让数据库替你兜住一半的脏数据查询量最大的两个场景是“按房态查房间”和“按入住单查消费”所以索引就往这两处加。t_checkin上建一个覆盖索引把room_id status作为键、in_time作为包含列查某间房的当前在住单时可以只走索引不回表。-- 房态看板status 是过滤条件floor_no 是排序条件 CREATE INDEX ix_room_status ON t_room(status, floor_no) INCLUDE (room_no, type_id); -- 查某间房当前是否有未退的入住单 CREATE INDEX ix_checkin_room ON t_checkin(room_id, status) INCLUDE (in_time, guest_id); -- 客人按手机号或证件号找回 CREATE INDEX ix_guest_phone ON t_guest(phone);视图v_room_detail把房间、房型和当前客人拼成一行界面层直接SELECT * FROM v_room_detail省掉在 Java 里做三次循环匹配的代码。这也是我习惯的分工能用一条 SQL 拼出来的展示数据就别放到 Java 里 for 循环。CREATE VIEW v_room_detail AS SELECT r.room_id, r.room_no, r.floor_no, r.status, t.type_name, t.price, g.name AS guest_name, g.phone AS guest_phone, c.checkin_id, c.in_time FROM t_room r JOIN t_room_type t ON r.type_id t.type_id LEFT JOIN t_checkin c ON c.room_id r.room_id AND c.status 0 LEFT JOIN t_guest g ON g.guest_id c.guest_id;注意这里用的是LEFT JOIN空闲房间没有入住单用内连接会直接从看板上消失这是很常见的翻车点——房态图上少了几间房还以为是数据丢了。3.3 触发器与存储过程结算置脏房、按月统计入住率触发器在真实业务系统里争议很大但在课设里它能直观展示“数据库端也能承载业务规则”。我保留了一个账单插入后自动把房间置为脏房。理由是万一哪天有人绕过 service 层直接写库房态也不会漏更新。CREATE TRIGGER tr_bill_insert_dirty ON t_bill AFTER INSERT AS BEGIN SET NOCOUNT ON; -- 账单挂在入住单上入住单挂在房间上两级跳过去改房态 UPDATE r SET r.status 4 -- 4 脏房等保洁确认后回到 0 FROM t_room r JOIN t_checkin c ON c.room_id r.room_id JOIN inserted i ON i.checkin_id c.checkin_id; END;逻辑说明inserted是 SQL Server 触发器里的虚拟表装着本次插入的所有行用JOIN而不是逐行游标批量结算时性能不会塌。注意这个触发器只管置脏房不管金额计算金额是 service 层算好再写进去的——把计算逻辑塞进触发器调试时你会怀疑人生。另一个实用对象是统计入住率的存储过程管理员面板上那个“本月入住率”数字就是调它拿的CREATE PROCEDURE sp_occupancy_rate start_date DATE, end_date DATE AS BEGIN SET NOCOUNT ON; DECLARE total_room_days INT; SELECT total_room_days COUNT(*) * DATEDIFF(DAY, start_date, end_date 1) FROM t_room WHERE status 3; -- 维修房不计入可售房 SELECT total_room_days AS sellable_days, ISNULL(SUM(DATEDIFF(DAY, c.in_time, ISNULL(c.out_time, GETDATE()))), 0) AS occupied_days, CAST(ISNULL(SUM(DATEDIFF(DAY, c.in_time, ISNULL(c.out_time, GETDATE()))), 0) * 100.0 / NULLIF(total_room_days, 0) AS DECIMAL(5,2)) AS rate_percent FROM t_checkin c WHERE c.status IN (0, 1) AND c.in_time start_date AND c.in_time DATEADD(DAY, 1, end_date); END;参数说明start_date和end_date是闭区间内部把结束日加一天再比较避免漏掉最后一天晚上入住的单子。NULLIF(total_room_days, 0)是防除零的老套路房间表被清空时不会抛异常而是返回 NULL。入住率按“间夜”算而不是按“人次”算这是行业口径别搞混。4. 核心模块拆解登录、开房、退房结算的完整链路4.1 登录鉴权加盐哈希与角色分流密码绝不能明文存。库里存两列pwd_hash存 SHA-256 摘要salt存每个用户独立的随机盐。校验时用同样方式算一遍再比对即使库被拖走彩虹表也打不动。public static String sha256(String raw, String salt) { try { MessageDigest md MessageDigest.getInstance(SHA-256); md.update((salt raw).getBytes(StandardCharsets.UTF_8)); byte[] digest md.digest(); StringBuilder sb new StringBuilder(64); for (byte b : digest) { sb.append(String.format(%02x, b)); // 转成 64 位小写十六进制 } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new IllegalStateException(当前 JVM 不支持 SHA-256, e); } }登录的 SQL 用PreparedStatement先把用户按用户名查出来再在 Java 里比对哈希不要在 SQL 里拼字符串—— OR 11这种老把戏到今天就该彻底绝迹。登录成功后把User对象塞进MainFrame的成员变量后面所有操作日志的operator字段都从这里取页面之间不互相传用户对象避免权限串台。角色分流只做粗粒度role1是管理员能进“房间维护”“用户管理”“报表”三个面板role0是前台只能进房态、开房、退房。实现方式是在MainFrame里按角色决定哪些按钮setEnabled(false)真正的拦截放在 service 层再判一次。界面禁用了但接口没拦是这类课设里最容易被追问的一个漏洞。4.2 开房事务一把锁锁住重复入住开房要连做五件事校验房态、建档或复用客人、写入住单、改房态、记押金。五步里任何一步失败都必须整体回滚否则就会出现“房间显示在住但查不到入住单”的黑匣子状态。事务模板只写一次所有业务复用public final class TxTemplate { public interface TxCallbackT { T doInTx(Connection conn) throws Exception; } public static T T execute(TxCallbackT callback) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关掉自动提交事务边界由业务决定 T result callback.doInTx(conn); conn.commit(); return result; } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) { } } // 业务异常原样抛出给前台看的是人话系统异常包一层避免泄露堆栈 if (e instanceof ServiceException) throw (ServiceException) e; throw new ServiceException(操作失败 e.getMessage(), e); } finally { DBUtil.close(conn); } } }有了模板开房逻辑本身就很短了。关键在于第一步查询时加WITH (UPDLOCK, ROWLOCK)把这一行锁住直到事务结束两个前台同时点“开 803”时后一个会等前一个提交完再读读到的已经是“在住”直接被拒。public int checkIn(CheckInForm form, String operator) { return TxTemplate.execute(conn - { // 1) 行级排他锁并发开同一间房时第二个请求会排队而不是穿透 int status; String sql SELECT status FROM t_room WITH (UPDLOCK, ROWLOCK) WHERE room_id ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, form.getRoomId()); try (ResultSet rs ps.executeQuery()) { if (!rs.next()) throw new ServiceException(房间不存在); status rs.getInt(status); } } if (status ! 0) throw new ServiceException(该房间当前不可入住状态码 status ); int guestId saveOrGetGuest(conn, form); // 老客人按证件号复用 int checkinId insertCheckin(conn, form, guestId, operator); updateRoomStatus(conn, form.getRoomId(), 2); // 2 在住 insertDeposit(conn, checkinId, form.getDeposit()); writeLog(conn, operator, 开房 roomId form.getRoomId()); return checkinId; }); }参数说明form.getDeposit()是押金单独记一条t_consume明细item_name押金退房时用负数冲抵这样账单流水是连续的对账时一眼能看懂。这里没把押金直接写进t_checkin.deposit就完事是因为后面还要支持“中途补交押金”走明细表才扩展得开。4.3 退房结算房费天数怎么算才不被投诉结算最容易吵架的地方是“住一晚收几天钱”。行业惯例是入住当天算一天次日 12:00 前退房不加收12:00 到 18:00 加半天18:00 之后加全天。用毫秒差整除86400000是最常见的错误写法会把下午入住、次日上午退房的单子算成 0 天。/** * 计费天数跨自然日按天算当天进出至少 1 天 * 12:00 后离店加半天18:00 后离店加全天 */ public static BigDecimal calcDays(LocalDateTime in, LocalDateTime out) { long days ChronoUnit.DAYS.between(in.toLocalDate(), out.toLocalDate()); if (days 0) { days 1; // 钟点房/当天退房保底一天 } LocalTime t out.toLocalTime(); if (t.isAfter(LocalTime.of(18, 0))) { days 1; } else if (t.isAfter(LocalTime.of(12, 0))) { return BigDecimal.valueOf(days).add(new BigDecimal(0.5)); } return BigDecimal.valueOf(days); }结算主流程读单价、算房费、汇总消费、扣押金、生成账单全部在一个事务里最后靠触发器把房态置成脏房public Bill settle(int checkinId, String payType, String operator) { return TxTemplate.execute(conn - { CheckIn ci checkInDao.findByIdForUpdate(conn, checkinId); // 同样带 UPDLOCK if (ci.getStatus() ! 0) throw new ServiceException(该入住单已结算或已取消); BigDecimal price roomDao.findPriceByRoomId(conn, ci.getRoomId()); BigDecimal days DateUtil.calcDays(ci.getInTime(), LocalDateTime.now()); BigDecimal roomFee price.multiply(days).setScale(2, RoundingMode.HALF_UP); BigDecimal consume consumeDao.sumByCheckin(conn, checkinId); // 押金在这里是负数 BigDecimal total roomFee.add(consume).setScale(2, RoundingMode.HALF_UP); Bill bill new Bill(checkinId, roomFee, consume, total, payType, operator); billDao.insert(conn, bill); // 触发器顺手把房间置脏房 checkInDao.markOut(conn, checkinId, LocalDateTime.now()); writeLog(conn, operator, 退房 checkinId checkinId 金额 total); return bill; }); }参数说明setScale(2, RoundingMode.HALF_UP)不能省DECIMAL乘法结果的小数位数会累加不显式收敛会出现四位小数的账单。markOut用UPDATE ... WHERE status 0影响行数为 0 就说明被别的进程抢先结过了这时候抛异常回滚天然做了幂等。4.4 Swing 界面层TableModel、渲染器与 SwingWorker房态看板是一张JTable数据源用自定义TableModel。最常见的错误是每次刷新都table.setModel(new RoomTableModel(list))结果滚动条位置、列宽、选中行全丢用户正在看的房间一刷新就跳走。正确做法是 model 只换数据、通知刷新。public class RoomTableModel extends AbstractTableModel { private static final String[] COLS {房号, 房型, 楼层, 单价, 状态, 客人, 备注}; private ListRoomVO data new ArrayList(); /** 只换数据源并通知别重建 model */ public void reload(ListRoomVO list) { this.data (list null) ? new ArrayList() : list; fireTableDataChanged(); } public RoomVO getRow(int index) { return data.get(index); } Override public int getRowCount() { return data.size(); } Override public int getColumnCount() { return COLS.length; } Override public String getColumnName(int c) { return COLS[c]; } Override public Object getValueAt(int r, int c) { RoomVO vo data.get(r); switch (c) { case 0: return vo.getRoomNo(); case 1: return vo.getTypeName(); case 2: return vo.getFloorNo(); case 3: return vo.getPrice(); case 4: return RoomStatus.text(vo.getStatus()); // 0 - 空闲 case 5: return vo.getGuestName(); default: return vo.getRemark(); } } /** 声明列类型JTable 才知道第 3 列按数字排序而不是按字符串 */ Override public Class? getColumnClass(int c) { return c 3 ? BigDecimal.class : String.class; } }房态用颜色区分写一个渲染器继承DefaultTableCellRenderer按状态改前景色。注意判断isSelected否则选中行的高亮会被覆盖掉看起来像界面坏了。public class StatusRenderer extends DefaultTableCellRenderer { Override public Component getTableCellRendererComponent( JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int col) { Component c super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, col); if (!isSelected) { String s String.valueOf(value); if (在住.equals(s)) c.setForeground(new Color(0x1B7F3B)); else if (空闲.equals(s)) c.setForeground(new Color(0x1F5FA8)); else if (维修.equals(s)) c.setForeground(new Color(0xA33A2B)); else c.setForeground(Color.DARK_GRAY); } return c; } }最后一条是线程纪律JDBC 查询绝不能在事件分发线程里跑。数据库稍有延迟整个界面就冻住连关闭按钮都点不动用户以为程序崩了。用SwingWorker把查询丢到后台线程done()回到 EDT 再更新 modelprivate void loadRooms() { new SwingWorkerListRoomVO, Void() { Override protected ListRoomVO doInBackground() { return roomService.listAll(); // 后台线程阻塞也不影响界面 } Override protected void done() { try { model.reload(get()); // get() 会把异常带回来别吞掉 } catch (Exception e) { JOptionPane.showMessageDialog(RoomPanel.this, 加载房态失败 e.getCause().getMessage(), 提示, JOptionPane.WARNING_MESSAGE); } } }.execute(); }参数说明done()里必须调get()而不是getQuietly之类因为后台抛的异常只有在get()时才会被重新抛出不调get()的话一次失败的查询会静悄悄什么都不发生房态一直停在旧数据上排查起来就是个玄学问题。5. 避坑与排查五个把我卡到凌晨的现场问题5.1 报 “The server selected protocol version TLS10 is not accepted”现象程序启动登录一点就弹异常堆栈里写着The server selected protocol version TLS10 is not accepted by client preferences数据库明明用客户端工具能连上。原因新版 JDK 出于安全考虑在java.security里禁用了 TLS 1.0/1.1而老版本 SQL Server 实例只协商到 TLS 1.0。客户端工具能连是因为它自带了协商逻辑跟 JDBC 走的不是一条路。解决优先给数据库实例打补丁升级到支持 TLS 1.2实在改不了就在db.properties的 URL 后面追加;sslProtocolTLSv1.2或者降级驱动到与实例匹配的版本。改java.security删掉那三行禁用配置也能生效但那等于把整台机器的安全性拉低不是好办法我一般不这么干。5.2 数据库里中文变成问号界面上显示成乱码现象客人姓名录进去界面看着正常重启程序再查出来是??但用数据库客户端查又是好的。原因建表时偷懒用了VARCHAR而不是NVARCHAR。JDBC 默认以 Unicode 参数发送字符串落到非 Unicode 列上超出当前排序规则代码页的字符就被替换成问号且这个过程不可逆。解决所有可能存中文的列——姓名、备注、房型名、消费项目——一律NVARCHAR。已经建好的表用ALTER TABLE t_guest ALTER COLUMN name NVARCHAR(30) NOT NULL;改改完把旧数据重录一遍。前期多打四个字母后期省一整天。5.3 两人同时开同一间房第二天对账少了一笔现象高峰期两个前台同时操作同一间房出现了两条status0的入住单退房时账单只结了一条另一条的房费凭空消失。原因开房流程先SELECT查房态、再UPDATE两步之间没有任何锁。两个事务都读到“空闲”都认为自己可以开双双提交。解决把查询改成SELECT status FROM t_room WITH (UPDLOCK, ROWLOCK) WHERE room_id ?让第一个事务在读到这行时就持有排他锁第二个请求会阻塞到前者提交。另外给t_checkin加一个过滤唯一索引CREATE UNIQUE INDEX ux_ci_live ON t_checkin(room_id) WHERE status 0;从数据库层面堵死“一间房两条在住单”双保险。5.4 结算金额差几毛钱客人当场质疑现象单价 268.00住了两晚加半天理论上是 670系统算出 669.99 或者 670.0001。原因中间过程用了double或者没做setScale。double存不了 0.1268 * 2.5在二进制里就是 670.0000000000001 这种值再乘个折扣系数误差继续放大。解决金额全链路用BigDecimal从实体字段、DAO 映射到页面展示一路到底每次乘除之后立刻setScale(2, RoundingMode.HALF_UP)。数据库列用DECIMAL(10,2)而不是MONEY或FLOAT。这条不是玄学是数学别赌。5.5 点“查询”按钮后界面白屏几秒任务管理器显示未响应现象数据量涨到几千条入住记录之后点查询整个窗口卡住标题栏变灰鼠标转圈。原因查询写在按钮的actionPerformed里而actionPerformed跑在 EDT 上。EDT 被 JDBC 阻塞界面就不能重绘、不能响应任何事件。解决所有耗时操作包进SwingWorkerdoInBackground里查库done里更新组件。顺带把SELECT *改成需要的列列表页别一次拉全表加个时间范围过滤和TOP 200。这两处改完同一个操作从卡三秒变成秒开。6. 二次开发把课设改成能演示的成品以及我的验证习惯从“能跑”到“敢演示”中间隔着的往往不是功能而是几个细节。我一般按下面三条动手改动量都不大但观感提升明显。第一条是补权限与日志。MainFrame里按role隐藏面板只解决了一半问题真正的拦截要在 service 每个写方法开头加一句checkRole(operator, Role.ADMIN)否则有人绕过界面直接调接口就穿了。日志表t_log加上后配合t_log上的create_time索引出一个“最近七天操作流水”的面板答辩时演示“谁在什么时候退了 803”非常直观。第二条是把数据库访问换成连接池。单机演示无所谓但一旦开两个客户端DriverManager每次新建物理连接的开销就藏不住了。改法很轻加一个 HikariCP 的 jar把DBUtil.getConnection()换成从HikariDataSource取db.properties里补上jdbc.maximumPoolSize10、jdbc.connectionTimeout3000TxTemplate和所有 DAO 一行都不用动——这就是前面坚持“连接只从 DBUtil 出”的回报。第三条是报表导出。管理员面板上那个入住率用Apache POI写成 xlsx 交给门店留存代码量大概三十行但演示效果远好于在表格里看数字。改完之后必须验一遍我固定跑这张清单缺一项都不算验收验证项操作预期结果冷启动数据库未启动时打开程序弹提示“数据库连接失败”不白屏不崩溃开房事务手动把t_checkin插入改成失败房态回滚为“空闲”入住单不存在并发开房两个客户端同时开同一房一个成功另一个提示“当前不可入住”计费边界12:00 整、18:00 整各退一次房分别按半天、全天计金额无小数溢出中文往返录入带生僻字的客人姓名重启后查询不变问号权限越界前台账号调用房间维护接口被 service 层拒绝并记录日志这份清单是我早年吃过亏之后立下的规矩。有次为了赶在演示前加上“会员折扣”把计费逻辑从BigDecimal改成了一个double的快捷方法账面上差了三毛现场没人发现结果对账时被门店会计一眼揪出来返工重算了两天的单子。从那以后我每次动金额相关的代码都强制走一遍上面那张表的计费边界和开房事务两行哪怕只改了一个常量。数据库课设看着简单真正值钱的从来不是界面有多花哨而是那几条保证“账实相符”的事务和约束有没有守到位。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?
咨询建站