简介:这是一套面向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。
实操步骤:
- 下载JDK1.8u361(官方最后安全更新版),配置
JAVA_HOME; - 下载Apache Tomcat 7.0.109(最后维护版),解压后在Eclipse中
Window → Preferences → Server → Runtime Environments添加; - 在Eclipse中
File → Import → Existing Projects into Workspace,选择解压目录,勾选“Copy projects into workspace”(避免路径权限问题); - 右键项目 →
Properties → Project Facets,确认Dynamic Web Module版本为3.0(对应Tomcat7),Java版本为1.8; - 右键项目 →
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&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean>建库与初始化步骤:
- 启动MySQL 5.7服务(确保端口3306空闲);
- 用Navicat11新建连接,用户名
root,密码123456; - 新建数据库
bankqueue,字符集选utf8mb4,排序规则utf8mb4_unicode_ci(比utf8更彻底支持emoji,避免中文乱码); - 执行建表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; - 插入初始测试数据:
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。根源往往不在代码,而在部署配置:
检查
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/。验证
springmvc-servlet.xml的<mvc:annotation-driven/>是否启用
此标签开启SpringMVC注解驱动(如@Controller、@RequestMapping)。若缺失,所有Controller方法不会被扫描,导致请求无响应:<mvc:annotation-driven /> <context:component-scan base-package="com.bankqueue.controller" />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(个人); - 叫号算法升级:窗口叫号时,按优先级查询:
VIP号(business_type=1)永远排在最前;SELECT * FROM t_number WHERE status = 0 ORDER BY business_type ASC, create_time ASC LIMIT 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文件,我都会立刻重命名并对比内容,因为很多“功能失效”其实是备份文件覆盖了主文件。希望帮到你。
本文还有配套的精品资源,点击获取