☰
JSP银行预约管理系统毕设实战:从数据库设计到防重复提交全流程
2026/10/4 7:50:27 网站建设 项目流程

简介:这是一份基于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.sql

DAO负责 SQL 操作,Servlet负责接收请求和跳转,JSP只负责展示,这个三层划分是 Java Web 课程里反复强调的分层思想,别把 SQL 直接写到 JSP 里。第一个能跑的首页很关键,先让整个项目在浏览器里亮起来,后面再往里填功能:

<%@ page contentType="text/html;charset=UTF-8" language="java" %> <!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> </html>

page指令里的contentType="text/html;charset=UTF-8"同时设置了响应类型和编码,language="java"表示页面里可以写 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" + "?useUnicode=true&characterEncoding=UTF-8" + "&useSSL=false&serverTimezone=Asia/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); } }

参数逐个说明:characterEncoding=UTF-8是数据库连接层面的中文支持,少了它,存入的中文大概率变问号;useSSL=false是关闭 MySQL 8 的 SSL 告警,本地开发没必要开;serverTimezone=Asia/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" method="post"> <input type="hidden" name="token" value="${sessionScope.appointToken}"> <select name="branchId"> <c:forEach items="${branchList}" var="branch"> <option value="${branch.id}">${branch.branchName}(${branch.address})</option> </c:forEach> </select> <input type="date" name="appointDate" required> <select name="timeSlot"> <option value="09:00-10:00">09:00-10:00</option> <option value="10:00-11:00">10:00-11:00</option> <option value="14:00-15:00">14:00-15:00</option> </select> <select name="businessType"> <option value="开户">开户</option> <option value="挂失">挂失</option> <option value="转账">转账</option> <option value="理财咨询">理财咨询</option> </select> <button type="submit">提交预约</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?msg=repeat"); 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?msg=fail"); }

这段代码里最关键的是最后两行的sendRedirect,而不是req.getRequestDispatcher(...).forward(...)。前者告诉浏览器“你去访问另一个地址”,浏览器的地址栏会变成新的 GET 请求;后者是服务端内部转发,地址栏还是原来的 POST 地址。如果用了 forward,用户提交成功后按 F5 刷新,浏览器会重放上一次 POST 请求,瞬间又生成一条记录。这个模式叫 PRG(Post/Redirect/Get),是 Web 开发里防表单重复提交的标配,答辩时能主动说出这个词会很加分。

3.3 个人信息展示页面:JSTL 渲染历史预约 + 图片坐标定位

预约提交成功后,用户需要一个能看到自己历史预约的页面,这就是 jsp 个人信息展示页面。它的技术难点不是遍历列表,而是怎么把状态从数字变成汉字、怎么把数据库里的标记准确展示到图片上。先看列表渲染部分:

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <div class="card"> <h3>近期预约</h3> <table> <tr><th>日期</th><th>时段</th><th>网点</th><th>业务</th><th>状态</th></tr> <c:forEach items="${appointmentList}" var="app"> <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> </div>

taglib指令引入了 JSTL 标签库,c:forEach的作用类似 Java 的增强 for 循环,c:choose/c:when是多分支判断。注意这里没有在 Java 里把状态提前拼成“待确认”,而是直接输出数字后在页面层判断,好处是后端逻辑简单、页面表意清楚。如果把状态字符串在 Java 里拼好,遇到“已取消”和“已完成”都需要改 Java 代码,页面层反而成了死逻辑。

很多做 jsp 图片如何对坐标定位的同学都在这一步踩坑。我在这套系统里设计了一个好演示的交互:用户上传证件照片后,在个人信息展示页面点一下照片,标记人脸中心位置,坐标存进隐藏域,后端保存下来用于后续裁剪。这里的坐标换算必须用图片的真实像素,而不是页面显示的像素:

<img id="idCardImg" src="${pageContext.request.contextPath}/upload/photo.jpg" alt="证件照片"> <input type="hidden" id="photoX" name="photoX"> <input type="hidden" id="photoY" name="photoY"> <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; }); </script>

offsetX拿到的是你在屏幕上看到的坐标,如果 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 文件头部用contentType="text/html;charset=UTF-8";Servlet 里在读取参数前执行req.setCharacterEncoding("UTF-8");JDBC URL 里加characterEncoding=UTF-8。还要注意 GET 请求的参数编码不走setCharacterEncoding,需要在 Tomcat 的server.xml里给 Connector 配置URIEncoding="UTF-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 页,刷新不重复提交用了 forward,F5 重放 POST
个人信息展示历史预约按时间倒序、状态显示汉字状态直接输出数字 0/1/2
后台确认待确认变为已确认,前台刷新能看到状态改了但没提交事务
取消预约状态变为已取消,保留记录直接 DELETE,统计数据缺失

我做这类系统最吃亏的一次,就是花了一晚上调页面样式,结果被老师问“同一个用户怎么防止重复预约”,只能站在台上现想。后来学乖了:先把表结构和约束讲清楚,再去折腾前端。数据库设计多花半小时,答辩能少挨三个追问。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询