
简介这是一份基于JSP的银行预约管理系统毕业设计资源面向计算机相关专业学生、初级Java开发者以及需要快速搭建预约类Web项目的人群可作毕业设计参考、课程实训或二次开发蓝本。系统采用Java、JSP、SQL Server数据库和Tomcat服务器实现整体涵盖在线预约、后台审核管理、公告信息发布、客户管理等常见模块并经过功能测试和权限漏洞排查从需求分析、数据库设计到前端编码均完整落地具备较好的教学与实用价值。资源压缩包共499个文件大小约9.07MB以JSP页面、Java类、CSS样式、HTML网页、JS脚本以及SQL脚本和JAR依赖为主前端展示、后端逻辑与数据库脚本分层清晰另附配套报告文档便于对照理解。目前已有179人学习使用。包含可运行的完整项目代码、数据库初始化脚本及配套报告目录结构规范、注释较完整可直接导入开发工具运行调试也能帮助读者梳理预约管理系统开发思路进行功能扩展或论文撰写。1. 从课堂作业到可答辩的毕设先看清 JSP 银行预约管理系统到底在做什么“JSP 银行预约管理系统”听起来像十年前的选题但真把它做完你会发现它比很多 Spring Boot 项目更经得起答辩追问。这套系统解决的场景很具体用户不用去银行大厅干等在网上选网点、选业务类型、选时间段到了网点直接办理管理员在后台确认预约、处理取消。整个链路就是注册登录、填预约单、查个人预约记录用 JSP Servlet MySQL 三件套就能完整落地。这篇笔记适合两类人一类是课程设计或毕业设计选了 Java Web 方向的学生另一类是打算把 Servlet/JSP 底层请求原理捋清楚的新手。跟着做一天能跑通核心流程剩下两天补后台管理、样式和状态机就能达到答辩能讲清、演示不翻车的程度。2. 技术栈与数据库设计为什么 JSP Servlet MySQL 是毕设最省事的组合2.1 选型理由老技术不等于坏技术关键是答辩时你能说清每一行毕设答辩时老师最常问的一句话是“这个功能是怎么实现的”。Spring Boot 帮你封装好了自动配置、依赖注入、ORM 映射一问就成了黑匣子JSP 不一样一次请求进来浏览器 → Tomcat → Servlet → JSP → DAO → JDBC → MySQL每一层你都能指出来。JSP 第一次被访问时容器会把它翻译成 Java 文件再编译成 Servlet所以写 JSP 本质上是写 Servlet。能把这句话讲清楚老师基本会认为你是真做了而不是背了别人的代码。再回到需求本身。银行预约管理系统的业务量不大用户注册登录、提交预约、查看记录、管理员审核。这类轻量业务用不上 Redis、消息队列、微服务硬上前后端分离反而要把跨域、Token 鉴权、接口文档全部做一遍毕设周期很容易失控。MySQL 免费、体积小Tomcat 部署简单出了问题搜索一下到处都是解决方案。用与业务复杂度匹配的技术本身就是工程能力的一种体现。如果你担心“面试时 JSP 已经没人用了”这个顾虑不成立。毕设考察的是把需求落地成代码的完整度不是技术栈的新旧。同一个系统用 Spring Boot 做你可能说不清「自动配置」到底配置了什么用 JSP 做你能把请求生命周期讲到 Tomcat 源码级别。这也是很多老师更愿意看到的结果。2.2 四张核心表与预约状态流转建表 SQL 与字段说明数据库设计是整套系统的地基。我给这个项目设计了四张表用户表、网点表、预约表外加一张业务类型字典表。业务类型虽然可以写死在页面下拉框里但独立成表能让后台扩展业务时不用改代码。下面是最核心的三张表的建表 SQL-- 用户表只保留预约系统必要的字段 CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(30) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, real_name VARCHAR(20) NOT NULL, phone VARCHAR(11) NOT NULL, id_card VARCHAR(18) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 网点表用于预约时选择办理地点 CREATE TABLE t_branch ( id INT PRIMARY KEY AUTO_INCREMENT, branch_name VARCHAR(50) NOT NULL, address VARCHAR(100) NOT NULL ); -- 预约表业务核心表 CREATE TABLE t_appointment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, branch_id INT NOT NULL, business_type VARCHAR(20) NOT NULL, appoint_date DATE NOT NULL, time_slot VARCHAR(10) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_time (user_id, appoint_date, time_slot), KEY idx_branch_date (branch_id, appoint_date) );几个字段说明business_type直接填“开户、挂失、转账、理财咨询”这样的一段式字符串比外键关联字典表更直白答辩时也容易解释time_slot用固定格式字符串“09:00-10:00”而不是时间戳因为业务上只需要判断时段不需要计算时长。status用 TINYINT 而不是 VARCHAR是为了排序和条件查询更快取值含义写进 COMMENT这个习惯在答辩时很加分。预约状态流转是这套系统的业务灵魂。我的设计是用户提交预约后状态为 0待确认管理员在后台确认后变为 1已确认用户到店办理完成变为 2已完成任意状态下用户或管理员都可以取消取消后状态为 3已取消。注意取消不是删除记录保留状态才能做后续的爽约率统计。这个状态机是你论文里“系统设计”章节的核心素材。2.3 项目目录结构与第一个能跑的 JSP 页面传统 Eclipse Dynamic Web Project 或 IDEA 的 Web 项目都可以目录结构保持下面这样不要用 Maven 的 src/main/java 结构毕设现场部署时反而容易绕晕bank_appoint/ ├── src/ │ ├── com/example/util/DBUtil.java │ ├── com/example/dao/UserDAO.java │ ├── com/example/dao/AppointmentDAO.java │ └── com/example/servlet/ │ ├── RegisterServlet.java │ ├── LoginServlet.java │ ├── AppointmentSubmitServlet.java │ └── CancelServlet.java ├── WebContent/ │ ├── index.jsp │ ├── register.jsp │ ├── login.jsp │ ├── appoint.jsp │ ├── profile.jsp │ ├── WEB-INF/web.xml │ └── css/ js/ upload/ └── database/init.sqlDAO负责 SQL 操作Servlet负责接收请求和跳转JSP只负责展示这个三层划分是 Java Web 课程里反复强调的分层思想别把 SQL 直接写到 JSP 里。第一个能跑的首页很关键先让整个项目在浏览器里亮起来后面再往里填功能% page contentTypetext/html;charsetUTF-8 languagejava % !DOCTYPE html html head title银行预约管理系统 - 首页/title /head body h2银行预约管理系统/h2 a href${pageContext.request.contextPath}/register.jsp用户注册/a a href${pageContext.request.contextPath}/login.jsp用户登录/a /body /htmlpage指令里的contentTypetext/html;charsetUTF-8同时设置了响应类型和编码languagejava表示页面里可以写 Java 片段。${pageContext.request.contextPath}是 EL 表达式拿到的部署路径写成这个形式后不管项目叫bank_appoint还是改成别的名字链接都不会 404。部署时把整个 WebContent 目录拷到 Tomcat 的 webapps 下启动 Tomcat浏览器访问http://localhost:8080/bank_appoint/就能看到页面。3. 预约主流程跑通Servlet 接收表单、防重复提交与个人信息展示页面3.1 DBUtil 连接类JDBC 驱动与连接参数一次配好所有 DAO 都需要数据库连接所以第一个工具类就是 DBUtil。很多网上教程喜欢用 c3p0 或 Druid 连接池但毕设阶段用 JDBC 直连最透明答辩时老师问“连接怎么管理”你能直接答出代码。下面是我常用的写法import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/bank_appoint ?useUnicodetruecharacterEncodingUTF-8 useSSLfalseserverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { // MySQL 8.x 的驱动类名5.7 及以下用 com.mysql.jdbc.Driver Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }参数逐个说明characterEncodingUTF-8是数据库连接层面的中文支持少了它存入的中文大概率变问号useSSLfalse是关闭 MySQL 8 的 SSL 告警本地开发没必要开serverTimezoneAsia/Shanghai解决 MySQL 驱动与时区的时差问题不写会在连接时报 CST 相关错误。驱动包要放到WebContent/WEB-INF/lib目录下不要配在 Build Path 里完事否则部署到 Tomcat 后运行时找不到类。这个类有几个容易被新手误用的点不要在每次getConnection()前都Class.forName静态块里执行一次就够也不要在这里关闭连接连接关闭要放在 DAO 方法里用finally块处理否则连接会一直占着。DBUtil 写的越薄越好只负责拿连接。3.2 预约提交的完整链路一次性 Token 防重 PRG 跳转预约提交是整个系统最核心的交互也是演示时最容易出丑的地方。前端页面上有一个表单用户选择网点、日期、时间段和业务类型然后提交。表单里除了业务字段还有一个隐藏的 Token 字段form action${pageContext.request.contextPath}/appointment/submit methodpost input typehidden nametoken value${sessionScope.appointToken} select namebranchId c:forEach items${branchList} varbranch option value${branch.id}${branch.branchName}${branch.address}/option /c:forEach /select input typedate nameappointDate required select nametimeSlot option value09:00-10:0009:00-10:00/option option value10:00-11:0010:00-11:00/option option value14:00-15:0014:00-15:00/option /select select namebusinessType option value开户开户/option option value挂失挂失/option option value转账转账/option option value理财咨询理财咨询/option /select button typesubmit提交预约/button /form这个 Token 不是随机生成的装饰品。渲染appoint.jsp时后台要在 Session 里放一个随机字符串页面用sessionScope读出来放进隐藏域提交后 Servlet 取到表单里的 Token 和 Session 里的比对匹配才继续处理处理完立刻把 Session 里的 Token 删掉。这样做是为了防止用户连着点两次提交按钮生成两条预约记录。服务端的AppointmentSubmitServlet核心逻辑如下protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding(UTF-8); // 1. 一次性 Token 校验防止重复提交 HttpSession session req.getSession(); String formToken req.getParameter(token); Object sessionToken session.getAttribute(appointToken); if (sessionToken null || !sessionToken.equals(formToken)) { resp.sendRedirect(req.getContextPath() /appoint.jsp?msgrepeat); return; } session.removeAttribute(appointToken); // 2. 读取表单字段并组装预约对象 User loginUser (User) session.getAttribute(loginUser); int branchId Integer.parseInt(req.getParameter(branchId)); String date req.getParameter(appointDate); String timeSlot req.getParameter(timeSlot); String businessType req.getParameter(businessType); // 3. 调用 DAO 落库成功则重定向到个人信息展示页面 AppointmentDAO dao new AppointmentDAO(); boolean ok dao.insert(loginUser.getId(), branchId, date, timeSlot, businessType); resp.sendRedirect(ok ? req.getContextPath() /profile : req.getContextPath() /appoint.jsp?msgfail); }这段代码里最关键的是最后两行的sendRedirect而不是req.getRequestDispatcher(...).forward(...)。前者告诉浏览器“你去访问另一个地址”浏览器的地址栏会变成新的 GET 请求后者是服务端内部转发地址栏还是原来的 POST 地址。如果用了 forward用户提交成功后按 F5 刷新浏览器会重放上一次 POST 请求瞬间又生成一条记录。这个模式叫 PRGPost/Redirect/Get是 Web 开发里防表单重复提交的标配答辩时能主动说出这个词会很加分。3.3 个人信息展示页面JSTL 渲染历史预约 图片坐标定位预约提交成功后用户需要一个能看到自己历史预约的页面这就是 jsp 个人信息展示页面。它的技术难点不是遍历列表而是怎么把状态从数字变成汉字、怎么把数据库里的标记准确展示到图片上。先看列表渲染部分% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % div classcard h3近期预约/h3 table trth日期/thth时段/thth网点/thth业务/thth状态/th/tr c:forEach items${appointmentList} varapp tr td${app.appointDate}/td td${app.timeSlot}/td td${app.branchName}/td td${app.businessType}/td td c:choose c:when test${app.status 0}待确认/c:when c:when test${app.status 1}已确认/c:when c:otherwise已结束/c:otherwise /c:choose /td /tr /c:forEach /table /divtaglib指令引入了 JSTL 标签库c:forEach的作用类似 Java 的增强 for 循环c:choose/c:when是多分支判断。注意这里没有在 Java 里把状态提前拼成“待确认”而是直接输出数字后在页面层判断好处是后端逻辑简单、页面表意清楚。如果把状态字符串在 Java 里拼好遇到“已取消”和“已完成”都需要改 Java 代码页面层反而成了死逻辑。很多做 jsp 图片如何对坐标定位的同学都在这一步踩坑。我在这套系统里设计了一个好演示的交互用户上传证件照片后在个人信息展示页面点一下照片标记人脸中心位置坐标存进隐藏域后端保存下来用于后续裁剪。这里的坐标换算必须用图片的真实像素而不是页面显示的像素img ididCardImg src${pageContext.request.contextPath}/upload/photo.jpg alt证件照片 input typehidden idphotoX namephotoX input typehidden idphotoY namephotoY script var img document.getElementById(idCardImg); img.addEventListener(click, function (e) { // e.offsetX 是相对元素左上角的 CSS 像素 // naturalWidth 是图片原始像素offsetWidth 是页面缩放后的 CSS 像素 var realX Math.round(e.offsetX * (img.naturalWidth / img.offsetWidth)); var realY Math.round(e.offsetY * (img.naturalHeight / img.offsetHeight)); document.getElementById(photoX).value realX; document.getElementById(photoY).value realY; }); /scriptoffsetX拿到的是你在屏幕上看到的坐标如果 CSS 里设置了width: 100%或固定宽度图片被拉伸这个坐标和原图真实像素就对不上。naturalWidth和naturalHeight是图片的原始尺寸用比例换算后得到的才是图片自身的像素坐标。这个技巧同样适用于管理员在网点示意图上标记窗口位置点击地图上的点把原图像素坐标写进表单比让管理员手填坐标人性化得多。4. 排查与避坑5 个让 JSP 毕设翻车的经典现场4.1 刷新页面弹出“确认重新提交表单”PRG 模式没落地现象用户提交预约后浏览器地址栏仍然是appoint.jsp页面显示提交成功但一按 F5 就弹窗“确认重新提交表单”点确认后生成了两条一模一样的预约。原因Servlet 里用了 forward 转发到 JSP请求方式始终是 POST刷新浏览器会重放上一次的 POST 请求。解决按 3.2 节的 PRG 模式改造处理完业务立即resp.sendRedirect()把 GET 请求交给下一个页面。配合一次性 Token即使快速双击按钮也不会插两条数据。这个坑在答辩演示当天出现频率最高因为台下老师最喜欢让你刷新页面看看。4.2 数据库查出来的中文全是问号三处编码必须统一现象前端页面显示正常但存进 MySQL 的中文变成“???”或者乱码直接看表数据时更明显。原因JSP 页面编码、请求体编码、JDBC 连接编码三处不一致。任何一处不是 UTF-8中文就可能在某一环被转码转坏。解决JSP 文件头部用contentTypetext/html;charsetUTF-8Servlet 里在读取参数前执行req.setCharacterEncoding(UTF-8)JDBC URL 里加characterEncodingUTF-8。还要注意 GET 请求的参数编码不走setCharacterEncoding需要在 Tomcat 的server.xml里给 Connector 配置URIEncodingUTF-8。这套组合配置齐了中文基本不会再出问题。4.3 导入的 Servlet 类全部报红Tomcat 10 把 javax 改成 jakarta现象用最新版 Tomcat 10 建项目javax.servlet.http.HttpServlet这个 import 直接编译不过项目一片红。原因Tomcat 10 起 Java EE 改名为 Jakarta EE包名从javax.*全部换成jakarta.*老代码的 import 全部失效。解决毕设阶段直接用 Tomcat 8.5 或 9网上资料和老师课件里的代码都能直接跑。如果你已经装了 Tomcat 10要么全局替换 import 里的javax.servlet为jakarta.servlet要么换个 Tomcat 版本。每年都有人在这上面耗掉一下午然后发现只是版本问题不是代码问题。4.4 点击图片拿到的坐标总是偏CSS 缩放和原图尺寸不是一回事现象在个人信息展示页点击图片标记人脸位置存库后回显标记点位置明显偏移。原因e.offsetX返回的是 CSS 像素而 CSS 里图片被设置了固定宽度或者width: 100%图片实际显示尺寸和原图自然尺寸差很多坐标自然不准。解决用 3.3 节的换算公式offsetX * (naturalWidth / offsetWidth)。我见过不少同学直接存offsetX到数据库在小屏笔记本上看着对换到大屏显示器就偏了这是典型的“换台电脑就翻车”的坑。4.5 Element 图标加载不出来CDN 引入顺序写反了现象JSP 页面里引入了饿了么团队的 Element UI 想做漂亮点的表单结果图标全变成小方框按钮也光秃秃的。原因Element UI 的 CSS 和 JS 引入顺序不对或者把 Vue 3 Element Plus 的用法套在了 Vue 2 Element UI 上——这两者组件名和图标用法完全不同。解决把 Element UI 的 CSS 放在 head 里先加载Vue 和 Element UI 的 JS 放在 body 最后按顺序引入版本上统一用 Vue 2 配 Element UI 2.x不要混用 Vue 3 的 Element Plus。不想引 CDN 就把这几个文件下载到本地WebContent/js和WebContent/css目录JSP 用${pageContext.request.contextPath}引用本地路径演示时断网也不慌。5. 给毕设加分的三个技巧时段冲突检测、取号页自动刷新与验收清单5.1 同一网点同一时段的名额校验唯一索引比先查后插更可靠防止用户同一时段重复预约用数据库约束兜底比应用层判断更省事。建表时已经加了UNIQUE KEY uk_user_time (user_id, appoint_date, time_slot)插入重复数据时 DAO 里捕获SQLIntegrityConstraintViolationException直接提示“该时段已预约过”-- 如果建表时漏了用这句补上 ALTER TABLE t_appointment ADD UNIQUE KEY uk_user_time (user_id, appoint_date, time_slot);如果还要限制每个网点每时段的名额上限就在插入前SELECT COUNT(*)一下满员就拒绝。并发极端场景下先查后插可能超卖但毕设项目用唯一索引加数量查询已经足够答辩时能说出“最终靠数据库约束兜底”这句话比纯程序判断高一个层次。5.2 取号页加载后自动刷新一次展示最新队列号又不死循环用户提交预约后跳转到取号页页面要显示最新队列号一个需求是页面加载后自动刷新一次来获取最新数据但如果每次加载都刷新就会死循环。解决办法是借用 sessionStorage 做一次性标记script if (!sessionStorage.getItem(queue_refreshed)) { sessionStorage.setItem(queue_refreshed, 1); location.reload(); } /script刷新前先检查标记没有标记就设置标记并刷新一次刷新后读到标记不再刷新。sessionStorage 在同一个标签页里跨刷新保持不变关闭标签页后自动清除比 cookie 干净得多。这个技巧不只是取号页能用任何“加载后刷新一次”的场景都适用。5.3 答辩前把状态机走一遍拿这张表自查页面/操作预期行为常见错误注册重复用户名被拦截没做唯一校验插入报错没捕获提交预约成功跳转 profile 页刷新不重复提交用了 forwardF5 重放 POST个人信息展示历史预约按时间倒序、状态显示汉字状态直接输出数字 0/1/2后台确认待确认变为已确认前台刷新能看到状态改了但没提交事务取消预约状态变为已取消保留记录直接 DELETE统计数据缺失我做这类系统最吃亏的一次就是花了一晚上调页面样式结果被老师问“同一个用户怎么防止重复预约”只能站在台上现想。后来学乖了先把表结构和约束讲清楚再去折腾前端。数据库设计多花半小时答辩能少挨三个追问。希望帮到你。本文还有配套的精品资源点击获取