
简介本资源是一套完整的基于Java开发的银行排号系统实战项目面向Java初学者、课程设计学生及中小型业务系统开发者旨在解决银行、政务大厅等服务场所排队混乱、效率低下、客户体验差等现实问题。项目采用C/S架构融合Swing图形界面、Socket网络通信、多线程并发控制与MySQL数据库技术完整覆盖需求分析、系统设计含用例图、流程图、E-R图、编码实现与测试验证全流程。压缩包为RAR格式大小69.98MB包含源码工程、开题与毕业设计风格的LW文档含技术原理、数据库设计、模块实现详解、答辩用PPT、SQL建库脚本及系统操作讲解视频内容环环相扣便于理解架构逻辑与调试要点。目前已有150人学习下载特别适合需要交付课程设计、夯实Java GUI网络数据库综合能力的学习者快速上手并拓展至其他叫号类应用场景。1. 项目概述从“排队”到“智慧服务”的进化每次去银行办业务最头疼的就是取号后那漫长的等待。你不知道前面有多少人不知道要等多久只能盯着那不断跳动的数字屏幕心里干着急。这种体验相信大家都不陌生。而一个设计良好的银行排号系统正是为了解决这个核心痛点而生的。它不仅仅是一个简单的“叫号机”而是一个集成了客户分流、业务预判、窗口调度和数据分析的综合服务平台。今天我想和大家深入聊聊如何从零开始用Java这门经典且强大的语言亲手搭建一个功能完备、逻辑清晰的银行排号系统。这个项目麻雀虽小五脏俱全涵盖了从后端业务逻辑、数据库设计到前端交互的完整流程非常适合作为Java Web开发的综合练手项目也能让你深刻理解一个看似简单的系统背后复杂的设计考量。这个系统要解决的核心问题是什么首先是秩序与效率。通过电子化取号杜绝了物理排队可能引发的插队、拥挤问题营造了公平、有序的办理环境。其次是体验与透明。客户取号后可以实时查看当前等待人数、预估等待时间并能通过显示屏或语音清晰获知叫号信息焦虑感大大降低。最后是管理与优化。银行管理者可以通过系统后台实时监控各窗口业务量、客户等待时长、业务类型分布等数据为人力资源的动态调配、服务流程的优化提供数据支撑。所以我们构建的不仅是一个工具更是一个提升银行网点服务质量和运营效率的智慧节点。2. 系统核心设计与架构思路拆解在动手写代码之前我们必须把系统的骨架——架构设计清楚。一个好的架构能让后续开发事半功倍也决定了系统的可维护性和扩展性。2.1 技术栈选型为什么是这些组合对于一个典型的Java Web项目技术选型需要兼顾成熟度、开发效率和团队技能。基于当前的主流实践我选择了以下组合后端核心Spring Boot Spring MVC MyBatis (SSM框架集成)。这是Java企业级开发的事实标准。Spring Boot极大地简化了初始配置让我们能快速搭建一个可独立运行的、生产级别的应用。Spring MVC提供了清晰的分层模型Controller, Service, DaoMyBatis则是一个半自动化的ORM框架它在SQL的灵活性和对象映射的便利性之间取得了很好的平衡。对于排号系统这种业务逻辑明确、数据库操作频繁的场景MyBatis比JPAHibernate更具掌控力。数据库MySQL。作为最流行的开源关系型数据库MySQL在事务一致性、并发处理和社区支持方面都非常出色。排号系统的数据模型并不特别复杂但要求高可靠性和快速的读写响应如频繁的取号、叫号更新MySQL完全能够胜任。后续如果数据量激增也可以考虑分库分表等优化方案。前端展示Thymeleaf Bootstrap jQuery。考虑到这是一个偏向内部管理和现场展示的系统对前端交互的实时性要求高但复杂度适中我没有选择重量级的前端框架如Vue/React而是采用了服务端渲染模板Thymeleaf。它的好处是能与Spring Boot无缝集成开发简单页面直出速度快。Bootstrap提供了现成的、响应式的UI组件能快速搭建出美观专业的后台管理界面和客户显示界面。jQuery则用于处理一些简单的DOM操作和Ajax请求比如实现叫号信息的局部刷新。关键中间件与工具WebSocket这是实现叫号信息实时推送的关键。传统的HTTP轮询不断向服务器询问“轮到我了吗”效率低下且延迟高。当柜员点击“叫号”时服务器通过WebSocket连接可以瞬间将“请A001号到3号窗口”这条消息推送到大堂的显示屏和客户的手机端如果有的话实现真正的实时更新。Redis用作缓存和分布式锁。高频查询的数据如当前各队列的等待人数、最新的叫号信息可以缓存在Redis中极大减轻数据库压力。更重要的是在并发取号时为了防止出现重号两个客户同时取到同一个号我们需要一个分布式锁机制。Redis的SETNX命令是实现轻量级分布式锁的经典方案。Maven项目构建和依赖管理工具让第三方库的引入和管理变得井然有序。注意技术选型没有绝对的对错只有是否适合当前场景。如果你的团队更熟悉 Vue完全可以用 Spring Boot 提供 RESTful API前端用 Vue 来开发这样前后端分离更彻底。这里的选择是基于“快速实现一个完整可用的系统”这一目标。2.2 业务模块划分系统由哪些部分组成根据业务流程我们可以将系统清晰地划分为以下几个核心模块取号模块客户交互的起点。提供取号界面可以是触摸屏、小程序入口等客户选择需要办理的业务类型如个人现金、对公业务、理财咨询等。系统根据业务类型将其归入不同的队列并生成一个唯一的号码如A001B023。队列管理模块系统的“大脑”。它维护着各个业务队列的状态包括队列中的号码列表、当前办理的号码、等待人数等。负责接收取号请求分配号码接收叫号请求从队列中取出下一个号码。叫号与显示模块柜员与客户交互的桥梁。柜员端有一个操作界面登录后可以看到分配给自己的队列点击“叫号”按钮系统会从对应队列中取出最前面的号码。取出的号码信息会通过WebSocket实时推送到大堂的LED显示屏、柜员窗口的显示屏以及可能的语音播报系统。柜员管理模块管理办理业务的柜员信息。包括柜员登录、权限分配普通柜员、经理、状态管理空闲、忙碌、暂停服务、以及柜员与可办理业务类型的关联。后台管理模块提供给银行管理人员使用。功能包括实时监控各队列情况、柜员状态、数据统计日/月业务量、平均等待时间、客户满意度、基础数据配置业务类型管理、窗口管理等。数据统计与分析模块这个模块可能不像前几个那样有直接的界面但它至关重要。它负责定时或触发式地分析历史排号数据生成报表帮助管理者发现业务高峰时段、评估柜员效率、优化窗口资源配置。2.3 数据库设计表结构如何支撑业务数据库设计是系统的基石。这里我列出几个核心表及其字段并解释设计意图ticket(排号票表)核心实体。id(主键),ticket_number(票号如 ‘A001’),business_type(业务类型),status(状态等待、办理中、已办理、过号),create_time(取号时间),call_time(叫号时间),start_process_time(开始办理时间),end_process_time(办理完成时间),counter_id(关联的窗口柜员)。设计思考status字段是流程驱动的关键。call_time和start_process_time可能不同因为叫号后客户可能未及时前来。记录完整的时间戳为后续分析等待时长、办理时长提供了数据基础。business_type(业务类型表)用于配置。id,type_code(类型代码如 ‘PERSONAL’)type_name(类型名称如 ‘个人现金业务’)prefix(号票前缀如 ‘A’)description,estimated_time(预估办理时长分钟)。设计思考将业务类型独立成表便于动态管理。prefix用于生成不同队列的票号。estimated_time可以用于在前端为客户提供更精准的等待时间预估。counter(窗口/柜员表)id,counter_number(窗口编号),clerk_id(关联的柜员ID),current_status(当前状态空闲、忙碌、暂停),current_ticket_id(当前正在处理的票ID)。设计思考将物理窗口与柜员账号解耦一个柜员可以登录到不同窗口。current_ticket_id直接关联到正在办理的业务方便追踪。clerk(柜员表)id,username,password(加密存储),real_name,role(角色柜员、经理)。display(显示设备表)管理大堂显示屏。id,device_code,location,status。设计思考为多块显示屏的管理预留接口可以指定不同的显示屏显示不同的队列信息。表之间的关系通过外键如ticket.counter_id-counter.id或逻辑关联来维护。在MyBatis中可以通过resultMap来实现复杂的对象关联映射。3. 核心功能实现与关键技术点解析有了清晰的架构和设计我们就可以深入每个模块看看代码是如何落地的。这里我会挑几个最具挑战性和代表性的功能点来详细讲解。3.1 并发取号与唯一票号生成如何避免“重号”这是系统面临的第一个挑战。想象一下在业务高峰期多台取号机或多个在线入口同时有客户点击“取号”。我们必须保证生成的票号全局唯一并且不能因为并发而出错。解决方案数据库序列Redis分布式锁单纯依靠数据库的自增IDAUTO_INCREMENT生成票号如A001是不行的因为我们需要包含业务前缀和自定义格式。一个常见的做法是使用一张单独的表来维护每个业务类型的最新序号。但并发更新这张表时需要处理锁竞争。我采用的方案是结合数据库和Redis格式定义票号 业务前缀 日期YYMMDD 4位自增序号。例如A2311050001。序号生成为每个“业务前缀日期”的组合在Redis中维护一个自增键。例如键为ticket:seq:A:231105值为最新的序号。并发控制// 伪代码示例 public String generateTicketNumber(String bizPrefix) { String dateStr LocalDate.now().format(DateTimeFormatter.ofPattern(yyMMdd)); String redisKey ticket:seq: bizPrefix : dateStr; // 使用Redis的INCR命令原子性自增避免并发问题 Long seq redisTemplate.opsForValue().increment(redisKey); // 设置键的过期时间避免无用数据长期占用内存例如24小时后过期 redisTemplate.expire(redisKey, 1, TimeUnit.DAYS); // 格式化为4位数字不足补零 String seqStr String.format(%04d, seq); return bizPrefix dateStr seqStr; }容灾考虑虽然Redis很可靠但为了防止极端情况下Redis不可用可以有一个降级方案。例如在数据库中维护一张ticket_sequence表通过SELECT ... FOR UPDATE行锁来获取序号但性能不如Redis。可以在代码中做一个开关正常情况下走Redis异常时降级到数据库。实操心得使用Redis的INCR命令是核心它是原子操作完美解决了并发问题。务必记得给这个序列键设置一个合理的过期时间比如业务结束后的几小时否则Redis会被无数个每日的序列键占满。另外生成的序号重置问题每天从0001开始是通过“键名包含日期”自然实现的。3.2 实时叫号与信息推送WebSocket如何集成叫号信息需要实时出现在大堂屏幕和柜员屏幕上HTTP的请求-响应模式无法满足。WebSocket提供了全双工通信通道。实现步骤引入依赖在pom.xml中添加Spring Boot的WebSocket starter。dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-websocket/artifactId /dependency配置WebSocket创建一个配置类启用WebSocket并注册一个ServerEndpointExporterBean。更重要的是配置一个自定义的TextWebSocketHandler来处理消息。Configuration EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { // 注册处理器指定连接路径允许所有源生产环境应限制 registry.addHandler(new TicketWebSocketHandler(), /ws/ticket) .setAllowedOrigins(*); } }实现消息处理器TicketWebSocketHandler需要继承TextWebSocketHandler重写afterConnectionEstablished连接建立和handleTextMessage处理消息等方法。核心是维护一个全局的ConcurrentHashMap来保存所有在线的客户端会话WebSocketSession。public class TicketWebSocketHandler extends TextWebSocketHandler { private static final MapString, WebSocketSession clients new ConcurrentHashMap(); Override public void afterConnectionEstablished(WebSocketSession session) { // 客户端连接将其加入Map。可以用设备ID或窗口ID作为key String clientId (String) session.getAttributes().get(clientId); clients.put(clientId, session); } Override public void handleTextMessage(WebSocketSession session, TextMessage message) { // 处理客户端发来的消息例如心跳包、特定请求 } // 提供一个静态方法供业务Service调用用于向特定或所有客户端推送消息 public static void sendMessageToClient(String clientId, String message) { WebSocketSession session clients.get(clientId); if (session ! null session.isOpen()) { try { session.sendMessage(new TextMessage(message)); } catch (IOException e) { // 处理异常可能需从Map中移除失效的session } } } public static void broadcast(String message) { for (WebSocketSession session : clients.values()) { // 向所有连接广播 } } }业务层触发推送在柜员执行叫号的Service方法中生成叫号信息JSON格式然后调用TicketWebSocketHandler.sendMessageToClient(“display_hall”, jsonMessage)推送给大堂显示屏同时推送给对应柜员的客户端。{ type: call, ticketNumber: A2311050008, counterNumber: 3, businessType: 个人现金业务 }前端连接与接收在前端页面显示屏页面使用JavaScript建立WebSocket连接并监听onmessage事件收到消息后解析JSON并更新DOM显示叫号信息。注意事项WebSocket连接不是永久可靠的网络波动会导致断开。前端需要实现断线重连机制例如每隔5秒检查一次连接状态断开则重连。同时为了保持连接活跃避免被代理服务器或防火墙关闭可以定期如每30秒从客户端向服务器发送一个心跳包ping/pong。3.3 队列管理与叫号算法不仅仅是“先进先出”队列管理是业务逻辑的核心。最简单的规则是先进先出FIFO但实际业务中可能需要更复杂的策略。基础FIFO队列实现我们可以利用数据库来实现一个持久化队列。ticket表中statusWAITING且按create_time排序的记录就是一个天然的FIFO队列。柜员叫号时执行一条SQLSELECT * FROM ticket WHERE business_type #{bizType} AND status WAITING ORDER BY create_time ASC LIMIT 1 FOR UPDATE;FOR UPDATE子句会在事务中锁定这条记录防止其他柜员同时叫到同一个号。更复杂的调度策略优先级队列VIP客户或特定业务如残疾人服务可以优先。可以在ticket表中增加一个priority字段叫号时按priority DESC, create_time ASC排序。过号处理客户被叫后未及时前来过号通常的处理方式是允许客户在回来后以“过号”身份插入当前等待队伍的最前面或特定位置。可以在ticket表中增加一个missed_count字段记录过号次数并在叫号逻辑中特殊处理statusMISSED的票。多队列负载均衡如果一个柜员可以办理多种业务叫号时不应只从一个队列取。可以设计一个“全局调度器”根据各队列等待人数、柜员业务熟练度等因素动态决定下一个叫哪个队列的号。这需要更复杂的算法可能涉及权重计算。我的实现建议对于大多数中小型网点按业务类型分列的FIFO队列 简单的过号重入机制已经完全够用。可以在叫号Service中通过一个QueueService来封装这些逻辑未来需要升级调度算法时只需修改这个Service的实现即可这是面向接口编程的好处。4. 数据库操作优化与MyBatis实战系统对数据库的读写操作非常频繁尤其是查询等待队列和更新票务状态。优化数据库访问是提升系统性能的关键。4.1 实体类与Mapper设计首先定义与数据库表对应的实体类如Ticket,BusinessType。然后为每个实体创建对应的Mapper接口和XML映射文件。TicketMapper.java示例Mapper public interface TicketMapper { // 插入一条新的排号记录 int insert(Ticket ticket); // 根据ID查询 Ticket selectById(Long id); // 根据状态和业务类型查询等待列表分页 ListTicket selectWaitingByBizType(Param(bizType) String bizType, Param(offset) int offset, Param(limit) int limit); // 更新票状态乐观锁版本控制 int updateStatus(Param(id) Long id, Param(oldStatus) String oldStatus, Param(newStatus) String newStatus, Param(version) Integer version); // 根据条件统计数量用于分页和监控 Long countByCondition(TicketQueryCondition condition); }对应的TicketMapper.xml片段!-- 查询等待列表并锁定最早的一条记录用于叫号 -- select idselectEarliestWaitingForUpdate resultTypeTicket SELECT * FROM ticket WHERE business_type #{bizType} AND status WAITING ORDER BY create_time ASC LIMIT 1 FOR UPDATE /select !-- 动态SQL用于复杂条件统计 -- select idcountByCondition resultTypejava.lang.Long SELECT COUNT(*) FROM ticket where if testbusinessType ! null and businessType ! AND business_type #{businessType} /if if teststatus ! null and status ! AND status #{status} /if if teststartTime ! null AND create_time #{startTime} /if if testendTime ! null AND create_time ![CDATA[ ]] #{endTime} /if /where /select4.2 事务管理与数据一致性银行系统对数据一致性要求极高。一个完整的“叫号”操作涉及多个步骤1) 查询并锁定待办票2) 更新该票状态为“办理中”3) 更新柜员当前业务4) 记录日志。这些操作必须在一个数据库事务中完成要么全部成功要么全部回滚。在Spring中使用Transactional注解可以轻松声明事务。Service public class CounterServiceImpl implements CounterService { Autowired private TicketMapper ticketMapper; Autowired private CounterMapper counterMapper; Transactional(rollbackFor Exception.class) // 发生任何异常都回滚 public CallResult callNextTicket(String clerkId, String bizType) { // 1. 获取当前柜员信息 Counter counter counterMapper.selectByClerkId(clerkId); if (counter null || !IDLE.equals(counter.getCurrentStatus())) { throw new BusinessException(柜员状态异常无法叫号); } // 2. 查询并锁定最早的一张等待票悲观锁 Ticket nextTicket ticketMapper.selectEarliestWaitingForUpdate(bizType); if (nextTicket null) { throw new BusinessException(当前队列无等待客户); } // 3. 更新票状态 nextTicket.setStatus(PROCESSING); nextTicket.setCallTime(new Date()); nextTicket.setCounterId(counter.getId()); ticketMapper.update(nextTicket); // 4. 更新柜员状态 counter.setCurrentStatus(BUSY); counter.setCurrentTicketId(nextTicket.getId()); counterMapper.update(counter); // 5. 构建叫号结果准备推送 CallResult result new CallResult(nextTicket, counter); // 6. 异步推送WebSocket消息注意事务提交后再推送更稳妥 // 可以通过 TransactionalEventListener 监听事务提交事件后再推送 websocketService.sendCallInfo(result); return result; } }踩坑记录这里有一个常见的陷阱。如果在Transactional方法内直接进行WebSocket推送或发送HTTP请求外部调用而这些操作又非常耗时会导致数据库连接被长时间占用影响性能甚至引发死锁。最佳实践是将这类非数据库操作如消息推送、日志记录到外部系统放在事务提交之后执行。可以使用Spring的TransactionalEventListener(phase TransactionPhase.AFTER_COMMIT)来监听事务提交成功的事件然后在这个监听器里执行推送操作。4.3 性能优化索引与缓存数据库索引这是提升查询效率最直接的手段。必须在频繁作为查询条件的字段上建立索引。ticket表(business_type, status, create_time)组合索引这对“查询某类业务的等待队列”这个核心查询至关重要。status和counter_id上的索引也对更新和关联查询有帮助。counter表clerk_id上建立唯一索引方便快速通过柜员ID查找窗口。应用层缓存Redis热点数据缓存例如“今日总取号数”、“各队列当前等待人数”。这些数据查询频繁但更新不实时每隔几分钟更新一次即可。我们可以定时如每5分钟从数据库统计一次存入Redis并设置过期时间。前端查询时直接读Redis。Scheduled(fixedDelay 300000) // 每5分钟执行一次 public void refreshQueueStats() { MapString, Long stats ticketService.calcWaitingCountByBizType(); redisTemplate.opsForHash().putAll(stats:queue:waiting, stats); redisTemplate.expire(stats:queue:waiting, 10, TimeUnit.MINUTES); }配置信息缓存business_type业务类型表的数据很少变动完全可以启动时加载到Redis或本地内存中避免每次取号都查数据库。5. 前端界面与用户体验关键点系统主要有三类用户界面客户取号界面、柜员操作界面、大堂显示屏和管理后台。这里重点讲前两者。5.1 客户取号界面简洁与引导取号界面通常运行在触摸屏设备或嵌入到银行官网/小程序。核心要求是简单、明了、防误操作。布局大按钮、大字体。清晰列出所有可办理的业务类型每个类型配以简明的图标和文字说明。交互客户点击业务类型后界面应给出明确反馈如按钮变色并立即打印出凭条或显示取号成功页面包含票号、前方等待人数、预估等待时间。预估等待时间这是一个能极大提升体验的功能。算法可以很简单预估时间 当前该队列等待人数 * 该业务平均办理时长。平均办理时长可以从历史数据中动态计算并维护在business_type表中。在取号时将这个估算值显示给客户。技术实现一个简单的Thymeleaf页面通过Ajax提交取号请求后端返回JSON包含票号和信息前端再渲染结果页。5.2 柜员操作界面高效与稳定柜员界面是生产力工具核心是稳定、快速、减少不必要的操作。登录与状态柜员使用工号和密码登录。登录后界面中央醒目显示“叫号”按钮。上方显示柜员当前状态空闲/忙碌和当前正在服务的客户票号。一键叫号点击“叫号”系统自动根据该柜员配置的可办理业务类型从对应的队列中取出下一个号码。所有复杂的队列查询、状态更新逻辑都在后端完成前端只负责触发和显示结果。业务办理与完成叫号后界面应能显示客户的基本信息如果与客户系统关联或票号信息。办理完成后点击“完成”按钮系统将票状态更新为“已办结”柜员状态恢复为“空闲”并自动准备下一次叫号。特殊操作提供“暂停服务”柜员临时离开、“过号”客户未到、“重呼”再叫一次当前号等按钮。这些操作都应通过清晰的二次确认对话框防止误触。实时同步柜员界面需要通过WebSocket与服务器保持连接以便接收管理端下发的指令如强制签退、消息通知。5.3 大堂显示屏实时与醒目这是一个“只读”界面核心是信息实时、字体巨大、布局清晰。内容通常分为几个区域当前正在办理的号码按窗口排列、最新叫到的号码滚动显示、各队列等待人数。技术一个全屏显示的网页通过WebSocket与服务器保持长连接。收到叫号消息后使用JavaScript动态更新DOM元素。为了达到“醒目”效果需要用到CSS动画比如新叫到的号码有一个放大、高亮、然后滚入列表的动画效果。容错显示屏客户端必须考虑网络中断的情况。除了断线重连在初始化时应该从服务器拉取一次完整的状态数据避免刚连接时屏幕空白。6. 项目部署、监控与常见问题排查开发完成只是第一步让系统稳定运行在生产环境同样重要。6.1 部署架构建议对于单个网点可以采用单体应用部署一台应用服务器部署Spring Boot Jar包。一台数据库服务器MySQL。一台Redis服务器。所有客户端取号机、柜员电脑、显示屏电脑通过网点内部网络访问应用服务器。为了高可用可以对数据库和Redis做主从复制。应用服务器也可以部署两台前面用Nginx做负载均衡和反向代理。Spring Boot应用打包与启动# 打包 mvn clean package -DskipTests # 会在target目录生成一个可执行的jar文件如 bank-queue-system-1.0.0.jar # 启动生产环境建议使用 nohup 或 systemd 服务 java -jar -Dspring.profiles.activeprod bank-queue-system-1.0.0.jar在application-prod.properties配置文件中需要设置生产环境的数据库连接、Redis连接、日志路径等。6.2 系统监控与日志没有监控的系统就是在“裸奔”。应用健康监控Spring Boot Actuator提供了丰富的端点/actuator/health,/actuator/metrics可以集成到监控平台如PrometheusGrafana中监控JVM内存、线程池、数据库连接池状态等。业务日志使用SLF4J Logback。针对关键业务节点记录结构化日志。Slf4j Service public class TicketService { public Ticket createTicket(String bizType) { // ... 业务逻辑 log.info(Ticket created. ticketNo:{}, bizType:{}, waitCount:{}, ticket.getTicketNumber(), bizType, currentWaitCount); return ticket; } }日志文件要按日期滚动并定期归档。排查问题时通过票号、时间等关键词能快速定位相关日志。数据库慢查询日志在MySQL中开启慢查询日志定期分析对执行时间过长的SQL进行优化。6.3 常见问题与排查技巧实录在实际运行中你肯定会遇到各种各样的问题。下面是我总结的一些常见问题及其排查思路问题现象可能原因排查步骤与解决方案取号缓慢界面卡顿1. 数据库查询慢。2. Redis连接超时或阻塞。3. 应用服务器GC频繁。1. 查看应用日志和数据库慢查询日志优化对应SQL检查索引。2. 使用redis-cli的info命令查看Redis状态检查网络和内存使用。3. 使用jstat或VisualVM监控JVM GC情况调整堆内存参数。叫号后显示屏更新有延迟1. WebSocket连接断开前端重连机制失效。2. 网络问题导致消息丢失。3. 后端推送消息时发生异常。1. 打开浏览器开发者工具查看WebSocket连接状态和网络请求。检查前端重连代码。2. 在后端推送消息处增加日志确认消息是否成功发出。检查服务器网络带宽和防火墙设置。3. 确保推送消息的逻辑不在数据库事务内长时间执行。出现重复票号1. 票号生成逻辑在极高并发下出现线程安全问题。2. Redis的INCR命令在集群模式下可能出现极端情况如主从切换。1.复查代码确保票号生成使用了Redis的原子操作INCR并且键的设计包含了日期。2.加强校验在ticket表的ticket_number字段上建立唯一索引作为最后一道防线。插入重复数据时会抛出数据库异常可以在业务层捕获并重试取号逻辑。3.考虑更严格的方案对于金融级要求可以使用数据库序列或分布式发号器如雪花算法。柜员点击“叫号”无反应1. 前端JavaScript错误。2. 后端接口返回错误或超时。3. 该柜员状态异常如已被管理员强制下线。1. 打开浏览器控制台查看是否有JS报错。查看点击按钮触发的网络请求看状态码和响应内容。2. 查看后端应用日志定位接口执行过程中的异常。3. 检查数据库中和该柜员关联的counter表状态字段。后台统计报表数据不准1. 统计SQL逻辑错误。2. 数据统计时有未提交的事务导致数据不一致。3. 缓存数据未及时更新。1. 复核统计SQL最好在测试环境用真实数据验证。2. 对于需要高实时性的统计可以考虑读取从库或者使用事务隔离级别相关的提示。3. 检查缓存更新策略确保在数据变更时如票状态更新能及时清除或更新相关缓存。我个人在实际开发这个系统时最深的一点体会是边界情况和异常处理的重要性远大于主流程开发。比如网络闪断时WebSocket如何优雅重连取号机在打印凭条时打印机卡纸了怎么办柜员端界面在交易过程中浏览器崩溃了如何恢复状态这些“倒霉”的情况虽然发生概率低但一旦发生就会严重影响用户体验和银行信誉。在设计和编码阶段就必须为这些异常流留出处理空间比如增加事务补偿机制、操作日志记录、状态恢复查询等功能。一个健壮的系统正是在对这些细节的不断打磨中构建起来的。本文还有配套的精品资源点击获取