Java银行排号系统源码与数据库设计:并发取号、队列调度及论文框架
2026/9/24 19:30:31 网站建设 项目流程

简介:这份资源是面向高校计算机专业学生与Java初学者的一套银行排号系统完整毕业设计资料,围绕服务器端与客户端双模块架构展开,可用于课程设计、毕设选题或Java桌面应用练手。系统功能划分清晰:服务器端涵盖取号、统计、删除、查询与通知,客户端则实现业务员登录、叫号、统计、删除及查询,同一时刻支持多个工作台并行办理业务,能帮助读者理解C/S通信、数据库存取与排队调度逻辑。压缩包为rar格式,整体约69.98MB,内含源码、演示视频、数据库脚本与配套论文,文件类型覆盖代码、视频与文档,便于对照学习与二次开发。目前已有468人学习下载,适合需要完整赛题方案、可运行工程与论文写作参考的读者,也能为排错与功能扩展提供思路。

1. 银行排号系统到底在解决什么问题:从叫号大厅到 Java 后台

去银行办业务,最直观的体验就是取号、等待、被叫号。很多人以为排号系统只是「打印一张小票」,但真正落到 Java 后台,它要解决的是并发取号不重号、多窗口公平调度、VIP 优先级插队、业务类型分流、断线重连后状态不丢这一整套问题。一个能跑通的银行排号系统,本质是一个带状态机的队列服务:号码生成器负责发号,队列管理器负责排队,窗口调度器负责叫号,前端大屏负责展示。它适合做 Java 课程设计、毕业设计,也适合想练手 Java 并发与数据库增删改查的开发者。标题里提到的源码、数据库、论文,对应的正是「能跑起来 + 能讲清楚 + 能写出来」三件事,下面按落地顺序拆开讲。

2. 技术选型与数据库设计:为什么用 Java 而不是别的

2.1 后端为什么锁定 Java + Spring Boot

银行排号系统的核心诉求是稳定、易维护、生态成熟。Java 在这个场景里几乎是默认答案:Spring Boot 把 Web 层、依赖注入、事务管理一次性配好,省掉大量样板代码;Java 的线程池和并发包天然适合处理「多个取号机同时发号」这种场景。常见做法是用 Spring Boot 2.x 搭单体应用,前端用 Thymeleaf 或直接静态页面加 AJAX,数据库用 MySQL。不推荐一上来就上微服务,排号系统业务边界清晰、并发量有限,单体足够,拆开反而增加部署和调试成本。

选型时还要考虑一个现实问题:课程设计或毕设的评审老师通常关心「你有没有用到 Java 的核心特性」。所以代码里最好能体现 synchronized、ReentrantLock、线程池、事务注解这些点,而不是纯 CRUD 堆砌。这也是为什么很多 Java 课程设计案例源码会选排号系统——它小而全,能覆盖 Java 基础、数据库、并发、前端交互四条线。

2.2 数据库表怎么设计才不返工

排号系统的数据库设计有个血泪经验:号码状态一定要有独立字段,不要靠时间戳推断。下面是我一般会用的核心表结构,字段名和类型可以直接抄。

表名关键字段类型说明
ticketidBIGINT 主键自增号码记录 ID
ticketticket_noVARCHAR(10)展示号码,如 A001
ticketbiz_typeVARCHAR(20)业务类型:个人/对公/VIP
ticketstatusTINYINT0 等待 1 办理中 2 已完成 3 过号
ticketcreate_timeDATETIME取号时间
ticketwindow_idINT办理窗口,未叫号时为 NULL
windowidINT 主键窗口编号
windowstatusTINYINT0 空闲 1 忙碌
windowcurrent_ticketVARCHAR(10)当前正在办理的号码

建表时注意两点:ticket_no 加唯一索引,防止并发取号重号;status 用 TINYINT 而不是字符串,查询和更新都更快。业务类型如果要做优先级,可以在 biz_type 上再加一个 priority 字段,VIP 给高值,叫号时按 priority 降序、create_time 升序取第一条。

2.3 号码生成器:并发取号不重号的最小实现

取号是整个系统最容易翻车的地方。两台取号机同时点「取号」,如果先查最大号再插入,中间有时间窗口,必然重号。常见做法有两种:数据库唯一索引 + 重试,或者用数据库自增主键拼号码。我一般用后者,简单可靠。

