尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

JavaWeb银行账目系统:复式记账与借贷平衡实战

JavaWeb银行账目系统:复式记账与借贷平衡实战 简介银行账目系统本质是遵循会计恒等式资产负债所有者权益的强一致性数据系统其核心原理在于复式记账——每笔业务必须生成至少两个方向相反、金额相等的会计分录并通过事务保障原子性与可追溯性。技术价值体现在对数据精度decimal精确计算、操作留痕不可篡改日志、业务约束借贷必相等校验的刚性实现广泛应用于金融核心系统、企业财务中台及高校毕设高阶实践场景。本文聚焦JavaWeb技术栈下真实银行级账目逻辑的落地涵盖MySQL事务控制、复式记账引擎编码、全局唯一流水号设计等关键环节。1. 这不是“又一个学生管理系统”而是银行级账目逻辑的落地切口你搜“JavaWeb 银行帐目管理系统 毕设”页面刷出来几十个同名项目——界面雷同、功能简陋、数据库字段命名混乱连“余额”字段都敢用 varchar 类型存数字。我带过三届毕业设计指导每年至少收到17份标着“银行级”的毕设代码其中14份连最基本的借贷平衡校验都没写转账操作直接 update 两个账户余额中间没有任何事务控制更别提日志留痕或操作回滚。这不是教学疏忽是学生根本没理解“银行帐目”四个字背后的硬约束它不是 CRUD 的练习场而是资金流动的法定凭证系统。这个标题里的“银行帐目管理系统”核心不在“管理”而在“帐目”——它必须体现会计恒等式资产 负债 所有者权益的实时校验必须支持复式记账一笔业务至少两个会计分录必须保证每一笔流水可追溯、不可篡改、可对账。所谓“源码数据库脚本”如果只是一堆增删改查的 Servlet JSP 堆砌那它连及格线都摸不到。真正有价值的毕设应该让你在答辩时能指着某段代码说“这里实现了借贷必相等的强制校验如果插入的借方总额≠贷方总额事务会自动回滚数据库不会留下任何不平账记录。”——这才是企业级财务系统最基础的门槛。我去年帮一个学生重构他的毕设他原方案用 ArrayList 存临时分录提交时再循环 insert 到数据库。我让他改成用 PreparedStatement 批量执行并在 service 层加了一层校验先计算所有分录的 debitSum 和 creditSum不等就 throw new AccountingException(借贷不平拒绝记账)。就这么一个改动他答辩时被老师追问了8分钟细节最后拿了优秀。因为老师知道能写出这种校验逻辑的人已经跨过了“写网页”的阶段开始思考数据本身的语义约束了。所以这篇不是教你如何拖拽出一个登录界面而是带你从零构建一个具备真实银行账目逻辑骨架的 JavaWeb 系统。它包含完整的复式记账引擎、基于 MySQL 的事务强一致性保障、可审计的操作日志、以及一套能直接导入本地 MySQL 的建库脚本——所有代码都经得起“如果这是真银行的生产环境它能不能跑”的拷问。适合计算机专业大四学生、刚转行的初级后端开发者或者想补足企业级财务系统底层逻辑的在职工程师。2. 数据库设计为什么“account_balance”不能是 varchar而“transaction_no”必须全局唯一且不可重用很多毕设数据库脚本里“账户余额”字段类型是 varchar(20)理由是“方便存小数”。这暴露了一个致命误区数据库字段类型不是为了“存得下”而是为了“算得准、查得快、锁得住”。银行账目系统里余额参与高频计算如实时余额查询、利息计算、限额校验如果用字符串存储每次运算前都要 parseDouble不仅性能损耗大更关键的是——它无法利用数据库的数值索引也无法触发 MySQL 的数值型约束比如 check (balance 0)。我们来看真实银行系统的核心表结构设计逻辑2.1 核心实体表account账户主表CREATE TABLE account ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, account_no varchar(32) NOT NULL UNIQUE COMMENT 账号如6228480000000000001, account_name varchar(50) NOT NULL COMMENT 户名, account_type tinyint NOT NULL DEFAULT 1 COMMENT 账户类型1-储蓄卡2-信用卡3-对公账户, balance decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 当前余额单位元精确到分, frozen_balance decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 冻结金额用于司法冻结、止付等, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0-销户1-正常2-挂失3-冻结, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 更新时间, PRIMARY KEY (id), KEY idx_account_no (account_no) USING BTREE COMMENT 账号索引高频查询字段, KEY idx_status (status) USING BTREE COMMENT 状态索引用于批量状态变更 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT账户主表;提示decimal(18,2)是银行系统的黄金标准。18位总长度2位小数既能覆盖中国最大单笔交易额理论上可达999,999,999,999,999,999.99元又能保证小数点后两位的绝对精度。用 float 或 double 会导致 0.10.2 ≠ 0.3 这类经典浮点误差在金融系统里是灾难性的。2.2 核心流水表journal_entry会计分录表这是区别于普通“订单表”“用户表”的关键——它不是记录“发生了什么”而是记录“这笔钱在会计科目上如何变化”。CREATE TABLE journal_entry ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, entry_no varchar(32) NOT NULL UNIQUE COMMENT 分录编号格式JE20240520000001, transaction_no varchar(32) NOT NULL COMMENT 关联的业务流水号如转账单号、存款单号, account_id bigint NOT NULL COMMENT 账户ID外键关联account.id, account_code varchar(20) NOT NULL COMMENT 会计科目编码如1001-库存现金1002-银行存款, account_name varchar(50) NOT NULL COMMENT 会计科目名称, debit_amount decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 借方金额, credit_amount decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 贷方金额, direction tinyint NOT NULL COMMENT 方向1-借方2-贷方, description varchar(200) DEFAULT NULL COMMENT 摘要, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_transaction_no (transaction_no) USING BTREE COMMENT 业务流水号索引, KEY idx_account_id (account_id) USING BTREE COMMENT 账户ID索引, KEY idx_account_code (account_code) USING BTREE COMMENT 科目编码索引, CONSTRAINT fk_journal_account FOREIGN KEY (account_id) REFERENCES account (id) ON DELETE RESTRICT ON UPDATE CASCADE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT会计分录表;注意一张业务流水如一次转账必须生成至少两条分录转出账户借记“其他应收款”或“在途资金”贷记“银行存款”转入账户借记“银行存款”贷记“其他应收款”或“在途资金”。这就是复式记账的核心——有借必有贷借贷必相等。transaction_no字段的作用就是把同一笔业务的所有分录串起来方便后续对账和冲正。2.3 业务流水表transaction_record业务流水主表它记录用户感知的“一笔业务”是分录表的父表CREATE TABLE transaction_record ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, transaction_no varchar(32) NOT NULL UNIQUE COMMENT 业务流水号全局唯一永不重复, transaction_type tinyint NOT NULL COMMENT 业务类型1-存款2-取款3-转账4-缴费, amount decimal(18,2) NOT NULL COMMENT 交易金额, status tinyint NOT NULL DEFAULT 1 COMMENT 状态0-失败1-成功2-处理中3-已冲正, from_account_no varchar(32) DEFAULT NULL COMMENT 转出账号, to_account_no varchar(32) DEFAULT NULL COMMENT 转入账号, remark varchar(200) DEFAULT NULL COMMENT 备注, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, complete_time datetime DEFAULT NULL COMMENT 完成时间, PRIMARY KEY (id), KEY idx_transaction_no (transaction_no) USING BTREE, KEY idx_from_account (from_account_no) USING BTREE, KEY idx_to_account (to_account_no) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT业务流水主表;关键设计点transaction_no必须全局唯一且永不重用。这是审计溯源的基石。很多学生用自增ID当流水号这是严重错误——自增ID可能因删除、回滚而产生空洞且无法体现业务含义。正确做法是用时间戳序列号生成例如TR20240520000001确保即使系统重启、数据库重建同一笔业务的流水号也绝不会重复。2.4 操作日志表operation_log不可篡改的操作留痕银行系统要求“谁在什么时候做了什么系统必须记得清清楚楚”且日志本身不能被删除或修改CREATE TABLE operation_log ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键ID, log_no varchar(32) NOT NULL UNIQUE COMMENT 日志编号格式LOG20240520000001, operator_id bigint NOT NULL COMMENT 操作员ID对应管理员表, operator_name varchar(50) NOT NULL COMMENT 操作员姓名, operation_type varchar(20) NOT NULL COMMENT 操作类型login, transfer, deposit, withdraw, freeze, target_id varchar(50) DEFAULT NULL COMMENT 操作目标ID如账户号、流水号, before_value text COMMENT 操作前状态快照JSON格式, after_value text COMMENT 操作后状态快照JSON格式, ip_address varchar(45) DEFAULT NULL COMMENT 操作IP地址, user_agent text COMMENT 浏览器/客户端信息, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, PRIMARY KEY (id), KEY idx_operator_id (operator_id) USING BTREE, KEY idx_target_id (target_id) USING BTREE, KEY idx_create_time (create_time) USING BTREE ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_0900_ai_ci COMMENT操作日志表仅插入禁止更新删除;实操心得日志表的before_value和after_value字段用 text 类型而非 json是为了兼容老版本 MySQL5.7.8并避免 JSON 函数的性能开销。实际存储时用 Jackson 序列化成紧凑 JSON 字符串如{balance: 1000.00, frozen_balance: 0.00}。这样既保证了可读性又便于后续用 SQL 查询特定字段变化如WHERE after_value LIKE %balance:500.00%。3. 复式记账引擎如何用 Java 代码强制实现“有借必有贷借贷必相等”数据库表设计只是骨架真正的“银行味”来自业务逻辑层——那个在用户点击“转账”按钮后默默执行复式记账、校验平衡、写入多张表的 Service 方法。很多毕设的 Service 层只有一行accountDao.updateBalance(fromId, -amount); accountDao.updateBalance(toId, amount);这叫“伪转账”不是银行转账。我们来拆解一个真实的transferService.transfer()方法它必须满足三个硬性条件原子性转账要么全部成功要么全部失败绝不允许出现“扣了A的钱B没收到”的中间态一致性转账前后全系统总余额不变忽略手续费场景且每笔分录的借方总额 贷方总额可追溯性生成的每条分录必须能通过transaction_no关联到原始业务流水并记录完整摘要。3.1 核心方法签名与事务边界Transactional(rollbackFor Exception.class) public TransferResult transfer(TransferRequest request) throws AccountingException { // 1. 参数校验与账户状态检查 Account fromAccount accountService.getAccountByNo(request.getFromAccountNo()); Account toAccount accountService.getAccountByNo(request.getToAccountNo()); if (fromAccount null || toAccount null) { throw new AccountingException(账户不存在); } if (fromAccount.getStatus() ! AccountStatus.NORMAL.getValue()) { throw new AccountingException(转出账户状态异常 fromAccount.getStatus()); } if (toAccount.getStatus() ! AccountStatus.NORMAL.getValue()) { throw new AccountingException(转入账户状态异常 toAccount.getStatus()); } if (fromAccount.getBalance().compareTo(request.getAmount()) 0) { throw new AccountingException(余额不足); } // 2. 生成全局唯一业务流水号 String transactionNo generateTransactionNo(); // 3. 构建会计分录列表核心 ListJournalEntry journalEntries buildTransferJournalEntries( fromAccount, toAccount, request.getAmount(), transactionNo); // 4. 强制校验所有分录的借方总额 贷方总额 BigDecimal debitSum journalEntries.stream() .map(JournalEntry::getDebitAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal creditSum journalEntries.stream() .map(JournalEntry::getCreditAmount) .reduce(BigDecimal.ZERO, BigDecimal::add); if (debitSum.compareTo(creditSum) ! 0) { throw new AccountingException( String.format(借贷不平借方总额%s贷方总额%s, debitSum, creditSum)); } // 5. 写入业务流水主表 TransactionRecord record new TransactionRecord(); record.setTransactionNo(transactionNo); record.setTransactionType(TransactionType.TRANSFER.getValue()); record.setAmount(request.getAmount()); record.setFromAccountNo(request.getFromAccountNo()); record.setToAccountNo(request.getToAccountNo()); record.setStatus(TransactionStatus.PROCESSING.getValue()); transactionRecordMapper.insert(record); // 6. 批量写入会计分录表 journalEntryMapper.insertBatch(journalEntries); // 7. 更新账户余额注意这里只更新余额不碰冻结金额 accountService.updateBalance(fromAccount.getId(), fromAccount.getBalance().subtract(request.getAmount())); accountService.updateBalance(toAccount.getId(), toAccount.getBalance().add(request.getAmount())); // 8. 记录操作日志 operationLogService.logTransferOperation( getCurrentOperator(), request, transactionNo); // 9. 更新流水状态为成功 record.setStatus(TransactionStatus.SUCCESS.getValue()); record.setCompleteTime(new Date()); transactionRecordMapper.updateById(record); return TransferResult.success(transactionNo); }关键点解析Transactional注解是原子性的第一道防线但仅靠它不够——它只能保证数据库操作回滚不能保证业务逻辑的完整性。第4步的借贷校验是灵魂。它发生在写入数据库之前是业务规则的前置守门员。即使数据库层面没做 check constraintJava 层的校验也能拦住 99% 的逻辑错误。第7步的余额更新必须放在分录写入之后。因为分录才是会计意义上的“记账”余额更新只是辅助展示。如果先更新余额再写分录万一写分录失败余额就错乱了且无法回滚因为余额更新是独立的 update 语句。3.2 构建分录的buildTransferJournalEntries方法这才是银行账目的“心脏”private ListJournalEntry buildTransferJournalEntries( Account fromAccount, Account toAccount, BigDecimal amount, String transactionNo) { ListJournalEntry entries new ArrayList(); // 分录1转出账户 - 银行存款贷方减少 JournalEntry debitEntry new JournalEntry(); debitEntry.setEntryNo(generateEntryNo()); debitEntry.setTransactionNo(transactionNo); debitEntry.setAccountId(fromAccount.getId()); debitEntry.setAccountCode(1002); // 银行存款科目编码 debitEntry.setAccountName(银行存款); debitEntry.setDebitAmount(BigDecimal.ZERO); debitEntry.setCreditAmount(amount); // 贷方记账 debitEntry.setDirection(Direction.CREDIT.getValue()); debitEntry.setDescription(String.format(转账支出至%s, toAccount.getAccountName())); entries.add(debitEntry); // 分录2转入账户 - 银行存款借方增加 JournalEntry creditEntry new JournalEntry(); creditEntry.setEntryNo(generateEntryNo()); creditEntry.setTransactionNo(transactionNo); creditEntry.setAccountId(toAccount.getId()); creditEntry.setAccountCode(1002); // 同样是银行存款科目 creditEntry.setAccountName(银行存款); creditEntry.setDebitAmount(amount); // 借方记账 creditEntry.setCreditAmount(BigDecimal.ZERO); creditEntry.setDirection(Direction.DEBIT.getValue()); creditEntry.setDescription(String.format(转账收入自%s, fromAccount.getAccountName())); entries.add(creditEntry); // 分录3内部清算科目可选模拟银行间清算 // 如果要体现更真实的银行间结算可增加 // 借清算资金往来1011 贷银行存款1002——转出银行 // 借银行存款1002 贷清算资金往来1011——转入银行 return entries; }实操心得科目编码1002是《企业会计准则》规定的“银行存款”标准编码。不要自己瞎编bank_deposit这种字段名科目体系是行业通用语言。我在指导学生时会让他们手抄一遍《企业会计准则——会计科目和主要账务处理》不是为了背诵而是建立对“钱在哪儿、怎么动”的直觉。当你看到一笔分录的account_code1002你就该立刻反应出这是在动银行里的钱。3.3 如何生成永不重复的流水号和分录号generateTransactionNo()和generateEntryNo()的实现决定了系统的可扩展性和可维护性private String generateTransactionNo() { // 格式TR 8位日期 6位序列号当日内递增 String datePart LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); int sequence getTodaySequence(transaction); // 从Redis或数据库获取当日序列 String sequencePart String.format(%06d, sequence); return TR datePart sequencePart; } private String generateEntryNo() { // 格式JE 8位日期 8位毫秒级时间戳 3位随机数 String datePart LocalDate.now().format(DateTimeFormatter.ofPattern(yyyyMMdd)); String timePart String.valueOf(System.currentTimeMillis() % 100000000L); String randomPart String.format(%03d, new Random().nextInt(1000)); return JE datePart timePart randomPart; }为什么不用 UUID因为 UUID 太长32位且无序不利于数据库索引和人工排查。TR20240520000001这种格式运维人员一眼就能看出这是2024年5月20日的第一笔业务比a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8直观一万倍。这也是银行系统“人机友好”的体现——既要机器高效也要人眼可读。4. Web 层安全加固为什么登录页不能只校验密码而转账接口必须二次确认很多毕设的 Web 层就是一个裸奔的 Servlet接收参数、调用 Service、返回 JSON。这在演示时没问题但在真实环境中它会被当成靶子打穿。银行账目系统Web 层不是通道而是第一道防火墙。4.1 登录认证从明文密码到防暴力破解的完整链路学生常犯的错误前端用 MD5 加密密码传给后端后端用password.equals(inputPassword)校验。这等于把密码明文存在数据库里。正确的做法是三层防护传输层加密强制 HTTPS杜绝中间人窃听存储层加密数据库存的是 BCrypt 加密后的哈希值不是 MD5MD5 已被证明不安全应用层防护登录接口自带防爆破机制。// LoginController.java PostMapping(/login) ResponseBody public ResultLoginResponse login(RequestBody LoginRequest request, HttpServletRequest httpRequest) { // 1. IP 频次限制每分钟最多5次 String ip getClientIp(httpRequest); Long loginCount redisTemplate.opsForValue() .increment(login:attempt: ip, 1L); if (loginCount 5) { throw new BusinessException(登录过于频繁请稍后再试); } redisTemplate.expire(login:attempt: ip, 1, TimeUnit.MINUTES); // 2. 用户名存在性校验快速失败不暴露是否存在 User user userService.findByUsername(request.getUsername()); if (user null) { // 统一返回“用户名或密码错误”不区分是用户名错还是密码错 throw new BusinessException(用户名或密码错误); } // 3. BCrypt 密码校验耗时操作天然防爆破 if (!BCrypt.checkpw(request.getPassword(), user.getPasswordHash())) { throw new BusinessException(用户名或密码错误); } // 4. 生成 JWT Token含用户ID、角色、过期时间 String token jwtUtil.generateToken(user.getId(), user.getRole()); // 5. 记录登录日志 loginLogService.recordSuccessLogin(user.getId(), ip); return Result.success(new LoginResponse(token, user.getRealName())); }关键细节BCrypt.checkpw()是一个故意设计为慢的算法默认 cost10约需 100ms它让暴力破解变得极其昂贵。而 MD5 是毫秒级的GPU 一秒能算几百万次。Redis 的login:attempt:192.168.1.100key实现了 IP 级限流比单纯数据库计数更高效。“统一错误提示”是安全常识如果告诉攻击者“用户名不存在”他就知道这个用户名没注册可以专注爆破其他用户名。4.2 转账接口为什么必须“输入密码 短信验证码”双因子验证学生毕设的转账接口往往只有一个transfer?fromxxxtoxxxamount100的 GET 请求连 CSRF Token 都没有。真实银行系统转账是最高危操作必须双因子// TransferController.java PostMapping(/transfer) ResponseBody public ResultTransferResult transfer(RequestBody TransferRequest request, HttpServletRequest httpRequest) { // 1. JWT Token 解析获取当前登录用户ID Long userId jwtUtil.getUserIdFromToken(getTokenFromHeader(httpRequest)); // 2. 校验当前用户是否为转出账户所有人 Account fromAccount accountService.getAccountByNo(request.getFromAccountNo()); if (!fromAccount.getUserId().equals(userId)) { throw new BusinessException(无权操作该账户); } // 3. 密码二次校验前端已加密后端再校验一次 if (!userService.checkPassword(userId, request.getPassword())) { throw new BusinessException(支付密码错误); } // 4. 短信验证码校验可选但强烈建议 String smsCode request.getSmsCode(); if (StringUtils.isNotBlank(smsCode)) { boolean valid smsService.verifyCode( request.getFromAccountNo(), smsCode); if (!valid) { throw new BusinessException(短信验证码错误); } } // 5. 调用核心转账服务 return Result.success(transferService.transfer(request)); }实操心得密码二次校验不是多此一举。JWT Token 可能被盗XSS 攻击但支付密码是用户主动输入的且不存于前端。这是最后一道“人控”防线。短信验证码的smsService.verifyCode()方法必须校验验证码是否在 5 分钟有效期内同一手机号/账号 1 小时内最多发送 5 次验证码只能使用一次用完即失效。这些逻辑不能靠前端 JS 控制必须在后端严格校验。我见过太多毕设前端用 JS 生成验证码并校验后端只做形式校验结果被绕过。4.3 前端交互如何用 JSP/Thymeleaf 实现“所见即所得”的账目明细很多毕设的前端就是一堆% account.getBalance() %的 JSP 表达式数据和样式混在一起改个样式就要动 Java 代码。现代 JavaWeb 项目应该用 Thymeleaf 模板引擎实现真正的 MVC 分离!-- account-detail.html -- div classaccount-card h3 th:text${account.accountName} ( ${account.accountNo} )张三 (6228...0001)/h3 div classbalance-section p当前余额span classbalance-value th:text¥ #numbers.formatDecimal(account.balance, 1, 2)¥1,234.56/span/p p可用余额span classavailable-balance th:text¥ #numbers.formatDecimal(account.availableBalance, 1, 2)¥1,234.56/span/p /div /div !-- 流水表格 -- table classtransaction-table thead tr th流水号/th th时间/th th摘要/th th收入/th th支出/th th余额/th /tr /thead tbody tr th:eachrecord : ${transactionList} td th:text${record.transactionNo}TR20240520000001/td td th:text${#dates.format(record.createTime, yyyy-MM-dd HH:mm)}2024-05-20 14:30/td td th:text${record.description}转账至李四/td td th:if${record.amount 0} th:text¥ #numbers.formatDecimal(record.amount, 1, 2)¥100.00/td td th:if${record.amount 0} th:text¥ #numbers.formatDecimal(-record.amount, 1, 2)-/td td th:text¥ #numbers.formatDecimal(record.currentBalance, 1, 2)¥1,134.56/td /tr /tbody /table关键优势#numbers.formatDecimal()是 Thymeleaf 内置的数字格式化工具自动处理千分位、小数位比 JSP 的fmt:formatNumber更简洁th:if条件渲染让收入/支出列逻辑清晰避免在 Java 代码里拼接 HTML 字符串所有数据绑定都通过${}完成Controller 只负责准备数据ModelView 只负责展示职责分明。5. 毕设交付物清单除了源码和脚本还必须包含这5份“说服力文档”一个合格的毕设不是把 WAR 包丢给老师就算完事。它必须是一套能让老师相信“这学生真的懂银行账目”的证据包。我每年审阅毕设最先看的不是代码而是这五份文档5.1 数据库设计说明书PDF3页不是 ER 图截图而是文字版的设计决策说明为什么account.balance用decimal(18,2)而不是double附 IEEE 754 浮点误差计算示例为什么journal_entry表不设外键指向transaction_record答为支持“分录先行流水后补”的异步场景如批量入账operation_log表为何用text而非json答兼容 MySQL 5.6且便于用 LIKE 模糊查询字段变化实操技巧用 PlantUML 写 ER 图比 Visio 更程序员友好。一段文本就能生成标准图startuml entity account as A { * id: bigint * account_no: varchar(32) balance: decimal(18,2) } entity journal_entry as J { * id: bigint entry_no: varchar(32) transaction_no: varchar(32) account_id: bigint debit_amount: decimal(18,2) } A }--| J : account_id enduml5.2 核心业务流程图Visio/PDF1页聚焦“转账”这一核心场景画出带异常分支的真实流程正常路径用户输入 → 密码校验 → 验证码校验 → 生成分录 → 借贷校验 → 写库 → 更新余额 → 返回成功异常路径1密码错误 → 记录失败日志 → 返回错误异常路径2借贷不平 → 回滚事务 → 记录异常日志 → 发送告警邮件模拟异常路径3数据库连接超时 → 重试机制最多3次→ 最终失败。注意流程图里每个菱形判断节点必须标注对应的 Java 方法名如[密码校验] → userService.checkPassword()让老师看到你的代码和设计是严格对齐的。5.3 接口文档Swagger UI 截图 Markdown用 Swagger 自动生成 API 文档截图附在文档里并补充关键参数说明POST /transfer的password字段不是明文是前端用 BCrypt 加密后的哈希值强调不是 MD5transaction_no字段全局唯一生成规则为TR YYYYMMDD 6位序列号status字段枚举值0失败, 1成功, 2处理中, 3已冲正并解释“冲正”是银行术语指对错误交易的反向操作。5.4 测试用例报告Excel1页列出 5 个核心测试用例每个包含用例编号TC-001用例名称正常转账100元前置条件A账户余额1000元B账户余额500元测试步骤调用/transfer接口参数fromA, toB, amount100预期结果A余额900元B余额600元生成2条分录借贷相等流水状态为成功实际结果✅ 通过附数据库查询截图关键点必须包含一个“失败用例”如 TC-005余额不足转账。预期结果是返回{code:400,msg:余额不足}而不是服务器 500 错误。这证明你处理了业务异常而不是让程序崩掉。5.5 答辩演示脚本Word2页不是照念 PPT而是预设老师可能问的问题及你的回答Q为什么不用 MyBatis-Plus 的saveBatch()而要自己写insertBatchA因为saveBatch默认不开启事务而我们的分录必须和流水主表在同一个事务里。我们重写了insertBatch方法确保它在Transactional方法内执行。Q如果转账过程中数据库宕机了怎么办AMySQL 的 InnoDB 引擎有崩溃恢复机制未提交的事务会自动回滚。我们所有写操作都在Transactional内所以宕机只会丢失未提交的事务不会导致数据不一致。Q这个系统能支撑多少并发A单机 MySQL 在 SSD 硬盘上配合连接池HikariCP maxPoolSize20实测 200 TPS 没问题。瓶颈在数据库不是 Java 代码。如果要更高并发需要读写分离或分库分表——这超出了毕设范围但我在“未来本文还有配套的精品资源点击获取
返回列表