☰
SSM银行叫号系统实战:状态机+行锁+实时推送
2026/10/1 11:46:19 网站建设 项目流程

简介:这是一套面向Java初学者与Web开发入门者的银行排队叫号系统实战项目,基于SSM框架(Spring+SpringMVC+MyBatis)构建,完整覆盖前台用户取号、后台叫号管理及状态实时展示等核心业务场景,适用于课程设计、毕业设计或企业级轻量级排队系统原型开发。资源包共856个文件,含87个Java后端逻辑类、49个JSP页面、217个JS交互脚本、160个PNG图标资源及97个CSS样式文件,辅以MySQL 5.7数据库脚本、Tomcat7部署配置与Eclipse项目元数据(.classpath、.project等),整体压缩包仅29.65MB,结构规范、开箱即用。已有465人学习下载,配套高清MP4演示视频直观呈现取号、叫号、重呼、暂停等全流程操作,源码注释清晰,关键模块如号段生成策略、窗口状态同步机制均有良好实现,便于理解MVC分层设计与前后端协同逻辑。

1. 银行窗口排队叫号系统:一个被低估的SSM实战练手项目,专治“只会写CRUD却搞不定真实业务流”的Java新手

你有没有试过在银行大厅等号——取号、叫号、过号、重叫、插队、VIP优先、临时暂停服务……这些看似简单的动作背后,是一套强状态、高并发、低延迟、带人工干预的实时调度逻辑。而这个基于JSP+Spring+SpringMVC+MyBatis(SSM)的银行排队叫号系统,不是玩具Demo,它完整跑通了从号源生成、窗口绑定、语音播报、LED屏同步、超时重排到管理员后台干预的全链路。它用JDK1.8 + Tomcat7 + MySQL5.7搭建,所有代码可直接导入Eclipse运行,附带演示视频验证流程闭环。适合两类人:一是刚学完SSM但没做过真实业务模块的Java初学者,用来补上“业务状态流转”这一课;二是准备Java面试的开发者,里面藏着大量高频考点——事务边界设计(比如叫号失败如何回滚号段)、Session与Cookie协同控制(多窗口登录隔离)、JSP自定义标签封装(叫号状态渲染)、MySQL行锁实践(号池原子扣减)。别被“JSP”吓退——它恰恰是理解Web层原始交互逻辑的绝佳切口。


2. 项目结构拆解与环境复现:从Eclipse导入到Tomcat启动的六步实操

2.1 项目目录结构还原:识别核心模块与隐藏依赖

拿到压缩包后,先解压观察文件层级。这不是标准Maven结构(没有pom.xml),而是典型的Eclipse动态Web项目布局:

BankQueueSystem/ ├── WebContent/ # JSP页面、静态资源主目录 │ ├── index.jsp.bak # 主入口页面备份(注意.bak后缀!) │ ├── topNav.jsp.bak # 顶部导航栏备份 │ ├── styles.css.bak # 样式表备份 │ ├── bootstrap.min.css # 前端框架(Bootstrap 3.x) │ ├── ueditor.css # 富文本编辑器样式(说明后台有公告管理模块) │ └── ... ├── src/ # Java源码(含controller/service/dao/entity) ├── WEB-INF/ │ ├── web.xml # Servlet配置中枢(关键!需检查servlet-mapping) │ ├── applicationContext.xml # Spring核心配置(bean扫描、事务管理器) │ └── springmvc-servlet.xml # SpringMVC配置(视图解析器、拦截器) ├── .classpath # Eclipse类路径定义(决定哪些jar被加载) ├── org.eclipse.wst.common.component # WTP(Web Tools Platform)部署描述 └── lib/ # 手动引入的jar包(重点看mysql-connector-java-5.1.47.jar)

提示:.bak后缀文件是原始开发者的备份习惯,实际运行需重命名为index.jsp等。org.eclipse.wst.common.component文件决定了Eclipse如何将项目发布到Tomcat——若缺失或错误,会导致“404找不到JSP”。

2.2 JDK与Tomcat版本锁定:为什么必须是JDK1.8 + Tomcat7?

项目明确要求JDK1.8和Tomcat7,这不是随意指定,而是由技术栈兼容性决定的:

  • JSP语法限制:项目中大量使用<c:forEach>、<fmt:formatDate>等JSTL标签,这些在JDK1.8+Tomcat7组合下默认启用。若升级到JDK17+Tomcat10,JSP引擎(Jasper)已移除对javax.servlet.*旧包的支持,会报java.lang.NoClassDefFoundError: javax/servlet/jsp/JspFactory。
  • Spring版本锚定:从applicationContext.xml中<context:component-scan>和<tx:annotation-driven>标签推断,此项目使用Spring 4.x(非5.x)。Spring 4.3.x是最后一个支持Tomcat7的主版本,且其AOP代理机制依赖JDK1.8的invokedynamic指令优化。
  • MySQL驱动匹配:lib/目录下mysql-connector-java-5.1.47.jar仅支持MySQL 5.7协议。若强行连接MySQL 8.0,会因认证插件(caching_sha2_password)不兼容而报Access denied for user。

实操步骤:

  1. 下载JDK1.8u361(官方最后安全更新版),配置JAVA_HOME;
  2. 下载Apache Tomcat 7.0.109(最后维护版),解压后在Eclipse中Window → Preferences → Server → Runtime Environments添加;
  3. 在Eclipse中File → Import → Existing Projects into Workspace,选择解压目录,勾选“Copy projects into workspace”(避免路径权限问题);
  4. 右键项目 →Properties → Project Facets,确认Dynamic Web Module版本为3.0(对应Tomcat7),Java版本为1.8;
  5. 右键项目 →Build Path → Configure Build Path,检查Libraries页签中JRE System Library指向JDK1.8,Web App Libraries包含mysql-connector-java-5.1.47.jar。

2.3 数据库初始化:Navicat11建库、导入SQL、校验字符集

项目未提供SQL脚本?别急——src/目录下通常藏有sql/子目录或DBScript.txt。若无,可从WEB-INF/web.xml中查找数据库连接配置,反向定位建库语句。本项目典型配置如下:

<!-- WEB-INF/web.xml --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param>

进入src/main/resources/applicationContext.xml(或src/applicationContext.xml),找到<bean id="dataSource">:

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/bankqueue?useUnicode=true&amp;characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean>

建库与初始化步骤:

  1. 启动MySQL 5.7服务(确保端口3306空闲);
  2. 用Navicat11新建连接,用户名root,密码123456;
  3. 新建数据库bankqueue,字符集选utf8mb4,排序规则utf8mb4_unicode_ci(比utf8更彻底支持emoji,避免中文乱码);
  4. 执行建表SQL(若无现成脚本,根据实体类src/entity/Number.java等反推):
    CREATE TABLE `t_number` ( `id` int(11) NOT NULL AUTO_INCREMENT, `number_code` varchar(20) NOT NULL COMMENT '号码编号,如A001', `window_id` int(11) DEFAULT NULL COMMENT '当前分配窗口ID', `status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-待叫,1-已叫,2-过号,3-完成', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_number_code` (`number_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  5. 插入初始测试数据:
    INSERT INTO t_number (number_code, status) VALUES ('A001', 0), ('A002', 0), ('B001', 0);

参数说明:useUnicode=true&characterEncoding=utf8在URL中是必需的,否则JSP页面提交的中文参数会变成??;utf8mb4字符集能完整存储微信昵称等四字节UTF-8字符,避免插入时报错。

2.4 Eclipse部署与启动:绕过常见404陷阱的三处关键配置

即使代码无误,Eclipse常报HTTP Status 404。根源往往不在代码,而在部署配置:

  1. 检查web.xml中的<welcome-file-list>
    确保首页面指向正确路径:

    <welcome-file-list> <welcome-file>index.jsp</welcome-file> <!-- 注意不是index.html --> </welcome-file-list>

    若index.jsp仍404,右键项目 →Properties → Web Project Settings,确认Context root为/bankqueue(而非默认/),访问地址应为http://localhost:8080/bankqueue/。

  2. 验证springmvc-servlet.xml的<mvc:annotation-driven/>是否启用
    此标签开启SpringMVC注解驱动(如@Controller、@RequestMapping)。若缺失,所有Controller方法不会被扫描,导致请求无响应:

    <mvc:annotation-driven /> <context:component-scan base-package="com.bankqueue.controller" />
  3. Tomcat部署路径修正
    Eclipse默认将项目发布到<Tomcat>/wtpwebapps/,但有时会生成冗余目录。手动清理:

    • 关闭Tomcat;
    • 删除<Tomcat>/wtpwebapps/bankqueue/及<Tomcat>/wtpwebapps/bankqueue.war;
    • 在Eclipse中右键项目 →Run As → Run on Server,选择Tomcat7;
    • 启动后观察Console输出,确认出现INFO: Server startup in [xxx] ms且无ClassNotFoundException。

3. 核心业务逻辑实现:号源管理、窗口调度与状态机设计

3.1 号源生成与原子扣减:MySQL行锁保障并发安全

银行叫号最怕“重复取号”或“号段跳号”。本项目采用“预生成号段+数据库行锁”双保险:

  • 号段预生成:管理员在后台设置每日号段范围(如A001-A500),系统启动时批量插入t_number表;

  • 取号原子操作:用户点击“取号”时,执行以下SQL(在NumberService.java中):

    UPDATE t_number SET status = 1 WHERE id = ( SELECT id FROM ( SELECT id FROM t_number WHERE status = 0 AND number_code LIKE 'A%' ORDER BY create_time ASC LIMIT 1 ) AS tmp ) LIMIT 1;

    逻辑说明:先子查询找到最早待叫号的id,再用UPDATE ... LIMIT 1更新其状态。LIMIT 1防止并发时多个线程同时更新同一行;WHERE status = 0确保只处理未分配号。

  • Java层校验:NumberService.getNumber()方法中,update返回值为int,若为0说明无可用号,需抛出业务异常:

    int rows = numberMapper.updateStatusById(id); if (rows == 0) { throw new BusinessException("当前无可用号码,请稍后再试"); }

3.2 窗口叫号状态机:五种状态流转与事务边界

叫号不是简单“查-改”,而是涉及多表联动的状态迁移。t_number表status字段定义了严格状态机:

状态码状态名触发条件关联操作
0待叫取号成功无
1已叫窗口点击“叫号”更新window_id,触发语音播报
2过号超时未应答(定时任务扫描)发送短信提醒,允许重叫
3完成用户点击“完成”记录服务时长,释放窗口
4暂停管理员手动暂停窗口窗口停止接收新号

事务设计要点:

  • 叫号操作必须包含:更新t_number.status、更新t_window.current_number、插入t_log日志——三者在一个@Transactional内;
  • 过号由独立定时任务OverTimeJob.java执行,不加事务(避免长事务阻塞),采用SELECT ... FOR UPDATE锁定待处理记录;
  • 完成操作需校验status==1(防止重复完成),使用@Transactional(propagation = Propagation.REQUIRED)确保原子性。

3.3 LED屏与语音播报集成:通过Servlet推送实时消息

前端LED屏和语音播报不是轮询实现,而是利用HttpServletResponse流式推送:

  • LED屏控制器:LedServlet.java监听/led/update请求,将JSON格式号段信息写入响应流:
    response.setContentType("text/event-stream"); response.setCharacterEncoding("UTF-8"); PrintWriter out = response.getWriter(); out.write("data: {\"current\":\"A005\",\"next\":\"A006\"}\n\n"); out.flush();
  • 前端JavaScript监听:
    const eventSource = new EventSource("/bankqueue/led/update"); eventSource.onmessage = function(event) { const data = JSON.parse(event.data); document.getElementById("current").innerText = data.current; document.getElementById("next").innerText = data.next; };
  • 语音播报:调用Windows系统TTS(文本转语音),VoiceUtil.java中执行命令:
    String cmd = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe -Command \"Add-Type –AssemblyName System.Speech; $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer; $speak.Speak('请A零零五号到一号窗口')\""; Runtime.getRuntime().exec(cmd);

参数说明:text/event-stream是Server-Sent Events(SSE)协议,比WebSocket轻量,适合单向实时推送;PowerShell调用TTS需确保服务器启用.NET Framework 3.5。


4. 避坑指南:Eclipse+Tomcat+MySQL组合下五个血泪经验

4.1 现象:Eclipse启动Tomcat后Console无日志,浏览器报404

原因:web.xml中<servlet-mapping>的<url-pattern>与Controller的@RequestMapping不匹配,或springmvc-servlet.xml未正确加载。
解决:

  • 检查web.xml中<servlet>的<servlet-class>是否为org.springframework.web.servlet.DispatcherServlet;
  • 确认<init-param>中contextConfigLocation指向classpath:springmvc-servlet.xml;
  • 在springmvc-servlet.xml中添加<mvc:default-servlet-handler/>,让静态资源(CSS/JS)可被Tomcat原生Servlet处理。

4.2 现象:取号成功但LED屏不刷新,语音无反应

原因:SSE连接被浏览器缓存,或PowerShell执行权限被禁用。
解决:

  • 前端EventSource URL后加时间戳参数:new EventSource("/bankqueue/led/update?t=" + Date.now());
  • Windows服务器需以管理员身份运行PowerShell,执行Set-ExecutionPolicy RemoteSigned -Scope CurrentUser解除脚本限制;
  • 替代方案:改用Java TTS库(如FreeTTS),避免依赖系统组件。

4.3 现象:MySQL插入中文变??,日志显示Incorrect string value

原因:数据库、表、字段三级字符集未统一为utf8mb4,或JDBC URL缺少useUnicode=true&characterEncoding=utf8mb4。
解决:

  • 执行ALTER DATABASE bankqueue CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;;
  • 对所有表执行ALTER TABLE t_number CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;;
  • JDBC URL改为:jdbc:mysql://localhost:3306/bankqueue?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=GMT%2B8。

4.4 现象:Eclipse中修改JSP后刷新无效,仍显示旧内容

原因:Tomcat的JSP编译缓存未清除,或Eclipse的自动构建被禁用。
解决:

  • 清理Tomcat工作目录:删除<Tomcat>/work/Catalina/localhost/bankqueue/;
  • Eclipse中Project → Clean整个项目;
  • 确保Project → Build Automatically已勾选。

4.5 现象:管理员登录后无法看到窗口列表,报NullPointerException

原因:WindowService.java中@Autowired的windowMapper为null,Spring未正确扫描Mapper接口。
解决:

  • 检查applicationContext.xml中<mybatis:scan base-package="com.bankqueue.mapper"/>是否配置;
  • 确认WindowMapper.java接口上有@Mapper注解(若用MyBatis注解方式),或mapper目录在src/main/resources下且XML文件命名规范(WindowMapper.xml对应WindowMapper.java);
  • 若用XML映射,检查WindowMapper.xml中namespace是否等于com.bankqueue.mapper.WindowMapper。

5. 进阶技巧:从演示系统到生产可用的四个加固点

5.1 号池扩容策略:支持多业务类型与优先级队列

当前系统仅支持单一字母前缀(如A/B),但真实银行有VIP、企业、个人、外币等多队列。改造方案:

  • 扩展t_number.number_code字段:增加business_type列(TINYINT),值为1(VIP)、2(企业)、3(个人);
  • 叫号算法升级:窗口叫号时,按优先级查询:
    SELECT * FROM t_number WHERE status = 0 ORDER BY business_type ASC, create_time ASC LIMIT 1;
    VIP号(business_type=1)永远排在最前;
  • 前端分离展示:LED屏分区域显示VIP当前:V001、普通当前:A005,需修改LedServlet组装JSON逻辑。

5.2 防过号机制:基于Redis的分布式超时监控

当前过号依赖单机定时任务,存在单点故障。引入Redis实现高可用:

  • 取号时写入Redis:
    redisTemplate.opsForValue().set("number:" + numberCode, "waiting", 120, TimeUnit.SECONDS);
  • 独立服务监听过期Key:利用Redis Keyspace Notifications,当number:A001过期时触发事件,自动更新数据库状态;
  • 优势:毫秒级响应,避免定时任务扫描延迟;支持集群部署。

5.3 日志审计增强:记录关键操作的完整上下文

现有日志仅记录INFO级别,无法追溯问题。在LogAspect.java中增强:

操作类型记录字段示例
取号用户IP、设备指纹、号段、时间`192.168.1.100
叫号窗口ID、操作员账号、被叫号码`WIN1
完成服务时长、评价星级(若有)`A001
@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping)") public Object logExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); // 获取Request对象获取IP ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes(); String ip = attributes.getRequest().getRemoteAddr(); logger.info("IP:{} | Method:{} | Time:{}ms", ip, joinPoint.getSignature(), end - start); return result; }

5.4 安全加固:防止越权访问与SQL注入

演示系统未做权限控制,生产环境必须补上:

  • URL级拦截:在web.xml中配置<security-constraint>,限制/admin/*路径仅ROLE_ADMIN可访问;
  • Mapper层防护:所有LIKE查询使用#{}而非${},避免拼接SQL:
    <!-- 安全 --> <if test="searchKey != null and searchKey != ''"> AND number_code LIKE CONCAT('%', #{searchKey}, '%') </if> <!-- 危险(SQL注入) --> <!-- AND number_code LIKE '%${searchKey}%' -->
  • 密码加密:User实体类中password字段存储BCrypt加密结果,登录时用BCryptPasswordEncoder.matches()校验。

从那以后我每次接手老项目,第一件事就是检查web.xml的<welcome-file-list>和<servlet-mapping>是否匹配,再验证springmvc-servlet.xml里<context:component-scan>的包路径有没有写错——这两个地方占了80%的404问题。另外,只要看到.bak文件,我都会立刻重命名并对比内容,因为很多“功能失效”其实是备份文件覆盖了主文件。希望帮到你。

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

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

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

立即咨询