
简介基于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) ) ENGINEInnoDB;queue_code是业务类型前缀对应取号单上的 A、B、C、VV 留给 VIP。seq_no是同一个业务类型当天内的自增序号full_no是屏显使用的完整号码比如A012。这里有两个细节第一不要把id直接当成号给客户看否则跨天、跨类型都无法解释第二uk_queue_seq把created_at也放进去是为了避免跨天重置时出现重复写入。priority越小越优先普通客户默认 5VIP 取号时传 1。窗口表保持简单id是窗口编号queue_code是窗口能受理的业务类型status表示空闲、忙碌、暂停。窗口与业务类型是多对一关系一个窗口通常只服务一种类型但叫号系统里经常出现“综合窗口”能收 A 和 B所以不要把队列耦合进窗口表而是由叫号引擎根据窗口传入的类型参数去取对应队列。2.2 号码状态机六个状态已经够用状态字段status我按下面的表来定义这套约定可以直接写进设计文档状态值状态名进入条件可流转去向0WAIT 等待取号成功进入队列1 叫号、5 弃号1CALLED 已叫号窗口点击“下一个”2 重新呼叫、4 办理、3 过号2RECALLED 重呼中首次呼叫超时再次被叫4 办理、3 过号3OVERDUE 已过号重呼后仍未到可由柜员手动召回回到 04SERVING 办理中客户到窗口开始办理5 弃号5FINISHED 已完成办结或客户未响应放弃终态不再参与排队这六种状态里最容易写错的是 2 和 3 的区别。实际业务语义是第一次叫号后客户没出现系统要把号重新放回队列里但它的优先级要高于新取的号否则一个晚到两分钟的客户会重新排到队尾这在网点里会直接引发投诉。状态 2 不是单独一条队列而是进到“重呼池”等下先于普通等待号被弹出。如果重呼之后客户仍然没出现才真正标记为 3 OVERDUE。过号之后由柜员手动操作“呼叫过号客户”号码重新回到状态 0。2.3 为什么队列主体放单进程内存而不是直接依赖数据库单网点的并发量不大几个取号机加十几个窗口高峰时段每分钟新增号码也就是几十个。常见做法是把队列结构放在应用服务的内存里数据库只负责持久化状态。这样做的原因有三个第一队列需要支持按优先级和创建时间排序内存堆结构比数据库查询直观第二叫号动作需要在一个锁内同时完成“弹出号码、更新窗口状态、写入流水”数据库事务做这套操作也能做但排查问题时会多一层心智负担第三单网点没有条件维护 Redis用本地 MySQL 或 PostgreSQL 最稳妥。内存队列用 Python 的heapq加字典实现key 是queue_codevalue 是一个堆堆里按(priority, seq_no)排序。这样 VIP 号码会优先被取到同优先级下先取号的人先被叫。数据库表里存的状态和内存队列可能短暂不一致这是允许的因为每次弹出号码前都会重新读一次数据库状态做校验后面第 3 章会写到这一步。3. 取号、叫号、过号重排核心链路实现这一章直接给可运行的核心代码语言用 Python框架无关只说明队列引擎部分的写法。取号机、窗口终端、大屏通过 HTTP 或 WebSocket 调用同一组 API所有状态变更都经过同一个进程内的queue_lock这是单网点部署下保证不重号的关键。3.1 取号接口先锁住业务类型的日序号取号动作的完整逻辑是生成当天该业务类型的序号写入ticket表同时把(priority, seq_no, ticket_id)放进内存堆。生成日序号必须用数据库行锁避免两个取号机同时按出同一个 A001def 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_codeApriority5 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$wqueue_codeA 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_cnt与total_cnt的比值超过 15% 时说明呼叫超时时间设置过短或者语音播报音量太小听不见优先检查这两项。wait_svc长时间大于 20 而窗口显示空闲通常不是队列引擎问题而是窗口状态卡在忙碌未复位去查service_window.status是否为 0。系统恢复线上跑起来之后把这张查询做成每日定时任务比人工巡屏可靠得多。本文还有配套的精品资源点击获取