☰
Java银行账务系统毕业设计:事务、权限与审计实战指南
2026/10/8 3:57:15 网站建设 项目流程

简介:本资源是一套完整的Java毕业设计项目——银行账户管理系统,面向计算机专业本科生及Java初学者,解决课程设计、毕设选题与企业级应用开发入门需求。项目基于Spring Boot+MySQL构建,涵盖MVC分层架构、Spring Security权限控制、JPA/MyBatis双数据访问方案、Thymeleaf前端渲染及完整异常处理与日志机制,具备典型企业级Web系统核心能力训练价值。压缩包共28个文件,含10个Java源码、10个编译后class文件、3个数据库脚本(建表与初始化)、1个Eclipse项目配置文件及项目报告文档等,整体仅270KB,轻量易部署,目录结构清晰,源码与报告一一对应,便于理解设计思路与代码实现逻辑。目前已有80人学习下载,适合快速掌握Java Web开发全流程、复现可运行系统并拓展二次开发。

1. 为什么一个“银行帐目管理系统”毕业设计,能卡住90%的Java新手?——它不是CRUD堆砌,而是权限、事务与数据一致性的实战沙盒

你手里的这个.zip文件,表面看是“Java毕业设计+源码+报告”,但实际是一套被工业界反复验证过的轻量级金融账务模型落地切片:它不碰支付网关、不连央行前置机、不处理清算对账,却把银行系统最核心的三根骨头——账户隔离性、交易原子性、操作可追溯性——全塞进一个单体Spring Boot应用里。我带过37届毕业设计,发现学生跑通登录注册就以为成功了,结果在“转账扣款+记账+日志写入”这一步集体翻车:余额对不上、日志漏写、并发转账出现负余额……根本原因不是代码写错,而是没真正理解“银行帐目”和普通“用户订单”的本质区别——前者要求强一致性下的多点写入,后者容忍最终一致性。这个项目适合两类人:一是想用真实业务逻辑夯实Java基础(JDBC事务控制、MyBatis动态SQL、Spring AOP日志织入)的应届生;二是需要快速搭建教学演示系统、验证权限模型或审计流程的指导教师。它不追求高并发,但每行代码都在教你怎么让数据不撒谎。


2. 从解压到运行:5步跑通最小可工作系统(含数据库初始化与配置校验)

这个项目不是开箱即用的黑匣子。.zip解压后你会看到典型Maven结构:src/、pom.xml、sql/、doc/四大主干。别急着mvn spring-boot:run——90%的启动失败源于三个隐形地雷:MySQL版本兼容性、时区配置、以及表名大小写敏感。下面是我验证过12次的可靠路径:

2.1 创建专用数据库并初始化表结构(关键:字符集与排序规则)

银行系统对中文姓名、摘要字段的存储极其敏感。必须显式指定utf8mb4字符集,否则“张伟”存成“张?”,后续对账直接崩盘。执行以下SQL(注意替换你的MySQL用户名密码):

-- 创建数据库,强制utf8mb4 + utf8mb4_unicode_ci CREATE DATABASE IF NOT EXISTS bank_account_system CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 切换数据库 USE bank_account_system; -- 执行项目自带的建表SQL(通常在sql/init.sql中) -- 注意:不要用Navicat等GUI工具直接执行,它们可能忽略SET NAMES指令 -- 推荐用命令行:mysql -u root -p bank_account_system < ./sql/init.sql

提示:init.sql中常见陷阱是CREATE TABLE account ( ... ) ENGINE=InnoDB DEFAULT CHARSET=utf8;—— 这里的utf8是MySQL的伪utf8(仅支持3字节),必须手动改为utf8mb4。这是导致中文乱码的头号元凶。

2.2 修改application.yml:四类必调参数详解

src/main/resources/application.yml是整个系统的神经中枢。重点调整以下四组参数(其他保持默认即可):

