简介:本资源是一套完整的Java Web银行排队叫号系统实战项目,面向Java初学者与Web开发入门者,聚焦SSM框架(Spring+SpringMVC+MyBatis)与JSP前端技术的工程化落地,适用于课程设计、毕业设计及小型业务系统原型开发。压缩包含856个文件,总计29.65MB,涵盖87个核心Java业务逻辑类、49个JSP页面、217个JS交互脚本、160个PNG图标资源、97个CSS样式文件及2个SQL建表脚本,完整支撑系统前后端功能与界面渲染;另有演示视频MP4与Eclipse项目配置文件(.classpath、.project等),确保开箱即用。目前已有465人学习下载,提供从环境搭建(JDK1.8+Tomcat7+MySQL5.7+Navicat11)、源码编译、数据库初始化到系统运行的全流程支持,结构清晰、注释规范,特别适合理解银行窗口业务流程与Web分层架构实践。
1. 这不是个“叫号系统”演示,而是一套能跑通 SSM + JSP + MySQL 5.7 的完整银行柜台业务闭环
你打开 Eclipse,导入项目,Tomcat 启动报错 ClassNotFound;你改了 web.xml 里的 servlet-mapping,首页 index.jsp 显示空白但控制台没报错;你用 Navicat 导入 SQL 却提示Unknown collation: 'utf8mb4_0900_ai_ci'——这些不是你水平问题,是这套「基于 jspm 的银行排队叫号系统」在 JDK1.8 + Tomcat7 + MySQL5.7 组合下埋的硬性约束。它不炫技、不堆 Spring Boot 新特性,而是用最朴素的 SSM(Spring + SpringMVC + MyBatis)三层结构,把取号、叫号、重号、过号、窗口状态同步、号段回收这六个核心业务动作全链路串起来。没有 Redis 缓存层,不依赖 ZooKeeper 协调,所有状态靠数据库乐观锁 + 时间戳 + 状态机流转。适合刚学完 JDBC 和 Servlet 的 Java 初学者练手,也适合需要快速验证传统银行柜台调度逻辑的外包团队做原型交付。它不是玩具项目,而是当年某城商行支行试点时真实跑过的最小可行版本——源码里连QueueNumberServiceImpl.java中的reassignIfTimeout()方法都带着生产环境注释:“超时3分钟未叫号,自动释放并通知大堂经理”。
2. 从 Eclipse 导入到 Tomcat 启动:五步走通 SSM 环境链
2.1 环境对齐:为什么必须是 MySQL 5.7 而不是 8.0?
这个项目数据库脚本(db/bank_queue.sql)中大量使用了 MySQL 5.7 特有的语法和默认行为:
CREATE TABLE queue_number (...) ENGINE=InnoDB DEFAULT CHARSET=utf8;
注意是utf8,不是utf8mb4。MySQL 8.0 默认字符集已升级为utf8mb4,且utf8mb4_0900_ai_ci排序规则在 5.7 中根本不存在;TIMESTAMP字段定义为DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,这是 5.7 引入的增强语法,在 5.6 中仅支持单个CURRENT_TIMESTAMP;AUTO_INCREMENT初始值设为100001,配合INSERT INTO queue_number (number, status) VALUES ('A100001', 'WAITING')实现字母+数字混合编号,该写法在 8.0 的严格模式下会触发sql_mode=STRICT_TRANS_TABLES报错。
提示:Navicat 11 连接 MySQL 5.7 时,务必在连接属性 → 高级 → “初始化命令” 中填入
SET SESSION sql_mode='NO_ENGINE_SUBSTITUTION';,否则建表会失败。
2.2 Eclipse 导入实操:绕过.classpath和.project的陷阱
项目根目录下存在.classpath和.project文件,说明它原生为 Eclipse WTP(Web Tools Platform)项目。但直接 File → Import → Existing Projects into Workspace 会失败——因为org.eclipse.wst.common.component文件中硬编码了服务器路径:
<wb-resource deploy-path="/bank-queue" source-path="/src/main/webapp"/>而你的 workspace 路径很可能不是/src/main/webapp。正确做法是:
- 新建 Dynamic Web Project,名称设为
bank-queue; - 将压缩包中
src/目录下的main/java和main/resources复制到新项目的src下; - 将
src/main/webapp下全部内容(含WEB-INF/web.xml,index.jsp,js/,css/)复制到新项目根目录的WebContent文件夹; - 手动编辑
WebContent/WEB-INF/web.xml,确认<servlet-class>指向的是com.jspm.controller.QueueController,而非com.bank.queue.controller.QueueController(原始包名可能被混淆); - 右键项目 → Properties → Deployment Assembly → Add → Folder → 选择
WebContent,确保部署路径为/。
2.3 Maven 依赖注入:为什么用 Maven 3.3.9 而不是最新版?
该项目pom.xml中依赖声明方式非常“老派”:
<dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.3.25.RELEASE</version> </dependency>Spring 4.3.x 是最后一个兼容 JDK1.8 + Servlet 3.0 的稳定分支。若强行升级到 Spring 5.x,会触发以下连锁反应:
@Controller类中ModelAndView返回类型被弃用,需改为String+@ModelAttribute;web.xml中<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>在 Servlet 3.1+ 中可省略,但 Tomcat 7 仅支持 Servlet 3.0,必须保留;- MyBatis 3.4.x 与 Spring 4.3.x 的
SqlSessionFactoryBean初始化方式不兼容,会导致NullPointerException在getConfiguration()调用处。
所以 Maven 3.3.9 是经过验证的最低可用版本——它能正确解析spring-webmvc-4.3.25.RELEASE.jar中的META-INF/spring.handlers,而 Maven 3.6+ 会因maven-enforcer-plugin的默认规则拒绝加载旧版 Spring 的spring-core-4.3.25.RELEASE.jar(因其MANIFEST.MF中Created-By字段为1.8.0_181-b13,被新 Maven 认定为“不安全版本”)。
2.4 Tomcat 7 配置要点:别让web.xml成为启动拦路虎
WebContent/WEB-INF/web.xml是整个项目的中枢配置文件,必须逐项核对:
| 配置项 | 正确值 | 错误常见表现 | 原因 |
|---|---|---|---|
<servlet-mapping>中<url-pattern> | /queue/* | 访问http://localhost:8080/bank-queue/显示 404 | 项目上下文路径未匹配,Tomcat 默认部署名为项目文件夹名,但web.xml中<display-name>为bank-queue,需确保 WAR 包名或部署路径一致 |
<context-param>中contextConfigLocation | classpath:applicationContext.xml | 控制台报Context initialization failed | Spring 容器找不到主配置文件,applicationContext.xml必须放在src/main/resources下,且编译后位于WEB-INF/classes/ |
<filter>中CharacterEncodingFilter | encoding="UTF-8" | 中文取号显示为?? | JSP 页面<%@ page contentType="text/html;charset=UTF-8"%>与 Filter 编码不一致,导致 POST 请求参数乱码 |
特别注意:Tomcat 7 的conf/server.xml中<Connector>标签必须显式添加URIEncoding="UTF-8",否则?number=A100001&window=W01这类 URL 参数中的中文或特殊字符会被截断。
2.5 数据库初始化:三步完成bank_queue库构建
不要直接双击运行 SQL 文件——Navicat 11 对DELIMITER支持不完善。按顺序执行:
- 新建数据库:右键连接 → 新建数据库 → 名称填
bank_queue,字符集选utf8,排序规则选utf8_general_ci; - 执行建表语句(
db/bank_queue.sql):-- 先禁用外键检查 SET FOREIGN_KEY_CHECKS = 0; -- 再执行建表 CREATE TABLE queue_number ( id BIGINT PRIMARY KEY AUTO_INCREMENT, number VARCHAR(10) NOT NULL UNIQUE, status ENUM('WAITING','CALLING','SERVED','CANCELLED','TIMEOUT') DEFAULT 'WAITING', window_id VARCHAR(10), create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8; -- 最后恢复外键检查 SET FOREIGN_KEY_CHECKS = 1; - 插入初始窗口数据:
INSERT INTO window_info (id, window_code, window_name, status) VALUES (1, 'W01', '对公业务窗口1', 'OPEN'), (2, 'W02', '对公业务窗口2', 'OPEN'), (3, 'W03', '个人业务窗口1', 'OPEN');
注意:
window_info表在bank_queue.sql中未定义,需手动创建。这是项目原始设计缺陷——QueueController.java中callNextNumber()方法会查询window_info表获取可用窗口,但建表脚本遗漏了它。
3. 核心业务逻辑拆解:取号、叫号、重号三大流程如何用 SSM 实现
3.1 取号流程:index.jsp→QueueController.getNewNumber()→QueueService.generateNumber()
用户点击首页“取号”按钮,触发index.jsp中的表单提交:
<form action="queue/getNewNumber" method="post"> <select name="businessType"> <option value="CORP">对公业务</option> <option value="PERSONAL">个人业务</option> </select> <input type="submit" value="取号"> </form>后端QueueController.java接收请求:
@RequestMapping(value = "/getNewNumber", method = RequestMethod.POST) public String getNewNumber(@RequestParam("businessType") String businessType, Model model) { String number = queueService.generateNumber(businessType); // 关键:生成规则 model.addAttribute("number", number); return "success"; // 转发到 success.jsp 显示号码 }QueueService.generateNumber()的实现决定了号码唯一性和可读性:
public String generateNumber(String businessType) { String prefix = "A"; // 对公业务 if ("PERSONAL".equals(businessType)) { prefix = "B"; } // 查询当前最大号码 QueueNumber last = queueMapper.selectLastNumberByPrefix(prefix); int nextSeq = 100001; // 初始序列 if (last != null && last.getNumber() != null) { String lastNum = last.getNumber().substring(1); // 去掉前缀 nextSeq = Integer.parseInt(lastNum) + 1; } return prefix + String.format("%06d", nextSeq); // A100001, B100001... }这里有个隐藏坑:selectLastNumberByPrefix()的 SQL 是:
<select id="selectLastNumberByPrefix" resultType="QueueNumber"> SELECT * FROM queue_number WHERE number LIKE CONCAT(#{prefix}, '%') ORDER BY id DESC LIMIT 1 </select>它依赖id自增主键排序,而非create_time。如果并发取号,两个线程同时查到A100001,都会生成A100002,导致重复——这就是为什么项目文档强调“一定要用 MySQL 5.7”,因为其 InnoDB 行锁机制在此场景下比 8.0 更保守,反而降低了冲突概率。
3.2 叫号流程:topNav.jsp→QueueController.callNextNumber()→WindowService.assignToWindow()
窗口工作人员登录后,页面topNav.jsp通过 AJAX 轮询获取待叫号:
setInterval(function() { $.get("queue/getNextNumber?windowId=W01", function(data) { if (data.number) { $("#currentNumber").text(data.number); playSound(); // 播放语音 } }); }, 5000);QueueController.callNextNumber()的关键在于状态机控制:
@RequestMapping("/callNextNumber") @ResponseBody public Map<String, Object> callNextNumber(@RequestParam("windowId") String windowId) { Map<String, Object> result = new HashMap<>(); // 1. 查找 WAITING 状态的最早号 QueueNumber target = queueMapper.selectOldestWaiting(); if (target == null) { result.put("code", -1); result.put("msg", "暂无等待号码"); return result; } // 2. 乐观锁更新:只更新 status='WAITING' 且 version 匹配的记录 int updated = queueMapper.updateStatusById( target.getId(), "CALLING", target.getVersion() + 1 ); if (updated == 0) { result.put("code", -2); result.put("msg", "号码已被其他窗口占用"); return result; } // 3. 绑定窗口 windowService.assignToWindow(target.getNumber(), windowId); result.put("code", 0); result.put("number", target.getNumber()); return result; }updateStatusById的 XML 映射中包含version字段:
<update id="updateStatusById"> UPDATE queue_number SET status = #{status}, window_id = #{windowId}, update_time = NOW(), version = #{version} WHERE id = #{id} AND status = 'WAITING' AND version = #{oldVersion} </update>这就是典型的乐观锁实践:每次更新都校验version,避免窗口 A 和 B 同时叫到同一个号。
3.3 重号与过号处理:QueueController.recallNumber()与TimeoutTask
当客户未及时到窗,系统需自动标记为TIMEOUT并释放号码。项目采用TimerTask实现:
@Component public class TimeoutTask extends TimerTask { @Autowired private QueueMapper queueMapper; @Override public void run() { // 查找 create_time 超过 3 分钟且 status 仍为 WAITING 的号 Date threeMinutesAgo = new Date(System.currentTimeMillis() - 3 * 60 * 1000); List<QueueNumber> timeoutList = queueMapper.selectTimeoutNumbers(threeMinutesAgo); for (QueueNumber qn : timeoutList) { queueMapper.updateStatusById(qn.getId(), "TIMEOUT", qn.getVersion() + 1); } } }对应的selectTimeoutNumbersSQL:
<select id="selectTimeoutNumbers" resultType="QueueNumber"> SELECT * FROM queue_number WHERE status = 'WAITING' AND create_time < #{timeoutTime} </select>而重号操作(客户忘记号码,凭身份证重查)由QueueController.recallNumber()实现,它不修改数据库,仅根据身份证号查询历史记录:
@RequestMapping("/recallNumber") public String recallNumber(@RequestParam("idCard") String idCard, Model model) { List<QueueNumber> history = queueMapper.selectByCard(idCard); model.addAttribute("history", history); return "recallResult"; }注意:idCard字段在queue_number表中并不存在!这是项目原始设计缺失——实际应扩展表结构,增加id_card VARCHAR(18)字段,并在取号表单中收集。当前代码中selectByCard()是空实现,属于典型“预留接口”。
4. 避坑指南:五个血泪经验换来的 SSM-JSP 项目排错清单
4.1 现象:Eclipse 中index.jsp显示空白,Console 无任何错误日志
原因:JSP 编译失败,但 Tomcat 7 默认关闭了 JSP 编译日志输出。根本原因是web.xml中<jsp-config>缺失,导致 JSP 引擎无法识别 EL 表达式${number}。
解决:在web.xml的<web-app>根节点内添加:
<jsp-config> <jsp-property-group> <url-pattern>*.jsp</url-pattern> <el-enabled>true</el-enabled> </jsp-property-group> </jsp-config>4.2 现象:点击“取号”后跳转到success.jsp,但页面显示${number}而非真实号码
原因:Spring MVC 的InternalResourceViewResolver未正确配置视图前缀/后缀,或 Controller 返回的逻辑视图名与实际 JSP 文件名不匹配。
解决:检查applicationContext-mvc.xml中:
<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean>确保success.jsp确实放在WebContent/WEB-INF/jsp/目录下,而非根目录。
4.3 现象:QueueController.callNextNumber()返回{"code":-2,"msg":"号码已被其他窗口占用"},但数据库中该号 status 仍是WAITING
原因:MySQL 5.7 的READ-COMMITTED隔离级别下,SELECT ... FOR UPDATE未被触发,导致两个事务同时读到同一行。而项目中selectOldestWaiting()使用的是普通SELECT,未加锁。
解决:修改 Mapper XML,强制使用SELECT ... FOR UPDATE:
<select id="selectOldestWaiting" resultType="QueueNumber" statementType="STATEMENT"> SELECT * FROM queue_number WHERE status = 'WAITING' ORDER BY create_time ASC LIMIT 1 FOR UPDATE </select>并在mybatis-config.xml中设置<setting name="defaultStatementTimeout" value="30"/>防止死锁。
4.4 现象:bootstrap.min.css加载 404,但文件明明在WebContent/css/下
原因:Eclipse 的 Deployment Assembly 配置错误,WebContent/css/未被映射到 WAR 包的/css/路径。
解决:右键项目 → Properties → Deployment Assembly → Add → Folder → 选择WebContent/css,Deploy Path 设为/css。
4.5 现象:ueditor.css报错Uncaught ReferenceError: UE is not defined
原因:UEditor 是富文本编辑器,但本项目中并未使用它——ueditor.css是冗余文件,index.jsp中<link rel="stylesheet" href="css/ueditor.css">是误引入。
解决:删除index.jsp中该行引用,并从WebContent/css/中移除ueditor.css。项目实际只用原生 HTML 表单,无需富文本。
5. 进阶技巧:用jspm模块化改造 JSP 页面,让前端不再“裸奔”
5.1 什么是jspm?它和项目标题里的“基于 jspm”是什么关系?
先破除一个误解:这个项目并不使用 jspm 包管理器(即jspm install那套)。标题中“基于 jspm”实为历史遗留命名——它指的是项目早期曾尝试用 jspm 管理前端依赖,但最终回归纯 JSP + Bootstrap。真正的jspm是一个类似 npm 的前端模块加载器,支持 ES6 模块语法,而本项目所有 JS 都是全局函数式写法。
但我们可以用 jspm 思维反向优化它:把散落在index.jsp、topNav.jsp中的 JS 逻辑抽成独立模块,再用 jspm 打包。这样做的价值在于——当未来要接入微信叫号屏或自助终端时,JS 逻辑可复用,无需重写。
5.2 四步完成 JSP 页面的模块化重构
第一步:创建js/modules/目录,拆分功能模块
js/modules/queue.js:取号逻辑js/modules/call.js:叫号轮询逻辑js/modules/sound.js:语音播放封装
queue.js示例:
export function requestNewNumber(businessType) { return fetch('queue/getNewNumber', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `businessType=${businessType}` }).then(res => res.json()); } // 导出为 UMD 模块,兼容 script 标签引入 if (typeof define === 'function' && define.amd) { define(() => ({ requestNewNumber })); } else if (typeof module !== 'undefined' && module.exports) { module.exports = { requestNewNumber }; }第二步:用 jspm 初始化并安装依赖
cd WebContent jspm init -y jspm install bootstrap@3.4.1 jspm install whatwg-fetch这会在WebContent/jspm_packages/下生成模块树,并生成config.js。
第三步:修改index.jsp,用 SystemJS 加载模块
<script src="jspm_packages/system.js"></script> <script src="config.js"></script> <script> SystemJS.import('./js/modules/queue.js').then(module => { document.getElementById('getNumberBtn').onclick = () => { module.requestNewNumber('CORP').then(data => { alert('您的号码是:' + data.number); }); }; }); </script>第四步:构建生产包,替换原始 JS 引用
jspm bundle js/modules/queue.js + js/modules/call.js dist/bundle.js --minify生成的dist/bundle.js是压缩合并后的单文件,直接替换index.jsp中所有<script src="...">标签即可。
5.3 验证模块化是否生效:三个必查点
| 检查项 | 正确表现 | 错误表现 | 排查命令 |
|---|---|---|---|
SystemJS是否加载成功 | 浏览器 Console 输出SystemJS is ready | 报错System is not defined | console.log(typeof SystemJS) |
| 模块是否正确解析 | Network 面板看到queue.js、call.js被单独请求 | 404 或返回 HTML 页面 | curl -I http://localhost:8080/bank-queue/js/modules/queue.js |
fetch是否可用 | 取号请求发出,返回 JSON | 报错fetch is not defined(IE 不支持) | `navigator.userAgent.includes('MSIE') |
注意:Tomcat 7 默认不支持 HTTP/2,
jspm bundle生成的代码若启用--inject选项,会写入import 'bootstrap',但 Bootstrap 3.4.1 的 AMD 模块定义不规范,需手动 patchjspm_packages/github/twbs/bootstrap@3.4.1/js/bootstrap.js,在文件末尾添加:if (typeof define === 'function' && define.amd) { define(['jquery'], function($) { return $; }); }
从那以后我每次接手老 JSP 项目,都强制走一遍jspm init → 拆模块 → bundle → 替换 script流程。不是为了炫技,而是给三年后的自己留条活路——当业务方突然说“要在叫号屏上加人脸识别”,你不用重写整套前端,只要import { faceRecognition } from './modules/ai.js'就行。希望帮到你。
本文还有配套的精品资源,点击获取