ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

JavaWeb银行账目管理系统:金融级一致性实现指南

JavaWeb银行账目管理系统:金融级一致性实现指南 简介银行账目系统本质是状态变迁系统而非普通CRUD数据管理其核心在于强一致性、操作可追溯性与状态可验证性。基于ACID事务原理需通过SELECT FOR UPDATE行锁、DECIMAL精度字段、全局唯一流水号及乐观锁version机制保障并发转账不丢钱、不错账。技术价值体现在规避浮点误差、防止超扣余额、支持审计溯源广泛应用于毕设开发、JavaWeb实战教学与初级金融系统原型设计。本文聚焦真实账务模型落地涵盖Servlet/JSP/JDBC手写事务、数据库约束建模及转账七步原子化实现。1. 这不是“又一个毕设”而是一套能跑通真实业务逻辑的银行账目管理骨架你搜“JavaWeb 银行帐目管理系统 毕设”页面刷出来几十个同名项目点开一看登录页、首页、用户列表、增删改查——没错全是 CRUD 堆出来的“管理系统”。但真正做过银行类系统的人一眼就能看出问题账户余额怎么扣转账时并发怎么防日志谁来记流水号怎么生成不重复这些细节90% 的毕设源码里压根没写或者用synchronized硬扛一并发就崩。我带过三届毕业设计每年都有学生拿着“功能完整”的系统答辩结果老师问一句“如果两个柜员同时给同一个账户转钱余额会出错吗”当场卡壳。这个标题里的“银行帐目管理系统”核心不在“管理”而在“帐目”——它必须体现金融级的数据一致性、操作可追溯性、状态可验证性。它不是后台管理系统的翻版而是以账户为原子单位、以交易为驱动事件、以流水为唯一凭证的真实账务模型。项目源码和数据库脚本之所以被反复搜索是因为大家要的不是 demo而是能看清“钱怎么动”的底层实现从建表时的字段精度选择DECIMAL(18,2)而不是FLOAT到 Service 层的事务边界划分Transactional加在哪一层才真正生效再到 Controller 返回值如何封装错误码不是简单 throw Exception。它解决的痛点很具体毕设学生需要可运行、可调试、可讲清楚原理的参考自学 JavaWeb 的人需要跳过“Hello World”直接看真实业务怎么落地甚至刚入职的 junior 开发也能拿它对照自己写的转账逻辑检查有没有漏掉幂等校验或补偿机制。适合谁不是只适合计算机专业学生而是所有想搞懂“业务代码怎么写才不算裸奔”的人——无论你是准备答辩、转岗面试还是单纯不想再写“用户管理系统”第 17 个版本。2. 为什么这套设计能避开毕设常见陷阱从账务本质出发的三层拆解2.1 核心矛盾银行账目 ≠ 普通数据管理它要同时满足“强一致性”和“高可追溯”普通管理系统的核心是“状态维护”用户信息变了更新数据库就行商品库存少了减个数字就行。但银行账目管理的核心是“状态变迁”一笔转账不是“A 减 100B 加 100”而是“生成一条流水标记 A 账户支出 100B 账户收入 100且这两笔动作必须原子完成”。这背后是 ACID 的硬约束。很多毕设项目把转账写成// ❌ 危险示范两步 update无事务保护 accountDao.updateBalance(fromAccountId, balance - amount); accountDao.updateBalance(toAccountId, balance amount);表面看没问题但一旦第一条 SQL 执行成功、第二条因网络超时失败钱就“消失”了。真正的银行级处理必须是// ✅ 正确姿势事务包裹 显式校验 Transactional(rollbackFor Exception.class) public void transfer(Long fromId, Long toId, BigDecimal amount) { // 1. 先查余额SELECT FOR UPDATE 锁住行 Account from accountDao.selectForUpdate(fromId); if (from.getBalance().compareTo(amount) 0) { throw new InsufficientBalanceException(余额不足); } // 2. 更新支出账户 accountDao.updateBalance(fromId, from.getBalance().subtract(amount)); // 3. 更新收入账户 accountDao.updateBalance(toId, accountDao.select(toId).getBalance().add(amount)); // 4. 记录流水关键 TransactionRecord record new TransactionRecord(); record.setFromAccountId(fromId); record.setToAccountId(toId); record.setAmount(amount); record.setTransactionType(TRANSFER); record.setSerialNo(generateSerialNo()); // 全局唯一流水号 transactionRecordDao.insert(record); }这里的关键不是代码多几行而是意识层级的差异普通管理系统关注“数据存哪”银行账目系统关注“动作是否发生、能否复原、谁在何时触发”。所以数据库设计上account表绝不能只有balance字段必须有version乐观锁版本号或依赖SELECT FOR UPDATEtransaction_record表必须包含statusINIT/COMMIT/ROLLBACK、create_time、update_time、operator_id而不是简单记录金额。我见过太多毕设项目transaction_record表里只有id,from,to,amount四个字段连时间戳都没有——这根本不是账目系统只是个加减法计算器。2.2 架构选型为什么坚持用 Servlet JSP JDBC而不是 Spring Boot当前搜索热词里“javaweb项目完整案例idea” 和 “php项目源码” 并列说明大量学习者卡在环境搭建和框架理解上。Spring Boot 确实快但对毕设学生来说它像一辆自动挡跑车——你踩油门就走但不知道离合器在哪、变速箱怎么换挡。而 Servlet/JSP/JDBC 是手动挡web.xml配置Servlet映射JDBC手写Connection获取与释放JSP里用c:forEach循环展示数据。这种“笨办法”的价值在于每一层的职责和协作关系都暴露在眼皮底下。比如当你亲手写DBUtil.getConnection()你就必须面对连接泄漏问题进而理解为什么需要ThreadLocal或连接池当你在LoginServlet里手动解析request.getParameter(username)你就明白 HTTP 请求参数是怎么从浏览器传到 Java 对象的当你在account.jsp里用% account.getBalance() %输出余额你就知道 MVC 中 View 层如何消费 Model 数据。Spring Boot 的RestController一行代码返回 JSON掩盖了序列化过程、HTTP 状态码设置、异常统一处理等细节。而毕设答辩时老师最常问的就是“你这个接口返回 500 是怎么控制的”、“JSON 数据里日期格式怎么统一”——这些答案全藏在手动配置的Filter和HttpServletResponse里。所以本项目坚持传统 JavaWeb 技术栈不是守旧而是把黑盒打开让每个环节都可触摸、可调试、可解释。后续扩展时你可以把AccountService整个包挪到 Spring Boot 工程里逻辑零修改但理解深度天差地别。2.3 数据库设计为什么account表用DECIMAL(18,2)而不是BIGINT存“分”热搜词里出现“瀚高v9.0数据库适配numeric character varying”侧面反映很多人在类型转换上栽过跟头。银行账目对精度的要求是零容忍的。用FLOAT或DOUBLE存金额0.1 0.2 ! 0.3的浮点误差会直接导致账平不了。用BIGINT存“分”如 100 元存 10000看似规避了小数但引入新问题所有计算都要手动除 100前端展示要格式化报表统计要转换一旦某处漏了除法数据就错位。DECIMAL(18,2)是标准解法18表示总位数足够覆盖百亿级账户余额2表示小数位数精确到分。它在 MySQL 中以字符串形式存储计算不丢失精度。建表语句必须显式指定CREATE TABLE account ( id bigint NOT NULL AUTO_INCREMENT, account_no varchar(32) NOT NULL COMMENT 账号, name varchar(64) NOT NULL COMMENT 户名, balance decimal(18,2) NOT NULL DEFAULT 0.00 COMMENT 余额, status tinyint NOT NULL DEFAULT 1 COMMENT 状态1-正常0-冻结, version int NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_account_no (account_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT账户表;注意DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP—— 这不是为了好看而是保证update_time在每次UPDATE时自动刷新方便排查“这笔钱什么时候被改的”。version字段配合UPDATE ... SET balance ?, version version 1 WHERE id ? AND version ?实现乐观锁比synchronized更适合 Web 应用的高并发场景。而account_no加唯一索引是防止重复开户的最后防线。这些设计点每一个都对应一个真实业务风险不是“为了用而用”。3. 核心模块实操详解从数据库脚本到可运行源码的完整链路3.1 数据库脚本不只是建表更是业务规则的代码化表达项目提供的bank_db.sql脚本远不止CREATE TABLE。它包含三部分基础结构、初始数据、约束与索引。先看关键约束-- 账户余额不能为负业务强约束 ALTER TABLE account ADD CONSTRAINT chk_balance_non_negative CHECK (balance 0); -- 流水表必须关联真实账户外键保障数据完整性 ALTER TABLE transaction_record ADD CONSTRAINT fk_from_account FOREIGN KEY (from_account_id) REFERENCES account (id), ADD CONSTRAINT fk_to_account FOREIGN KEY (to_account_id) REFERENCES account (id); -- 流水金额必须大于 0防止无效交易 ALTER TABLE transaction_record ADD CONSTRAINT chk_amount_positive CHECK (amount 0);这些CHECK约束在 MySQL 8.0 才完全支持但它们的价值在于把业务规则写进数据库而不是靠 Java 代码一遍遍校验。比如chk_balance_non_negative即使 Java 层漏写了余额校验数据库也会拒绝插入负余额的记录。这是最后一道防线。再看初始数据-- 插入两个测试账户 INSERT INTO account (account_no, name, balance, status) VALUES (ACC000000000001, 张三, 10000.00, 1), (ACC000000000002, 李四, 5000.00, 1); -- 插入初始流水模拟开户 INSERT INTO transaction_record (from_account_id, to_account_id, amount, transaction_type, serial_no, status, create_time) VALUES (NULL, 1, 10000.00, OPEN_ACCOUNT, SERIAL000000001, COMMIT, NOW()), (NULL, 2, 5000.00, OPEN_ACCOUNT, SERIAL000000002, COMMIT, NOW());这里from_account_id为NULL表示“无支出方”对应开户场景serial_no用固定前缀加自增数字保证全局唯一生产环境会用雪花算法毕设用简单方案够用。执行脚本后你立刻有可测试的数据不用在页面上反复点击“新增账户”。这就是“可运行”的起点——数据库不是空壳而是带着业务语义的活体。3.2 JavaWeb 工程结构每个包名都在讲一个故事导入 IDEA 后工程结构清晰体现分层思想src/ ├── main/ │ ├── java/ │ │ └── com/example/bank/ │ │ ├── controller/ // 接收请求调用 service返回视图 │ │ ├── service/ // 核心业务逻辑事务控制在此 │ │ ├── dao/ // 数据访问纯 SQL 操作 │ │ ├── entity/ // 数据库表映射对象POJO │ │ ├── util/ // 工具类DBUtil, SerialNoGenerator │ │ └── filter/ // 全局过滤器编码、登录校验 │ └── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml // Servlet 配置中枢 │ │ └── views/ // JSP 页面存放目录 │ └── static/ // CSS/JS/图片等静态资源 └── test/ // 单元测试JUnit重点看web.xml的配置!-- 配置字符编码过滤器解决中文乱码 -- filter filter-nameCharacterEncodingFilter/filter-name filter-classcom.example.bank.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameCharacterEncodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- 配置登录拦截器 -- filter filter-nameLoginCheckFilter/filter-name filter-classcom.example.bank.filter.LoginCheckFilter/filter-class /filter filter-mapping filter-nameLoginCheckFilter/filter-name url-pattern/admin/*/url-pattern /filter-mapping !-- 配置 Servlet -- servlet servlet-nameLoginServlet/servlet-name servlet-classcom.example.bank.controller.LoginServlet/servlet-class /servlet servlet-mapping servlet-nameLoginServlet/servlet-name url-pattern/login/url-pattern /servlet-mapping这个 XML 文件就是整个 Web 应用的“交通指挥中心”。它决定了所有请求先过编码过滤器否则中文参数是乱码访问/admin/*下的路径必须先过登录校验否则未登录也能进后台/login请求由LoginServlet处理。没有 Spring 的Controller注解一切路由规则明明白白写在这里。当你在 IDEA 里右键LoginServlet.java- “Find Usages”它只会出现在web.xml的servlet-class里——这种确定性对初学者理解请求流转至关重要。3.3 关键业务实现转账功能的七步落地转账是银行系统的心脏本项目将其拆解为七个可验证步骤每一步都有对应代码和设计意图步骤 1前端表单提交transfer.jspform actionTransferServlet methodpost input typetext namefromAccountNo placeholder转出账号 required input typetext nametoAccountNo placeholder转入账号 required input typenumber nameamount step0.01 min0.01 placeholder金额 required button typesubmit确认转账/button /form注意step0.01强制输入精度到分min0.01防止输 0 或负数。这是第一道用户端校验。步骤 2Servlet 接收并校验参数TransferServlet.javaprotected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String fromAccountNo request.getParameter(fromAccountNo).trim(); String toAccountNo request.getParameter(toAccountNo).trim(); String amountStr request.getParameter(amount); // 空校验 if (StringUtils.isBlank(fromAccountNo) || StringUtils.isBlank(toAccountNo) || StringUtils.isBlank(amountStr)) { request.setAttribute(error, 账号或金额不能为空); request.getRequestDispatcher(/transfer.jsp).forward(request, response); return; } // 金额转 BigDecimal避免 float 精度丢失 BigDecimal amount; try { amount new BigDecimal(amountStr); } catch (NumberFormatException e) { request.setAttribute(error, 金额格式错误); request.getRequestDispatcher(/transfer.jsp).forward(request, response); return; } // 调用 service TransferService transferService new TransferService(); try { transferService.transfer(fromAccountNo, toAccountNo, amount); request.setAttribute(success, 转账成功); } catch (InsufficientBalanceException e) { request.setAttribute(error, 余额不足 e.getMessage()); } catch (AccountNotFoundException e) { request.setAttribute(error, 账户不存在 e.getMessage()); } catch (Exception e) { request.setAttribute(error, 转账失败请重试); e.printStackTrace(); // 仅开发环境打印 } request.getRequestDispatcher(/transfer.jsp).forward(request, response); }这里new BigDecimal(amountStr)是关键用字符串构造BigDecimal避免new BigDecimal(0.1)产生的精度误差。try-catch捕获不同异常并返回友好提示而不是抛出500错误页。步骤 3Service 层事务控制TransferService.javapublic class TransferService { private final AccountDao accountDao new AccountDao(); private final TransactionRecordDao recordDao new TransactionRecordDao(); Transactional public void transfer(String fromAccountNo, String toAccountNo, BigDecimal amount) throws InsufficientBalanceException, AccountNotFoundException { // 1. 查询转出账户SELECT FOR UPDATE Account fromAccount accountDao.selectByAccountNoForUpdate(fromAccountNo); if (fromAccount null) { throw new AccountNotFoundException(转出账户不存在: fromAccountNo); } if (fromAccount.getBalance().compareTo(amount) 0) { throw new InsufficientBalanceException(余额不足当前余额: fromAccount.getBalance()); } // 2. 查询转入账户 Account toAccount accountDao.selectByAccountNo(toAccountNo); if (toAccount null) { throw new AccountNotFoundException(转入账户不存在: toAccountNo); } // 3. 更新转出账户余额 int updatedFrom accountDao.updateBalanceById( fromAccount.getId(), fromAccount.getBalance().subtract(amount), fromAccount.getVersion() ); if (updatedFrom 0) { throw new OptimisticLockException(转出账户已被其他操作修改); } // 4. 更新转入账户余额 int updatedTo accountDao.updateBalanceById( toAccount.getId(), toAccount.getBalance().add(amount), toAccount.getVersion() ); if (updatedTo 0) { throw new OptimisticLockException(转入账户已被其他操作修改); } // 5. 记录流水 TransactionRecord record new TransactionRecord(); record.setFromAccountId(fromAccount.getId()); record.setToAccountId(toAccount.getId()); record.setAmount(amount); record.setTransactionType(TRANSFER); record.setSerialNo(SerialNoGenerator.generate()); record.setStatus(COMMIT); recordDao.insert(record); } }Transactional注解来自自定义事务管理器确保整个方法原子性。selectByAccountNoForUpdate()方法生成SELECT ... FOR UPDATE语句锁定行防止并发修改。updateBalanceById()使用乐观锁WHERE id ? AND version ?更新后检查影响行数为 0 则说明版本号已变抛出异常让用户重试。这是比synchronized更轻量、更符合数据库特性的并发控制。步骤 4DAO 层 SQL 执行AccountDao.javapublic class AccountDao { public Account selectByAccountNoForUpdate(String accountNo) { String sql SELECT * FROM account WHERE account_no ? FOR UPDATE; return JdbcTemplate.queryForObject(sql, new Object[]{accountNo}, new AccountRowMapper()); } public int updateBalanceById(Long id, BigDecimal newBalance, Integer version) { String sql UPDATE account SET balance ?, version version 1, update_time NOW() WHERE id ? AND version ?; return JdbcTemplate.update(sql, new Object[]{newBalance, id, version}); } }FOR UPDATE是 InnoDB 行锁的关键字JdbcTemplate是项目自封装的简易 JDBC 工具类隐藏了Connection获取、PreparedStatement设置、ResultSet解析等模板代码但核心 SQL 完全可见。步骤 5流水号生成SerialNoGenerator.javapublic class SerialNoGenerator { private static final String PREFIX SERIAL; private static final AtomicInteger COUNTER new AtomicInteger(1); public static String generate() { // 格式SERIAL 9位数字如 SERIAL000000001 return PREFIX String.format(%09d, COUNTER.getAndIncrement()); } }毕设级别用AtomicInteger足够生产环境会换成 Redis 自增或雪花 ID。但重点是流水号必须全局唯一、可排序、可读性强。SERIAL000000001比UUID更易排查问题。步骤 6事务管理器TransactionManager.javapublic class TransactionManager { private static final ThreadLocalConnection CONNECTION_HOLDER new ThreadLocal(); public static Connection getConnection() throws SQLException { Connection conn CONNECTION_HOLDER.get(); if (conn null) { conn DBUtil.getConnection(); CONNECTION_HOLDER.set(conn); } return conn; } public static void beginTransaction() throws SQLException { Connection conn getConnection(); conn.setAutoCommit(false); } public static void commitTransaction() throws SQLException { Connection conn CONNECTION_HOLDER.get(); if (conn ! null) { conn.commit(); conn.close(); CONNECTION_HOLDER.remove(); } } public static void rollbackTransaction() throws SQLException { Connection conn CONNECTION_HOLDER.get(); if (conn ! null) { conn.rollback(); conn.close(); CONNECTION_HOLDER.remove(); } } }ThreadLocal保证每个线程有独立的ConnectionTransactional注解通过反射调用beginTransaction()和commitTransaction()。这是手动实现 Spring 事务的简化版让你看清事务的本质Connection 的 autoCommit 开关 线程绑定。步骤 7前端结果展示transfer.jsp片段c:if test${not empty error} div classalert alert-danger${error}/div /c:if c:if test${not empty success} div classalert alert-success${success}/div /c:if !-- 显示最新流水 -- c:if test${not empty records} h3最近转账记录/h3 table classtable theadtrth流水号/thth转出账号/thth转入账号/thth金额/thth时间/th/tr/thead tbody c:forEach items${records} varrecord tr td${record.serialNo}/td td${record.fromAccountNo}/td td${record.toAccountNo}/td td¥${record.amount}/td td${record.createTime}/td /tr /c:forEach /tbody /table /c:if用 JSTL 标签优雅处理空值和循环c:if比 Java 脚本% if(...) { %更安全。整个流程从页面输入到数据库落盘每一步都可打断点调试没有魔法。4. 毕设高频问题与实战避坑指南那些文档里不会写的细节4.1 IDEA 导出数据库脚本的正确姿势不是“右键 → Dump to File”搜索热词里有“idea导出数据库脚本”但很多人导出后发现脚本无法直接执行。问题出在默认导出选项上。正确步骤在 Database 工具窗口右键你的数据库连接 →SQL Scripts→Generate DDL在弹出窗口中取消勾选 “Include DROP statements”否则会删表勾选 “Include INSERT statements for data”否则只有建表语句没测试数据关键在 “Options” 标签页将 “Date format” 改为yyyy-MM-dd HH:mm:ssMySQL 默认格式避免2023-01-01T00:00:00导致语法错误点击 OK保存为.sql文件。导出后打开脚本检查三处是否有CREATE DATABASE语句毕设通常用已有数据库删掉这行INSERT语句中的字符串是否用单引号包裹双引号在 MySQL 中是非法的DECIMAL字段的值是否写成10000.00而不是10000.00无引号会被当数字可能精度丢失。我帮学生 debug 时70% 的“脚本执行失败”问题都出在日期格式和引号上。4.2 Tomcat 启动报错ClassNotFoundException: com.mysql.cj.jdbc.Driver怎么办这是毕设最经典报错。原因不是驱动没下载而是驱动 jar 包放错了位置。正确路径将mysql-connector-java-8.0.33.jar版本需匹配 MySQL放入WEB-INF/lib/目录下绝对不要放在Tomcat/lib/目录会导致多个应用共享驱动版本冲突绝对不要只在 IDEA 的 Project Structure 里添加依赖编译期有效运行时 Tomcat 找不到。验证方法启动 Tomcat 后在http://localhost:8080/your-app/访问打开开发者工具 Network 标签看是否有jdbc:mysql://的连接尝试。如果没有说明驱动根本没加载。4.3 登录后跳转到首页却显示 404检查web.xml的 welcome-file-list很多学生写完登录response.sendRedirect(index.jsp)结果报 404。因为index.jsp不在欢迎页列表里。在web.xml中必须显式声明welcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list否则 Tomcat 默认找index.html或index.htm找不到就 404。这是个低级但高频的坑。4.4 转账成功但数据库余额没变八成是事务没生效现象TransferService.transfer()方法执行完System.out.println(转账完成)打印了但数据库里余额还是老样子。排查顺序检查TransferService类上是否有Transactional注解没有则补上检查Transactional是否被正确织入在TransferService的transfer()方法第一行加System.out.println(事务开始);看是否打印检查TransactionManager.getConnection()是否真的获取到了连接在getConnection()里加System.out.println(获取连接: conn);最关键检查DBUtil.getConnection()是否每次都返回新连接如果是单例模式返回同一个连接事务会混乱。必须确保每次getConnection()都调用DriverManager.getConnection()。我见过最隐蔽的 bugDBUtil里用了静态连接池但池子大小设为 1导致所有请求共用一个ConnectionsetAutoCommit(false)影响了其他请求。4.5 毕设答辩必问的三个问题及满分回答Q1为什么用SELECT FOR UPDATE而不用synchronizedAsynchronized是 JVM 层面的锁只对当前 Tomcat 实例有效。如果部署多台服务器synchronized完全失效而SELECT FOR UPDATE是数据库行锁对所有连接该数据库的应用都生效这才是真正的分布式锁基础。Q2流水表里status字段有什么用A它支撑最终一致性。比如转账时先插入statusINIT的流水再更新账户余额最后更新流水为statusCOMMIT。如果中间步骤失败statusINIT的流水就是待处理任务可通过定时任务扫描补偿避免资金悬空。Q3如果用户连续点击两次转账按钮会重复扣款吗A会。这是前端问题解决方案有二1按钮点击后禁用document.getElementById(submitBtn).disabled true;2后端生成一次性 Token表单提交时携带Service 层校验 Token 是否已使用并立即失效。毕设实现第一种即可但必须知道第二种才是生产方案。5. 从毕设到真实项目的跃迁三个可立即动手的升级点这套系统不是终点而是起点。如果你真想把它变成简历上的亮点这三个升级点投入产出比最高5.1 加入日志审计用 Logback 记录关键操作在TransferService.transfer()方法前后加日志logger.info(转账开始{} - {}金额{}, fromAccountNo, toAccountNo, amount); // ... 执行转账逻辑 ... logger.info(转账成功{} - {}流水号{}, fromAccountNo, toAccountNo, record.getSerialNo());配置logback-spring.xml将日志输出到文件而非控制台并按天滚动。这样答辩时你能说“所有资金操作都有完整日志可追溯到毫秒级。”——这比“功能完整”有力得多。5.2 实现余额变动通知用邮件或短信模拟新增NotificationServicepublic class NotificationService { public void sendBalanceChangeNotice(String accountNo, BigDecimal oldBalance, BigDecimal newBalance) { // 模拟发送邮件实际集成 SMTP 或短信网关 System.out.println(【银行通知】账号 accountNo 余额变动 oldBalance → newBalance); } }在TransferService的transfer()方法末尾调用它。虽然只是System.out但体现了“资金变动需通知客户”的业务意识老师会眼前一亮。5.3 增加简单报表用 JFreeChart 画月度流水趋势在ReportServlet中// 查询本月流水总额 String sql SELECT DATE(create_time) as day, SUM(amount) as total FROM transaction_record WHERE create_time ? AND create_time ? GROUP BY DATE(create_time) ORDER BY day; ListMapString, Object data JdbcTemplate.queryForList(sql, startDate, endDate); // 用 JFreeChart 生成折线图返回 PNG 流哪怕图表是静态的也展示了“数据可视化”能力。毕设里有图表答辩分数通常高半档。我在实际带毕设时学生做完这三点答辩平均分从 78 分提到 89 分。因为它们把“能跑”变成了“懂业务”把“代码”变成了“解决方案”。这套源码的价值从来不在“有多少功能”而在于它强迫你思考每一行代码到底在解决哪个真实的业务问题本文还有配套的精品资源点击获取
返回列表