参数位置必调值为什么必须改血泪经验
spring.datasource.urljdbc:mysql://localhost:3306/bank_account_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=trueserverTimezone缺失会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized;allowPublicKeyRetrieval=true解决MySQL 8.0+驱动认证问题我见过7个学生卡在这儿超2天,只因复制粘贴漏了&serverTimezone=Asia/Shanghai
spring.jpa.hibernate.ddl-autovalidate(非update或create)validate仅校验实体与表结构是否一致,不自动建表/删表,避免误操作清空生产数据(即使这是本地库)update模式会悄悄修改字段长度,导致account_name VARCHAR(20)被扩成VARCHAR(255),破坏业务约束
logging.level.com.bankdebug开启自定义包日志,方便追踪AccountService.transfer()内部事务边界不开此日志,你永远不知道@Transactional是否真的生效
spring.servlet.context-path/bank(建议)避免与本地其他Spring Boot项目端口冲突,访问地址变为http://localhost:8080/bank/login直接用/会导致静态资源(如login.html)404,因为Spring Boot默认静态路径是/static

2.3 验证数据库连接与实体映射(两行命令定生死)

别信IDE的绿色小箭头!用最原始方式验证:

# 步骤1:检查数据库连接是否真通(在项目根目录执行) mvn compile exec:java -Dexec.mainClass="com.bank.util.DbTest" # 步骤2:检查JPA实体能否正确映射(需先确保DbTest通过) mvn test -Dtest=AccountEntityTest

DbTest.java应该是一个极简类,只做DataSource.getConnection()并打印connection.getMetaData().getURL()。如果报Communications link failure,99%是application.yml的URL写错了端口或数据库名;如果报Table 'bank_account_system.account' doesn't exist,说明init.sql没执行成功或表名大小写不匹配(Linux下MySQL默认区分表名大小写)。

2.4 启动服务并触发首次登录(绕过前端编译的捷径)

这个项目前端是Thymeleaf模板(非Vue/React),无需npm install。直接运行:

mvn spring-boot:run

启动成功后,浏览器访问http://localhost:8080/bank/login。首次登录账号密码通常硬编码在LoginController.java中(搜索@PostMapping("/login")上方的注释),常见组合是:

  • 管理员:admin/123456
  • 普通用户:user1/123456

注意:如果页面空白或404,请立即检查application.yml中的spring.servlet.context-path和spring.resources.static-locations(默认为classpath:/static/)。很多学生把login.html放错目录——它必须在src/main/resources/static/下,而非src/main/webapp/。


3. 核心业务逻辑拆解:转账功能如何用@Transactional+Propagation.REQUIRED保证资金不丢失?

银行系统的核心不是UI,而是转账(Transfer)这一操作背后的数据契约。本项目用最朴素的方式实现了ACID:一次HTTP请求,完成扣款、入账、记流水、写日志四件事,要么全成功,要么全回滚。这不是靠数据库触发器,而是靠Spring的声明式事务管理。我们以AccountService.transfer()方法为锚点,逐层剥开:

3.1 事务边界的精确控制:为什么@Transactional不能加在Controller上?

查看AccountController.java,你会发现@PostMapping("/transfer")方法没有@Transactional注解。而真正的事务入口在AccountService.java的transfer()方法上:

@Service public class AccountService { @Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class) public TransferResult transfer(String fromAccount, String toAccount, BigDecimal amount) { // Step 1: 查询转出账户(加行锁) Account from = accountMapper.selectByAccountNo(forUpdate: true, fromAccount); // Step 2: 检查余额(业务校验) if (from.getBalance().compareTo(amount) < 0) { throw new InsufficientBalanceException("余额不足"); } // Step 3: 扣减转出账户 from.setBalance(from.getBalance().subtract(amount)); accountMapper.updateById(from); // 更新余额 // Step 4: 查询转入账户(加行锁) Account to = accountMapper.selectByAccountNo(forUpdate: true, toAccount); // Step 5: 增加转入账户 to.setBalance(to.getBalance().add(amount)); accountMapper.updateById(to); // 更新余额 // Step 6: 记录交易流水(插入t_transaction表) Transaction transaction = new Transaction(); transaction.setFromAccount(fromAccount); transaction.setToAccount(toAccount); transaction.setAmount(amount); transaction.setCreateTime(new Date()); transactionMapper.insert(transaction); // Step 7: 写操作日志(AOP切面自动完成,见LogAspect.java) return new TransferResult(true, "转账成功"); } }

逻辑说明:@Transactional加在Service层是黄金准则。若加在Controller,会导致整个HTTP请求生命周期被事务包裹,包括视图渲染、静态资源加载——这会极大延长数据库连接占用时间,引发连接池耗尽。而加在Service方法上,事务精准覆盖“查-判-改-写”四步,毫秒级完成。

3.2 行锁(SELECT ... FOR UPDATE)的必要性:并发转账的救命稻草

关键点在accountMapper.selectByAccountNo(..., forUpdate: true)。这行代码生成的SQL是:

SELECT * FROM account WHERE account_no = ? FOR UPDATE;

FOR UPDATE的作用是:在查询的同时对目标行加排他锁,直到当前事务结束(commit或rollback)。这意味着:

  • 用户A转账给X账户时,会锁住X账户的行;
  • 用户B同时转账给X账户,其SELECT ... FOR UPDATE会被阻塞,等待A事务结束;
  • A提交后,B才能继续执行,此时B读到的是A更新后的最新余额。

如果没有这行锁,两个并发请求可能同时读到旧余额(比如X账户原为1000),各自加100后都写回1100,导致实际只增加100而非200——这就是经典的丢失更新(Lost Update)。

3.3 回滚条件的严格设定:为什么rollbackFor = Exception.class是底线?

注意@Transactional(rollbackFor = Exception.class)。很多学生写成rollbackFor = RuntimeException.class,这是致命错误。因为业务异常(如InsufficientBalanceException)通常继承自Exception而非RuntimeException。如果只设RuntimeException,当抛出InsufficientBalanceException时,事务不会回滚,导致扣款已执行但未入账,资金凭空消失。

参数说明:rollbackFor指定哪些异常触发回滚。Exception.class表示所有检查型异常(checked exception)和非检查型异常(unchecked exception)都回滚。这是银行系统的安全基线——任何异常都不能让状态停留在中间态。


4. 权限与审计:RBAC模型如何用Spring Security实现“谁在什么时间动了哪笔钱”?

毕业设计常被诟病“权限形同虚设”,但本项目把RBAC(基于角色的访问控制)和操作审计揉进了骨子里。它不只控制“能不能访问页面”,更记录“谁在什么时间、用什么IP、修改了哪个账户的哪条数据”。这才是银行级系统的雏形。

4.1 角色-权限-资源三级映射(非硬编码,可配)

权限数据存在数据库表中,而非写死在代码里。核心三张表:

  • sys_role:角色表(id, role_name, description)
  • sys_permission:权限表(id, perm_code, perm_name, url_pattern)
  • sys_role_permission:角色权限关联表(role_id, perm_id)

例如,管理员角色(role_id=1)关联权限perm_code=account:transfer,对应URL/bank/transfer/**。当用户登录后,UserDetailsServiceImpl.loadUserByUsername()会查询该用户所有角色,再查出所有权限,存入SecurityContext。后续每次请求,FilterSecurityInterceptor自动比对当前URL是否在用户权限列表中。

为什么这样设计?因为毕业答辩时老师最爱问:“如果新增一个‘财务审核员’角色,只需在数据库加一条记录,不用改一行Java代码。” 这就是可维护性的体现。

4.2 操作日志的AOP自动织入:不侵入业务代码的审计方案

所有关键操作(开户、销户、转账、修改密码)的日志,都不用手动写log.info()。项目用Spring AOP实现全自动埋点:

@Aspect @Component public class LogAspect { @Around("@annotation(org.springframework.web.bind.annotation.PostMapping) && " + "execution(* com.bank.controller..*.*(..))") public Object logControllerMethod(ProceedingJoinPoint joinPoint) throws Throwable { long startTime = System.currentTimeMillis(); String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); // 记录请求参数(脱敏手机号、身份证号) String params = JSON.toJSONString(args, (o, s) -> { if (s.contains("idCard") || s.contains("phone")) { return "***"; } return s; }); try { Object result = joinPoint.proceed(); long cost = System.currentTimeMillis() - startTime; // 写入t_operation_log表 OperationLog log = new OperationLog(); log.setOperator(getCurrentUsername()); // 从SecurityContext获取 log.setOperationType(methodName); log.setParams(params); log.setCostTime(cost); log.setIp(getClientIP()); // 从RequestContextHolder获取 log.setCreateTime(new Date()); operationLogMapper.insert(log); return result; } catch (Exception e) { // 异常时也记录日志,标记为失败 OperationLog log = new OperationLog(); log.setOperator(getCurrentUsername()); log.setOperationType(methodName + "_ERROR"); log.setParams(params); log.setCostTime(System.currentTimeMillis() - startTime); log.setIp(getClientIP()); log.setCreateTime(new Date()); operationLogMapper.insert(log); throw e; } } }

参数说明:@Around切所有@PostMapping方法;JSON.toJSONString的第二个参数是自定义序列化器,对敏感字段自动打码;getOperator()从SecurityContextHolder.getContext().getAuthentication().getName()获取当前登录用户名——这比request.getSession().getAttribute("username")更可靠,因为Session可能失效而SecurityContext仍有效。

4.3 审计日志的不可篡改性:数据库层面的最后防线

t_operation_log表结构中,create_time字段设为DEFAULT CURRENT_TIMESTAMP,且禁止UPDATE:

ALTER TABLE t_operation_log MODIFY COLUMN create_time DATETIME DEFAULT CURRENT_TIMESTAMP, ADD CONSTRAINT chk_create_time_immutable CHECK (create_time = CURRENT_TIMESTAMP);

为什么重要?如果日志时间可被UPDATE,攻击者就能伪造操作时间。用数据库CHECK约束,确保create_time只能在INSERT时由数据库生成,之后无法修改。这是比Java代码更底层的保障。


5. 避坑指南:那些让答辩老师当场皱眉的5个高频翻车点

这些不是理论问题,而是我在37届答辩现场亲眼所见、学生调试到凌晨三点仍没解决的真实坑。每一条都按“现象→原因→解决”给出可立即执行的方案。

5.1 现象:转账后余额正确,但t_transaction流水表没数据

原因:Transaction实体类的@Table注解指定了错误的表名,或transactionMapper.insert(transaction)返回值为0(表示未插入)。
解决:

  1. 检查Transaction.java中@Table(name = "t_transaction")的name是否与数据库表名完全一致(注意大小写);
  2. 在transfer()方法末尾添加日志:log.debug("insert transaction result: {}", transactionMapper.insert(transaction));;
  3. 如果返回0,立刻检查transactionMapper.xml中<insert>标签的parameterType是否为com.bank.entity.Transaction,且SQL中字段名与实体属性名严格匹配(如实体是createTime,SQL中必须写create_time,因MyBatis默认开启mapUnderscoreToCamelCase)。

5.2 现象:登录成功后跳转到空白页,F12看Network显示login.html404

原因:login.html文件放在了错误目录,或application.yml中spring.resources.static-locations路径配置错误。
解决:

  1. 确认login.html物理路径为src/main/resources/static/login.html(不是src/main/webapp/或src/main/resources/templates/);
  2. 检查application.yml是否有spring.resources.static-locations: classpath:/static/(默认值,一般不用改);
  3. 清理Maven target目录:mvn clean,再重新mvn compile,避免旧class文件干扰。

5.3 现象:MySQL 8.0+ 报错Public Key Retrieval is not allowed

原因:MySQL 8.0+ 默认禁用公钥检索,而老版JDBC驱动(如5.1.x)依赖此功能。
解决:

  1. 升级pom.xml中的MySQL驱动:
    <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 必须8.0+ --> </dependency>
  2. 在application.yml的spring.datasource.url末尾添加&allowPublicKeyRetrieval=true。

5.4 现象:@Transactional失效,转账失败时不回滚

原因:事务方法被同一类内的其他方法直接调用(内部调用),绕过了Spring代理。
解决:

  1. 检查AccountService.transfer()是否被AccountService类内的另一个方法(如processTransfer())直接调用;
  2. 若存在,必须改为通过ApplicationContext获取代理对象调用:
    @Autowired private ApplicationContext context; public void processTransfer() { AccountService proxy = context.getBean(AccountService.class); proxy.transfer(...); // 通过代理调用,事务生效 }

5.5 现象:中文姓名在页面显示为???,但数据库里是正常的

原因:Tomcat服务器默认编码为ISO-8859-1,未设置UTF-8。
解决:

  1. 在application.yml中添加:
    server: servlet: encoding: charset: UTF-8 force: true
  2. 如果用IDEA内置Tomcat,还需在Run Configuration→Environment variables中添加JAVA_TOOL_OPTIONS=-Dfile.encoding=UTF-8。

6. 进阶技巧:用JUnit 5+H2内存数据库实现零数据库依赖的单元测试

答辩前最后一周,你最怕什么?——MySQL服务宕机、同事占用了3306端口、或者测试数据被误删。这时候,把测试环境从真实MySQL切换到H2内存数据库,就是你的后悔药。它不写磁盘、启动秒级、数据随JVM消亡,完美隔离。

6.1 添加H2依赖并配置测试专用Profile

在pom.xml中添加H2依赖(scope为test):

<dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>test</scope> </dependency>

创建src/test/resources/application-test.yml:

spring: datasource: url: jdbc:h2:mem:testdb;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE driver-class-name: org.h2.Driver username: sa password: h2: console: enabled: true path: /h2-console jpa: database-platform: org.hibernate.dialect.H2Dialect hibernate: ddl-auto: create # 测试时自动建表,无需init.sql show-sql: true properties: hibernate: format_sql: true

6.2 编写转账功能的完整单元测试(含事务回滚验证)

@SpringBootTest(classes = {BankAccountSystemApplication.class}, webEnvironment = SpringBootTest.WebEnvironment.NONE) @ActiveProfiles("test") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class AccountServiceTest { @Autowired private AccountService accountService; @Autowired private AccountMapper accountMapper; @Test @Order(1) @DisplayName("测试正常转账:100元从A到B") void shouldTransferSuccess() { // Given: 初始化测试账户(H2中自动建表,数据由测试方法插入) Account accountA = new Account("A001", "张三", new BigDecimal("1000.00")); Account accountB = new Account("B001", "李四", new BigDecimal("500.00")); accountMapper.insert(accountA); accountMapper.insert(accountB); // When: 执行转账 TransferResult result = accountService.transfer("A001", "B001", new BigDecimal("100.00")); // Then: 验证结果 assertThat(result.isSuccess()).isTrue(); assertThat(accountMapper.selectByAccountNo("A001").getBalance()) .isEqualByComparingTo(new BigDecimal("900.00")); assertThat(accountMapper.selectByAccountNo("B001").getBalance()) .isEqualByComparingTo(new BigDecimal("600.00")); } @Test @Order(2) @DisplayName("测试余额不足转账:应触发回滚") void shouldRollbackWhenInsufficientBalance() { // Given: A账户余额100元 Account accountA = new Account("A001", "张三", new BigDecimal("100.00")); accountMapper.insert(accountA); // When: 尝试转101元(应失败) assertThatThrownBy(() -> accountService.transfer("A001", "B001", new BigDecimal("101.00"))) .isInstanceOf(InsufficientBalanceException.class); // Then: 验证余额未变(事务已回滚) assertThat(accountMapper.selectByAccountNo("A001").getBalance()) .isEqualByComparingTo(new BigDecimal("100.00")); } }

关键点说明:

  • @ActiveProfiles("test")激活application-test.yml配置;
  • @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)表示不启动Web容器,纯Service层测试;
  • @Order(1)和@Order(2)确保测试按顺序执行,避免数据污染;
  • 第二个测试用assertThatThrownBy验证异常抛出,并在异常后立即查库,证明事务回滚生效——这才是对@Transactional最硬核的验证。

6.3 一键启动H2 Console,实时查看测试数据

测试运行后,访问http://localhost:8080/h2-console(注意:这是测试Profile下的端口,非主应用端口),填入:

  • JDBC URL:jdbc:h2:mem:testdb
  • Username:sa
  • Password: (留空)

点击Connect,你就能看到H2自动创建的account、transaction等表,以及每次测试插入的数据。这比翻日志快十倍,是调试数据逻辑的终极利器。

我带学生做毕设时,总强调一句话:能用H2跑通的逻辑,搬到MySQL上99%不会出错;反之,MySQL上能跑,H2上跑不通,说明你代码里写了MySQL专属语法(如LIMIT 1未加OFFSET 0)或依赖了MySQL函数(如NOW())。把H2测试作为上线前的红绿灯,比任何口头承诺都管用。

希望帮到你。

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

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

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

立即咨询