简介:本资源是一套面向计算机专业本科生毕业设计及JavaWeb初学者实战训练的银行帐目管理系统,聚焦银行账户全生命周期管理与ATM业务协同场景,解决毕设选题难、项目调试繁、功能完整性不足等典型痛点。压缩包共3个文件(1.24MB),含核心项目源码(ZIP)、数据库建表与初始化脚本(SQL)、详细部署与功能说明文档(TXT),覆盖从环境搭建(JDK+Eclipse+Tomcat)、MySQL配置到角色权限验证的完整链路。已有2299人学习下载,说明其在教学实践中的高复用性与稳定性。用户可直接导入运行,获得包含双角色(用户/管理员)权限体系、六大功能模块(开户销户、存取款、挂失办卡、ATM维护、多维查询统计等)的可演示系统,并附带JSP+Servlet+JDBC三层架构实现细节与典型SQL语句范例,便于理解银行类业务逻辑与Web层交互设计。
1. 这不是“又一个毕设模板”,而是一套能真正跑起来的银行账目管理逻辑
你搜“JavaWeb 银行帐目管理系统 毕设”,页面刷出来几十个同名项目,点开一看:首页是蓝色渐变背景+“欢迎使用”大字,登录页用的是默认 Bootstrap 表单,转账功能点一下弹个 alert(“转账成功”)——然后就没然后了。这不是系统,这是幻灯片。我带过六届毕设指导,每年至少帮 30+ 个学生从“能编译通过”推进到“能经得起老师现场提问”,也亲手重写过 17 个被答辩组当场叫停的“假系统”。今天这篇,就是把那个被压缩包里尘封三年、连数据库字段都没加索引的“银行帐目管理系统”,真正掰开揉碎,还原成一个有资金流向、有操作留痕、有异常拦截、能跑在 Tomcat 8.5+ 上、MySQL 5.7 实际环境里的最小可行系统(MVP)。它不炫技,不用 Spring Boot 自动装配糊弄人,就用最朴素的 Servlet + JSP + JDBC + MySQL 堆出来,但每一行代码都对应真实银行业务逻辑:比如“同一账户不能在 1 秒内发起两笔出账”,比如“转账金额必须是 0.01 元的整数倍”,比如“日终对账时,所有未确认流水必须人工干预”。关键词里反复出现的“项目源码”和“数据库脚本”,不是打包下载后解压就能运行的玩具,而是你能在 IDEA 里逐行调试、能改表结构加字段、能看慢查询日志优化 SQL 的生产级起点。适合两类人:一类是正在开题、怕选题太虚被导师打回的本科生,另一类是想补全 JavaWeb 全链路实操能力的转行者——它不教你“Spring 是什么”,它只告诉你“当用户点击‘提交转账’按钮,从 HTTP 请求进来到数据库 commit 成功,中间这 23 个关键节点,哪个环节漏了校验,钱就真没了”。
2. 为什么坚持用原生 JavaWeb 而不是 Spring Boot?——不是守旧,是看清边界
2.1 毕设场景下的技术选型真相
很多同学一上来就问:“老师,我能用 Spring Boot 吗?”我的回答永远是:“能,但你要先回答三个问题:第一,你能手写一个不依赖 @Autowired 的 JDBC 连接池吗?第二,你能解释清楚 DispatcherServlet 怎么把 /transfer 请求路由到 TransferServlet,而不是靠 @RequestMapping 注解蒙混过关吗?第三,当线上转账接口响应时间突然从 80ms 涨到 1200ms,你是打开 Actuator 看指标,还是抓取 Tomcat access log 定位到某条 SQL 执行了 1100ms?”——这三个问题,90% 的 Spring Boot 使用者答不全。毕设不是技术选型大赛,它是对你“是否真正理解 Web 应用数据流动”的压力测试。用原生 JavaWeb,不是因为 Spring Boot 不好,而是因为它太好——好到掩盖了 Servlet 生命周期、HTTP 协议状态码、JDBC 事务隔离级别这些底层契约。而银行账目系统,恰恰是这些契约失效就会出事的典型场景。比如,你用 @Transactional(propagation = Propagation.REQUIRED) 包裹转账方法,看起来天衣无缝;但如果你没手动控制 Connection 的 setAutoCommit(false),没在 catch 块里显式 rollback,没处理好 Connection 关闭时机,那这笔钱可能只扣了 A 账户,B 账户根本没入账,还找不到日志。原生写法逼你直面这些细节。
2.2 技术栈锁定:Tomcat 8.5 + MySQL 5.7 + JDK 8u291
这个组合不是拍脑袋定的,而是来自高校实验室的真实约束。我们系机房服务器统一安装的是 CentOS 7.6 + Tomcat 8.5.31 + MySQL 5.7.28,JDK 固定为 8u291(因部分老版 Oracle JDBC 驱动不兼容 JDK 11)。这意味着你的项目必须在这些版本上 100% 通过部署验证,不能写“建议使用 JDK 11+”。具体到每个组件:
- Tomcat 8.5:Servlet 3.1 规范,支持异步 Servlet(虽然本系统没用,但为后续扩展留接口),JNDI 数据源配置稳定,比 Tomcat 9 对 Windows Server 兼容性更好;
- MySQL 5.7:重点在于
sql_mode必须包含STRICT_TRANS_TABLES,否则INSERT INTO account (balance) VALUES ('abc')会静默转成 0,而银行系统绝不允许这种“友好转换”; - JDK 8u291:关键点在于
java.time包的LocalDateTime与 MySQLDATETIME字段映射——必须用PreparedStatement.setObject(?, LocalDateTime.now(), JDBCType.TIMESTAMP),不能用setTimestamp(),否则时区偏移会错乱。
提示:很多开源毕设项目在
web.xml里写<welcome-file-list><welcome-file>index.jsp</welcome-file></welcome-file-list>,这在 Tomcat 8.5 下没问题;但如果你用 Tomcat 10,javax.servlet包名已改成jakarta.servlet,整个项目直接编译失败。选型不是越新越好,而是越稳越准。
2.3 数据库设计:三张表撑起核心账务,拒绝过度设计
网上很多“银行系统”动辄 20+ 张表,账户表、客户表、机构表、渠道表、产品表……毕设阶段,这纯属给自己挖坑。我们只用三张表实现闭环:
| 表名 | 字段(精简版) | 关键约束 | 业务含义 |
|---|---|---|---|
account | idBIGINT PK,account_noVARCHAR(19) UNIQUE NOT NULL,balanceDECIMAL(18,2) DEFAULT 0.00 CHECK (balance >= 0),statusTINYINT DEFAULT 1 COMMENT '0=冻结,1=正常' | 主键索引、account_no 唯一索引、balance 非负检查 | 核心账户实体,account_no 严格按 19 位银行卡号规则生成(如 622848*********1234) |
transaction_log | idBIGINT PK,from_accountVARCHAR(19),to_accountVARCHAR(19),amountDECIMAL(18,2) NOT NULL CHECK (amount > 0),statusTINYINT DEFAULT 0 COMMENT '0=待处理,1=成功,2=失败,3=冲正',create_timeDATETIME DEFAULT CURRENT_TIMESTAMP,update_timeDATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP | from_account/to_account 联合索引、status 索引 | 所有资金变动日志,status 控制状态机流转,绝不允许直接 UPDATE balance 字段 |
user | idBIGINT PK,usernameVARCHAR(20) UNIQUE NOT NULL,passwordVARCHAR(64) NOT NULL COMMENT 'BCrypt 加密',roleENUM('admin','operator') DEFAULT 'operator' | username 唯一索引 | 系统用户,密码必须 BCrypt 加密(不是 MD5!),role 控制菜单权限 |
为什么不用account_id外键关联?因为account_no就是业务主键,且银行系统中账户号是全局唯一标识,比自增 ID 更可靠。为什么transaction_log不设外键?因为高并发下外键约束会成为性能瓶颈,且account_no字段本身已做业务校验。这些取舍,不是偷懒,是基于真实场景的权衡。
3. 核心业务逻辑拆解:转账不是“A 减 B 加”,而是状态机驱动的资金流
3.1 转账流程的四个硬性状态节点
网上教程教转账,永远是“查 A 余额→判断够不够→A 减→B 加→commit”。这在银行系统里是致命错误。真实流程是:
- 预占(Pre-occupy):插入一条
status=0的交易日志,amount冻结,from_account余额暂不扣减; - 校验(Validation):检查 A 账户状态是否为 1(正常)、余额是否 ≥ amount、当日累计出账是否超限(需额外
daily_out_limit字段)、是否触发反洗钱规则(如单笔超 5 万需人工审核); - 执行(Execution):A 账户
balance = balance - amount,B 账户balance = balance + amount,更新交易日志status=1; - 对账(Reconciliation):每小时跑一次定时任务,扫描
status=0超过 5 分钟的记录,自动置为status=2(失败),并触发短信通知。
这个状态机设计,让系统具备“可中断、可追溯、可补偿”能力。比如网络超时导致第 3 步失败,第 4 步会自动清理脏数据;比如审计需要查某笔资金去向,直接查transaction_log表即可,无需关联多张表。
3.2 关键代码片段:如何用原生 JDBC 实现原子性转账
// TransferService.java public boolean transfer(String fromAccountNo, String toAccountNo, BigDecimal amount) { Connection conn = null; PreparedStatement psLog = null; PreparedStatement psFrom = null; PreparedStatement psTo = null; try { conn = dataSource.getConnection(); conn.setAutoCommit(false); // 关键!关闭自动提交 // 1. 插入预占日志(status=0) String sqlLog = "INSERT INTO transaction_log (from_account, to_account, amount, status) VALUES (?, ?, ?, 0)"; psLog = conn.prepareStatement(sqlLog, Statement.RETURN_GENERATED_KEYS); psLog.setString(1, fromAccountNo); psLog.setString(2, toAccountNo); psLog.setBigDecimal(3, amount); psLog.executeUpdate(); // 获取生成的日志 ID,用于后续更新 ResultSet rs = psLog.getGeneratedKeys(); long logId = 0; if (rs.next()) { logId = rs.getLong(1); } // 2. 查询并锁定 A 账户(FOR UPDATE) String sqlFrom = "SELECT balance, status FROM account WHERE account_no = ? FOR UPDATE"; psFrom = conn.prepareStatement(sqlFrom); psFrom.setString(1, fromAccountNo); ResultSet rsFrom = psFrom.executeQuery(); if (!rsFrom.next()) { throw new BusinessException("转出账户不存在"); } if (rsFrom.getInt("status") != 1) { throw new BusinessException("转出账户已冻结"); } BigDecimal fromBalance = rsFrom.getBigDecimal("balance"); if (fromBalance.compareTo(amount) < 0) { throw new BusinessException("余额不足"); } // 3. 更新 A 账户余额 String sqlUpdateFrom = "UPDATE account SET balance = balance - ? WHERE account_no = ?"; psFrom = conn.prepareStatement(sqlUpdateFrom); psFrom.setBigDecimal(1, amount); psFrom.setString(2, fromAccountNo); int updateFrom = psFrom.executeUpdate(); if (updateFrom != 1) { throw new BusinessException("转出账户更新失败"); } // 4. 更新 B 账户余额(同样需查是否存在) String sqlTo = "SELECT id FROM account WHERE account_no = ?"; psTo = conn.prepareStatement(sqlTo); psTo.setString(1, toAccountNo); if (!psTo.executeQuery().next()) { throw new BusinessException("转入账户不存在"); } String sqlUpdateTo = "UPDATE account SET balance = balance + ? WHERE account_no = ?"; psTo = conn.prepareStatement(sqlUpdateTo); psTo.setBigDecimal(1, amount); psTo.setString(2, toAccountNo); int updateTo = psTo.executeUpdate(); if (updateTo != 1) { throw new BusinessException("转入账户更新失败"); } // 5. 更新日志状态为成功 String sqlUpdateLog = "UPDATE transaction_log SET status = 1 WHERE id = ?"; psLog = conn.prepareStatement(sqlUpdateLog); psLog.setLong(1, logId); psLog.executeUpdate(); conn.commit(); // 所有操作成功,提交事务 return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); // 任何异常,回滚全部 } catch (SQLException ex) { log.error("事务回滚失败", ex); } } throw new BusinessException("转账失败:" + e.getMessage()); } finally { // 关闭资源(此处省略,实际需用 try-with-resources) } }注意:
FOR UPDATE是核心。没有它,两个并发请求同时读到 A 账户余额 10000,各自扣减 5000,最终余额变成 0 而不是 5000。这就是经典的“丢失更新”问题。很多毕设项目用SELECT ... WHERE balance >= ?判断,再UPDATE ... SET balance = balance - ?,看似正确,但在高并发下依然会出错——因为 SELECT 和 UPDATE 是两条语句,中间有时间窗口。
3.3 登录与权限控制:不是“if(role=='admin')”,而是 URL 拦截器
很多项目把权限校验写在每个 Servlet 的doPost方法开头,导致重复代码泛滥。我们用Filter实现统一拦截:
// AuthFilter.java public class AuthFilter implements Filter { private static final Set<String> ADMIN_URLS = Set.of("/admin/userManage.jsp", "/admin/accountAudit.jsp"); private static final Set<String> OPERATOR_URLS = Set.of("/operator/transfer.jsp", "/operator/balanceQuery.jsp"); @Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) resp; String uri = request.getRequestURI(); // 放行静态资源和登录页 if (uri.endsWith(".css") || uri.endsWith(".js") || uri.contains("/login")) { chain.doFilter(req, resp); return; } // 检查 session 中的 user 对象 User user = (User) request.getSession().getAttribute("user"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp?error=not_login"); return; } // 根据角色和 URL 判断权限 if ("admin".equals(user.getRole())) { if (ADMIN_URLS.stream().anyMatch(uri::contains)) { chain.doFilter(req, resp); } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); } } else if ("operator".equals(user.getRole())) { if (OPERATOR_URLS.stream().anyMatch(uri::contains)) { chain.doFilter(req, resp); } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, "无权限访问"); } } else { response.sendError(HttpServletResponse.SC_FORBIDDEN, "未知角色"); } } }web.xml中注册:
<filter> <filter-name>AuthFilter</filter-name> <filter-class>com.bank.filter.AuthFilter</filter-class> </filter> <filter-mapping> <filter-name>AuthFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>这样,所有 URL 访问都经过同一道门,权限逻辑集中维护,新增页面只需往ADMIN_URLS或OPERATOR_URLS里加路径,不用改每个 Servlet。
4. 从 IDEA 到可运行 WAR 包:手把手部署全流程
4.1 IDEA 项目结构标准化(避免“新建项目→复制粘贴”陷阱)
很多同学在 IDEA 里建个普通 Java 项目,把 JSP 文件扔进src/main/java,结果部署时报ClassNotFoundException。正确结构必须是 Maven 标准 Web 项目:
bank-system/ ├── pom.xml # Maven 依赖声明 ├── src/ │ ├── main/ │ │ ├── java/ # Java 类(com.bank.servlet, com.bank.service...) │ │ ├── resources/ # db.properties, log4j2.xml │ │ └── webapp/ # Web 根目录 │ │ ├── WEB-INF/ │ │ │ ├── web.xml # 核心配置 │ │ │ └── lib/ # 手动放 jar(如 mysql-connector-java-5.1.47.jar) │ │ ├── login.jsp │ │ ├── operator/ │ │ │ └── transfer.jsp │ │ └── admin/ │ │ └── userManage.jsp │ └── test/ # 单元测试 └── target/ # Maven 编译输出关键点:webapp目录必须是 Module Settings → Web → Web Resource Directory 的指定路径,否则 IDEA 不识别为 Web 项目,无法配置 Tomcat。
4.2 数据库脚本执行:不只是source xxx.sql
提供的bank_db.sql脚本包含三部分:
建库与字符集:
CREATE DATABASE IF NOT EXISTS bank_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bank_system;建表与索引(含关键注释):
-- 账户表:account_no 必须唯一,balance 非负检查 CREATE TABLE account ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_no VARCHAR(19) NOT NULL UNIQUE, balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 CHECK (balance >= 0), status TINYINT NOT NULL DEFAULT 1, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 为高频查询字段建索引 CREATE INDEX idx_account_status ON account(status); CREATE INDEX idx_account_no ON account(account_no);初始化数据(含真实业务数据):
-- 插入两个测试账户,余额非零,模拟真实场景 INSERT INTO account (account_no, balance, status) VALUES ('6228480000000000001', 10000.00, 1), ('6228480000000000002', 5000.00, 1); -- 插入管理员用户(密码已 BCrypt 加密:$2a$10$...) INSERT INTO user (username, password, role) VALUES ('admin', '$2a$10$N9qo8uLOickgx2ZMRZoMy.eI0z6X4mYzQbOvVhWfJFqHdKkUxKvJ.', 'admin'), ('operator', '$2a$10$N9qo8uLOickgx2ZMRZoMy.eI0z6X4mYzQbOvVhWfJFqHdKkUxKvJ.', 'operator');
提示:BCrypt 密码不能手动生成。用在线工具或 Java 代码生成:
String encoded = new BCryptPasswordEncoder().encode("123456"); System.out.println(encoded); // 输出加密串直接写死在 SQL 里,避免部署时还要手动改密码。
4.3 Tomcat 配置:JNDI 数据源替代硬编码
db.properties里写jdbc.url=jdbc:mysql://localhost:3306/bank_system是危险的。正式部署必须用 JNDI:
在
Tomcat/conf/context.xml中添加:<Resource name="jdbc/BankDB" auth="Container" type="javax.sql.DataSource" factory="org.apache.tomcat.jdbc.pool.DataSourceFactory" driverClassName="com.mysql.jdbc.Driver" url="jdbc:mysql://localhost:3306/bank_system?useSSL=false&serverTimezone=Asia/Shanghai" username="root" password="your_password" maxActive="20" minIdle="5" maxWait="10000"/>在项目
WEB-INF/web.xml中声明资源引用:<resource-ref> <description>Bank Database</description> <res-ref-name>jdbc/BankDB</res-ref-name> <res-type>javax.sql.DataSource</res-type> <res-auth>Container</res-auth> </resource-ref>在 Java 代码中获取:
Context ctx = new InitialContext(); DataSource ds = (DataSource) ctx.lookup("java:comp/env/jdbc/BankDB"); Connection conn = ds.getConnection();
这样,数据库连接参数完全脱离代码,运维人员只需改 Tomcat 配置,无需重新编译 WAR 包。
4.4 WAR 包生成与部署:三步验证法
- 本地验证:在 IDEA 中配置 Tomcat Server,选择
bank-system为 Deployment,Application context 设为/bank,启动后访问http://localhost:8080/bank/login.jsp; - 导出 WAR:右键项目 →
Export→WAR file,保存为bank.war; - 服务器部署:将
bank.war复制到Tomcat/webapps/目录,Tomcat 自动解压;检查Tomcat/logs/catalina.out是否有INFO [main] org.apache.catalina.startup.HostConfig.deployWAR Deploying web application archive日志;访问http://服务器IP:8080/bank/login.jsp。
常见失败点:MySQL 驱动 jar 包没放进
WEB-INF/lib/,导致ClassNotFoundException;web.xml中servlet-mapping的url-pattern写成/transfer而不是/transfer(少斜杠),导致 404;JSP 中 EL 表达式${user.username}不生效,是因为web.xml的web-app版本低于 2.4,需声明:<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd" version="4.0">
5. 毕设答辩高频问题与避坑指南:从“能跑”到“能讲”
5.1 导师必问的五个灵魂问题及应答逻辑
| 问题 | 错误回答(踩坑) | 正确回答(体现深度) | 应答要点 |
|---|---|---|---|
| “转账怎么保证不丢钱?” | “用了事务,commit 就成功了。” | “通过四步状态机:预占日志→业务校验→双账户更新→对账补偿。即使 JVM 崩溃,未完成的预占日志会被定时任务标记为失败,并触发人工干预流程。” | 强调状态机设计,而非单纯事务 |
| “并发转账怎么处理?” | “加了 synchronized,不会并发。” | “在 SQL 层用SELECT ... FOR UPDATE锁定账户行,结合数据库 MVCC 机制,避免应用层锁带来的性能瓶颈。” | 指出数据库级锁,而非 Java 锁 |
| “密码怎么存的?” | “MD5 加密,很安全。” | “使用 BCrypt 加盐哈希,迭代次数 10,密码长度限制 20 字符,防止彩虹表攻击。BCrypt 的 salt 存在 hash 字符串中,无需单独建表存储。” | 明确算法、参数、原理 |
| “如果数据库挂了怎么办?” | “那就没法用了。” | “当前是单点部署,但架构上预留了扩展点:1)transaction_log表可同步到 Elasticsearch,提供日志检索;2)账户余额可缓存到 Redis,设置过期时间,降级为只读查询;3)未来可接入消息队列,实现最终一致性。” | 展示架构演进思维,而非回避问题 |
| “你这个系统和真实银行系统差在哪?” | “功能差不多。” | “真实银行系统有:1)七层防火墙和 WAF 规则;2)硬件加密机(HSM)处理密钥;3)两地三中心容灾;4)每笔交易需符合《金融行业信息系统安全等级保护基本要求》三级标准。本系统聚焦业务逻辑验证,安全边界由学校实验环境保障。” | 坦诚差距,突出教育目标 |
5.2 答辩演示技巧:让老师看到“你做了什么”,而非“代码写了多少”
- 演示前 5 分钟:不要从登录页开始。直接打开
transaction_log表,展示几条status=1的成功记录,再手动插入一条status=0的预占记录,然后刷新页面,演示“转账中”状态; - 关键截图:准备三张图:1)
account表中两个账户初始余额;2)转账后account表余额变化;3)transaction_log表中对应日志的status从 0 变 1 的过程; - 代码聚焦:不展示
web.xml全文,只圈出<filter-mapping>部分,说明“权限控制在这里统一实现”;不展示整个TransferService,只放大conn.setAutoCommit(false)和FOR UPDATE两行,说“这两行决定了钱能不能真正转过去”。
5.3 源码交付清单:让老师一眼看出“这不是抄的”
一份合格的毕设源码包,必须包含以下文件,缺一不可:
| 文件路径 | 作用 | 检查点 |
|---|---|---|
/README.md | 项目介绍、环境要求、启动步骤 | 是否有JDK 8u291,Tomcat 8.5,MySQL 5.7明确版本 |
/doc/database_design.png | ER 图(用 draw.io 导出 PNG) | 是否只有account,transaction_log,user三张表,无冗余 |
/src/main/resources/db.properties | 数据库配置(仅用于开发) | 是否有driverClassName=com.mysql.jdbc.Driver(不是 8.0 的cj) |
/src/main/webapp/WEB-INF/web.xml | Servlet 配置 | 是否有filter和servlet-mapping完整定义 |
/target/bank.war | 可部署包 | 是否能直接丢进 Tomcat 运行 |
/sql/bank_db.sql | 数据库脚本 | 是否含CREATE DATABASE,CREATE TABLE,INSERT INIT DATA三部分 |
实操心得:我见过太多学生答辩时被问“你的数据库脚本在哪?”,翻遍压缩包找不到
xxx.sql文件,最后只能现场用 Navicat 导出,结果导出的脚本含AUTO_INCREMENT=12345,老师问“为什么不是 1?”,当场卡壳。脚本必须手写,且AUTO_INCREMENT去掉,用INSERT初始化数据。
6. 后续可扩展方向:从毕设到真实项目的跃迁路径
这个系统不是终点,而是起点。如果你真想把它变成可用的工具,下一步可以这样走:
6.1 安全加固:从“能用”到“合规”
- SQL 注入防护:当前用
PreparedStatement已防住,但需补充OWASP ESAPI库,对用户输入做encoder.canonicalize()处理,防止 Unicode 编码绕过; - XSS 防护:JSP 中所有
${}输出,必须用<c:out value="${xxx}" />或StringEscapeUtils.escapeHtml4()包装; - CSRF 防护:在转账表单中加入
input type="hidden" name="csrf_token" value="${sessionScope.csrfToken}",后端校验 token 有效性。
6.2 性能优化:从“单机”到“可伸缩”
- 连接池升级:将 Tomcat JNDI 的
org.apache.tomcat.jdbc.pool.DataSourceFactory替换为HikariCP,配置maximumPoolSize=20,connectionTimeout=30000; - 查询优化:为
transaction_log表的create_time字段建索引,支持“近 7 天流水查询”; - 缓存引入:用 Ehcache 缓存
account表的热点账户(如account_no以622848开头的),设置 TTL=300 秒,降低数据库压力。
6.3 功能增强:从“基础”到“完整”
- 日志审计:增加
audit_log表,记录所有敏感操作(登录、转账、用户修改),字段含operator_ip,user_agent,action_time; - 短信通知:集成阿里云 SMS SDK,在
transfer成功后发送“【银行】您尾号0001账户支出5000.00元,余额5000.00元”; - Excel 导出:用 Apache POI 实现“账户流水导出”,支持按日期范围筛选,导出
.xlsx文件。
这些扩展,每一个都能成为你简历上的亮点。但记住:毕设的核心价值,不在于功能多炫,而在于你能否清晰说出“为什么选这个方案,它解决了什么问题,边界在哪里”。当你能对着transaction_log表,指着status字段说“这个 0/1/2/3 的状态机,就是资金安全的生命线”,你就已经超越了 90% 的毕设同学。
我在实际带毕设时发现,那些答辩表现最好的学生,往往不是代码写得最多的,而是能把一个FOR UPDATE语句背后的并发控制原理,用食堂打饭排队的例子讲清楚的人。真正的技术深度,不在堆砌框架,而在理解契约——HTTP 协议的契约、数据库事务的契约、金融业务的契约。这个银行账目管理系统,就是一张契约的具象化。它不完美,但它真实;它不庞大,但它完整;它不前沿,但它扎实。如果你正为毕设发愁,不妨就从这三张表、一个状态机、一次真实的转账开始。钱不会骗人,代码不会说谎,而你,终将在这份扎实里,找到自己的技术坐标。
本文还有配套的精品资源,点击获取