news 2026/9/17 7:57:52

银行排队叫号系统设计:核心表、状态机与队列实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
银行排队叫号系统设计:核心表、状态机与队列实现

简介:基于Java与JSP技术的银行排队叫号系统毕业设计论文,面向计算机相关专业学生及Web应用开发人员,针对传统业务管理效率低、客户排队体验差等现实问题,完整呈现了从需求分析到系统实现的全过程。文档遵循软件工程常规流程,涵盖市场调研、需求分析、概要设计、详细设计、编码与测试各环节,详细阐述了B/S模式下的分层架构设计,以及系统个人中心、显示管理、客户管理、排队管理、服务业务管理、客户评价管理、等候区管理等核心模块的实现思路,并给出了客户信息表、排队信息表、服务业务表、客户评价表等数据库表结构设计。作为毕业设计成果,论文逻辑完整、章节安排合理,对计算机相关专业的信息系统类课题具有较好的参考和借鉴价值。资源包共包含1份docx格式论文文件,大小约1.15MB,已有170人浏览学习。

1. 银行排队叫号系统的核心问题不是排队,而是叫号

银行排队叫号系统,听起来要解决的是“排队秩序”,实际要解决的是“叫号状态”。一个 FIFO 队列只回答“谁是下一个”,叫号要回答的是一串状态流转:取号进入等待、窗口发出呼叫、客户未到位、再次呼叫、超时过号、开始办理。每一步都必须在大厅屏、窗口屏和语音播报之间保持一致。如果只当成消息队列来写,第一版上线就会遇到两类典型事故:号叫出去了柜员看板不更新,或者客户晚到两分钟号码就永久从队列里消失。下面这套做法把范围圈在单个网点:四个核心表、六个状态、一个单进程队列引擎,最后附上运维能直接抄的参数和验证命令。适合智慧网点、政务大厅的自研叫号项目,前提是网点自己掌握部署环境,不依赖外部排队平台。

2. 先定实体和状态机,再谈队列存储

做叫号系统最容易犯的错,是直接开一张表存号码,然后按时间排序取第一条。这种方法在单窗口演示时没有问题,一旦多个窗口同时叫号、客户过号要重排、VIP 要优先,排序逻辑就会膨胀到无法维护。正确的顺序是先把实体、状态、流转条件写明白,再考虑队列用什么结构存。

2.1 四个核心表:票号、窗口、业务类型、叫号流水

我一般最小化到四张表:ticket存号码当前状态,service_window存窗口归属和忙闲,service_type存业务前缀和优先级规则,call_log存每一次叫号动作的流水。ticket是最关键的一张,建表语句可以直接用:

CREATE TABLE ticket ( id BIGINT AUTO_INCREMENT PRIMARY KEY, queue_code VARCHAR(8) NOT NULL, seq_no INT NOT NULL, full_no VARCHAR(16) NOT NULL, priority INT NOT NULL DEFAULT 5, status TINYINT NOT NULL DEFAULT 0, called_count INT NOT NULL DEFAULT 0, window_id VARCHAR(16) DEFAULT NULL, created_at DATETIME NOT NULL, called_at DATETIME DEFAULT NULL, updated_at DATETIME DEFAULT NULL, UNIQUE KEY uk_queue_seq (queue_code, seq_no, created_at) ) ENGINE=InnoDB;

queue_code是业务类型前缀,对应取号单上的 A、B、C、V,V 留给 VIP。seq_no是同一个业务类型当天内的自增序号,full_no是屏显使用的完整号码,比如A012。这里有两个细节:第一,不要把id直接当成号给客户看,否则跨天、跨类型都无法解释;第二,uk_queue_seqcreated_at也放进去,是为了避免跨天重置时出现重复写入。priority越小越优先,普通客户默认 5,VIP 取号时传 1。

窗口表保持简单:id是窗口编号,queue_code是窗口能受理的业务类型,status表示空闲、忙碌、暂停。窗口与业务类型是多对一关系,一个窗口通常只服务一种类型,但叫号系统里经常出现“综合窗口”能收 A 和 B,所以不要把队列耦合进窗口表,而是由叫号引擎根据窗口传入的类型参数去取对应队列。

2.2 号码状态机,六个状态已经够用

状态字段status我按下面的表来定义,这套约定可以直接写进设计文档:

状态值状态名进入条件可流转去向
0WAIT 等待取号成功,进入队列1 叫号、5 弃号
1CALLED 已叫号窗口点击“下一个”2 重新呼叫、4 办理、3 过号
2RECALLED 重呼中首次呼叫超时,再次被叫4 办理、3 过号
3OVERDUE 已过号重呼后仍未到可由柜员手动召回,回到 0
4SERVING 办理中客户到窗口,开始办理5 弃号
5FINISHED 已完成办结或客户未响应放弃终态,不再参与排队

这六种状态里,最容易写错的是 2 和 3 的区别。实际业务语义是:第一次叫号后客户没出现,系统要把号重新放回队列里,但它的优先级要高于新取的号,否则一个晚到两分钟的客户会重新排到队尾,这在网点里会直接引发投诉。状态 2 不是单独一条队列,而是进到“重呼池”,等下先于普通等待号被弹出。如果重呼之后客户仍然没出现,才真正标记为 3 OVERDUE。过号之后由柜员手动操作“呼叫过号客户”,号码重新回到状态 0。

2.3 为什么队列主体放单进程内存,而不是直接依赖数据库

单网点的并发量不大,几个取号机加十几个窗口,高峰时段每分钟新增号码也就是几十个。常见做法是把队列结构放在应用服务的内存里,数据库只负责持久化状态。这样做的原因有三个:第一,队列需要支持按优先级和创建时间排序,内存堆结构比数据库查询直观;第二,叫号动作需要在一个锁内同时完成“弹出号码、更新窗口状态、写入流水”,数据库事务做这套操作也能做,但排查问题时会多一层心智负担;第三,单网点没有条件维护 Redis,用本地 MySQL 或 PostgreSQL 最稳妥。

内存队列用 Python 的heapq加字典实现,key 是queue_code,value 是一个堆,堆里按(priority, seq_no)排序。这样 VIP 号码会优先被取到,同优先级下先取号的人先被叫。数据库表里存的状态和内存队列可能短暂不一致,这是允许的,因为每次弹出号码前都会重新读一次数据库状态做校验,后面第 3 章会写到这一步。

3. 取号、叫号、过号重排:核心链路实现

这一章直接给可运行的核心代码,语言用 Python,框架无关,只说明队列引擎部分的写法。取号机、窗口终端、大屏通过 HTTP 或 WebSocket 调用同一组 API,所有状态变更都经过同一个进程内的queue_lock,这是单网点部署下保证不重号的关键。

3.1 取号接口先锁住业务类型的日序号

取号动作的完整逻辑是:生成当天该业务类型的序号,写入ticket表,同时把(priority, seq_no, ticket_id)放进内存堆。生成日序号必须用数据库行锁,避免两个取号机同时按出同一个 A001:

def take_number(queue_code: str, priority: int = 5) -> dict: with queue_lock: today = date.today().strftime("%Y-%m-%d") row = db.execute( """SELECT COALESCE(MAX(seq_no), 0) + 1 AS next_seq FROM ticket WHERE queue_code = :qc AND date(created_at) = :today FOR UPDATE""", {"qc": queue_code, "today": today}, ).fetchone() seq_no = row["next_seq"] full_no = f"{queue_code}{seq_no:03d}" ticket_id = db.execute( """INSERT INTO ticket (queue_code, seq_no, full_no, priority, status, created_at, updated_at) VALUES (:qc, :seq, :full, :prio, 0, NOW(), NOW())""", {"qc": queue_code, "seq": seq_no, "full": full_no, "prio": priority}, ).lastrowid heapq.heappush( pending_queues[queue_code], (priority, seq_no, ticket_id), ) db.commit() return {"ticket_id": ticket_id, "full_no": full_no}

