资讯详情

资讯详情

JSP+Servlet+SQL Server银行预约系统拆解:从JDBC到二维码与部署排错

简介这套基于 JSP 的银行预约管理系统毕业设计资源适合计算机相关专业毕业生或正在开发同类课题的开发者参考。项目采用 Java、JSP、SQLServer、Tomcat 等主流技术完成涵盖预约登记、排队叫号、后台管理等典型业务并经过权限与漏洞测试是一套完整的课设/毕设解决方案。压缩包为 ZIP 格式共 499 个文件约 9.07MB。其中 JSP 页面 92 个构成核心业务界面210 个 GIF 与 35 个 PNG/JPG 用于界面元素和操作展示JS 与 CSS 共 74 个支撑前端交互及页面样式另有 16 个 JAR 依赖包、Java 源码与 Class 文件、SQL 和 DB 数据库脚本以及 DOCX 配套报告等项目结构清晰便于按模块查阅。目前已有 179 人学习。下载后即可获得完整项目代码、数据库脚本及配套报告文档既能直接运行查看效果也可对照源码理解预约流程的实现细节还可基于现有模块扩展新的业务功能对完成毕业设计或积累项目经验都很有帮助。1. 为什么一个“过时”的JSP项目比Spring Boot更值得拆银行预约管理系统JSPServletSQL ServerTomcat的组合在今天的主流互联网公司已经不多见但这类项目在中小型信息系统和毕业设计中仍然大量存在。它没有微服务、没有前后端分离却把传统Java Web的关键机制全部暴露出来JSP如何被编译成Servlet、DAO层如何封装JDBC、控制器如何完成页面流转、二维码预约凭证怎么生成——这些恰恰是很多五年以上经验的人在复盘老项目时也未必能立刻说得清的部分。下面从这套可运行的完整项目出发把分层结构、预约流程、分页与二维码、部署排错按照实战路线拆开。源码包里带设计报告和数据库脚本class文件清单几乎就是系统的目录结构逐个模块对照着理解能极大节省阅读和调试时间。2. 从CommDAO到MainCtrl传统JSP项目的分层与流程控制2.1 class文件清单里藏着整套系统的骨架拿到项目源码包之后先不要急着启动Tomcat把class文件列表当作阅读地图CommDAO是数据访问层MainCtrl是统一控制器PageManager负责分页Upload处理文件上传StrUtil做字符串清洗SetChar统一字符编码QRCodeUtil和QRCode处理预约二维码。每个类几乎都能在Spring体系里找到对应物MainCtrl对应DispatcherServletCommDAO对应MyBatis的MapperPageManager对应PageHelper。这套传统三层结构放在今天依然是理解Java Web最直观的教材而且不需要依赖任何框架就能跑通非常适合在答辩时从底层原理讲起。常见做法是先跑通再改但更推荐先读CommDAO。因为CommDAO决定了全系统的查询风格如果它是返回ListMap的通用型DAO那么所有JSP页面都依赖列名取值如果返回的是Entity对象那么每个表都要配一个JavaBean。从项目正文给出的class文件看CommDAO这种命名方式更偏向前者——用一个通用查询方法覆盖全部业务表配合JSP里的EL表达式用列名拿值。你拿到源码后可以搜一下JSP里是否出现${row.userName}这种写法有就说明确实是Map风格。2.2 CommDAO通用CRUD的设计与隐患CommDAO的核心通常长这样用JDBC裸写连接和查询所有业务表共用一套方法public class CommDAO { private Connection getConn() throws Exception { Class.forName(com.microsoft.sqlserver.jdbc.SQLServerDriver); String url jdbc:sqlserver://localhost:1433;DatabaseNamebank_reserve; return DriverManager.getConnection(url, sa, 123456); } public ListMapString, Object query(String sql, Object... params) { ListMapString, Object list new ArrayList(); try (Connection conn getConn(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } ResultSet rs ps.executeQuery(); ResultSetMetaData md rs.getMetaData(); while (rs.next()) { MapString, Object row new HashMap(); for (int i 1; i md.getColumnCount(); i) { row.put(md.getColumnLabel(i), rs.getObject(i)); } list.add(row); } } catch (Exception e) { e.printStackTrace(); } return list; } public int update(String sql, Object... params) { // 同样的连接获取方式执行executeUpdate() } }这段代码有两个关键细节第一用了PreparedStatement做参数化查询这是全系统防SQL注入的第一道闸后面还会展开第二把ResultSet转成ListMap之后业务层和JSP页面都不需要依赖实体类写起来快但代价是类型不安全、字段名拼错要运行时才发现。如果这套系统上线后要长期维护我一般会先给核心表补Entity再让CommDAO保留给次要查询用。连接管理方面每次调用query或update都新建Connection在课堂演示和低并发答辩场景下问题不大。但压测时连接会反复创建与销毁表现就是页面越来越慢最终抛“Connection refused”。改造方案很直接引入DBCP或c3p0连接池把getConn方法替换成从DataSource获取对比项每次新建Connection连接池复用并发支持低100并发就可能打满可配置稳定支撑几百并发代码改动量无需改造替换getConn实现适合阶段功能演示答辩压测场景2.3 MainCtrl单控制器如何完成页面流转MainCtrl是典型的ActionServlet思想一个Servlet通过action参数分发。下面是一段贴近原项目写法的伪代码描述了登录和预约两个分支public class MainCtrl extends HttpServlet { private CommDAO dao new CommDAO(); protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String action request.getParameter(action); if (login.equals(action)) { String username request.getParameter(username); String password request.getParameter(password); ListMapString, Object users dao.query( SELECT * FROM users WHERE login_name? AND password? AND status1, username, password); if (users ! null !users.isEmpty()) { request.getSession().setAttribute(loginUser, users.get(0)); response.sendRedirect(index.jsp); } else { request.setAttribute(msg, 用户名或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); } } else if (appoint.equals(action)) { String userId String.valueOf(request.getSession().getAttribute(userId)); String slotId request.getParameter(slotId); // 预约逻辑详见第3章 request.getRequestDispatcher(appoint_success.jsp).forward(request, response); } } }注意sendRedirect和forward的区别是拆这类项目时最容易踩的坑sendRedirect让浏览器重新发一次请求request里的属性全丢只有session里的数据能跨请求保留适合登录成功后的跳转forward是服务器内部转发request属性可以带到下一个JSP适合把预约结果数据带过去展示。如果发现有页面跳转后取不到参数优先检查这两个方法是否用反了。2.4 SetChar与StrUtil老项目的中文三连SetChar这个工具类解决的是请求和响应的字符集问题这是JSP时代中文乱码的第一个入口public class SetChar { public static void setEncoding(HttpServletRequest request, HttpServletResponse response) throws UnsupportedEncodingException { request.setCharacterEncoding(UTF-8); response.setContentType(text/html;charsetUTF-8); } }这里只解决传输层没解决存储层。SQL Server 2012以上建议把数据库排序规则设为Chinese_PRC_CI_AS建表时不要另起排序规则否则中文写入正常、查询条件里的中文却匹配不上。实际排错顺序应该是先确认JSP页面顶部pageEncodingUTF-8再检查Tomcat的conf/server.xml里Connector是否配置了URIEncodingUTF-8——get方式提交的中文参数如果在这里丢了解析后面所有页面级的编码设置都救不回来。StrUtil则是反向工具负责把外部输入中的单引号、百分号、尖括号清洗掉。它承担了一部分安全职责但能力有限遇到恶意构造的参数还是会失效安全加固方案放在第5章统一讲。3. 预约核心表结构设计、状态机与防并发3.1 预约表怎么建时段、容量与唯一约束银行预约系统的业务核心围绕四张表用户表、服务项目表、时段表、预约单表。时段表是最能体现设计功底的部分最简单版本是这样CREATE TABLE time_slot ( id INT IDENTITY(1,1) PRIMARY KEY, slot_date DATE NOT NULL, start_time VARCHAR(5) NOT NULL, end_time VARCHAR(5) NOT NULL, total_capacity INT DEFAULT 10, booked_count INT DEFAULT 0, CONSTRAINT uk_slot UNIQUE (slot_date, start_time) );total_capacity代表该时段可受理客户数booked_count记录已约数量。每次预约提交时通过事务把booked_count加1同时判断加之前是否已达上限。这里要注意两个约束设计把slot_date和start_time做联合唯一约束防止后台重复插入同一时段booked_count不能直接使用“统计预约单数量”代替因为还可能出现后台人工放号、临时停办这类场景保留计数器字段的扩展性更好。很多初学者在这里会写成“查出booked_countJava里if判断再更新”但那是并发下最容易出问题的方式。预约单表appointment需要单独一个status字段来维护生命周期建议这样设计状态值含义对应操作0已提交用户提交预约占住容量1已确认后台或自动确认可核销2已完成到店办理完毕3已取消用户取消释放容量4已过期超时未到店释放容量3.2 状态机流转与容量释放的时序状态流转的核心逻辑是提交→确认→完成提交后超过预约时间未到店由定时任务批量置为过期取消只能发生在确认之前且取消时要把booked_count减回去并释放容量。这里有个容易被忽视的细节释放容量和取消预约必须放在同一个数据库事务里先执行UPDATE appointment SET status3 WHERE id?再执行UPDATE time_slot SET booked_countbooked_count-1 WHERE id?。顺序反了或者漏掉第二个语句就会出现预约单已取消但时段容量没恢复的情况客户明明取消了却约不进去后台还显示已满排查起来非常隐蔽。这套状态机在源码里对应MainCtrl的appoint、cancel、confirm三个action分支。状态初始值是0还是1取决于系统是否有人工审核环节。课堂项目里建议直接进入已确认状态否则演示时要等管理员在后台点“确认”体验很割裂。我一般会在代码里留一个“自动确认开关”从配置项读取autoConfirmtrue就跳过人工审核答辩时能省出几分钟的操作时间也不会破坏状态机的完整性。3.3 并发抢同一时段为什么count加if不可靠最经典的错误写法是先SELECT booked_count FROM time_slot WHERE id?在Java里判断是否小于total_capacity再执行UPDATE booked_countbooked_count1。两个请求同时读到booked_count9都判断“可以预约”都执行加1结果预约单多了两条容量变成11。这在银行预约场景里是事故级的缺陷。正确的做法是在SQL Server里用事务加锁提示把临界区控制住。下面这段存储过程的写法值得直接复制到项目里BEGIN TRAN; DECLARE bc INT; SELECT bc booked_count FROM time_slot WITH (UPDLOCK, ROWLOCK) WHERE id ?; IF bc 10 -- 10 total_capacity BEGIN UPDATE time_slot SET booked_count bc 1 WHERE id ?; INSERT INTO appointment(user_id, slot_id, status, reserve_time) VALUES (?, ?, 0, GETDATE()); COMMIT; -- 返回成功和预约单ID END ELSE BEGIN ROLLBACK; -- 返回“该时段已约满” ENDUPDLOCK让SELECT在事务内直接获得更新锁阻止另一个事务同时读取同一行ROWLOCK把锁粒度降到行级避免锁住整个time_slot表拖慢其他时段。如果项目代码里没有这种事务封装你需要在CommDAO之上补一层把Connection挂到ThreadLocal或者干脆在Service方法里用同一个连接串起多个操作保证commit和rollback的范围一致。提示如果不想动事务代码还有个折中方案用原子更新条件UPDATE time_slot SET booked_countbooked_count1 WHERE id? AND booked_count10然后判断受影响行数是1说明占位成功。这个写法在串行执行时没问题但并发下仍可能产生间隙所以在银行预约这类业务里还是推荐显式加锁。4. PageManager分页、QRCodeUtil二维码与文件上传4.1 PageManagerSQL Server 2008/2012的正确分页姿势PageManager在JSP时代解决的问题是数据展示页的“上一页/下一页”状态保持。它的内部逻辑一般是先执行COUNT(*)拿到总记录数再按页号取当前页数据。数据库是SQL Server时有两个分支写法——2012以上可以用OFFSET/FETCH2008及以前只能用ROW_NUMBER()。兼容写法如下WITH Ordered AS ( SELECT *, ROW_NUMBER() OVER (ORDER BY id DESC) AS rn FROM appointment ) SELECT * FROM Ordered WHERE rn BETWEEN ? AND ?;第一个参数是(pageNo-1)pageSize1第二个参数是pageNopageSize。PageManager返回的对象里持有pageNo、pageSize、totalCount、totalPage、list五个属性JSP页面用JSTL的c:forEach渲染list再用c:if判断是否渲染“上一页”和“下一页”按钮。这里最容易翻车的是排序字段的选择表里如果只有create_time且同一秒内有大量预约记录翻页时同一行会出现在前后两页里因为排序不稳定。推荐始终用id DESC这类唯一字段排序这是一个面试高频考点也是实际调分页bug最常见的根因。4.2 QRCodeUtil预约成功页的二维码怎么生成银行预约系统里客户预约成功后页面会生成一个二维码作为到店凭证柜员扫描后核销。QRCodeUtil这个类做的就是这件事最常见的是用zxing库生成public static BufferedImage createQr(String content, int width, int height) throws Exception { MapEncodeHintType, Object hints new HashMap(); hints.put(EncodeHintType.CHARACTER_SET, UTF-8); hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M); hints.put(EncodeHintType.MARGIN, 1); BitMatrix matrix new MultiFormatWriter() .encode(content, BarcodeFormat.QR_CODE, width, height, hints); BufferedImage image new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); for (int x 0; x width; x) { for (int y 0; y height; y) { image.setRGB(x, y, matrix.get(x, y) ? 0x000000 : 0xFFFFFF); } } return image; }生成后的图片有两种输出方式一种是直接写入response输出流ContentType设为image/png这种适合动态生成接口另一种更实用转成Base64嵌在JSP页面的img标签里避免Tomcat对动态路径的映射问题。二维码内容不要直接放手机号或用户ID建议用预约单号加固定盐做一次签名例如MD5(id BankReserve2024)把签名作为二维码内容服务端核销时再验算一次。否则别人遍历单号就能伪造预约凭证。注意zxing版本差异。老项目里面常用的包路径是com.google.zxing.BarcodeFormat从2.x到3.x变化不大如果源码里还依赖了QRCode.class这样的自绘类说明作者可能嫌zxing太重自己用Graphics2D画了矩阵。改造时优先保留zxing因为自绘方案在容错级别和畸变纠正上远远不如zxing成熟。zxing版本BarcodeFormat包路径常见编译问题2.xcom.google.zxing.BarcodeFormat与较新JDK兼容一般3.xcom.google.zxing.BarcodeFormatJava 8以上推荐4.x模块化后部分类变更需要额外的jars4.3 Upload身份证材料上传的实现要点银行预约在某些业务分支上需要上传身份证照片或证明文件Upload.class承担了这部分功能90%用的是Apache Commons-FileUpload。核心处理流程稳定下面是一段贴近实操的写法DiskFileItemFactory factory new DiskFileItemFactory(); ServletFileUpload upload new ServletFileUpload(factory); ListFileItem items upload.parseRequest(request); for (FileItem item : items) { if (!item.isFormField()) { String ext FilenameUtils.getExtension(item.getName()); String newName System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) . ext; File dir new File(request.getServletContext().getRealPath(/upload)); if (!dir.exists()) dir.mkdirs(); item.write(new File(dir, newName)); } }isFormField判断是普通表单字段还是文件getExtension拿到原文件后缀然后用时间戳加UUID片段重命名避免中文文件名和路径穿越问题getRealPath拿到的是项目部署的绝对路径文件会落在webapps下面的upload目录。这个方案有个隐患Tomcat重新部署或清理work目录时upload下的用户文件可能一起被清掉。演示场景问题不大如果希望文件持久化要在配置里把上传目录指到应用外的磁盘路径例如/var/bank_upload。4.4 个人信息展示页与JSONArray的数据对接“我的预约”页面是这个系统信息展示的核心页面需求往往是按日期时段分组展示预约记录同时支持取消操作。JSP时代的做法是后端用JSONArray把时段数据序列化后直接传给页面JSJSONArray arr new JSONArray(); for (MapString, Object m : list) { JSONObject obj new JSONObject(); obj.put(slotId, m.get(id)); obj.put(date, m.get(slot_date).toString()); obj.put(time, m.get(start_time) - m.get(end_time)); obj.put(status, m.get(status)); arr.add(obj); } request.setAttribute(slots, arr);页面上用scriptvar slots ${slots};/script把数据交给前端JS渲染或者通过JSTL循环直接生成表格。这里最常见的编译坑是Import了错误的JSONArraynet.sf.json.JSONArray和com.alibaba.fastjson.JSONArray包名不同方法签名也不同一旦import写错JSP编译阶段就报错错误信息指向的却是“找不到符号”而不是实际的包冲突排查起来很绕。解决办法是统一项目里JSON库的依赖不要net.sf.json、fastjson、jackson混着用。5. Tomcat部署、JSP编译路径与最后一公里排查5.1 JSP编译后的Java类到底存在哪里JSP的本质是运行时由Jasper引擎编译成Servlet编译产物在Tomcat的work目录下路径规律如下${CATALINA_HOME}/work/Catalina/localhost/项目名/org/apache/jsp/ # 例如 /usr/local/tomcat/work/Catalina/localhost/bank/org/apache/jsp/在这个目录下能看到index_jsp.java和index_jsp.class这类文件JSP文件名中的点和特殊字符会被转义比如index.jsp对应index_jspuser_list.jsp对应user_005flist_jsp。页面报500但Tomcat日志里没有完整堆栈时直接打开对应的_jsp.java文件看_jspService方法里哪一行抛了异常效率远高于反复刷新浏览器。如果想固定编译产物路径用于排查可以在Tomcat的context配置里加scratchdir属性指定一个临时目录生产环境不建议这样做但排查JSP编译问题非常有用。5.2 屏蔽JSP离开页面提示的标准写法预约填写页有一个常见交互用户填了一半点其他链接时浏览器弹出“确定要离开吗”。很多人用onbeforeunload实现但提交成功跳转时也被拦截体验很差。标准做法是用标志位控制var needConfirm true; window.onbeforeunload function (e) { if (needConfirm) { e.preventDefault(); e.returnValue 预约信息尚未提交确定离开吗; } }; document.getElementById(submitBtn).onclick function () { needConfirm false; };关键是提交按钮点击时把标志位置为false这样表单submit不会被拦截未提交时的关闭、刷新、跳转仍会触发提示。注意onbeforeunload是页面级事件如果页面里有多处跳转入口每个入口都要维护好这个标志位否则会出现“明明提交了还弹窗”的误报。5.3 编码、安全与容量回收的检查清单把整个系统的中文链路列成一张表逐一排查能覆盖90%的乱码问题层级配置位置正确值JSP页面pageEncoding属性UTF-8服务器响应contentType属性text/html; charsetUTF-8GET请求Tomcat Connector的URIEncodingUTF-8POST请求SetChar.filter的setCharacterEncodingUTF-8数据库SQL Server排序规则Chinese_PRC_CI_AS安全方面重点检查两点第一CommDAO内部是否所有SQL都走了PreparedStatement如果发现SELECT * FROM appointment WHERE id id这种字符串拼接说明存在SQL注入漏洞要统一改成参数化查询第二输出到页面里的用户内容要做HTML转义比如客户填写的备注里包含script标签不转义就会在别人浏览器里执行这是典型的XSS漏洞可以在StrUtil里补一个escapeHtml方法处理尖括号和引号。最后给一个容量回收的小技巧在SQL Server里为appointment表建一个定时任务每天早上8点执行一条UPDATE把状态为0或1且预约时间早于当前的记录批量置为4过期同时把time_slot的booked_count减回去。这样整个系统的状态机能自动运转演示时不需要手动清理脏数据报告里还能写“通过SQL Server代理作业实现了容量自动回收”项目深度和完整性都上一个台阶。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →