news 2026/10/22 17:59:31

SSM+JSP+MySQL5.7银行叫号系统实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SSM+JSP+MySQL5.7银行叫号系统实战指南

简介:本资源是一套完整的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。正确做法是:

  1. 新建 Dynamic Web Project,名称设为bank-queue;
  2. 将压缩包中src/目录下的main/java和main/resources复制到新项目的src下;
  3. 将src/main/webapp下全部内容(含WEB-INF/web.xml,index.jsp,js/,css/)复制到新项目根目录的WebContent文件夹;
  4. 手动编辑WebContent/WEB-INF/web.xml,确认<servlet-class>指向的是com.jspm.controller.QueueController,而非com.bank.queue.controller.QueueController(原始包名可能被混淆);
  5. 右键项目 → 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>中contextConfigLocationclasspath:applicationContext.xml控制台报Context initialization failedSpring 容器找不到主配置文件,applicationContext.xml必须放在src/main/resources下,且编译后位于WEB-INF/classes/
<filter>中CharacterEncodingFilterencoding="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支持不完善。按顺序执行:

  1. 新建数据库:右键连接 → 新建数据库 → 名称填bank_queue,字符集选utf8,排序规则选utf8_general_ci;
  2. 执行建表语句(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;
  3. 插入初始窗口数据:
    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 definedconsole.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'就行。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 0:43:48

用随机森林分类泰坦尼克号:sklearn建模全流程实战

把泰坦尼克号做成一个随机森林分类项目&#xff0c;我一开始觉得有点“杀鸡用牛刀”。但真正跑完之后&#xff0c;我才发现这个组合比想象中更适合用来理解 sklearn 的建模套路&#xff1a;特征工程、交叉验证、超参数调优&#xff0c;每一步都能拿到足够直接的反馈。几行代码下…

作者头像 李华
网站建设 2026/10/22 17:59:27

IE、Firefox、Chrome 浏览器兼容差异与实战解决方案

1. 浏览器三巨头的差异到底在哪搞前端或者做企业内网系统的朋友&#xff0c;大概率都遇到过这种场景&#xff1a;同一个页面&#xff0c;在 Chrome 上跑得好好的&#xff0c;到了 Firefox 上某个动画就卡住了&#xff0c;再换到 IE 上直接白屏。这不是代码写得烂&#xff0c;而…

作者头像 李华
网站建设 2026/10/16 21:04:19

分布式计算框架优化实战:从数据倾斜到小文件治理

上个月我们团队接到一个棘手的任务&#xff1a;核心报表链路每天跑一次&#xff0c;耗时从年初的半小时慢慢涨到了两个多小时&#xff0c;业务方每天早上都在群里催数。一开始大家习惯性想加资源&#xff0c;但加了executor、扩了队列&#xff0c;效果也就那样&#xff0c;成本…

作者头像 李华
网站建设 2026/10/22 17:59:26

Oracle汉字转拼音PL/SQL包:从GBK到UTF8的完整解决方案

简介&#xff1a;在Oracle数据库中&#xff0c;将汉字批量转成拼音是数据检索和索引优化时的常见需求。这套PL/SQL包面向数据库开发与数据分析人员&#xff0c;支持UTF8编码&#xff0c;可直接用于分词检索、拼音码排序、前端模糊搜索等场景。压缩包体积仅一百五十六KB&#xf…

作者头像 李华
网站建设 2026/10/16 8:24:04

C语言TCP网络通信编程实战:从三次握手到粘包处理

简介&#xff1a;基于TCP的客户/服务器通信程序大作业&#xff0c;采用C语言实现&#xff0c;覆盖注册、登陆、单聊、私聊、在线人数列表、退出等完整功能&#xff0c;以有连接服务为主、无连接服务为辅&#xff0c;结合事件对象I/O管理完成通信调度&#xff0c;适合计算机网络…

作者头像 李华
网站建设 2026/10/13 22:51:13

夜间灯光数据校正全流程:从DMSP-OLS到NPP-VIIRS的多年面板构建

1. 这套夜间灯光数据&#xff0c;到底解决了什么问题 第一次拿到1992-2024年跨年度的夜间灯光数据集时&#xff0c;说实话我的第一反应不是兴奋&#xff0c;是头大。资料里厚厚一摞文件告诉我&#xff0c;这批栅格数据来自DMSP-OLS和NPP-VIIRS两代传感器&#xff0c;中间还隔了…

作者头像 李华