逻辑说明:MAX(seq_no) + 1在同一事务内执行FOR UPDATE,锁住的是ticket表上满足条件的索引区间,两个取号机并发时后到的事务会等待前一个提交。full_no:03d补零到三位,排队超过 999 号的网点需要改成:04d,这个参数写在配置里更合适。pending_queues是全局字典,在服务启动时从数据库把当天状态仍为 0 的号码加载进内存,保证重启不丢队列。

3.2 叫号引擎:从一个队列里弹出一个等待号码

窗口点击“呼叫下一个”时,引擎要根据权重取号。取号顺序是:先看该窗口对应类型的重呼池,有重呼号码就先叫,没有再从等待堆里弹出。弹出前必须校验数据库里的状态,防止两条业务请求重复消费同一个号码:

def call_next(window_id: str, queue_code: str): with queue_lock: # 1. 先看重呼池,重呼池是普通 FIFO recall_ids = recall_pools[queue_code] if recall_ids: ticket_id = recall_ids.popleft() else: # 2. 等待堆按 (priority, seq_no) 弹出 while pending_queues[queue_code]: _, _, ticket_id = heapq.heappop(pending_queues[queue_code]) break else: return {"error": "empty_queue"} # 3. 从数据库重新读取,状态必须是 0 才能叫 ticket = db.execute( "SELECT * FROM ticket WHERE id = :id FOR UPDATE", {"id": ticket_id}, ).fetchone() if ticket["status"] != 0: # 已被叫过或已过号,跳过,继续弹下一个 return call_next(window_id, queue_code) db.execute( """UPDATE ticket SET status = 1, window_id = :wid, called_at = NOW(), called_count = called_count + 1, updated_at = NOW() WHERE id = :tid""", {"wid": window_id, "tid": ticket_id}, ) db.commit() publish_ws({ "event": "call", "window": window_id, "full_no": ticket["full_no"], "voice": f"请 {ticket['full_no']} 到 {window_id} 号窗口", }) return {"full_no": ticket["full_no"]}

逻辑说明:窗口只有一个动作按钮,系统不允许同一时间向一个窗口推送两个待服务号码。status != 0时递归取下一个号码,递归层数由队列长度限制,正常情况下最多跳一两张已被并发请求改状态的票。publish_ws是给大屏、窗口屏、语音播报的推送入口,参数里带上event是为了让大屏区分“叫号”和“过号”事件。这个接口的关键是FOR UPDATE,多线程甚至多进程部署时,这一行能防止两个窗口同时取到同一张票。

3.3 呼叫超时与过号重排的兜底任务

窗口叫号之后客户不一定马上到,系统需要一个后台任务轮询处理超时。轮询间隔 1 到 2 秒足够,不需要再短。逻辑如下:

def check_timeout_loop(): while True: with queue_lock: overdue_now = db.execute( """SELECT * FROM ticket WHERE status = 1 AND called_at < NOW() - INTERVAL :seconds SECOND ORDER BY called_at""", {"seconds": CALL_TIMEOUT}, ).fetchall() for t in overdue_now: if t["called_count"] >= RECALL_MAX: # 已经是第二次呼叫,标记过号 db.execute( "UPDATE ticket SET status = 3, updated_at = NOW() WHERE id = :id", {"id": t["id"]}, ) publish_ws({"event": "overdue", "full_no": t["full_no"]}) else: # 首次超时,放回重呼池,状态回到等待 db.execute( "UPDATE ticket SET status = 0, updated_at = NOW() WHERE id = :id", {"id": t["id"]}, ) recall_pools[t["queue_code"]].append(t["id"]) db.commit() time.sleep(1)

