简介:基于JavaWeb开发的银行帐目管理系统毕业设计项目,面向计算机专业毕设学生以及需要项目实战的Java学习者,采用B/S结构,以JSP、Servlet、JDBC作为后台框架,搭配MySQL数据库,可在Eclipse+Tomcat开发环境中直接部署运行。系统包含用户与管理两种角色,功能模块较为完整:帐户管理支持存款、取款、开户、销户、修改信息、办卡与挂失;取款机信息管理兼顾管理员维护和客户查询取款;用户查询模块用于自助查看帐户信息;查询统计模块覆盖VIP用户统计、ATM业务量统计、异动查询、持卡消费统计与工作量负荷统计。项目经过调试可运行,包含源码、数据库脚本及说明文档,共3个文件:txt项目说明辅助理解、sql脚本用于初始化数据、zip源码工程供导入开发工具使用,整个资源包大小1.24MB。已有2301人学习下载,适合毕设参考、课程设计演示或JavaWeb自学实践。
1. JavaWeb银行帐目管理系统:一套能跑通“开户-存取-转账-对账”的毕设骨架
很多学生拿到“基于JavaWeb的银行帐目管理系统”这个毕设题,第一反应是把它做成一个“系统管理后台”,结果代码写了一堆,账目却对不上。实际上这个题目的灵魂不是页面,是账目本身:账户余额怎么存、流水怎么记、转账怎么保证两边同时成功。这类项目通常以源码加数据库脚本的方式交付,源码覆盖登录、开户、存取款、转账和流水查询,数据库脚本导入后就能拿到一套可直接演示的账目数据。这套方案特别适合计算机专业正在做毕设选题、需要快速跑通一个JavaWeb完整案例的人,也适合想用真实业务练手初级开发的从业者。这篇笔记把选型逻辑、实现步骤和常见坑一次讲透。
2. 选型逻辑与账目建模:先从业务上把账目设计立住
在动手导入源码、配置Tomcat之前,先回答两个问题:这套系统用哪种JavaWeb技术栈,以及账目表应该怎么建。这两个问题决定了后面所有代码读起来顺不顺,也决定了答辩时被问到“为什么这么设计”时你答不答得上来。
2.1 技术栈怎么定:Servlet/JSP、SSM还是Spring Boot
银行帐目管理系统在网上的交付形态五花八门,最常见的无非三种:纯JSP+Servlet+JDBC,SSM(Spring+SpringMVC+MyBatis),以及Spring Boot。选哪个不完全是“哪个新选哪个”,要看你的课设主线、答辩老师的熟悉度,以及你想把时间花在业务还是配置上。
| 方案 | 答辩讲解成本 | 开发效率 | 事务与SQL管理 | 适合人群 |
|---|---|---|---|---|
| 纯JSP+Servlet+JDBC | 低,请求链路直观 | 中,DAO样板代码多 | 手动管理Connection | 课程主线是JSP/Servlet的学生 |
| SSM(Spring+SpringMVC+MyBatis) | 中,分层清晰 | 中高 | @Transactional+Mapper XML | 多数高校JavaWeb课的经典主线 |
| Spring Boot | 高,要解释自动配置 | 高 | JPA或MyBatis,配置少 | 以后打算走工程开发路线的学生 |
我一般建议按自己的课程主线来,但更偏向SSM。理由是答辩时老师大概率围绕“Controller怎么调Service、Service的事务边界在哪、Mapper的SQL怎么传参”提问,SSM的每一层都能对应到具体文件,讲起来不虚。纯Servlet栈也能做,只是转账事务部分要自己写连接管理,细节多一些。
2.2 先别急着写代码:账户、流水、操作员三张表怎么建模
不管前端是JSP还是Vue,这套系统的核心都在数据库表设计上。项目数据库脚本里通常至少有四张表:bank_account(账户表)、trade_log(交易流水表)、sys_user(操作员表)、account_snapshot(日终快照表)。前三张是基础,快照表属于加分项,后面第6章会用到。
CREATE TABLE IF NOT EXISTS bank_account ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键', account_no VARCHAR(20) NOT NULL COMMENT '账号,业务编号而非主键', customer_name VARCHAR(64) NOT NULL COMMENT '户名', balance DECIMAL(16,2) NOT NULL DEFAULT 0.00 COMMENT '当前余额', status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0冻结', create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_account_no (account_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='银行账户表';流水表是这套系统里最核心的一张表,每个账户的每次资金变动都写在这里,它的设计决定了后面查询“某段时间的交易明细”和“日终对账”的复杂度。
CREATE TABLE IF NOT EXISTS trade_log ( id BIGINT NOT NULL AUTO_INCREMENT, trade_no VARCHAR(32) NOT NULL COMMENT '业务流水号,全局唯一', account_no VARCHAR(20) NOT NULL COMMENT '发生交易的账户', trade_type TINYINT NOT NULL COMMENT '1存款 2取款 3转入 4转出', amount DECIMAL(16,2) NOT NULL COMMENT '交易金额', balance_after DECIMAL(16,2) NOT NULL COMMENT '本次交易后的账户余额快照', operator_id BIGINT NOT NULL COMMENT '操作员ID,关联sys_user', trade_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_trade_no (trade_no), KEY idx_account_time (account_no, trade_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='交易流水表';这两个DDL里有几个细节是银行账目系统特有的。余额字段必须用DECIMAL(16,2),不能用FLOAT或DOUBLE,否则金额大了会出现 0.01 级别的尾差,日终对账时百分百翻车。account_no 单独建唯一索引,它是业务上的账号,主键id只在内部关联时使用。trade_log里的balance_after是个冗余字段,但冗余得很有必要:查询“这个账户某笔交易后的余额是多少”不用再去账户表回查,而且它天然就是对账的线索。
2.3 余额字段为什么敢冗余:账目系统的写模型与对账约束
很多新手在网上找的JavaWeb记账案例,余额是在查询时用“SUM(收入)-SUM(支出)”现算出来的。数据量小的时候没问题,账目一多,每次查询都扫全表,日终对账更是慢得没法忍。账目系统的正确写法是“余额在写入时就算好,查询时直接读”,这就是常说的写模型。
另一个常见错误是先SELECT余额,在Java代码里判断够不够扣,再执行UPDATE。两个并发请求同时读到余额100元,都能通过判断,结果余额扣成负数。正确做法是把“判断余额充足”和“扣款”合并进一条UPDATE:
UPDATE bank_account SET balance = balance - ? WHERE account_no = ? AND balance >= ?;这条SQL执行后,影响行数为0就说明余额不足,直接抛业务异常。数据库的行锁保证同一时刻只有一个扣款能改这行数据,不需要额外加同步锁。这套系统的所有存取款、转账逻辑,本质上都是这个写模型的变体,读源码时看到这种写法,说明作者是真的理解账目系统的边界在哪。
3. 把源码跑起来:IDEA运行JavaWeb项目配置的完整实操
拿到源码包的第一步不是读代码,是先让它跑起来。跑不起来的项目,源码写得再漂亮,答辩时也撑不住。这一步的坑集中在环境版本和IDEA的Tomcat配置上,按下面顺序操作基本一遍过。
3.1 环境清单:JDK、Tomcat、MySQL的版本搭配
这套系统常见的版本组合是JDK 8、Tomcat 8.5/9、MySQL 5.7或8.0。JDK 11可以配Tomcat 9,但如果你拿到的是老项目,编译用的class版本可能是JDK 8,直接用高版本JDK跑没问题,反向才会出问题。先核对三个版本:
java -version mysql --version catalina.sh version # Linux/macOS;Windows直接看Tomcat bin目录下的version.bat逻辑说明:这三个命令分别检查的是代码运行环境、数据库环境、Web容器环境。最容易踩坑的是Tomcat的JMX端口(默认1099)和HTTP端口(默认8080)被占用,启动日志会直接报Address already in use。如果8080被占用,我不建议改Tomcat的server.xml,直接在IDEA的Tomcat配置里把HTTP port改成8090更省事。
另外,MySQL 5.7和8.0在认证插件上有差异(8.0默认caching_sha2_password),老项目用的JDBC驱动如果太旧,连不上8.0。碰上这种情况,要么把驱动换成mysql-connector-java 8.x,要么给项目里的数据库账号改成mysql_native_password插件,两者都能解,推荐前者。
3.2 导入数据库脚本:建库顺序与中文数据乱码预防
数据库脚本的导入顺序看起来简单,但这里埋着两个高频坑:字符集和触发器。推荐直接用命令行导入,别用Navicat的“运行SQL文件”一键执行。
mysql -u root -p --default-character-set=utf8 < doc/bank_account.sql逻辑说明:--default-character-set=utf8 保证脚本里的中文注释和初始数据以UTF-8字节流进入MySQL,避免脚本里的“张三”“活期”变成乱码。如果脚本里没有CREATE DATABASE语句,需要先手动建库再导入:
CREATE DATABASE bank DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE bank; SOURCE /path/to/bank_account.sql;注意:建库时字符集一定要指定utf8mb4,它比utf8多覆盖了emoji和生僻字,银行系统里如果户名有生僻字,utf8在MySQL 5.7上可能报“Incorrect string value”错误。导入完成后用这条SQL验证数据是否正常:
SELECT account_no, customer_name, balance FROM bank_account LIMIT 5;如果这里中文显示正常,后面页面乱码就只可能是连接或JSP的问题;如果这里已经乱码,说明导入环节出了问题,从脚本文件本身的编码开始查。
3.3 IDEA中配置Tomcat四步走:Deployment、Artifacts、Application Context
IDEA运行JavaWeb项目的配置,核心就一个概念:把项目“部署”到Tomcat里。很多人配完启动不报错,但浏览器访问就404,基本都是下面几步没配对。
第一步,Run菜单打开Edit Configurations,点左上角加号,选择Tomcat Server -> Local,Application Server里选你本机解压的Tomcat目录。
第二步,切到Deployment页签,点加号,选择Artifact,然后选“项目名:war exploded”。这里强烈建议选war exploded而不是war。war exploded是解压目录形式,IDEA会把编译产物直接当发布目录,改完JSP或静态资源刷新就能看到效果,不用重新打包;war格式每次改动都要重新构建整个war包,开发时很痛苦。
第三步,在Deployment页签下方把Application context改成“/bank”。这个值就是浏览器访问的上下文路径,最终访问地址是 http://localhost:8080/bank/ 。如果不改,默认值通常是一长串“项目名_war_exploded”,又丑又难记。
第四步,切回Server页签,确认HTTP port和JMX port不冲突,JRE选你本机装的JDK目录。启动后看IDEA的Console日志,出现这几行才算部署成功:
INFO: Deployment of web application archive ... has finished INFO: Starting ProtocolHandler ["http-nio-8080"] INFO: Server startup in [4567] milliseconds说明:第一行代表应用部署完成,第二行代表HTTP监听启动,第三行代表容器完全启动。如果只有第二行没有第三行,说明某个应用部署时抛了异常,切到Tomcat Localhost Log页签看堆栈,那里才是真正的报错现场。
4. 读源码的下手点:转账事务、流水号生成、登录拦截
项目跑起来以后,第一次浏览源码不用全读,按“账目是怎么动的”这条线索读。我推荐按三个点切入:转账业务、流水号生成、登录拦截。这三个点对应银行账目系统的三个核心约束:一致性、可追溯、权限边界。
4.1 转账业务:从一个UPDATE到一个事务边界
转账是这套系统里最有技术含量的业务。从A账户扣款、给B账户加款,两边都得成功,否则就是账目事故。SSM版本的项目里,这件事通常写在Service层:
@Service public class TransferService { @Autowired private JdbcTemplate jdbcTemplate; @Transactional(rollbackFor = Exception.class) public void transfer(String fromAccount, String toAccount, BigDecimal amount) { // 扣款:where里带balance >= ?,利用数据库行锁防超扣 int deductRows = jdbcTemplate.update( "UPDATE bank_account SET balance = balance - ? " + "WHERE account_no = ? AND balance >= ?", amount, fromAccount, amount); if (deductRows != 1) { throw new InsufficientBalanceException("余额不足或账户不存在"); } // 入账:注意这两步之间不能有return或异常被吞掉 jdbcTemplate.update( "UPDATE bank_account SET balance = balance + ? WHERE account_no = ?", amount, toAccount); // 写两条流水,分别记录转出和转入 jdbcTemplate.update( "INSERT INTO trade_log (trade_no, account_no, trade_type, amount, trade_time, operator_id) " + "VALUES (?, ?, ?, ?, NOW(), ?)", generateTradeNo(), fromAccount, "TRANSFER_OUT", amount, currentUserId()); jdbcTemplate.update( "INSERT INTO trade_log (trade_no, account_no, trade_type, amount, trade_time, operator_id) " + "VALUES (?, ?, ?, ?, NOW(), ?)", generateTradeNo(), toAccount, "TRANSFER_IN", amount, currentUserId()); } }逻辑说明:扣款SQL里的balance >= ?条件就是第2章说的写模型,数据库在更新时自行判断余额是否充足,影响行数不是1就抛异常。@Transactional声明了事务边界,方法里任何一个SQL抛异常,前面所有UPDATE和INSERT都会回滚,不会出现“扣了钱没到账”的状态。这里有个细节必须注意:rollbackFor = Exception.class是必要的,Spring默认只对RuntimeException回滚,如果业务代码抛的是自定义检查异常,不加这个参数事务就不会回滚。
如果项目是纯Servlet+JDBC实现的,事务要手动管理:从连接池拿连接后先setAutoCommit(false),try块里执行完所有SQL再commit,catch里rollback,finally里把连接归还连接池。这个写法容易漏finally,漏一次就是连接池耗尽,项目跑两天就卡死。
4.2 流水号生成:日期+序号与并发重复的取舍
trade_log表里有个trade_no字段,它是业务流水号,不是自增id。流水号的作用是让每笔交易能在业务层面被唯一定位,排错时看一眼流水号就能知道这笔交易发生在什么时候。最直白的生成方式是“时间戳+随机数”:
private static final String DATE_FORMAT = "yyyyMMddHHmmss"; public synchronized String generateTradeNo() { String datePart = new SimpleDateFormat(DATE_FORMAT).format(new Date()); String randomPart = String.format("%04d", (int) (Math.random() * 10000)); return datePart + randomPart; }逻辑说明:这个方法拿当前时间到秒,再加上四位随机数,形如“202605151430221836”。synchronized保证同一时刻只有一个线程在生成流水号,避免SimpleDateFormat内部共享Calendar导致的时间错乱。这个方案在毕设场景下完全够用,但如果同一秒内交易量超过一万笔,随机数部分就可能撞车。生产环境更常见的做法是用数据库序列或者Redis的INCR命令生成自增序号,这个边界要心里有数。
4.3 登录过滤器和Session:权限边界在Filter里,不在页面里
很多JavaWeb毕设项目只在登录JSP页里判断了用户名密码,后端其他Servlet直接裸奔,浏览器改个URL就能绕过登录访问数据页面。银行账目系统的权限控制必须落在Filter里:
public class LoginFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; String uri = req.getRequestURI(); // 放行名单:登录页、登录处理Servlet、静态资源 if (uri.endsWith("/login.jsp") || uri.endsWith("/login") || uri.contains("/static/")) { chain.doFilter(request, response); return; } // 核心校验:session里没有用户就重定向回登录页 Object loginUser = req.getSession().getAttribute("loginUser"); if (loginUser == null) { resp.sendRedirect(req.getContextPath() + "/login.jsp"); return; } chain.doFilter(request, response); } }逻辑说明:在web.xml里把过滤器注册成/*,所有请求都会经过这里。放行名单要仔细,登录页和登录处理接口必须在名单里,否则会死循环重定向;静态资源也要放行,不然CSS和JS被拦下,页面错乱得像个“黑匣子”。这段代码最坑的地方是结尾的chain.doFilter不能漏,漏了过滤器后面的Servlet根本不会执行,页面白屏且Tomcat日志里没有任何报错,你会以为是自己代码写崩了。
5. 避坑记:银行账目系统跑起来之后,最容易翻车的5个细节
这套系统能在网上流通,本身就说明它被很多人跑通过,但“能跑通”和“自己跑一次不踩坑”是两回事。下面这五个问题是我见过频率最高的,每个都按现象、原因、解决的顺序列清楚。
5.1 中文乱码:从Tomcat到MySQL,一条编码链上有五个环节
现象:页面上户名显示成“???”或者“浜斿崄”,数据库里看却是正常的。
原因:中文乱码是编码链问题,JSP页面编码、Tomcat的URIEncoding、JDBC连接串、MySQL库表字符集、脚本导入时客户端字符集,任何一个环节是ISO-8859-1或utf8,全链就断。
解决:先从数据库层验证,在MySQL命令行里执行SELECT customer_name FROM bank_account,如果这里正常,问题就在连接或JSP;如果这里就乱,回到3.2节的导入步骤检查字符集。JDBC连接串是大多数项目的最后一块短板:
jdbc:mysql://localhost:3306/bank?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai参数说明:useUnicode=true和characterEncoding=utf8必须成对出现,MySQL驱动只有在这两个参数同时存在时才启用UTF-8编码。JSP页面顶部也要写全pageEncoding和contentType,只写一个都不算数。
5.2 配置完Tomcat访问还是404:Artifacts和Application context没对上
现象:Tomcat启动日志正常,没有报错,但浏览器访问 http://localhost:8080/bank/login.jsp 就是404。
原因:Deployment页签里没有添加Artifact,Tomcat确实启动了,但没加载任何Web应用;或者Application context默认值不是/bank,你按自己的路径访问当然找不到资源。还有一个很隐蔽的情况:有人把JMX端口当成HTTP端口访问,1099端口根本不是给浏览器用的。
解决:回到3.3节重新核对四步。重点看IDEA启动日志里“Deployment of web application archive ... has finished”之前有没有一行“Deploying web application archive ...”,这两行之间的上下文路径,才是Tomcat实际部署的路径。浏览器访问的路径必须和它完全一致。
5.3 转账扣了钱,对方没到账:事务可能根本没生效
现象:A账户扣款成功,B账户金额纹丝不动,而且重新执行一次,A账户还会再扣一次。
原因:@Transactional注解失效是JavaWeb项目的老大难。最常见的有两种:一是转账方法写在Controller里,Spring事务代理只对Service层的public方法生效;二是同类里自调用,方法A调用同一个类的方法B,B上的@Transactional被Spring忽略,因为调用没有穿过代理对象。
解决:把转账业务独立放到TransferService里,Controller只负责接收请求、调用Service、处理返回值。自调用的典型错误长这样:
// 错误示范:同类内自调用,outer并没有事务 public void outerMethod() { this.doTransfer(...); // doTransfer上的@Transactional失效 }排查时先看两个点:转账方法是不是public,是不是被另一个Bean调用的。这两点都确认了还是不行,检查Spring配置里Service扫描是否包住了转账类。手动管理JDBC事务的项目则直接检查finally里有没有关闭连接,连接没归还连接池,后面所有请求都会卡在获取连接上。
5.4 从另一台机器导入SQL脚本报错:触发器和库名被“绑死”
现象:把项目的SQL脚本拿到另一台电脑导入,前面建表语句都执行成功了,中间到触发器的位置就报错中断,后面数据全没进去。
原因:导出SQL脚本时带了CREATE DATABASE和USE语句,目标机器上数据库名和脚本里的不一致,就会在USE那一步报“Unknown database”;触发器和存储过程在导出时容易被工具跳过,或者因为权限不足被静默丢弃。
解决:导出时用命令行显式带上触发器和存储过程:
mysqldump -u root -p --databases bank --triggers --routines --single-transaction > bank_backup.sql参数说明:--triggers导出触发器,--routines导出存储过程和函数,--databases会带上CREATE DATABASE语句,适用于整库迁移。导入前先清干净旧库,保证是可重复执行的环境:
DROP DATABASE IF EXISTS bank; CREATE DATABASE bank DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;导入完成后别急着跑,用SHOW TRIGGERS检查触发器是否真的进去了。很多人拿到项目在Navicat里右键“转储SQL文件”,默认选项不带触发器,这就是“明明脚本里有,导入却失败”的根源。
5.5 交易时间慢了8小时:时区配置的三个位置
现象:页面上交易时间比实际时间晚了8小时,或者日终对账统计的“昨天”边界不对。
原因:MySQL连接串没指定serverTimezone,Java应用所在的系统时区与数据库会话时区不一致。MySQL 8.0默认支持多时区,如果全局时区是UTC,而应用用的是北京时间,差距就是整整8小时。
解决:第一优先级是JDBC连接串加参数:
serverTimezone=Asia/Shanghai然后在数据库里执行SELECT NOW(),确认当前会话时间与北京时间一致。如果项目用了Druid或HikariCP连接池,连接串配置写在jdbc.properties里,改完要重启Tomcat才生效。这个坑还会牵连日终批处理:如果对账脚本判断“昨天的流水”用的是CURDATE() - INTERVAL 1 DAY,时区错了,“昨天”的边界就跟着错,账目对不平你还找不到原因。
6. 给毕设加分:演示剧本、日终对账与答辩前检查
6.1 演示剧本:让答辩评委跟着你的手指走
答辩演示最怕冷场,也怕评委不知道你在干什么。开场的标准动作是:登录系统,点开账户列表,选一个测试账户,做一笔存款,再做一笔转账,然后打开流水页。每个动作做完停两秒,指着界面上的余额数字说一句“这笔交易后,余额从XX变成了XX”。让评委始终盯着数字变化,比展示一堆页面截图有效得多。演示前记得把测试户名改成“演示用户”,别用当时建库随便敲的“测试1”,这种细节很加分。
6.2 增量亮点:日终对账查询,证明你理解流水和余额的关系
答辩时评委常问“账目错了你怎么发现”,答案不是人肉翻流水,而是日终对账。account_snapshot表存了每个账户昨日的日终余额,跑一次这条查询,就能把“昨日余额 + 昨日净变动 ≠ 当前余额”的账户全部筛出来:
SELECT a.account_no, a.customer_name FROM account_snapshot s JOIN bank_account a ON a.account_no = s.account_no LEFT JOIN ( SELECT account_no, SUM(CASE WHEN trade_type IN (1,3) THEN amount WHEN trade_type IN (2,4) THEN -amount END) AS net_amount FROM trade_log WHERE DATE(trade_time) = CURDATE() - INTERVAL 1 DAY GROUP BY account_no ) t ON t.account_no = a.account_no WHERE s.snapshot_date = CURDATE() - INTERVAL 1 DAY AND ABS(s.pre_balance + IFNULL(t.net_amount, 0) - a.balance) > 0.01;逻辑说明:trade_type里1和3是资金流入,2和4是资金流出,子查询先算出每个账户昨日的净变动,外层再和余额快照、当前余额做差异比对,差超过1分的账户就是账目异常。这条SQL跑一遍,比讲十页概念都管用,因为它证明你理解“流水和余额必须能互相验证”。
6.3 答辩前检查:重置脚本、README和最后的习惯
答辩前最该做的事情,是把数据库整库导出一份干净的脚本放进项目doc目录,文档里写清JDK、Tomcat、MySQL的版本号和启动步骤。我处理过最惨的一次线上账目事故,不是代码写错,而是跑完对账后没人看结果,从那以后我养成了“任何自动输出都要有人确认”的习惯,也会顺手把验证SQL写进项目说明。答辩现场最怕环境不一致,提前用重置脚本把数据库初始化一遍,确认从零能复现,这是你最后一张底牌。希望帮到你。
本文还有配套的精品资源,点击获取