news 2026/9/14 12:54:48

银行排号系统Java源码解析:MVC架构与并发控制核心设计

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
银行排号系统Java源码解析:MVC架构与并发控制核心设计

简介:一款基于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、WindowInfo;
  • View 层:用户取号界面、柜员叫号界面、大屏展示界面,源码里以 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而不是String,0 到 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?useSSL=false&characterEncoding=utf8"; 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 里的characterEncoding=utf8是必须的,不加的话插入中文姓名、中文业务类型容易变乱码。useSSL=false是为了本地开发免去 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,这比从零重构省力,也保留了项目报告的原有结构。

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

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

微信小程序智能机器人:消息链路设计与云开发实战

简介:面向微信小程序开发者和人工智能对话初学者,这份智能机器人小程序源码可帮助快速掌握页面搭建、消息交互与机器人服务对接方法。压缩包共19个文件,整体体积仅15KB,其中包含5个逻辑脚本文件、4个样式表文件、3个页面结构文件、…

作者头像 李华
网站建设 2026/9/14 12:43:11

DB-GPT 启动报端口 5670 “Address already in use“ 怎么排查?

DB-GPT 启动报端口 5670 "Address already in use" 怎么排查? 【免费下载链接】DB-GPT open-source agentic AI data assistant for the next generation of AI Data products. 项目地址: https://gitcode.com/GitHub_Trending/db/DB-GPT 当你用 …

作者头像 李华
网站建设 2026/9/14 12:42:39

NeMo Lightning 模块解析:PTL 与 Megatron Core 之间的训练桥接层

NeMo Lightning 模块解析:PTL 与 Megatron Core 之间的训练桥接层 【免费下载链接】Speech A scalable generative AI framework built for researchers and developers working on Large Language Models, Multimodal, and Speech AI (Automatic Speech Recognitio…

作者头像 李华
网站建设 2026/9/14 12:41:09

基于NSGA-Ⅱ的多能源系统协同优化Matlab实现

1. 项目背景与核心价值区域多能源系统协同优化是当前能源互联网领域的前沿研究方向。我在参与某省级智慧能源项目时,深刻体会到传统单能源系统独立运行的局限性——电、热、气等能源形式各自为政,导致整体能效低下,可再生能源消纳能力不足。这…

作者头像 李华
网站建设 2026/9/14 12:40:54

Nginx Rewrite模块详解:从基础到高级应用

1. Nginx Rewrite基础概念解析 Rewrite是Nginx服务器中一个强大的URL重写模块,它允许我们在请求到达后端应用前对URI进行修改和重定向。这个功能在日常运维和开发中扮演着关键角色,特别是在以下场景: 保持旧URL兼容性同时进行站点结构更新 …

作者头像 李华