简介:基于JavaWeb技术栈的银行帐目管理系统毕业设计项目,面向计算机相关专业正在准备毕设的学生,以及需要项目实战练习的Java学习者。系统采用B/S结构,后端基于JSP、Servlet、JDBC框架,数据库使用MySQL,开发环境涵盖JDK、Eclipse与Tomcat,项目经过严格调试可确保直接运行。资源包共含3个文件,包括项目源码、MySQL数据库脚本及项目说明文档,整体大小仅1.24MB,部署所需核心材料一应俱全。功能覆盖用户与管理员双角色,包含帐户管理(存款、取款、开户、销户、办卡、挂失卡)、取款机信息管理、用户查询,以及VIP统计、ATM业务量统计、消费统计等查询统计模块,可作为完整毕设参考方案或课程设计项目使用。目前已有2301人学习浏览,系统功能完善、界面简洁、操作便捷,具有较高实际应用价值。
1. JavaWeb银行帐目管理系统为什么是毕设的“安全牌”:一个选题与落地的快照
“基于JavaWeb的银行帐目管理系统【项目源码+数据库脚本】(毕设)”这个标题每年都出现在各种源码包里,它之所以常青,不是因为技术新,而是因为它把JavaWeb的核心链路——Servlet、JSP、JDBC、Session、Tomcat、MySQL——完整地串了一遍。它解决的问题非常具体:毕设阶段需要一套能快速跑通、能讲清楚原理、能拿上台答辩的Web应用;它适合的人也明确:对JavaWeb还没有形成完整闭环、想用一套经典代码把从前端表单到后端数据库所有环节打通的同学。选题够老不等于没坑,源码拿到手不是双击就能跑,数据库脚本导入失败、Tomcat版本和IDEA配置不匹配,都足够卡你一两天。这篇笔记按“拆结构、跑通、调JDBC、避坑、增强”的顺序,把这条路的完整走法写给你。
2. 拆开项目源码:技术栈选型与三张核心表结构设计
拿到一个JavaWeb毕设源码包,第一步不是急着点运行,而是先看清它用的是什么技术栈、业务模块分了几块、数据库表结构长什么样。这三件事直接决定你能不能跑起来、答辩时能不能讲明白。
2.1 为什么还选Servlet+JSP+JDBC,而不是SpringBoot全家桶
很多同学拿到源码包的第一反应是:都什么年代了还在用Servlet?毕设难道不该上SpringBoot?这个想法在“做项目”的语境里是对的,但在“毕设答辩”的语境里要打个问号。
毕设考察的是你对JavaWeb核心知识的掌握程度,Servlet+JSP+JDBC这套组合恰好把HTTP请求到数据库访问的每一个环节都裸露出来:Servlet处理请求、JSP渲染页面、JDBC操作数据库、Filter做编码过滤、Session管登录态。这些恰恰是SpringBoot帮你封装掉的部分。老师问“你的过滤器在哪”、“事务怎么控制的”、“Session什么时候失效”,用这套老组合你能直接指到代码行;用SpringBoot的话,回答往往变成“自动配置的”,这就失去了答辩的主动权。
另外,这套组合在源码包里“能跑通”的概率极高。Servlet+JSP项目对Tomcat版本、JDK版本的要求没那么苛刻,Tomcat 8.5配JDK 8基本是标准组合。相比之下,SpringBoot项目动不动就遇到依赖版本冲突、Maven仓库下载失败,对毕设来说这些完全是额外风险。
当然,答辩时要会说一句话:真实银行系统不会用JSP,这里选Servlet+JSP是为了把JavaWeb的技术闭环完整显现出来。这句表态能让老师觉得你清楚工程实际和教学场景的区别。
2.2 银行帐目管理系统的四个业务模块分别管什么
不管源码包里的类名怎么起,银行业务的核心模块逃不出下面这四块。你拿到的项目源码大概率是按照这个结构组织Servlet和JSP页面的。
第一是登录与权限管理。系统有管理员和柜员两类角色,登录后把用户对象放进Session,每次请求通过Filter或Servlet基类校验登录态。这里要注意的是:密码不能明文入库,常见做法是MD5摘要后入库,有的项目会给MD5加盐,这在答辩时是一个加分细节。
第二是账户管理。包括开户、销户、冻结、修改客户信息。开户时生成唯一账号,初始余额一般设为0;销户通常是逻辑删除,把账户状态置为0,而不是物理删除记录,原因后面讲表结构时会提到。
第三是存取款与转账。这是整个系统的核心。存款单账户加钱并写一条流水;取款要判断余额是否充足;转账涉及两个账户,转出方扣款、转入方加款,同时产生两条流水。转账是唯一必须上数据库事务的地方,后续避坑章节会专门展开。
第四是查询与报表。按账户查流水、按日期范围查交易汇总、打印当日账单。这个模块通常用一条带时间条件的SQL加一个分页就够,是答辩演示用得最多的页面。
2.3 数据库脚本里的三张核心表:sys_user、account、trade_record
数据库脚本是整个系统能不能跑起来的前提。我见过太多项目,源码能编译,但数据库脚本是SQL Server方言,导入MySQL报错一堆。所以拿到脚本先认表结构,下面这个建表方案是JavaWeb银行系统最常见的模板,可以直接对照你的源码包。
-- 系统用户表:存登录系统的人,不存银行客户 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5摘要,不要存明文', real_name VARCHAR(50) COMMENT '柜员真实姓名', role TINYINT DEFAULT 1 COMMENT '1-管理员 2-柜员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表'; -- 银行账户表:一人可持多账户 CREATE TABLE account ( id INT PRIMARY KEY AUTO_INCREMENT, account_no VARCHAR(20) NOT NULL UNIQUE COMMENT '账号,业务生成不靠自增', customer_name VARCHAR(50) NOT NULL COMMENT '客户姓名', balance DECIMAL(15,2) NOT NULL DEFAULT 0.00 COMMENT '余额,禁止用double', status TINYINT DEFAULT 1 COMMENT '1-正常 0-冻结 2-销户', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行账户表'; -- 交易流水表:每一笔存取、转账留下痕迹 CREATE TABLE trade_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, account_id INT NOT NULL COMMENT '所属账户,程序层关联account', trade_type TINYINT COMMENT '1-存款 2-取款 3-转入 4-转出', amount DECIMAL(15,2) NOT NULL COMMENT '交易金额,恒为正数', balance_after DECIMAL(15,2) NOT NULL COMMENT '交易后余额,用于对账', remark VARCHAR(255) COMMENT '备注,可存对方账号', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_account_time (account_id, create_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易流水表';这段SQL里最关键的设计是balance DECIMAL(15,2)和trade_record.balance_after。用DECIMAL而不是double,是因为浮点数在二进制里无法精确表示十进制小数,0.1加0.2在double下等于0.30000000000000004,银行账目分毫不差是底线,这个坑后面专门讲。balance_after记录每笔交易后的余额,等于给每次余额变动留下快照,答辩时说“可以用它做全链路对账”,比单纯说“我建了流水表”高一个层次。
另一个细节是:三张表之间我没有写物理外键。这不是偷懒,而是毕设项目里物理外键的收益很低、坑很多:销户时如果有流水子记录,外键约束会挡住删除;修改主键ID时也要连带修改子表。常见做法是在应用层维护逻辑关联,trade_record.account_id指向account.id,但不建FOREIGN KEY。这个取舍答辩时也可以主动讲,老师通常认可。
3. 用IDEA跑通源码:从导入项目到Tomcat启动的六个步骤
源码包到手,最急迫的问题是“怎么在IDEA里跑起来”。这个环节翻车率最高,但翻车点非常集中,基本集中在项目识别、Artifact配置和Tomcat版本三个地方。按下面六步走,能避掉八成问题。
3.1 拿到源码包先看这四处,避免盲猜
不要解压后直接双击.iml或.idea文件让IDEA打开,那样经常导入成普通Java项目,Web目录识别不了。我一般先解压,然后用文件管理器看四样东西。
第一是README或部署文档,里面通常写了数据库用户名密码、MySQL版本、Tomcat版本、JDK版本。第二是sql目录,看脚本第一行是MySQL的CREATE DATABASE还是SQL Server的IF NOT EXISTS,这决定你后面能不能顺利导入。第三是web/WEB-INF/web.xml,看<servlet-version>是多少、有没有配置<welcome-file>、有没有过滤器。第四是源码目录里有没有残留的out或target文件夹,有的话建议先删掉,防止IDEA识别到旧的编译产物。
一个常见坑是解压出双层目录,比如BankAccountSys-master/BankAccountSys,导入时选错层级会导致后面所有路径对不上。建议先把目录改名成无中文、无空格的BankAccountSys,放到一个路径里没有中文的目录下。中文路径在Tomcat部署时偶尔会触发奇怪的静态资源404,属于玄学问题,提前规避。
下面是这个类型项目最常见的目录骨架,你可以对照自己的源码包看缺了什么。
BankAccountSys/ ├── src/ │ └── com/edu/bank/ │ ├── servlet/ # 所有Servlet控制器 │ ├── dao/ # JDBC数据访问层 │ ├── service/ # 业务逻辑层 │ ├── utils/ # DBUtil、MD5工具类 │ └── model/ # 实体类 ├── web/ │ ├── WEB-INF/ │ │ ├── web.xml # 部署描述符 │ │ └── lib/ # mysql-connector-java.jar 必须在这 │ ├── css/ │ ├── js/ │ └── *.jsp ├── sql/ │ └── bank.sql # 数据库脚本 └── README.md判断这个骨架是否完整的关键是web/WEB-INF/lib下有没有mysql-connector-java.jar。这个jar是连接MySQL的桥梁,很多项目源码里没带,需要你自己下载对应版本放进去,否则后面会报ClassNotFoundException。
3.2 IDEA导入JavaWeb项目的两种方式与Web目录识别
在IDEA里导入有两种常见操作。第一种,也是我推荐的方式:File > New > Project from Existing Sources,选中解压后的根目录,IDEA会弹出导入向导。如果你是Maven项目就选Maven,但这个技术的毕设项目绝大多数不是Maven,直接选Next到底,让IDEA自动识别src为Sources Root、web为Web Resource Directory。
第二种方式是File > Open直接打开,然后手动进Project Structure > Modules调整。这种方式容易漏配Web资源目录,导致JSP页面找不到,我不太建议新手用。
导入完成后,打开Project Structure > Project确认三件事:SDK是JDK 8或11;Language Level设为8;Modules的Web页签里能看到web目录被标记为Web Resource Directory。这三项不对,后面编译和部署都会出幺蛾子。
如果项目里带着.class文件,建议先在Build > Rebuild Project前把它们清理干净,防止旧的字节码覆盖新编译结果。这个操作虽小,但能避免很多“我改了代码没生效”的灵异事件。
3.3 Tomcat部署参数:Artifact、context path和端口
项目导入成功后,接着配置Tomcat运行环境。先确认你本地装了Tomcat 8.5或9.x,然后按下面的顺序操作。
Run > Edit Configurations,点左上角加号,选择Tomcat Server > Local。在Application server那里选中你本地的Tomcat目录。如果下拉框是空的,说明IDEA没识别到Tomcat,需要先Settings > Build, Execution, Deployment > Application Servers手动添加Tomcat路径。
切到Deployment页签,点加号选Artifact,弹出的列表里选择xxxx:war exploded。这里有两个关键点:选exploded而不是war,exploded表示解压目录部署,改动Java代码后热更新更快,毕设调试阶段用这个合适;Application context建议填成/bank,这样访问路径是http://localhost:8080/bank/login.jsp,如果留空或填/,会把Tomcat根路径整个占掉,和Tomcat自带应用冲突。
启动前顺手把三个参数调好,见下表:
| 配置项 | 推荐值 | 说明 |
|---|---|---|
| VM options | -Dfile.encoding=UTF-8 | 防止运行时读取资源文件中文乱码 |
| On Update Action | Update classes and resources | 改Java/JSP后能快速生效 |
| On Frame Deactivation | Update classes and resources | 切走窗口时自动更新资源 |
| HTTP port | 8080或8081 | 8080被占用时换8081,并检查是否有残留进程 |
点Debug启动,控制台出现Connected to server基本就算成功了一半。然后验证HTTP服务是否真的通了,用下面的命令检查:
# 在终端验证Tomcat是否返回HTTP 200 curl -I http://localhost:8080/bank/login.jsp如果返回HTTP/1.1 200,说明项目和Tomcat已经打通;如果404,先看是不是Application context填错了,再看web.xml里的欢迎页配置。这一步能帮你把“IDEA问题”和“项目问题”分开排查。
4. 让数据库脚本说话:导入bank.sql并打通JDBC数据链路
Tomcat能启动只是表象,真正让系统“活”起来的是数据库。JavaWeb银行系统里,几乎每个页面后面都站着至少一条SQL。数据库脚本导入不成功、JDBC连接参数不对,系统就是一个空壳。
4.1 执行bank.sql的两种方式:Navicat与命令行
拿到bank.sql,先看里面有没有CREATE DATABASE语句。很多脚本开头自己建库,有些则不建,需要你手动先建好库再导入。
用Navicat的话,操作路径是:连接MySQL后,新建一个数据库,名字要和脚本里的USE xxx一致或者和JDBC URL里的库名一致,然后右键这个库,选择运行SQL文件。弹窗里记得把编码选成UTF-8,这个选项在Navicat里容易被忽略,选错会出现中文乱码或建表失败。
用命令行的话,可以分两步执行。先建库并设置字符集,再导入脚本:
# 建库,指定utf8mb4字符集 mysql -uroot -p -e "CREATE DATABASE IF NOT EXISTS bank_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;" # 导入脚本 mysql -uroot -p bank_db < sql/bank.sql逻辑说明:-uroot是用户名,-p会提示输入密码,bank_db是目标数据库名,<表示把文件内容喂给mysql客户端执行。
参数说明:DEFAULT CHARACTER SET utf8mb4这一步很关键。如果建库时用了默认的latin1,后面中文数据一律乱码,而且再改库字符集也救不回已经插入的乱码数据。脚本路径建议用绝对路径或先cd到项目根目录再执行,避免<路径解析出错。
导入成功的标准不是“没有报错”,而是库里真的有三张表。用SHOW TABLES;确认,再用DESC trade_record;看amount字段类型是不是DECIMAL(15,2)。如果你用的IDEA,也可以在下方的Database面板里右键数据库导出脚本,但本地开发阶段不会用这个操作,它更适合交作业时生成一份干净的bank.sql。
4.2 JDBC连接参数逐个调:MySQL8.0的四个必填参数
很多项目代码里自带一个DBUtil.java或JdbcUtils.java,封装了数据库连接逻辑。拿到手别急着运行,先看连接参数是不是匹配你本机的MySQL版本。下面是兼容MySQL 8.0的标准写法:
public class JdbcUtils { private static final String DRIVER = "com.mysql.cj.jdbc.Driver"; private static final String URL = "jdbc:mysql://localhost:3306/bank_db" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false" + "&serverTimezone=Asia/Shanghai" + "&allowPublicKeyRetrieval=true"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName(DRIVER); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL驱动jar未找到", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }逻辑说明:Class.forName是加载驱动类,MySQL 8.0之后的驱动类名是com.mysql.cj.jdbc.Driver,旧版的是com.mysql.jdbc.Driver。如果你的项目用的是旧类名同时装的是新驱动,一样会报ClassNotFoundException。
参数说明:serverTimezone=Asia/Shanghai是为了规避MySQL 8.0要求显式指定时区的报错;useSSL=false在本地开发时关闭SSL握手,既省时间又避免警告;characterEncoding=utf8配合useUnicode=true保证中文写入不乱码;allowPublicKeyRetrieval=true是给MySQL 8.0默认的caching_sha2_password认证插件用的,不设置时偶尔会报Public Key Retrieval is not allowed。
密码里的特殊字符也要注意,比如密码是abc@123,这个@在URL里会被解析成参数分隔符,必须写成abc%40123。这不是JDBC的锅,而是URL语法本身的规定。
4.3 从登录到查流水:一条SQL如何走完MVC链路
连接打通后,用登录这个最基础的功能看一遍数据是怎么流动的。用户在前端输入用户名密码,表单POST到LoginServlet,Servlet拿到参数调用DAO层,DAO用PreparedStatement执行SQL,结果封装成User对象存在Session里,跳转页面。
这段代码是DAO层查询用户的核心:
public User findByUsernameAndPassword(String username, String passwordMd5) { String sql = "SELECT id, username, real_name, role FROM sys_user " + "WHERE username = ? AND password = ?"; try (Connection conn = JdbcUtils.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, passwordMd5); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { User u = new User(); u.setId(rs.getInt("id")); u.setUsername(rs.getString("username")); u.setRealName(rs.getString("real_name")); u.setRole(rs.getInt("role")); return u; } } } catch (SQLException e) { e.printStackTrace(); } return null; }逻辑说明:PreparedStatement的?占位符由setString传入,底层做了参数化处理,天然防止SQL注入。写WHERE username = '"+username+"'这种字符串拼接的项目,在答辩时等于送分给老师抓。
参数说明:Password存的是MD5摘要,不是明文,所以这里传入的是passwordMd5。校验逻辑是把用户输入的密码做一次MD5再比对,而不是把数据库里的密文解回明文。这里常见的做法是给MD5加盐,即md5(username + "固定的盐" + password),能有效挡掉彩虹表碰撞,源码包里如果没做盐,你可以自己补上,这是低成本加分项。
Servlet层的登录处理和跳转也有讲究:
@WebServlet("/login") public class LoginServlet extends HttpServlet { private UserDao userDao = new UserDao(); protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { req.setCharacterEncoding("UTF-8"); String username = req.getParameter("username"); String password = req.getParameter("password"); User user = userDao.findByUsernameAndPassword(username, MD5Utils.md5(password)); if (user != null) { req.getSession().setAttribute("loginUser", user); resp.sendRedirect(req.getContextPath() + "/main.jsp"); } else { req.setAttribute("errorMsg", "用户名或密码错误"); req.getRequestDispatcher("/login.jsp").forward(req, resp); } } }逻辑说明:登录成功用sendRedirect重定向,失败用forward转发。区别在于:转发是服务器内部跳转,地址栏不变,request域的errorMsg还能带到JSP页面上;重定向是浏览器重新发起一次GET请求,request域里的值会丢,但能避免用户刷新页面时表单重复提交。这两个动作的语义区别是JavaWeb必考题。
参数说明:req.setCharacterEncoding("UTF-8")必须放在第一次调用getParameter之前,否则POST请求的中文参数还是乱码。如果你在过滤器里统一处理了编码,这里可以省略,但一定要保证过滤器在web.xml中的url-pattern覆盖/login。
5. 毕设级避坑手册:五个最容易翻车的JavaWeb现场
跑通和跑好之间,隔着几个经典坑。这些坑我几乎在每个JavaWeb项目里都见过,有的能快速定位,有的会让你怀疑是编码问题还是Tomcat坏了。按“现象、原因、解决”挨个说。
5.1 登录页能打开但跳转404:web.xml和context path的事
现象:启动正常,访问login.jsp正常,但输入账号密码提交后浏览器报404。
原因:通常不是Servlet代码错了,而是三个地方没对上。第一,表单的action路径和@WebServlet的映射不一致;第二,web.xml里没有配置welcome-file,或者配置了但文件名写错;第三,部署时Application context填了/bank,但页面里写的跳转地址是/login而不是/bank/login。
解决:先在浏览器地址栏手动访问http://localhost:8080/bank/login,看Servlet本身通不通。如果手动通而表单提交不通,问题在表单action。如果手动访问Servlet返回404,问题在Servlet映射或web.xml。再顺手检查一下IDEA控制台有没有Invalid mapping之类的启动警告,这个警告会直接告诉你是哪个Servlet路径没注册上。
另外有个排查习惯:每次改了web.xml或Servlet注解,必须重启Tomcat,热部署在这里不总是生效。如果改完还是404,用Build > Rebuild Project强制刷新一下,很多时候是IDEA没重新编译。
5.2 数据库连不上:ClassNotFound、Access denied、Communications link failure
现象:启动后访问查询页面,页面报500,控制台刷出一堆异常堆栈。
原因:这个坑其实是三个不同的现象盖在一顶帽子里。先看堆栈最底行写的是什么。
ClassNotFoundException: com.mysql.cj.jdbc.Driver,说明驱动jar没进WEB-INF/lib,或者IDEA的Libraries里没引用。解决方式:把jar直接复制到web/WEB-INF/lib下,然后在Project Structure > Libraries里确认它被当前Module引用,重启Tomcat。
Access denied for user 'root'@'localhost',说明用户名、密码或权限不对。先手动在命令行里mysql -uroot -p试试能不能连上,能连上说明密码没问题,那就要看URL里的密码有没有被特殊字符坑了。密码含@、&、#都会出问题,最省事的做法是把密码改成纯字母数字。
Communications link failure或The server time zone value,这俩本质是JDBC URL参数问题。serverTimezone=Asia/Shanghai必填,MySQL 8的用户建议把allowPublicKeyRetrieval=true也加上。端口也要看,MySQL默认3306,如果你本机装的是MariaDB或者改了端口,URL里的3306要改。
5.3 中文乱码:POST、GET和响应输出三个方向不一样
现象:注册的客户姓名在页面上显示成“???”,或者存入数据库后变成“å§æ”。
原因:Java里中文乱码的来源极其统一——字符从A编码写进去,被B解码读出来。POST和GET的处理方向还不一样。POST请求看的是request.setCharacterEncoding("UTF-8")有没有在读取参数前执行;GET请求看的是Tomcat连接器解码URI时用的字符集,Tomcat 8.0之后默认UTF-8所以问题少,但老项目配Tomcat 7就要改server.xml里的URIEncoding="UTF-8"。
解决:写一个过滤器统一处理,比在每个Servlet里重复设置靠谱得多:
@WebFilter("/*") public class EncodingFilter implements Filter { public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request = (HttpServletRequest) req; if (request.getMethod().equalsIgnoreCase("POST")) { request.setCharacterEncoding("UTF-8"); } resp.setCharacterEncoding("UTF-8"); resp.setContentType("text/html;charset=UTF-8"); chain.doFilter(req, resp); } }逻辑说明:@WebFilter("/*")代表拦截所有请求,POST请求在读参数之前统一设置请求编码,响应统一设置为UTF-8。注意setCharacterEncoding必须在getParameter之前调用,放到过滤器里就是为了保证这个顺序。
参数说明:如果项目是旧式web.xml配置过滤器,顺序很重要——过滤器声明顺序就是执行顺序,编码过滤器要放在其他过滤器前面。另外JSP文件头部的pageEncoding和contentType也要写成UTF-8,这属于页面静态编码,过滤器管不到。
5.4 转账后只有一条流水:事务在哪里失效了
现象:转账功能填完金额点提交,转出方扣了钱,转入方没到账,流水表里只有一条取款记录,日志里没有任何异常。
原因:这个现象我在好几个项目里见过,根子在于事务边界被切碎了。很多DAO类里的每个方法自己调JdbcUtils.getConnection()拿连接,然后自己commit自己close。这样转出方DAO提交了,转入方DAO虽然失败回滚,但转出方已经提交成功,留在数据库里。每一个数据库连接是独立事务,想让两条更新成为同一个事务,就必须让它们用同一个连接。
解决:事务应该放在Service层统一控制,连接在Service层创建,并作为参数传给DAO的方法:
Connection conn = JdbcUtils.getConnection(); try { conn.setAutoCommit(false); // 扣款,条件里带 balance >= ? 可以防超扣 int c = accountDao.decreaseBalance(conn, fromId, amount); if (c != 1) { conn.rollback(); throw new RuntimeException("余额不足"); } // 加款 accountDao.increaseBalance(conn, toId, amount); // 写两条流水 tradeDao.insert(conn, fromId, 4, amount); // 转出 tradeDao.insert(conn, toId, 3, amount); // 转入 conn.commit(); } catch (Exception e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); conn.close(); }逻辑说明:setAutoCommit(false)之后,后面所有SQL都在同一个事务里,直到commit()或rollback()。decreaseBalance返回受影响行数,如果为0说明条件没满足,比如余额不足或账户不存在,直接回滚。
注意:DAO接口里凡是涉及这个事务的方法,都不能再自己调getConnection(),必须接收外部传入的Connection。这是“事务跨方法”的典型写法,也是黑马JavaWeb笔记里反复强调的那个点。答辩时能主动说出“我用Service层控制事务边界”这句话,比写十页PPT管用。
5.5 金额四舍五入差一分:余额字段别用double
现象:每日流水汇总后,页面显示的总金额和数据库里逐笔累加的结果差几分钱,经常是出现一长串小数,比如1586.2300000000001。
原因:double是浮点数,二进制无法精确表示0.1,累计运算次数多了误差就会暴露。银行系统的金额是十进制精确量,用double等于埋雷。
解决:数据库层用DECIMAL(15,2),Java层用BigDecimal。查询结果用getBigDecimal取出,运算用add、subtract,不要用+、-。金额计算示例如下:
BigDecimal balance = rs.getBigDecimal("balance"); BigDecimal amount = new BigDecimal("100.00"); BigDecimal newBalance = balance.subtract(amount).setScale(2, RoundingMode.HALF_UP);逻辑说明:BigDecimal构造建议传字符串,new BigDecimal(0.1)会把二进制浮点误差一起带进来,相当于白转。setScale(2, RoundingMode.HALF_UP)做的是四舍五入到两位小数,这是账户金额显示的标准规则。
参数说明:页面显示金额时也不要随手doubleValue(),转成String输出。如果项目里还有金额字段用float的,一律改成DECIMAL。这个坑在银行题目里几乎是必考的,改一处就少一个答辩软肋。
6. 从“能跑”到“能答辩”:三个低成本增强点
系统能跑通只是及格线。想在答辩时让老师觉得“这个系统不是抄的”,在原有代码上补三个增强点,性价比最高。这三个点都不需要换技术栈,也不需要引入Spring,用现有Servlet+JDBC就能做。
第一个是流水查询分页。很多源码包里的查询是SELECT * FROM trade_record,数据一多页面直接卡死。改成LIMIT ?, ?秒级见效:先查SELECT COUNT(*) FROM trade_record WHERE account_id = ?拿总数,再查当前页数据,LIMIT的两个参数用setInt传,不能拼进SQL字符串。顺带加一个ORDER BY create_time DESC,按时间倒序展示,客户看账单的体验完全不一样。
第二个是当日交易汇总导出。不用上POI那套重依赖,导出CSV是更轻的路线。设置响应头response.setHeader("Content-Disposition", "attachment;filename=daily.csv"),输出流里指定GBK编码,因为Excel用GBK打开CSV才不乱码。字段里如果含逗号或换行要加双引号转义,这是一行代码级别的细节,但能看出你考虑过真实导出场景。
第三个是余额对账SQL。这是我觉得整个系统里最“银行”的一个功能——用流水表倒推账户余额,跟账户表里的余额比对:
SELECT a.id, a.account_no, a.balance, (SELECT t.balance_after FROM trade_record t WHERE t.account_id = a.id ORDER BY t.id DESC LIMIT 1) AS last_balance FROM account a HAVING a.balance <> last_balance;逻辑说明:balance_after存的是每笔交易后的余额,取最近一笔的balance_after,理论上应该等于当前余额。如果两边对不上,说明存在手工改数据或者事务丢失的情况。这个查询一出,老师能直观看到表结构设计的伏笔,比你自己介绍十句“我做了对账功能”都有效。
我这些年带毕设的一个习惯是:拿到任何源码包,先让数据库脚本跑通,再让项目在Tomcat里起来,最后才开始读代码。数据库不通,项目就是一幅空壳;项目能跑,再谈改业务。只要你把这三张表和一条转账链路摸透了,这个选题就没有什么黑匣子可言。希望帮到你。
本文还有配套的精品资源,点击获取