SSM+JSP+MySQL5.7银行叫号系统实战指南 简介本资源是一套完整的Java Web银行排队叫号系统实战项目面向Java初学者与Web开发入门者聚焦SSM框架SpringSpringMVCMyBatis与JSP前端技术的工程化落地适用于课程设计、毕业设计及小型业务系统原型开发。压缩包含856个文件总计29.65MB涵盖87个核心Java业务逻辑类、49个JSP页面、217个JS交互脚本、160个PNG图标资源、97个CSS样式文件及2个SQL建表脚本完整支撑系统前后端功能与界面渲染另有演示视频MP4与Eclipse项目配置文件.classpath、.project等确保开箱即用。目前已有465人学习下载提供从环境搭建JDK1.8Tomcat7MySQL5.7Navicat11、源码编译、数据库初始化到系统运行的全流程支持结构清晰、注释规范特别适合理解银行窗口业务流程与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 新特性而是用最朴素的 SSMSpring 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 (...) ENGINEInnoDB DEFAULT CHARSETutf8;注意是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_TIMESTAMPAUTO_INCREMENT初始值设为100001配合INSERT INTO queue_number (number, status) VALUES (A100001, WAITING)实现字母数字混合编号该写法在 8.0 的严格模式下会触发sql_modeSTRICT_TRANS_TABLES报错。提示Navicat 11 连接 MySQL 5.7 时务必在连接属性 → 高级 → “初始化命令” 中填入SET SESSION sql_modeNO_ENGINE_SUBSTITUTION;否则建表会失败。2.2 Eclipse 导入实操绕过.classpath和.project的陷阱项目根目录下存在.classpath和.project文件说明它原生为 Eclipse WTPWeb 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 groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version4.3.25.RELEASE/version /dependencySpring 4.3.x 是最后一个兼容 JDK1.8 Servlet 3.0 的稳定分支。若强行升级到 Spring 5.x会触发以下连锁反应Controller类中ModelAndView返回类型被弃用需改为StringModelAttributeweb.xml中listener-classorg.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中contextConfigLocationclasspath:applicationContext.xml控制台报Context initialization failedSpring 容器找不到主配置文件applicationContext.xml必须放在src/main/resources下且编译后位于WEB-INF/classes/filter中CharacterEncodingFilterencodingUTF-8中文取号显示为??JSP 页面% page contentTypetext/html;charsetUTF-8%与 Filter 编码不一致导致 POST 请求参数乱码特别注意Tomcat 7 的conf/server.xml中Connector标签必须显式添加URIEncodingUTF-8否则?numberA100001windowW01这类 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 ) ENGINEInnoDB DEFAULT CHARSETutf8; -- 最后恢复外键检查 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 actionqueue/getNewNumber methodpost select namebusinessType option valueCORP对公业务/option option valuePERSONAL个人业务/option /select input typesubmit 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 idselectLastNumberByPrefix resultTypeQueueNumber 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?windowIdW01, function(data) { if (data.number) { $(#currentNumber).text(data.number); playSound(); // 播放语音 } }); }, 5000);QueueController.callNextNumber()的关键在于状态机控制RequestMapping(/callNextNumber) ResponseBody public MapString, Object callNextNumber(RequestParam(windowId) String windowId) { MapString, Object result new HashMap(); // 1. 查找 WAITING 状态的最早号 QueueNumber target queueMapper.selectOldestWaiting(); if (target null) { result.put(code, -1); result.put(msg, 暂无等待号码); return result; } // 2. 乐观锁更新只更新 statusWAITING 且 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 idupdateStatusById 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); ListQueueNumber timeoutList queueMapper.selectTimeoutNumbers(threeMinutesAgo); for (QueueNumber qn : timeoutList) { queueMapper.updateStatusById(qn.getId(), TIMEOUT, qn.getVersion() 1); } } }对应的selectTimeoutNumbersSQLselect idselectTimeoutNumbers resultTypeQueueNumber 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) { ListQueueNumber 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-enabledtrue/el-enabled /jsp-property-group /jsp-config4.2 现象点击“取号”后跳转到success.jsp但页面显示${number}而非真实号码原因Spring MVC 的InternalResourceViewResolver未正确配置视图前缀/后缀或 Controller 返回的逻辑视图名与实际 JSP 文件名不匹配。解决检查applicationContext-mvc.xml中bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp/ / property namesuffix 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 UPDATEselect idselectOldestWaiting resultTypeQueueNumber statementTypeSTATEMENT SELECT * FROM queue_number WHERE status WAITING ORDER BY create_time ASC LIMIT 1 FOR UPDATE /select并在mybatis-config.xml中设置setting namedefaultStatementTimeout value30/防止死锁。4.4 现象bootstrap.min.css加载 404但文件明明在WebContent/css/下原因Eclipse 的 Deployment Assembly 配置错误WebContent/css/未被映射到 WAR 包的/css/路径。解决右键项目 → Properties → Deployment Assembly → Add → Folder → 选择WebContent/cssDeploy Path 设为/css。4.5 现象ueditor.css报错Uncaught ReferenceError: UE is not defined原因UEditor 是富文本编辑器但本项目中并未使用它——ueditor.css是冗余文件index.jsp中link relstylesheet hrefcss/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 bootstrap3.4.1 jspm install whatwg-fetch这会在WebContent/jspm_packages/下生成模块树并生成config.js。第三步修改index.jsp用 SystemJS 加载模块script srcjspm_packages/system.js/script script srcconfig.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 definedconsole.log(typeof SystemJS)模块是否正确解析Network 面板看到queue.js、call.js被单独请求404 或返回 HTML 页面curl -I http://localhost:8080/bank-queue/js/modules/queue.jsfetch是否可用取号请求发出返回 JSON报错fetch is not definedIE 不支持navigator.userAgent.includes(MSIE)注意Tomcat 7 默认不支持 HTTP/2jspm bundle生成的代码若启用--inject选项会写入import bootstrap但 Bootstrap 3.4.1 的 AMD 模块定义不规范需手动 patchjspm_packages/github/twbs/bootstrap3.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就行。希望帮到你。本文还有配套的精品资源点击获取