
简介一款基于Java的银行排号系统案例面向需要完成课程设计或毕业设计的Java学习者提供从项目报告、答辩PPT到源代码、数据库的完整资料可用来理解银行排队取号、预约管理等业务场景的落地实现。压缩包整体约1.69MB包含项目报告、答辩PPT、Java源代码及数据库文件覆盖了需求分析、系统架构、业务逻辑、GUI界面、异常处理与测试等关键环节。项目采用Java技术栈与MVC设计模式将业务逻辑、数据处理和用户界面分离数据库遵循关系范式设计存储用户信息、预约记录与队列状态后台可通过Servlet或Spring Boot处理请求辅助构建完整Web应用。已有151人学习适合希望掌握Java Web开发流程、数据库设计及团队协作能力的学习者参考与二次开发。1. 为什么银行排号系统的核心不是取号机而是那张表早上八点半的银行网点取号机吐出一张 A012大屏显示 A007 正在 3 号窗口办理等候区还有 14 个人。这套动作看着简单但真正动手写过的人都知道麻烦全在你看不见的地方一个号码从生成到作废要经过几个状态、窗口叫号时怎么保证同一时刻只有一个人被叫到、柜员临时离席时队列怎么调整。银行排号系统的本质不是一个取号界面而是围绕「号码生命周期」构建的状态机加并发控制。这份基于 Java 的银行排号系统项目包把系统设计、源代码、数据库脚本、项目报告和答辩 PPT 都放在了一起适合正在做 Java 课程设计、毕业设计或者想补一块完整 Web 业务实战经验的读者。报告写得比较规范源码本身也保留了典型的 MVC 分层拿来跑通之后改造成自己的业务场景并不难。2. MVC 分层与 Java 代码骨架先看清楚项目里每层干什么2.1 为什么要用 MVC 而不是把业务写在界面里初学 Java 的时候很多人喜欢把按钮点击事件里直接写 SQL跑通是跑通了但后面加一个「预约取号」功能就要动界面、动数据库、动逻辑三处。这套源码走的 MVC 设计模式本质是让视图、控制器、模型各管一段。拆开项目报告可以确认系统的分层是这样的Model 层对应实体类和数据库表映射比如 Customer、QueueTicket、WindowInfoView 层用户取号界面、柜员叫号界面、大屏展示界面源码里以 JavaFX/Swing 窗体为主也有 JSP 页面配合Controller 层接收界面动作调用 Service 完成业务再把结果送回视图Service 层真正处理生成号码、分配窗口、更新队列状态这些核心逻辑DAO 层封装 JDBC 操作负责和 MySQL/SQLite 交互。// 典型包结构课程设计项目里的常见划分方式 com.bank.queue ├── controller // 取号、叫号、窗口管理控制器 ├── service // 业务逻辑号码生成、队列流转 ├── dao // 数据库访问封装JDBC ├── model // 实体类QueueTicket、WindowInfo等 ├── view // Swing/JavaFX 界面 └── util // 数据库连接工具类提示自己写项目时最容易犯的错是 Controller 里直接写 JDBC。前期图快后期改需求时每改一处数据库字段就要把所有界面类翻一遍。按分层结构走View 不碰数据库Controller 不写 SQL维护成本会低很多。2.2 三个核心界面分别处理什么业务银行排号系统从使用者角度可以拆成三类角色源码和报告也都是围绕这三块展开的。角色界面核心操作涉及实体用户取号机界面点击取号选择业务类型拿到号码条QueueTicket柜员窗口叫号界面呼叫下一个办理完成暂停服务WindowInfo大堂经理/观众大屏展示界面实时看到当前叫号、等待人数QueueStatus用户点一次取号Controller 收到请求后调用 Service 层。Service 先查当前业务类型的最新号码编号再生成一个新号码并持久化到数据库最后把号码返回给界面显示。柜员点「呼叫下一个」时Controller 调用 Service 的 nextTicket 方法从等待队列里取出符合窗口业务类型的、状态为「等待」的号码将其状态改成「已叫号」同时更新大屏显示。这三条链路都绕不开一个东西号码状态。2.3 Service 层是整个项目里信息密度最高的地方项目报告里专门有一节讲核心业务逻辑这部分在源码中的落点就是 service 包下的 QueueService。它至少承担四件事查询当前最大号码、生成新号码、呼叫下一个、完成/挂起业务。以取号为例常见做法是先锁住号码序号的更新操作再插入新纪录。逻辑上可以写成这样// 取号核心逻辑摘取项目常见实现思路 public QueueTicket takeTicket(String serviceType) { // 1. 生成号码A001、A002 这种业务前缀流水号 String ticketNo ticketGenerator.next(serviceType); // 2. 创建票号实体并初始化状态 QueueTicket ticket new QueueTicket(); ticket.setTicketNo(ticketNo); ticket.setServiceType(serviceType); ticket.setStatus(TicketStatus.WAITING); ticket.setCreateTime(new Date()); // 3. 落库 ticketDao.insert(ticket); return ticket; }这里参数 serviceType 决定了前缀比如对公业务走 G 开头个人业务走 A 开头。实际项目里的 TicketGenerator 会查数据库里同类业务的最新流水号再自行加一这么做的好处是号码本身具有业务可读性排错时看号码就知道是哪个队列的。3. 号码生成与窗口分配的并发控制状态机才是排号系统的内核3.1 号码状态流转等待、叫号、办理、完成初看排号系统觉得「取号、叫号」两步就完了。但真正常出 Bug 的地方在状态边界比如用户过号了怎么办、柜员叫了三次没人来又怎么办。源码里把这部分收敛成了一个状态枚举围绕状态写死流转规则。WAITING等待 - CALLED已叫号 - SERVING办理中 - DONE完成 \- NO_SHOW过号 - WAITING重新排队每个状态变更都是在一个事务里完成的避免出现「界面显示已叫号数据库还是等待」这种不一致。我拆源码时注意到项目在状态字段上用了Int而不是String0 到 3 分别对应上述四种状态。用数字做状态枚举的好处一是省空间二是避免中英文混乱导致判断失效坏处则是看数据库表时需要一份注释对照。// 状态枚举对应数据库 ticket.status 字段 public enum TicketStatus { WAITING(0), // 等待中 CALLED(1), // 已叫号 SERVING(2), // 办理中 DONE(3); // 已完成/已作废 private final int code; TicketStatus(int code) { this.code code; } public int getCode() { return code; } }3.2 并发取号时号码唯一性怎么保证多窗口同时取号、多柜员同时叫号是测试时最容易复现并发问题的场景。两台取号机同时按下去如果没有并发控制两个线程可能读到同一个 A011生成两条一模一样的号码。解决手段通常是两种数据库唯一索引兜底、Java 层同步控制。// 伪代码并发场景下生成号码的常见做法 public synchronized String next(String serviceType) { String maxNo ticketDao.selectMaxTicketNo(serviceType); int nextNo parseSeq(maxNo) 1; return serviceType String.format(%03d, nextNo); }synchronized保证单机环境下同一时刻只有一个线程进入号码生成方法selectMaxTicketNo查到当前最大流水号后自行加一再加%03d补齐三位。这里注意如果将来拆成微服务多实例部署synchronized就失效了要换成数据库行锁或 Redis 自增课程设计阶段单机跑足够。测试吧里最容易出问题的是忘在 DAO 层给ticket_no加唯一索引导致压测时一条修复代码就能让项目跑出大量重复号码。3.3 窗口叫号是怎么避免「抢同一单」的柜员点「呼叫下一个」时系统的查询条件要同时限定两件事队列状态为等待、业务类型匹配窗口类型。但两个窗口如果都匹配同一条等待记录就可能出现都被叫到。源码里的方案是在 Service 调用 DAO 更新时加了一个条件判断更新的同时做状态比对典型写法是// 叫号逻辑只有状态为WAITING的记录才能被更新为CALLED int rows queueTicketDao.updateStatusByIdAndStatus( ticket.getId(), TicketStatus.WAITING.getCode(), TicketStatus.CALLED.getCode() ); if (rows 0) { // 更新行数为0说明这条记录已经被其他窗口取走 throw new BusinessException(该号码已被受理或已过号); }重点是 SQL 层面的UPDATE ... WHERE id ? AND status 0更新结果影响行数为 0 时说明状态已经被别人改掉了业务上就认为本次叫号失败。这种乐观锁思路在排号场景里比纯 Java 锁更实用因为它把控制范围缩小到单条记录不会因为长时间锁表拖慢其他队列。3.4 过号重排与队列切换的边界处理用户过号不是走完整个流程而是从完成态回到等待态重新排队。这类特殊情况最怕代码里写一堆 if-else 把状态值写死后面改需求时牵一发动全身。项目报告里提到的解决方案是把状态流转封装成一个方法任何入口都走这个逻辑方法。public QueueTicket reQueue(String ticketNo) { QueueTicket ticket ticketDao.selectByTicketNo(ticketNo); if (ticket.getStatus() TicketStatus.CALLED.getCode()) { ticket.setStatus(TicketStatus.WAITING.getCode()); ticketDao.update(ticket); return ticket; } throw new BusinessException(只有已叫号未办理的票才能重新排队); }这里传入的 ticketNo 是字符串查询前最好做一次非空校验否则会出现空指针直接打到界面上。过号的重新排队和取号生成新号是不同链路一个是状态变更一个是新建记录测试时容易混淆建议在项目报告测试章节里分开描述。4. 数据库表设计与 DAO 持久化从 ER 图到 SQL 脚本怎么落地4.1 数据表拆几张、为什么拆成这几张数据库设计这一块报告里花了不小篇幅核心设计目标是满足第三范式避免一个表里塞太多重复信息。拆开 SQL 脚本可以发现系统核心表围绕「票号」和「窗口」展开共五张左右queue_ticket排号单记录号码、业务类型、状态、创建时间、叫号时间、完成时间window_info窗口信息窗口编号、业务类型、状态空闲/忙碌/暂停service_type业务类型字典个人业务、对公业务、挂失业务等customer用户信息预约取号时关联queue_log操作日志记录叫号、过号操作方便排错。-- 核心表结构MySQL 语法源码里的简化版本 CREATE TABLE queue_ticket ( id BIGINT PRIMARY KEY AUTO_INCREMENT, ticket_no VARCHAR(10) NOT NULL COMMENT 号码如A001, service_type VARCHAR(20) NOT NULL COMMENT 业务类型编码, status INT NOT NULL DEFAULT 0 COMMENT 0等待 1已叫号 2办理中 3完成, customer_id BIGINT COMMENT 关联用户可为空, create_time DATETIME NOT NULL, call_time DATETIME, finish_time DATETIME, UNIQUE KEY uk_ticket_no (ticket_no) -- 防止重复号码兜底并发 ) COMMENT 排号单表;建表时把ticket_no设为唯一索引是表格本身对并发取号做的最后一道防线。就算 Java 层因为某种极端情况生成了重复号插入数据库时也会报错不会默默留下脏数据。default 0保证新插入的票默认是等待状态少写一行代码也少一个出错点。4.2 数据库连接与 DAO 层封装项目包里数据库文件可能是 SQL 脚本也可能是 SQLite 文件两种情况处理方式不同。如果是 SQL 脚本拿到的就是上面的建表和插入语句需要自己在 MySQL 里执行如果直接是.db或.sqlite文件就用 SQLite 工具打开。对应到源码里util 包下的DBUtil负责加载驱动和获取连接这是整套 DAO 的基础。// JDBC工具类读取配置文件中的数据库连接参数 public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bank_queue?useSSLfalsecharacterEncodingutf8; private static final String USER root; private static final String PASSWORD 123456; static { try { // 加载MySQL驱动 Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { // 驱动没导入会走到这里 e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }URL 里的characterEncodingutf8是必须的不加的话插入中文姓名、中文业务类型容易变乱码。useSSLfalse是为了本地开发免去 SSL 配置生产环境不建议这么写。改造这个项目时把 IP、用户名、密码挪到 properties 配置文件里是第一步否则换台电脑跑就要改源码重新编译。4.3 第三范式在排号业务里怎么取舍报告提到的第三范式核心是非主键字段不能传递依赖于主键。放到排号系统里queue_ticket表只存service_type编码不存「业务类型名称」要看业务名需要去service_type表关联。项目报告里评估性能时提到等连接查询在大屏刷新场景下访问频率高可以适当冗余一个service_type_name字段这就是「先满足第三范式在性能瓶颈处再反规范化」的典型设计思路。-- 业务类型字典表 CREATE TABLE service_type ( code VARCHAR(20) PRIMARY KEY, name VARCHAR(50) NOT NULL, prefix VARCHAR(5) NOT NULL COMMENT 号码前缀A/G等, sort_no INT COMMENT 排序用控制大屏展示顺序 ); INSERT INTO service_type (code, name, prefix, sort_no) VALUES (PERSONAL, 个人业务, A, 1), (BUSINESS, 对公业务, G, 2), (REPORT_LOSS, 挂失业务, H, 3);有一点值得注意prefix字段决定了号码前缀这意味着换一个前缀不用改 Java 代码只要在数据库里插入一条记录即可。这个设计对答辩展示比较友好演示时新增一种业务类型评委看到的是数据驱动业务扩展而不是源码层面写死。4.4 DAO 层写 SQL 时常见的坑源码里 DAO 层全部走的是 PreparedStatement这是加分项。它不只是防 SQL 注入更实际的好处是 Java 代码不用手动拼接字符串查询条件里的中文和特殊字符不会因为转义问题报错。selectMaxTicketNo这类聚合查询最容易踩的坑是没处理空结果集表里一条数据都没有时MAX(ticket_no)返回 null直接用 null 做字符串拼接会出现null这种号码。// 查询当前最大号码处理无记录时的null情况 public String selectMaxTicketNo(String serviceType) { String sql SELECT MAX(ticket_no) FROM queue_ticket WHERE service_type ?; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, serviceType); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { // 这里必须判空否则首条记录会出现null前缀 return rs.getString(1); } return null; } } catch (SQLException e) { throw new RuntimeException(e); } }这段代码的返回值在 Service 层要做一次是否为 null 的判断null 表示还没有任何号码直接从 1 开始非 null 则解析出数字部分加一。用 try-with-resources 写PreparedStatement是 JDK 7 之后的标准做法资源自动关闭避免连接泄漏。数据库连接泄漏在长期运行的窗口管理程序里是隐性炸弹跑一天可能没问题跑一周数据库连接数就满了。5. 答辩与调试验证逻辑、运行排错和 PPT 讲解技巧5.1 本地跑通项目的三件事打开项目压缩包后先做三件事确认数据库脚本类型、导入到对应数据库、调整 DBUtil 连接参数。数据库工具用 Navicat 或命令行都可以执行完 SQL 脚本后用一条SELECT COUNT(*) FROM queue_ticket;验证表是否建成功。然后启动主类先走一遍完整流程用户取号 → 柜员叫号 → 办理完成 → 大屏状态变化。启动时最常遇到的三个异常异常信息原因处理方式ClassNotFoundExceptionJDBC 驱动 jar 没导入把 mysql-connector 放入 lib 目录并添加到构建路径Communications link failure数据库没启动或 URL 配错检查 MySQL 服务核对端口和库名Unknown database bank_queue数据库没创建执行CREATE DATABASE bank_queue;5.2 验证逻辑有没有写对看这四个用例答辩最怕的是评委现场点几下就暴露逻辑漏洞。我建议按下面四条路径在演示前自己过一遍第一连续取十张票检查号码是否连续且前缀正确第二开两个窗口同时点叫下一个看系统会不会把同一个人叫到两个窗口第三把一张已叫号的票点「过号」再重新排队检查它在队列中的位置第四把当前等待队列清空点叫号看系统是报错还是友好提示。第四条特别容易翻车很多实现里查询结果为空时直接抛异常并没有提示「当前暂无等待客户」。源码里如果没处理建议在 Service 层补一个if (ticketList.size() 0)的判断返回一个空结果由 Controller 组装成提示信息返回到界面。5.3 答辩 PPT 讲「设计」而不是讲「步骤」答辩 PPT 里最容易出现的问题是整页贴代码、通篇讲「我做了什么操作」。评委更想听的是「为什么这么设计」。讲 MVC 分层时用模块图说明三层各自职责强调 Controller 不碰 SQL、DAO 不碰界面讲数据库时用第三范式解释为什么业务类型单独建表讲并发控制时明确说出「用 UPDATE 影响行数做乐观锁比直接锁表更适合这类高并发取号场景」。项目报告里提到的性能评估答辩时可以拿真实数据说事比如 100 个线程并发取号压测后没有产生重复号码这个数据比任何形容词都有说服力。最后提醒一句如果源码包里的界面是 Swing 写的改造时保留原界面的取号、叫号、大屏三个主要面板只替换掉数据访问层换成 MyBatis 或 Spring JDBC 就能变成一套更像企业级写法的项目如果想往 Spring Boot 方向靠优先把 DAO 层替换成 JPA 或 MyBatis这比从零重构省力也保留了项目报告的原有结构。本文还有配套的精品资源点击获取