简介:本资源是一套完整的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.url | jdbc:mysql://localhost:3306/bank_account_system?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true | serverTimezone缺失会导致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-auto | validate(非update或create) | validate仅校验实体与表结构是否一致,不自动建表/删表,避免误操作清空生产数据(即使这是本地库) | update模式会悄悄修改字段长度,导致account_name VARCHAR(20)被扩成VARCHAR(255),破坏业务约束 |
logging.level.com.bank | debug | 开启自定义包日志,方便追踪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=AccountEntityTestDbTest.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(表示未插入)。
解决:
- 检查
Transaction.java中@Table(name = "t_transaction")的name是否与数据库表名完全一致(注意大小写); - 在
transfer()方法末尾添加日志:log.debug("insert transaction result: {}", transactionMapper.insert(transaction));; - 如果返回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路径配置错误。
解决:
- 确认
login.html物理路径为src/main/resources/static/login.html(不是src/main/webapp/或src/main/resources/templates/); - 检查
application.yml是否有spring.resources.static-locations: classpath:/static/(默认值,一般不用改); - 清理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)依赖此功能。
解决:
- 升级
pom.xml中的MySQL驱动:<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <!-- 必须8.0+ --> </dependency> - 在
application.yml的spring.datasource.url末尾添加&allowPublicKeyRetrieval=true。
5.4 现象:@Transactional失效,转账失败时不回滚
原因:事务方法被同一类内的其他方法直接调用(内部调用),绕过了Spring代理。
解决:
- 检查
AccountService.transfer()是否被AccountService类内的另一个方法(如processTransfer())直接调用; - 若存在,必须改为通过
ApplicationContext获取代理对象调用:@Autowired private ApplicationContext context; public void processTransfer() { AccountService proxy = context.getBean(AccountService.class); proxy.transfer(...); // 通过代理调用,事务生效 }
5.5 现象:中文姓名在页面显示为???,但数据库里是正常的
原因:Tomcat服务器默认编码为ISO-8859-1,未设置UTF-8。
解决:
- 在
application.yml中添加:server: servlet: encoding: charset: UTF-8 force: true - 如果用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: true6.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测试作为上线前的红绿灯,比任何口头承诺都管用。
希望帮到你。
本文还有配套的精品资源,点击获取