ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

JSPM银行排队叫号系统实战:从取号到叫号完整业务闭环

2026/10/4 11:33:11 拓冰建站 浏览量
JSPM银行排队叫号系统实战:从取号到叫号完整业务闭环 简介这份资源是一套基于SSM框架与JSP技术实现的银行排队叫号系统完整项目面向正在学习Java Web开发、需要课程设计或毕业设计参考的高校学生与初级开发者。项目采用JDK1.8、Tomcat7与MySQL5.7环境配合Eclipse或IDEA开发涵盖取号、叫号、窗口调度等典型业务场景适合用来理解SSM整合与JSP页面交互的完整流程。压缩包共856个文件约29.65MB其中87个Java源文件承载核心业务逻辑49个JSP页面与25个HTML构成前端界面另有217个JavaScript、97个CSS及大量png、gif、jpg图片资源支撑交互与视觉呈现并附带sql脚本、xml配置与演示视频目录结构清晰。目前已有465人学习下载读者可借助源码与录屏快速搭建运行环境对照业务模块梳理数据库设计与请求处理链路为二次开发或答辩准备提供可复用的参考方案。1. 银行排队叫号系统从取号到叫号的完整业务闭环去银行办业务最烦的是什么不是排队本身而是你不知道前面还有几个人、要等多久。一套银行排队叫号系统要解决的核心问题就三个客户取号后知道自己排第几、柜员按顺序叫号不混乱、大堂经理能实时看到各窗口的排队压力。这个 Java 项目基于 JSPM 架构实现了一套完整的排队叫号系统包含取号机端、窗口叫号端和管理后台三个角色。适合正在做 Java 课程设计的学生、需要快速搭建排队系统的开发者以及想理解 JSPM 分层架构如何落地到实际业务场景的工程师。源码和演示视频可以直接跑起来看效果下面把整个系统的设计思路和实现细节拆开讲。2. JSPM 分层架构为什么不用 Spring Boot 也能撑起排队系统2.1 JSPM 到底是什么和 MVC 有什么区别JSPM 不是某个框架的缩写而是一种在 Java Web 教学和中小型项目中常见的分层约定JSP 负责视图渲染Servlet 负责请求分发POJO 承载业务数据MySQL 做持久化。和标准 MVC 的区别在于JSPM 把 Servlet 当作纯控制器用业务逻辑下沉到 Service 层POJO 既是实体类也是 DAO 的操作对象。这种结构在课程设计和中小型项目里很常见因为不依赖 Spring 容器Tomcat 一挂就能跑。我一般会这样划分包结构com.bank.queue ├── controller # Servlet处理 HTTP 请求 ├── service # 业务逻辑叫号算法、队列管理 ├── dao # 数据库操作JDBC 封装 ├── entity # POJOTicket、Window、Counter ├── util # 工具类数据库连接池、日期格式化 └── filter # 编码过滤器、登录拦截这个结构的好处是每一层职责清晰调试时能快速定位问题出在哪个环节。坏处是 JDBC 代码写起来啰嗦需要自己封装连接池和事务管理。2.2 数据库表设计与核心字段说明排队系统的数据模型不复杂但有几个字段设计不好会直接影响叫号逻辑。下面是我实际建表时用的 SQL-- 号票表每取一个号生成一条记录 CREATE TABLE ticket ( id INT PRIMARY KEY AUTO_INCREMENT, ticket_no VARCHAR(10) NOT NULL COMMENT 号票编号如A001, ticket_type CHAR(1) NOT NULL COMMENT 业务类型A个人 B对公 CVIP, status TINYINT DEFAULT 0 COMMENT 0等待 1已叫号 2已完成 3已过号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, call_time DATETIME COMMENT 被叫号时间, window_id INT COMMENT 受理窗口ID, INDEX idx_status_type (status, ticket_type) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 窗口表 CREATE TABLE window ( id INT PRIMARY KEY AUTO_INCREMENT, window_no VARCHAR(5) NOT NULL COMMENT 窗口编号如01、02, status TINYINT DEFAULT 0 COMMENT 0空闲 1服务中 2暂停, current_ticket_id INT COMMENT 当前正在服务的号票ID ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;ticket_no的生成规则是「业务类型字母 三位递增数字」比如 A001、A002、B001。这里有个坑并发取号时如果直接用SELECT MAX(ticket_no)1会拿到重复号必须用数据库自增 ID 或者加锁。我一般用id做唯一标识ticket_no只在展示时拼接生成。status字段的四个状态是整个系统的状态机核心。等待状态的号票按ticket_type分组排队叫号时从对应队列头部取。过号状态需要大堂经理手动处理不能自动跳过否则客户回来时会引发纠纷。2.3 叫号算法的实现优先级队列与窗口分配叫号逻辑是这套系统的灵魂。核心需求是VIP 优先、同类型按取号顺序、空闲窗口自动分配。用 Java 的PriorityQueue可以很优雅地实现public class CallService { // 按业务类型维护多个优先级队列 private MapString, PriorityQueueTicket queues new ConcurrentHashMap(); public CallService() { queues.put(A, new PriorityQueue(Comparator.comparing(Ticket::getCreateTime))); queues.put(B, new PriorityQueue(Comparator.comparing(Ticket::getCreateTime))); queues.put(C, new PriorityQueue(Comparator.comparing(Ticket::getCreateTime))); } // 叫号VIP队列优先其次按业务类型轮询 public synchronized Ticket callNext(int windowId) { String[] order {C, A, B}; // CVIP优先 for (String type : order) { PriorityQueueTicket q queues.get(type); if (q ! null !q.isEmpty()) { Ticket t q.poll(); t.setStatus(1); t.setCallTime(new Date()); t.setWindowId(windowId); // 更新数据库状态 ticketDao.updateStatus(t); return t; } } return null; // 所有队列为空 } }callNext方法加了synchronized关键字因为多个窗口同时点叫号按钮时会出现并发竞争。不加锁的话两个窗口可能叫到同一个号。order数组定义了叫号优先级C 类 VIP 永远最先被叫A 类和 B 类按顺序轮询。如果业务上 VIP 也需要排队把 C 放到最后即可。PriorityQueue的排序依据是createTime也就是取号时间。这里要注意时间精度问题如果两个号在同一秒内取出createTime可能相同排序结果就不稳定。解决办法是在Ticket类里加一个自增的seq字段作为第二排序键。3. 从零跑通环境搭建、部署与取号叫号全流程验证3.1 开发环境与依赖版本这套系统对环境的依赖比较轻不需要 Maven 或 Gradle直接把 jar 包扔进WEB-INF/lib就能跑。我整理了一份经过验证的版本组合组件版本说明JDK1.8项目用了 Lambda 表达式最低 1.8Tomcat8.5 或 9.0不要用 Tomcat 10Servlet 包名变了MySQL5.7 或 8.08.0 需要改 JDBC 驱动类名JDBC 驱动mysql-connector-java-5.1.49对应 MySQL 5.7JSTL1.2JSP 页面标签库Servlet API3.1Tomcat 自带不需要额外引入注意Tomcat 10 把javax.servlet改成了jakarta.servlet如果直接部署会报 ClassNotFoundException。要么换 Tomcat 9要么把所有 import 改一遍。3.2 数据库初始化与连接配置建库建表之后需要配置数据库连接。项目里一般用db.properties文件管理连接参数jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/bank_queue?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.passwordyour_password jdbc.initialSize5 jdbc.maxActive20initialSize和maxActive是连接池参数。排队系统的并发量不大5 个初始连接、20 个最大连接足够支撑几十个窗口同时操作。如果银行网点窗口超过 50 个把maxActive调到 50 以上否则叫号请求会排队等连接。连接池的实现可以用 Druid 或者自己写一个简单的DataSource封装。课程设计里我一般用DruidDataSource配置简单自带监控页面。3.3 取号端与叫号端的核心 Servlet 实现取号端的逻辑是客户点击业务类型按钮 → 生成号票 → 写入数据库 → 返回号票编号。叫号端的逻辑是柜员点击叫号 → 从队列取下一个号 → 更新窗口状态 → 推送显示。// 取号 Servlet WebServlet(/takeNumber) public class TakeNumberServlet extends HttpServlet { private CallService callService new CallService(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String type req.getParameter(type); // A/B/C Ticket ticket callService.generateTicket(type); resp.setContentType(application/json;charsetutf-8); resp.getWriter().write({\ticketNo\:\ ticket.getTicketNo() \}); } }generateTicket方法里做了两件事生成号票编号、把号票加入对应队列。号票编号的生成用AtomicInteger保证线程安全private AtomicInteger counterA new AtomicInteger(0); private AtomicInteger counterB new AtomicInteger(0); private AtomicInteger counterC new AtomicInteger(0); public Ticket generateTicket(String type) { int seq; switch (type) { case A: seq counterA.incrementAndGet(); break; case B: seq counterB.incrementAndGet(); break; default: seq counterC.incrementAndGet(); break; } String ticketNo type String.format(%03d, seq); Ticket t new Ticket(ticketNo, type); ticketDao.insert(t); queues.get(type).offer(t); return t; }AtomicInteger的incrementAndGet是原子操作多个取号机同时请求也不会拿到重复序号。String.format(%03d, seq)保证编号至少三位超过 999 之后会变成四位展示时要注意 LED 屏的宽度。3.4 用 Postman 和浏览器验证完整叫号流程部署到 Tomcat 之后按这个顺序验证第一步访问取号页面http://localhost:8080/bank-queue/take.jsp点击「个人业务」按钮页面返回{ticketNo:A001}。再点一次返回A002。第二步访问叫号页面http://localhost:8080/bank-queue/call.jsp选择窗口 01点击「叫号」。页面显示当前叫号A001窗口状态变为「服务中」。第三步用 Postman 发送POST /callNext请求参数windowId2返回A002。说明多窗口并发叫号正常。第四步检查数据库ticket表A001 和 A002 的status应该都是 1window_id分别是 1 和 2。如果叫号返回空检查queues里对应类型的队列是否为空。如果返回重复号检查synchronized是否生效或者数据库唯一索引是否冲突。4. 避坑与排查叫号系统上线后最容易翻车的 5 个点4.1 号票重复并发取号时序号冲突现象两台取号机同时点击都返回 A005客户拿到两张一样的号。原因用了SELECT MAX(ticket_no)再1的方式生成编号两个请求同时查到相同的最大值。解决改用AtomicInteger内存计数或者用数据库自增 ID 拼接号票编号。如果必须用数据库生成加FOR UPDATE行锁。4.2 叫号后页面不刷新LED 屏显示滞后现象柜员点了叫号数据库已经更新但大厅 LED 屏还显示上一个号。原因LED 屏页面没有做轮询或 WebSocket 推送只靠手动刷新。解决在显示页面加setInterval每 2 秒请求一次/currentCall接口或者用 WebSocket 做服务端推送。轮询间隔不要低于 1 秒否则数据库压力大。4.3 过号处理逻辑混乱客户回来要求优先现象客户过号后回来柜员不知道是该重新取号还是直接插入当前队列。原因系统没有定义过号后的处理规则status3的号票没有后续操作入口。解决在大堂经理端加「过号重排」功能把过号票重新插入队列头部并记录重排次数。同一个号最多重排一次防止无限循环。4.4 窗口暂停后队列卡死现象某个窗口点了「暂停服务」但该窗口之前叫的号没有完成新号也无法分配到这个窗口。原因窗口状态和号票状态没有联动暂停时没有把当前服务的号票标记为完成或转移。解决暂停窗口时先检查current_ticket_id是否为空。如果不为空弹窗提示柜员先完成当前业务或转移号票。转移逻辑是把号票重新插入队列头部并清空窗口的current_ticket_id。4.5 数据库连接泄漏跑一天后 Tomcat 报连接池满现象系统运行几个小时后所有数据库操作超时重启 Tomcat 恢复。原因DAO 层有些方法没有在finally块里关闭Connection或者用了连接池但没有正确归还。解决统一用 try-with-resources 写法或者用 Druid 的DruidDataSource自带泄漏检测。在db.properties里加removeAbandonedtrueremoveAbandonedTimeout300超过 5 分钟没归还的连接自动回收。5. 进阶技巧用 JSPM 做多网点排队数据汇总单网点的排队系统跑通之后下一步自然是多网点数据汇总。总行需要看到每个网点的排队人数、平均等待时间、窗口利用率。这个需求不需要改现有架构加一张汇总表和定时任务就能实现。5.1 汇总表设计与定时任务CREATE TABLE daily_summary ( id INT PRIMARY KEY AUTO_INCREMENT, branch_id INT NOT NULL COMMENT 网点ID, stat_date DATE NOT NULL, total_tickets INT DEFAULT 0 COMMENT 取号总数, avg_wait_seconds INT DEFAULT 0 COMMENT 平均等待秒数, max_wait_seconds INT DEFAULT 0 COMMENT 最大等待秒数, active_windows INT DEFAULT 0 COMMENT 活跃窗口数, UNIQUE KEY uk_branch_date (branch_id, stat_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;定时任务用Timer或ScheduledExecutorService每天凌晨跑一次统计前一天的数据public class SummaryTask { private ScheduledExecutorService scheduler Executors.newScheduledThreadPool(1); public void start() { // 每天凌晨2点执行 long initialDelay getDelayToNext2AM(); scheduler.scheduleAtFixedRate(this::doSummary, initialDelay, 24 * 60 * 60, TimeUnit.SECONDS); } private void doSummary() { String sql INSERT INTO daily_summary (branch_id, stat_date, total_tickets, avg_wait_seconds) SELECT branch_id, DATE(create_time), COUNT(*), AVG(TIMESTAMPDIFF(SECOND, create_time, call_time)) FROM ticket WHERE DATE(create_time) DATE_SUB(CURDATE(), INTERVAL 1 DAY) GROUP BY branch_id, DATE(create_time) ON DUPLICATE KEY UPDATE total_tickets VALUES(total_tickets); // 执行 SQL } }ON DUPLICATE KEY UPDATE保证同一天重复执行不会产生重复记录。TIMESTAMPDIFF计算的是取号到叫号的秒数也就是客户的实际等待时间。如果客户取了号没等就走了call_time为空AVG会自动忽略这些记录。5.2 多网点数据隔离与权限控制多网点之后每个网点只能看自己的数据总行能看全部。在window表和ticket表里加branch_id字段查询时根据登录用户的角色拼接WHERE条件// 根据角色拼接数据权限 String branchFilter ; if (user.getRole().equals(BRANCH)) { branchFilter AND branch_id user.getBranchId(); } String sql SELECT * FROM daily_summary WHERE stat_date ? branchFilter;这里有个安全坑branchFilter是字符串拼接如果branchId来自用户输入会有 SQL 注入风险。正确做法是用PreparedStatement参数化把branch_id作为参数传入。5.3 验证汇总数据是否准确跑完定时任务后用这条 SQL 交叉验证-- 手动统计某天的数据和 daily_summary 对比 SELECT branch_id, COUNT(*) AS total, AVG(TIMESTAMPDIFF(SECOND, create_time, call_time)) AS avg_wait FROM ticket WHERE DATE(create_time) 2025-01-15 GROUP BY branch_id;如果daily_summary里的avg_wait_seconds和手动统计对不上检查call_time为空的记录是否被正确排除。另外注意时区问题CURDATE()取的是数据库服务器时间如果服务器时区和业务时区不一致统计日期会偏移。这套 JSPM 银行排队叫号系统的核心就是队列管理和状态流转把这两个点吃透剩下的都是增删改查。我踩过最深的坑是并发取号重复当时用SELECT MAX查了一个下午才发现问题换成AtomicInteger之后世界都安静了。希望帮到你。本文还有配套的精品资源点击获取