简介这是一套基于Java与MySQL 8的银行核心业务管理系统实战项目面向Java初学者及数据库应用开发者聚焦银行账户管理、资金操作与安全控制等典型场景助力理解企业级事务处理与前后端协同开发逻辑。资源包共38个文件含13个Java源码文件实现开户、转账、密码修改等核心业务逻辑、13个编译后class文件、4个关键jar包含commons-dbcp-1.4、mysql-connector-java-8.0.11等支撑数据库连接池与驱动、4个XML配置文件定义数据源与Spring相关配置及2个properties文件如dbcp.properties整体压缩包仅2.59MB轻量易部署。已有2006人学习下载项目结构清晰包含完整src源码目录、lib依赖库、IDEA工程配置.iml/.idea及git忽略规则开箱即用特别适合用于课程设计、毕业实训或JavaMySQL综合能力训练可深入掌握事务一致性保障、密码加密生成、双账户余额同步更新等关键实现细节。1. 银行管理系统Java MySQL 8不是练手Demo而是能跑通存取款、账户冻结、日志审计的真实业务闭环你在网上搜“银行管理系统 Java”十有八九点开的是学生课设——登录页带红色「Bank System」标题用户表里只有 id、name、balance 三列转账功能不校验余额、不加事务、不记流水连并发两笔转出都可能把账户刷成负数。这不是银行系统这是高危教学玩具。真正的银行级管理后台哪怕只是模拟最小可行场景也必须踩过三道坎MySQL 8 的严格模式与窗口函数支持、Java 层的事务传播控制与敏感操作留痕、以及业务逻辑里绕不开的「资金不可逆」铁律。本文讲的就是用 Java 17LTS、Spring Boot 3.x、MyBatis-Plus 3.5 和 MySQL 8.0.33 搭建一个能真实模拟开户、实时扣款、冻结解冻、操作日志可追溯、且所有资金变动自带幂等校验和回滚能力的轻量级银行管理系统。它不追求大而全但每一步都按金融级数据一致性要求落地——适合想从 CRUD 跳到真实业务逻辑的 Java 开发者也适合作为面试中「如何设计一个带事务边界的银行模块」的完整技术脚手架。2. 用 Spring Boot MyBatis-Plus 快速搭建核心骨架从零初始化项目结构与依赖对齐2.1 创建工程并锁定关键版本组合避坑第一关银行类系统对底层稳定性极度敏感版本错配是高频翻车点。不要用 Spring Initializr 默认选最新版——Spring Boot 3.x 要求 Java 17而 MyBatis-Plus 3.5.x 是目前与 MySQL 8 兼容最稳的版本4.x 对JSON类型支持尚不完善。以下是经实测通过的pom.xml核心依赖片段删减了 test 和 build 插件部分仅保留运行时必需properties java.version17/java.version spring-boot.version3.2.5/spring-boot.version mybatis-plus.version3.5.5/mybatis-plus.version mysql-connector-java.version8.0.33/mysql-connector-java.version /properties dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId version${spring-boot.version}/version /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-spring-boot3-starter/artifactId version${mybatis-plus.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-j/artifactId version${mysql-connector-java.version}/version scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-aop/artifactId /dependency /dependencies提示MySQL Connector/J 8.0.33 是当前2024 年中与 MySQL 8.0.33 官方二进制包匹配度最高的驱动。若用 8.0.32 或更低版本连接 MySQL 8.0.33可能出现Public Key Retrieval is not allowed错误——这不是密码问题是驱动未适配新版默认加密握手协议。2.2 初始化 MySQL 8 数据库启用严格模式与金融级字符集银行系统绝不允许字段被静默截断或日期自动补零。MySQL 8 默认开启STRICT_TRANS_TABLES但需确认是否生效并强制使用utf8mb4支持 emoji 和四字节 Unicode避免客户姓名存入乱码-- 登录 MySQL 后执行 SET GLOBAL sql_mode STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_ENGINE_SUBSTITUTION; CREATE DATABASE IF NOT EXISTS bank_system CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bank_system; -- 创建用户表含身份证号脱敏存储字段 CREATE TABLE user_info ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 主键ID, account_no VARCHAR(20) NOT NULL UNIQUE COMMENT 银行账号唯一, real_name VARCHAR(50) NOT NULL COMMENT 真实姓名, id_card_encrypted VARCHAR(128) NOT NULL COMMENT 身份证号AES加密后密文, status TINYINT NOT NULL DEFAULT 1 COMMENT 状态1-正常2-冻结3-销户, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 创建账户余额表分离用户信息与资金状态符合 DDD 聚合根设计 CREATE TABLE account_balance ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_no VARCHAR(20) NOT NULL COMMENT 关联账号, balance DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 可用余额, frozen_amount DECIMAL(18,2) NOT NULL DEFAULT 0.00 COMMENT 冻结金额, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, FOREIGN KEY (account_no) REFERENCES user_info(account_no) ON DELETE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci; -- 创建交易流水表所有资金变动必须落库不可只更新余额 CREATE TABLE transaction_log ( id BIGINT PRIMARY KEY AUTO_INCREMENT, tx_id VARCHAR(64) NOT NULL UNIQUE COMMENT 全局唯一交易ID雪花算法生成, account_no VARCHAR(20) NOT NULL COMMENT 操作账号, tx_type TINYINT NOT NULL COMMENT 类型1-存款2-取款3-转账出4-转账入5-冻结6-解冻, amount DECIMAL(18,2) NOT NULL COMMENT 变动金额取款/转账出为负, balance_after DECIMAL(18,2) NOT NULL COMMENT 操作后余额, remark VARCHAR(200) COMMENT 备注如转账附言, operator VARCHAR(50) NOT NULL COMMENT 操作人系统或管理员, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_account_tx (account_no, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;参数说明DECIMAL(18,2)是金融系统黄金标准18 位总长2 位小数覆盖最大 999,999,999,999,999.99 元精度无丢失version字段用于乐观锁控制并发更新避免「超卖」式余额透支tx_id使用雪花算法Snowflake生成保证分布式环境下全局唯一且有序便于后续对账与排查INDEX idx_account_tx是高频查询支撑按账号查最近 10 笔流水必须走索引否则分页会拖垮数据库。2.3 编写 MyBatis-Plus 实体与 Mapper让 SQL 与 Java 对象真正对齐MyBatis-Plus 的TableName和TableId注解必须显式声明否则在 MySQL 8 的严格模式下字段名大小写敏感可能导致映射失败尤其 Windows 开发环境 vs Linux 生产环境// src/main/java/com/bank/entity/UserInfo.java import com.baomidou.mybatisplus.annotation.*; import java.time.LocalDateTime; TableName(user_info) public class UserInfo { TableId(type IdType.AUTO) private Long id; private String accountNo; // 注意Java 属性用驼峰DB 字段用下划线 private String realName; private String idCardEncrypted; private Integer status; private LocalDateTime createdAt; private LocalDateTime updatedAt; // getter/setter 省略 } // src/main/java/com/bank/entity/AccountBalance.java TableName(account_balance) public class AccountBalance { TableId(type IdType.AUTO) private Long id; private String accountNo; private BigDecimal balance; private BigDecimal frozenAmount; Version // 启用乐观锁 private Integer version; // getter/setter 省略 }Mapper 接口继承BaseMapper即可获得基础 CRUD但银行系统绝不直接暴露updateById()——所有余额变更必须走业务 Service 方法确保事务与日志同步// src/main/java/com/bank/mapper/AccountBalanceMapper.java Mapper public interface AccountBalanceMapper extends BaseMapperAccountBalance { // 自定义方法原子性更新余额 版本号校验 Update(UPDATE account_balance SET balance balance #{amount}, frozen_amount frozen_amount #{frozenChange}, version version 1 WHERE account_no #{accountNo} AND version #{version}) int updateBalanceAndFrozen(Param(accountNo) String accountNo, Param(amount) BigDecimal amount, Param(frozenChange) BigDecimal frozenChange, Param(version) Integer version); }逻辑说明该 SQL 使用AND version #{version}实现 CASCompare And Swap更新若并发修改导致版本号不匹配updateBalanceAndFrozen()返回 0上层 Service 可捕获并重试或抛出OptimisticLockException。这是比SelectKey更可控的并发控制方式。3. 实现四大核心业务开户、存取款、冻结解冻、流水查询的事务边界与异常兜底3.1 开户流程强校验 事务回滚 账号生成策略开户看似简单实则涉及多表插入与业务规则校验。关键点身份证号必须 AES 加密存储、账号需全局唯一且符合银行编码规范如 6 位机构号 10 位序列号、失败时所有 DB 操作必须回滚。Service Transactional(rollbackFor Exception.class) public class AccountService { Resource private UserInfoMapper userInfoMapper; Resource private AccountBalanceMapper accountBalanceMapper; Resource private TransactionLogMapper transactionLogMapper; public ResultString openAccount(String realName, String idCard) { // 1. 身份证号格式校验15 或 18 位含 X if (!IdCardUtil.isValid(idCard)) { return Result.fail(身份证号格式错误); } // 2. 加密存储生产环境应使用 HSM 或 KMS此处简化为 AES String encryptedIdCard AesUtil.encrypt(idCard, bank-key-2024); // 3. 生成账号模拟前缀 时间戳 随机数 String accountNo generateAccountNo(); // 4. 插入用户信息 UserInfo user new UserInfo(); user.setAccountNo(accountNo); user.setRealName(realName); user.setIdCardEncrypted(encryptedIdCard); user.setStatus(1); // 正常 userInfoMapper.insert(user); // 5. 插入初始余额记录余额为 0 AccountBalance balance new AccountBalance(); balance.setAccountNo(accountNo); balance.setBalance(BigDecimal.ZERO); balance.setFrozenAmount(BigDecimal.ZERO); accountBalanceMapper.insert(balance); // 6. 记录开户流水类型为 0 或单独定义 TransactionLog log new TransactionLog(); log.setTxId(SnowflakeIdGenerator.nextIdStr()); log.setAccountNo(accountNo); log.setTxType(0); // 开户 log.setAmount(BigDecimal.ZERO); log.setBalanceAfter(BigDecimal.ZERO); log.setRemark(开户成功); log.setOperator(SYSTEM); transactionLogMapper.insert(log); return Result.success(accountNo); } private String generateAccountNo() { // 实际应对接核心系统或使用 DB 序列此处用时间戳随机数模拟 return 8888 System.currentTimeMillis() % 10000000000L; } }参数说明Transactional(rollbackFor Exception.class)是底线任何Exception包括 unchecked都触发回滚避免「用户插入成功但余额没建」的脏数据IdCardUtil.isValid()是国产身份证校验工具类验证 18 位末尾校验码比正则更可靠AesUtil.encrypt()使用 AES-128-CBC 模式密钥硬编码仅用于演示生产必须由密钥管理系统KMS动态获取SnowflakeIdGenerator.nextIdStr()返回字符串型雪花 ID避免 long 型在 JS 中精度丢失。3.2 存取款基于乐观锁的原子更新与余额防透支取款是风险最高操作。必须满足余额充足校验在 DB 层完成防止应用层判断后被并发修改、更新语句自带WHERE balance #{amount}条件、失败时返回明确错误码而非吞异常。public ResultString withdraw(String accountNo, BigDecimal amount, String operator) { // 1. 查询当前余额与冻结金额SELECT FOR UPDATE 不推荐锁粒度大易死锁 AccountBalance current accountBalanceMapper.selectOne( new QueryWrapperAccountBalance().eq(account_no, accountNo) ); if (current null) { return Result.fail(账号不存在); } BigDecimal available current.getBalance().subtract(current.getFrozenAmount()); if (available.compareTo(amount) 0) { return Result.fail(余额不足可用 available); } // 2. 执行原子更新关键WHERE 条件包含余额校验 int rows accountBalanceMapper.updateBalanceAndFrozen( accountNo, amount.negate(), // 取款为负 BigDecimal.ZERO, // 冻结金额不变 current.getVersion() ); if (rows 0) { // 版本号不匹配 → 并发冲突重试或提示稍后操作 return Result.fail(操作冲突请重试); } // 3. 记录流水 TransactionLog log buildTransactionLog(accountNo, 2, amount.negate(), operator); transactionLogMapper.insert(log); return Result.success(取款成功); } private TransactionLog buildTransactionLog(String accountNo, int txType, BigDecimal amount, String operator) { TransactionLog log new TransactionLog(); log.setTxId(SnowflakeIdGenerator.nextIdStr()); log.setAccountNo(accountNo); log.setTxType(txType); log.setAmount(amount); log.setBalanceAfter(getCurrentBalance(accountNo).add(amount)); // 实际应查最新余额 log.setOperator(operator); log.setRemark(自助终端取款); return log; }逻辑说明第一步selectOne()仅用于展示当前状态真正的余额校验放在 UPDATE 的 WHERE 子句中见updateBalanceAndFrozen的 SQL这是防止「读-改-写」竞态的终极手段amount.negate()是 JavaBigDecimal的正确取反方式-amount会编译报错getCurrentBalance()方法应通过SELECT balance - frozen_amount FROM account_balance实时计算而非依赖缓存——金融系统宁可慢不可错。3.3 冻结与解冻状态变更与资金隔离的双重保障冻结操作本质是「将可用余额的一部分移入冻结池」解冻则是反向操作。必须保证冻结金额不能超过当前可用余额、解冻不能超过已冻结金额、状态字段status与frozen_amount必须同步更新。public ResultString freezeAccount(String accountNo, BigDecimal freezeAmount, String operator) { AccountBalance current accountBalanceMapper.selectOne( new QueryWrapperAccountBalance().eq(account_no, accountNo) ); if (current null) return Result.fail(账号不存在); BigDecimal available current.getBalance().subtract(current.getFrozenAmount()); if (available.compareTo(freezeAmount) 0) { return Result.fail(冻结金额超出可用余额); } int rows accountBalanceMapper.updateBalanceAndFrozen( accountNo, BigDecimal.ZERO, // 余额不变 freezeAmount, // 冻结金额增加 current.getVersion() ); if (rows 0) return Result.fail(冻结失败请重试); // 更新用户状态为「冻结中」注意仅当 freezeAmount balance 时才设 status2 if (freezeAmount.compareTo(current.getBalance()) 0) { UserInfo user new UserInfo(); user.setAccountNo(accountNo); user.setStatus(2); // 冻结 userInfoMapper.update(user, new QueryWrapperUserInfo().eq(account_no, accountNo)); } TransactionLog log buildTransactionLog(accountNo, 5, freezeAmount, operator); log.setRemark(账户冻结); transactionLogMapper.insert(log); return Result.success(冻结成功); }参数说明冻结不改变balance只增加frozen_amount这样balance - frozen_amount始终等于可用余额逻辑清晰status2仅在「全额冻结」时设置部分冻结仍保持status1符合银行业务习惯如司法冻结 vs 临时风控冻结流水tx_type5专指冻结与取款2、转账3/4区分便于审计追踪。4. 避坑MySQL 8 与 Java 事务协同的 4 个血泪经验4.1 现象转账时 A 账户扣款成功B 账户入账失败但 A 的扣款未回滚原因Service 方法未加Transactional或事务传播行为配置错误如REQUIRES_NEW导致子事务独立提交。解决转账必须在一个事务内完成使用默认REQUIRED传播级别并确保整个方法链路包括调用的 DAO 方法都在同一事务上下文中。检查Transactional是否被this.调用绕过即内部方法调用不触发代理。4.2 现象MySQL 8 报错java.sql.SQLException: The query did not generate a result set原因MyBatis-Plus 的Update方法返回int但 XML 中写了select标签或Select注解误用于更新语句。解决严格区分Select/Update/Insert/Delete自定义 SQL 更新必须用Update且方法返回值为int影响行数。4.3 现象BigDecimal运算结果出现0.00000000000000000001类似精度漂移原因使用double构造BigDecimal如new BigDecimal(0.1)double本身二进制存储不精确。解决永远用String构造BigDecimal如new BigDecimal(0.1)所有金额输入前端必须传字符串后端解析时校验格式。4.4 现象TableField(fill FieldFill.INSERT)自动填充created_at失效原因MySQL 8 默认explicit_defaults_for_timestampONCURRENT_TIMESTAMP作为默认值需显式声明。解决建表时created_at字段必须写DEFAULT CURRENT_TIMESTAMP且实体类TableField注解要配合fill FieldFill.INSERT同时updated_at用DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP。4.5 现象本地开发用 MySQL 8.0.33测试环境用 8.0.28JSON_CONTAINS函数行为不一致原因MySQL 8.0.28 对JSON_CONTAINS的空数组处理存在 bug8.0.30 修复。解决所有环境 MySQL 小版本号必须统一建议 8.0.33Docker Compose 中固定image: mysql:8.0.33禁止用mysql:8标签。5. 日志审计与对账能力用 MySQL 8 窗口函数实现 T1 对账报表银行系统的核心价值不在功能多而在「每一笔钱都能说清来龙去脉」。MySQL 8 原生支持窗口函数让我们无需 Java 循环就能生成对账所需的关键指标当日累计交易笔数、最大单笔金额、余额波动标准差、是否存在未匹配流水。5.1 构建对账视图用ROW_NUMBER()和SUM() OVER()提升分析效率-- 创建对账视图每日凌晨调度执行 CREATE OR REPLACE VIEW daily_reconciliation_view AS SELECT account_no, DATE(created_at) AS trade_date, COUNT(*) AS tx_count, MAX(ABS(amount)) AS max_single_amount, ROUND(STDDEV_SAMP(ABS(amount)), 2) AS amount_stddev, SUM(CASE WHEN tx_type IN (1,4) THEN amount ELSE 0 END) AS total_in, SUM(CASE WHEN tx_type IN (2,3) THEN amount ELSE 0 END) AS total_out, -- 检查是否存在「有出无入」或「有入无出」的异常流水需结合核心系统对账文件 COUNT(*) FILTER (WHERE tx_type 3 AND NOT EXISTS ( SELECT 1 FROM transaction_log t2 WHERE t2.tx_id CONCAT(MATCH-, SUBSTRING(t1.tx_id, 1, 32)) )) AS unmatched_outgoing FROM transaction_log WHERE created_at DATE_SUB(NOW(), INTERVAL 1 DAY) GROUP BY account_no, DATE(created_at);参数说明STDDEV_SAMP()计算样本标准差识别异常大额交易如标准差突增 3 倍触发风控预警COUNT(*) FILTER (...)是 MySQL 8.0.22 支持的过滤聚合比CASE WHEN更简洁unmatched_outgoing字段用于标记未收到对手方确认的转账出账是银企直连对账的关键字段。5.2 Java 层调用视图生成对账报告避免内存溢出视图数据量可能达百万级Java 层必须流式处理而非ListTransactionLog全量加载Repository public class ReconciliationDao { Autowired private JdbcTemplate jdbcTemplate; public void generateDailyReport(LocalDate reportDate) { String sql SELECT * FROM daily_reconciliation_view WHERE trade_date ?; // 使用 ResultSetExtractor 流式处理每行处理后立即释放 jdbcTemplate.query(sql, new Object[]{reportDate}, new ResultSetExtractorVoid() { Override public Void extractData(ResultSet rs) throws SQLException, DataAccessException { while (rs.next()) { String accountNo rs.getString(account_no); int txCount rs.getInt(tx_count); BigDecimal maxAmount rs.getBigDecimal(max_single_amount); // 写入对账文件或发送 Kafka writeToFile(accountNo, txCount, maxAmount); } return null; } }); } private void writeToFile(String accountNo, int txCount, BigDecimal maxAmount) { // 实际写入 SFTP 或对象存储此处简化为本地文件 try (FileWriter fw new FileWriter(/data/reports/ accountNo .csv, true)) { fw.append(String.format(%s,%d,%.2f%n, accountNo, txCount, maxAmount.doubleValue())); } catch (IOException e) { log.error(写入对账文件失败, e); } } }逻辑说明JdbcTemplate.query()的ResultSetExtractor回调在 JDBCResultSet迭代时逐行执行内存占用恒定 O(1)避免queryForList()将百万行全载入堆内存导致 GC 频繁甚至 OOM。5.3 行级权限控制用 Spring Security 数据库视图实现「柜员只能看自己经办的流水」银行内部系统必须支持细粒度权限。不推荐在 Java 层if (user.getBranch() ! log.getBranch()) throw——易被绕过。最佳实践是用数据库视图 Spring Security 的PreAuthorize组合-- 创建柜员专属视图只显示其 operator 字段匹配的流水 CREATE VIEW teller_transaction_view AS SELECT * FROM transaction_log WHERE operator SUBSTRING_INDEX(USER(), , 1); -- MySQL USER() 返回 teller001localhostRestController RequestMapping(/api/teller) public class TellerController { GetMapping(/transactions) PreAuthorize(hasRole(TELLER)) public ListTransactionLog getTellerTransactions() { // 直接查询视图DB 层已过滤 return transactionLogMapper.selectList( new QueryWrapperTransactionLog().apply(11) // 强制走视图 ); } }参数说明SUBSTRING_INDEX(USER(), , 1)提取 MySQL 当前连接用户名如teller001视图自动过滤非本人操作流水PreAuthorize是二次校验双保险。6. 最后一公里用单元测试覆盖资金安全边界让每次上线都有底气写完代码不等于安全。银行系统必须用测试证明「不可能发生的事真的不会发生」。我坚持的三类必测场景6.1 并发取款测试模拟 100 线程争抢 1 元余额Test void concurrentWithdrawShouldNotOverdraft() throws InterruptedException { // 初始化账号余额为 1.00 元 AccountBalance init new AccountBalance(); init.setAccountNo(TEST001); init.setBalance(new BigDecimal(1.00)); init.setFrozenAmount(BigDecimal.ZERO); accountBalanceMapper.insert(init); // 启动 100 个线程每个尝试取 1 元 CountDownLatch latch new CountDownLatch(100); AtomicInteger successCount new AtomicInteger(0); ExecutorService executor Executors.newFixedThreadPool(100); for (int i 0; i 100; i) { executor.submit(() - { try { ResultString result accountService.withdraw(TEST001, new BigDecimal(1.00), test); if (result.isSuccess()) { successCount.incrementAndGet(); } } finally { latch.countDown(); } }); } latch.await(); // 断言最多只有 1 次成功余额只够 1 次取款 assertThat(successCount.get()).isLessThanOrEqualTo(1); // 验证最终余额 ≥ 0 AccountBalance finalBalance accountBalanceMapper.selectOne( new QueryWrapperAccountBalance().eq(account_no, TEST001) ); assertThat(finalBalance.getBalance()).isGreaterThanOrEqualTo(BigDecimal.ZERO); }为什么有效此测试不依赖「sleep 模拟并发」而是用CountDownLatch确保所有线程几乎同时发起请求真实复现竞争窗口。若未加乐观锁successCount会远大于 1。6.2 事务回滚测试故意在存入后抛异常验证余额不变Test Rollback // Spring Test 自动回滚但需验证 DB 状态 void depositWithExceptionShouldRollback() { // 初始余额 0 AccountBalance before accountBalanceMapper.selectOne( new QueryWrapperAccountBalance().eq(account_no, TEST002) ); assertThat(before.getBalance()).isEqualByComparingTo(BigDecimal.ZERO); // 执行存入 100 元但在 Service 内部手动抛异常 assertThatThrownBy(() - accountService.depositWithError(TEST002, new BigDecimal(100.00))) .isInstanceOf(RuntimeException.class); // 查询余额必须仍是 0 AccountBalance after accountBalanceMapper.selectOne( new QueryWrapperAccountBalance().eq(account_no, TEST002) ); assertThat(after.getBalance()).isEqualByComparingTo(BigDecimal.ZERO); }参数说明Rollback是 Spring Test 的默认行为但必须显式assertThatDB 状态因为Rollback只保证事务回滚不保证你的 DAO 方法没偷偷 commit。6.3 MySQL 8 特性兼容测试验证JSON_EXTRACT在实体映射中可用// 假设 transaction_log 表新增 json_extra 字段存储扩展信息 Test void jsonExtractShouldWorkInMyBatisPlus() { // 插入含 JSON 的流水 TransactionLog log new TransactionLog(); log.setTxId(TX123); log.setAccountNo(TEST003); log.setTxType(1); log.setAmount(new BigDecimal(50.00)); log.setJsonExtra({\channel\:\ATM\,\device_id\:\ATM-001\}); transactionLogMapper.insert(log); // 查询并提取 JSON 字段 TransactionLog found transactionLogMapper.selectOne( new QueryWrapperTransactionLog().eq(tx_id, TX123) ); // MyBatis-Plus 3.5.5 支持 TableField(el json_extra-$.channel) 映射 assertThat(found.getChannel()).isEqualTo(ATM); // channel 是实体新增 String 字段 }逻辑说明MySQL 8 的 JSON 函数是处理复杂业务元数据的利器但必须验证 ORM 框架能否正确解析-操作符。此测试确保TableField(el ...)在真实环境中生效。我带过的团队上线前必跑这三类测试。不是为了覆盖率数字好看而是每次看到BUILD SUCCESS时心里那块石头才算落地——钱的事没有「差不多」只有「确定不会错」。希望帮到你。本文还有配套的精品资源点击获取
企业数字化 ERP 产品动态
相关推荐
本地声音克隆:从素材准备到批量出稿的配音工作流实操 1. 项目概述与核心设计思路做有声书、广告口播和短视频配音这几年,我电脑里攒下来的声音素材比项目文件还多。每接一个新单,都是同一个循环:翻半天录音找参考,进工具做AI配音试听,不满意再调参数再生成,最后… · 2026/11/4 12:16:30
阶梯型矩阵全解析:判定、手算、RREF与代码实现 说到阶梯型矩阵,很多人脑子里第一反应是"楼梯"——一行比一行往右缩,看着挺直观。但我在带着几个同学做线性代数练习、以及后来写数值计算脚本的时候发现,真正卡住人的从来不是"什么是阶梯",而是"什么时… · 2026/11/4 12:17:05
DCASE源码复现:电量分离与家庭电器故障检测的频谱图分类实践 简介:基于电量分离技术的家庭电器故障检测系统,是为DCASE声音事件检测挑战赛准备的完整源码方案。面向从事声音信号处理、智能家居故障诊断研究的学生与工程师,解决家用电器异常声纹识别与早期故障预警问题。包内共39个文件,以17个… · 2026/11/3 13:24:39
普工要电工证吗 别踩坑教你避开培训陷阱 普工要电工证吗 别踩坑教你避开培训陷阱 怕考不过白交培训费,这是大多数想考电工证的普工最怕的事。你是不是也听说了,现在越来越多的岗位要求有电工证?那问题来了, 普工要电工证吗 ?答案是: 要看岗位,别盲目跟风 ,否则 千万别踩坑… · 2026/11/7 10:19:03
河南电工证有必要考吗保姆级教程:学历不高也能拿下电工证 河南电工证有必要考吗保姆级教程:学历不高也能拿下电工证 学历不高怕报不上名?河南电工证有必要考吗?这个问题在我们这个圈子里被问得太多了,尤其是像你这样的市政公用工程从业者,手里没个电工证,连基本的岗位职责都难以履行。今天我就从【保姆级教程】… · 2026/11/7 10:19:03
古蔺电工证报名多少钱?怕考不过白交培训费?别急,我来帮你搞懂 古蔺电工证报名多少钱?怕考不过白交培训费?别急,我来帮你搞懂 你是不是也担心, 古蔺电工证报名 要花多少钱?怕培训费交了,考试却考不过,白忙一场?别急,这篇文章就帮你搞清楚到底该怎么考,考什么,还有那些容易踩的坑,全都给你讲明白!… · 2026/11/7 10:19:03
河南电工证有必要考吗电子证书值不值得考 河南电工证有必要考吗电子证书值不值得考 怕考不过白交培训费,这是很多河南电工证考生的心声,尤其是像你这样的郑州劳务班组负责人,想通过考证提升团队竞争力,但又担心时间、精力和金钱打水漂。电工证作为国家强制要求的特种作业操作证之一,不仅是从事电… · 2026/11/7 10:19:03
电工证三孔插座值不值得考?三门峡电工告诉你真相 电工证三孔插座值不值得考?三门峡电工告诉你真相 急需特种作业证才能上岗找工作,这话说得一点都不夸张。特别是在建筑、电力、制造业等行业,电工证三孔插座这类特种作业证,是你能否持证上岗、拿到工资的“敲门砖”。很多人会问:“电工证三孔插座值不值得… · 2026/11/7 10:18:03
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 10:29:23
澳洲水电工证考试:证书过期了不知道怎么复审延期?电子证书怎么弄? 澳洲水电工证考试:证书过期了不知道怎么复审延期?电子证书怎么弄? 证书过期了不知道怎么复审延期?别慌,很多人在拿到【澳洲水电工证考试】的电子证书后,一不小心就忽略了复审时间,结果证书失效,工作受阻,甚至被单位追责。今天就从【澳洲水电工证考试… · 2026/11/7 0:00:41
电工作业提示版通过率揭秘:怎么考才能一次过? 电工作业提示版通过率揭秘:怎么考才能一次过? 怕考不过白交培训费?别急,今天我就从驻马店应届工程类毕业生的视角,带你全面了解电工作业提示版的考试流程、备考要点和真实通过率,帮你少走弯路、提高一次通过率。… · 2026/11/7 0:00:41
电工证年审报名千万别踩坑 外省证转过来要搞清这几点 电工证年审报名千万别踩坑 外省证转过来要搞清这几点 之前考的证在外省能不能转过来?这个问题是很多电工从业者最关心的,特别是像你这样在商丘从事公路工程的人员,经常需要跨省流动,证书的 有效性 和 可迁移性… · 2026/11/7 0:00:41
双极步进电机控制实战:DRV8818PWPR与PIC18F86K22驱动调试 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:27:59
STM32F207ZG + DRV8818PWPR 步进电机驱动实战解析 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/5 10:28:03
DRV8818PWPR+dsPIC30F4013工业级双极步进电机控制方案 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/11/6 10:29:23