逻辑说明:这里用called_count区分首次超时和再次超时。CALL_TIMEOUT默认 120 秒,RECALL_MAX默认 1,表示最多重呼一次,再不到就过号。重呼池是普通的deque,新追加的重呼号排在同类型重呼队列的尾部,但整体优先于新取的等待号。这个设计的业务依据是:过号重排客户已经等过一遍,新客户多等一两分钟是可接受的,但晚到客户重新取号会造成号码跳变和现场混乱。

3.4 屏显和语音播报走一条推送通道

大屏和窗口屏不需要自己查数据库,应用进程里维护一个 WebSocket 客户端集合,所有状态变更都通过publish_ws广播。窗口屏收到call事件后显示号码,收到overdue事件后把号码从“当前服务”区域移到“过号”区域。应用重启时,各屏会重新连接,连接成功后再调用一次快照接口,把当前正在办理的号码和队列等待数量一次性拉回去。注意不要把 WebSocket 服务单独拆出去,单网点就让它和应用进程同生共死,部署简单很多。

4. 并发叫号的边界条件与三个必调参数

跑上一章代码之前,先确认三个参数和一个并发边界。这三个参数在演示环境里看不出差别,放到真实网点半天就能暴露问题。这里把它们列成一张参数表,每项都给出建议值和调整依据。

4.1 三个参数:呼叫超时、重呼次数、窗口闲置保护

参数建议值说明
CALL_TIMEOUT120 秒从叫号到系统判定未响应的时间。网点空间大,客户从等候区走到柜台可能需要 30 秒以上,设 60 秒以内很容易误伤
RECALL_MAX1首次超时进入重呼池,重呼再超时标记过号。设 0 表示叫一次不到就过号,适合强调叫号的网点
IDLE_WINDOW_TIMEOUT300 秒窗口空闲超过该时间自动标记暂停,暂停窗口不接受叫号。防止柜员离岗后号码一直被叫出却无人办理

IDLE_WINDOW_TIMEOUT是第三个要单独说明的。窗口终端是上位机或者工控机,柜员可能在离开时忘记点击“暂停”,后台任务需要定期检查窗口状态:如果窗口超过 5 分钟没有办理动作且当前无待服务号码,自动把窗口置为暂停,同时在管理端给出提示。这个逻辑避免了窗口“假空闲”导致号码越积越多。

4.2 多窗口并发叫号如何避免重号与空号

多个窗口同时点击“下一个”时,最怕出现同一张票被两个窗口同时叫到,或者窗口明明空闲却叫不出号码。单进程加全局锁的写法已经能解决,但要理解是哪一层解决了问题:queue_lock保证内存堆的弹出是原子操作,FOR UPDATE保证数据库状态读取是串行的。两道防线缺一不可,如果只有内存锁,进程重启瞬间会产生状态回放;如果只有数据库锁,两个并发请求仍可能在内存堆里同时各取到一个号码。

多节点部署时不能再用内存heapq,常见做法是改用 Redis 的有序集合保存等待队列,弹号脚本用 Lua 保证原子性。但单网点完全不需要走到这一步,数据库的FOR UPDATE在千级并发以下不会成为瓶颈。这个结论可以写进技术文档的“架构选型”部分,能帮你挡掉很多不必要的技术讨论。

4.3 当天积压、跑批归档和号段空洞的处理

叫号系统跑一整天之后,ticket表里会有大量状态为 4 和 5 的历史记录。查询“正在等待人数”时,推荐直接计数内存队列长度,不要每次COUNT(*)数据库表。每日闭市后做一次归档:把当天状态为 4、5 的记录转移到ticket_history表,清空业务流水表里的旧数据。号码重置用CREATE TABLE ... LIKE ticket或者TRUNCATE的方式处理,不要手动改自增主键。

号段空洞是另一个需要提前约定的地方:取号机吐出的序号应该连续,但叫号过程中出现的过号、弃号会让当天的 A 序列看起来不连续,这在银行网点是正常的。不要在交付文档里承诺“号码必然连续”,改成“号码由同一序列生成器产生,过号和弃号会造成逻辑空洞”,这个描述才是准确的。