// 基于数据库自增 ID 生成号码,避免并发重号 @Service public class TicketService { @Autowired private TicketMapper ticketMapper; // 业务类型前缀映射 private static final Map<String, String> PREFIX = Map.of( "PERSONAL", "A", "CORPORATE", "B", "VIP", "V" ); @Transactional public String generateTicket(String bizType) { Ticket ticket = new Ticket(); ticket.setBizType(bizType); ticket.setStatus(0); ticket.setCreateTime(new Date()); // 先插入,拿到自增 ID ticketMapper.insert(ticket); // 用自增 ID 拼号码,保证全局唯一 String prefix = PREFIX.getOrDefault(bizType, "A"); String ticketNo = prefix + String.format("%03d", ticket.getId()); ticket.setTicketNo(ticketNo); ticketMapper.updateTicketNo(ticket.getId(), ticketNo); return ticketNo; } }

这段代码的逻辑是:先插入一条记录拿到数据库自增 ID,再用 ID 拼出展示号码。自增 ID 由数据库保证唯一,所以号码不会重。参数方面,PREFIX 决定不同业务的前缀,VIP 用 V 开头方便窗口识别;String.format("%03d", id) 保证号码至少三位,超过 999 会自动扩展。注意 @Transactional 要加,否则插入和更新之间失败会留下脏数据。如果并发量特别大,可以把 updateTicketNo 合并成一次插入,用触发器或应用层计算,但课程设计级别这样写足够。

3. 叫号调度与窗口管理:队列怎么排、窗口怎么叫

3.1 队列调度策略:FIFO 还是优先级

排号系统的队列不是简单先进先出。真实银行里 VIP 要优先,对公业务可能单独排队,过号后要重新排队或直接作废。我一般会设计成「按业务类型分队列 + 优先级排序」:每个业务类型一个逻辑队列,叫号时先看 VIP 队列有没有人,再看普通队列。实现上不用真的建多个 Queue,直接在数据库查询时排序即可。

-- 叫号时取下一个号码:VIP 优先,同优先级按取号时间 SELECT * FROM ticket WHERE status = 0 AND biz_type = #{bizType} ORDER BY priority DESC, create_time ASC LIMIT 1;

这条 SQL 的关键是 ORDER BY priority DESC, create_time ASC。priority 字段在取号时根据业务类型写入,VIP 给 10,对公给 5,个人给 1。这样叫号逻辑不用改代码,改数据就行。注意 status = 0 这个条件必须加,否则会把已完成的号码又叫一遍。如果要做「过号重排」,把过号号码的 create_time 更新为当前时间再放回等待队列即可。

3.2 窗口叫号的状态流转

窗口叫号是一个典型的状态机:窗口空闲 → 请求叫号 → 取到号码 → 窗口忙碌 → 办理完成 → 窗口空闲。每一步都要更新 ticket 和 window 两张表,而且要在同一个事务里,否则会出现「号码显示办理中但窗口还是空闲」的玄学问题。