提示:如果现场反馈“叫号后窗口终端没声音”,先看 WebSocket 推送链路,不要把精力集中在队列引擎上。语音播报终端在断线重连后要主动拉取一次当前叫号状态,否则重连期间漏掉的事件就永久丢了。

5. 把叫号系统的验收证据写进交付文档

交付给网点时,运维方最关心的事情是“系统跑到一半进程挂了,恢复后队列和数据库还对不对得上”。针对这一点,验收手段应该聚焦在状态一致性上,而不是界面是否好看。

5.1 用脚本模拟一次网点早高峰

准备阶段在取号机上不实际按键,直接调用接口连续取 30 个 A 类号码,然后让三个窗口同时执行 10 次“呼叫下一个”:

for i in $(seq 1 30); do curl -s "http://127.0.0.1:8000/api/take?queue_code=A&priority=5" done for w in W01 W02 W03; do for j in $(seq 1 10); do curl -s "http://127.0.0.1:8000/api/call?window_id=$w&queue_code=A" done done

执行完查一次数据库:状态为 1 的记录数应该等于 30 次呼叫中成功返回full_no的次数,窗口表的current_ticket_id必须指向最后一次成功叫到的号码。再把其中两张票手动超时,等 130 秒后确认它们进入重呼池,整个过程按这个步骤写下来,就是验收文档的主干。

5.2 用三个指标判断系统是否需要人工干预

日常运维不一定要看调用链。下面这条 SQL 直接反映当天的叫号健康度:

SELECT queue_code, SUM(CASE WHEN status = 1 OR status = 2 THEN 1 ELSE 0 END) AS wait_svc, SUM(CASE WHEN status = 3 THEN 1 ELSE 0 END) AS overdue_cnt, SUM(CASE WHEN status = 4 THEN 1 ELSE 0 END) AS serving_cnt, COUNT(*) AS total_cnt FROM ticket WHERE date(created_at) = CURRENT_DATE GROUP BY queue_code;

overdue_cnttotal_cnt的比值超过 15% 时,说明呼叫超时时间设置过短,或者语音播报音量太小听不见,优先检查这两项。wait_svc长时间大于 20 而窗口显示空闲,通常不是队列引擎问题,而是窗口状态卡在忙碌未复位,去查service_window.status是否为 0。系统恢复线上跑起来之后,把这张查询做成每日定时任务,比人工巡屏可靠得多。

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

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

2026年AI编程工具全景解析:五条主线与实战选型指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 7:55:57

6Valley 14.2多商户跨境电商PHP源码部署与二次开发实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/17 7:54:32

AI协作的三次范式跃迁与Harness工程实践

1. 从指令编写到系统整合的范式迁移去年夏天&#xff0c;当我第一次尝试用自然语言描述需求来生成代码时&#xff0c;需要反复调整七八次prompt才能得到可用的结果。而今天&#xff0c;我已经可以用一套标准化的工具链&#xff0c;将AI能力无缝嵌入到持续交付流程中——这个转变…

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

Vivado/Vitis 2024.2升级报错“找不到现有安装”:原因与解决指南

升级 Vivado/Vitis 2024.2 到 2024.2.1 的时候&#xff0c;安装器弹出“找不到现有安装”&#xff0c;踩过这个坑的人应该不少。更难受的是&#xff0c;这个提示往往不是出现在刚开始&#xff0c;而是在你等了几分钟安装器初始化之后才突然冒出来&#xff0c;让人很懵&#xff…

作者头像 李华
网站建设 2026/9/17 7:53:50

工业旋转机械故障诊断:对抗性单域泛化与样本平衡技术

1. 项目背景与核心价值旋转机械作为工业领域的核心设备&#xff08;如风力发电机、航空发动机、工业泵组等&#xff09;&#xff0c;其故障诊断的准确性直接关系到生产安全与经济效益。传统诊断方法面临两大核心痛点&#xff1a;一是实际工况下采集的训练数据往往来自单一工作域…

作者头像 李华