@Transactional public String callNext(int windowId, String bizType) { // 1. 查窗口状态,必须是空闲 Window window = windowMapper.selectById(windowId); if (window.getStatus() != 0) { throw new BizException("窗口正在办理业务"); } // 2. 取下一个等待号码 Ticket next = ticketMapper.selectNext(bizType); if (next == null) { return null; // 无人等待 } // 3. 更新号码状态为办理中 ticketMapper.updateStatus(next.getId(), 1, windowId); // 4. 更新窗口状态为忙碌 windowMapper.updateStatus(windowId, 1, next.getTicketNo()); return next.getTicketNo(); }

逻辑说明:四步操作包在一个事务里,任何一步失败整体回滚。参数 windowId 是窗口编号,bizType 是当前窗口办理的业务类型。注意第 1 步的窗口状态检查不能省,否则两个窗口同时叫号可能叫到同一个号码。如果要做「转移窗口」,把 updateStatus 的 windowId 改掉即可,状态不用变。

3.3 大屏展示与前端轮询

大屏展示一般用轮询,不用 WebSocket,因为排号系统对实时性要求没那么高,轮询实现简单、调试方便。前端每隔 2 秒请求一次接口,拿到当前叫号号码和等待人数。

// 大屏轮询:每 2 秒刷新一次叫号信息 setInterval(async () => { const res = await fetch('/api/display/current'); const data = await res.json(); document.getElementById('currentNo').innerText = data.currentTicket || '--'; document.getElementById('waitCount').innerText = data.waitingCount; document.getElementById('windowNo').innerText = data.windowId || '--'; }, 2000);

这段代码每 2 秒拉一次 /api/display/current,更新大屏上的当前号码、等待人数和窗口号。参数 2000 是轮询间隔,毫秒。如果银行大厅人多,可以降到 1000;如果服务器压力大,升到 3000 也行。注意接口要做缓存或限流,避免大屏多了以后数据库压力大。常见做法是在后端加一个 1 秒的本地缓存,多个大屏共享。

4. 避坑与排查:排号系统最容易翻车的 5 个点

4.1 并发取号重号

现象:两台取号机同时操作,出现两个 A001。原因:先查后插有时间窗口,或者号码生成用了「查最大值 +1」。解决:用数据库自增 ID 拼号码,或者给 ticket_no 加唯一索引并在插入失败时重试。我一般直接用自增 ID,省事。

4.2 叫号叫到已完成的号码

现象:窗口叫号,叫出来的号码是昨天已经办完的。原因:查询下一个号码时漏了 status = 0 条件,或者 status 更新失败但事务没回滚。解决:SQL 里强制加 status 条件,叫号方法加 @Transactional,更新失败直接抛异常。

4.3 窗口状态和号码状态不一致

现象:大屏显示某窗口正在办理 A005,但窗口实际空闲。原因:更新 ticket 和 window 不在同一个事务,或者更新顺序反了。解决:把两步更新放进同一个 @Transactional 方法,先更新 ticket 再更新 window,失败一起回滚。

4.4 过号后无法重新排队

现象:客户过号了,想重新排,系统里找不到入口。原因:过号时直接把 status 改成 3,没有提供「重新激活」接口。解决:加一个接口,把 status 从 3 改回 0,同时把 create_time 更新为当前时间,让它排到队尾。注意要限制次数,防止恶意刷号。

4.5 数据库连接池耗尽

现象:大屏轮询频繁,系统跑一段时间后报「连接池已满」。原因:每次请求都开新连接,或者慢查询没加索引。解决:给 ticket 表的 status 和 biz_type 加联合索引,大屏接口加缓存,连接池最大连接数调到 20 以上。课程设计级别用 HikariCP 默认配置基本够,但索引一定要加。

5. 从能跑到能写:论文框架与源码组织的进阶技巧

5.1 论文框架怎么搭才不空

银行排号系统的论文最容易写成「需求分析 + 截图 + 总结」的流水账。我一般会按「问题 → 方案 → 验证」三段式搭:第一章讲排号系统的并发和调度难点,第二章讲技术选型和数据库设计,第三章讲核心模块实现(号码生成、队列调度、窗口管理),第四章讲测试和并发验证,第五章讲不足和改进。关键是把「为什么这么设计」写清楚,比如为什么用自增 ID 而不是查最大值,为什么用轮询而不是长连接。这些决策点才是论文的得分点。

5.2 源码怎么组织才像真实项目

很多课程设计源码把所有类塞在一个包,评审一看就减分。我一般按分层组织:controller 放接口,service 放业务逻辑,mapper 放数据库操作,entity 放实体类,config 放配置。号码生成、队列调度、窗口管理各一个 service,互不干扰。这样论文里写「模块化设计」才有东西可写。

5.3 一个验证并发的小技巧

想验证取号不重号,不用真的开两台机器。写一个单元测试,起 50 个线程同时调 generateTicket,最后检查 ticket_no 有没有重复。

@Test public void testConcurrentGenerate() throws Exception { int threadCount = 50; ExecutorService pool = Executors.newFixedThreadPool(threadCount); Set<String> ticketNos = ConcurrentHashMap.newKeySet(); CountDownLatch latch = new CountDownLatch(threadCount); for (int i = 0; i < threadCount; i++) { pool.submit(() -> { try { String no = ticketService.generateTicket("PERSONAL"); ticketNos.add(no); } finally { latch.countDown(); } }); } latch.await(); pool.shutdown(); // 如果 set 大小等于线程数,说明没有重号 assertEquals(threadCount, ticketNos.size()); }

这段测试用 50 个线程并发取号,用 ConcurrentHashMap 的 keySet 去重,最后断言集合大小等于线程数。参数 threadCount 可以按需调整,课程设计跑 50 足够说明问题。注意 latch 要放在 finally 里,否则线程异常会导致主线程一直等。这个测试跑通,论文里的「并发验证」章节就有实打实的数据了。

我自己做这类系统最大的教训是:别一上来就追求功能多,先把取号、叫号、完成这三步的状态流转跑通,再往上加 VIP、过号、大屏。状态机稳了,后面都是锦上添花;状态机不稳,功能越多越乱。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询