☰
Java Swing+MyBatis+MySQL实战:ATM模拟系统课程设计全链路解析
2026/10/28 4:16:49 网站建设 项目流程

简介:这份资源是面向高校Java课程设计学习者与初学Java的开发者的一份ATM柜员机模拟系统完整课程设计文档,围绕用户登录、账户查询、存款、取款、更改密码等核心业务展开,帮助读者理解如何用面向对象思想与Swing图形界面完成一个可运行的自助银行模拟项目。压缩包内共1个docx文件,约1.21MB,内容涵盖需求分析、功能图、界面设计、取款存款代码示例、逻辑校验、SQL语句及项目总结等章节,结构完整,可直接作为课程设计报告参考或项目实现蓝本。已有387人学习,适合需要完成Java课设、练习GUI开发与数据库交互的读者对照查阅,从中获取功能拆解思路、代码组织方式与文档撰写框架。

1. 从一份课程设计说起:为什么这个 ATM 模拟系统值得拿来练手

很多 Java 课程设计的命运是交完就删,但这个 ATM 柜员机模拟系统不太一样——它把 Swing 图形界面、JDBC 数据库读写、事务提交、金额校验这几件事串成了一条完整链路,跑起来就是一个能登录、能存取款、能改密的桌面程序。对刚学完面向对象、想找个东西把「类、对象、封装、继承」落到实处的同学,它是一个成本极低、反馈极快的练手项目;对需要交课程设计又不想写纯控制台程序的人,它自带界面和数据库,改改就能交。整个系统围绕登录、查询、存款、取款、转账、改密六个动作展开,代码量不大,但每一块都踩在 Java 桌面开发的关键点上,属于那种「看着简单、真跑起来会翻车」的类型。

2. 技术栈与目录结构:Swing 界面 + MyBatis + MySQL 的三层拼装

2.1 技术选型拆解:为什么是这套组合

这个项目的技术栈不复杂,但每一层都有明确分工。界面层用 Java Swing,靠JFrame、JPanel、CardLayout做窗口切换,用JButton的ActionListener绑定业务动作;数据层用 MyBatis 做 ORM 映射,SqlSessionFactory开 session、Mapper接口调 SQL、手动commit()提交事务;存储层是 MySQL,一张tb_user表存卡号、密码、余额。这套组合在课程设计里很常见,原因是它把「界面—业务—数据」三层切得干净,学生能清楚看到一次点击是怎么一路走到数据库的。

选 Swing 而不是 JavaFX,主要是课程环境兼容性好,JDK 自带,不用额外配依赖;选 MyBatis 而不是纯 JDBC,是因为 SQL 写在 XML 里,改起来直观,#{money}这种占位符还能防注入。我一般会建议把连接配置单独抽出来,别硬编码在 Java 里,后面换库换密码不用满项目找。

2.2 目录结构:按职责分包的典型布局

项目按职责分包,结构大致如下:

src/ ├── main/ │ ├── java/ │ │ ├── view/ # 界面层:MainView、ChangeMoneyPanel 等 │ │ ├── service/ # 业务层:HandleServiceImpl │ │ ├── mapper/ # 数据层:MysqlOperateMapper 接口 │ │ ├── entity/ # 实体:User、Result │ │ └── util/ # 工具:StringUtils、SqlSessionFactory 构建 │ └── resources/ │ ├── mapper/ # MyBatis XML:addMoneyById、minusMoneyById │ ├── images/ # 背景图资源 │ └── mybatis-config.xml

view包只负责摆控件和收事件,service包做校验和事务,mapper包只声明 SQL 方法,实体类User对应表字段,Result<T>是个通用返回包装,带data和massage两个字段(原文拼写如此,改的时候顺手统一成message更好)。这种分法对课程设计够用,二次开发时想加功能,基本就是「加一个 Panel、加一个 service 方法、加一条 SQL」三步走。

2.3 数据库表与 Mapper 映射

核心表只有一张,字段设计直接决定后面代码怎么写:

字段类型说明
idBIGINT主键,同时充当卡号
passwordVARCHAR登录密码
moneyDECIMAL账户余额,用 DECIMAL 避免浮点误差
nameVARCHAR持卡人姓名

余额字段用DECIMAL而不是DOUBLE是血泪经验——金额用浮点存,存取几次之后会出现999.9999999这种玄学尾数,对账时非常难受。Mapper 接口里对应两个更新方法,SQL 写在 XML 中:

<update id="addMoneyById"> update tb_user set money = money + #{money} where id = #{id} </update> <update id="minusMoneyById"> update tb_user set money = money - #{money} where id = #{id} </update>

这里用money = money + #{money}而不是先查出来再set,好处是把加减交给数据库原子完成,减少并发下的覆盖问题。#{money}是预编译占位符,参数会被当成值处理,不会拼进 SQL 字符串,这是防注入的基本操作。参数id是当前登录用户的卡号,money是BigDecimal类型,调用前必须保证已经转好类型,别直接传字符串进去。

3. 核心功能实现:从一次点击到数据库落账的完整链路

3.1 存取款面板:一个 Panel 复用两种业务

存取款界面没有写两个类,而是用一个ChangeMoneyPanel通过构造参数type区分「存入」和「取出」,这是这个项目里比较聪明的一处设计。核心代码:

public class ChangeMoneyPanel extends JPanel { private final Image backgroundImage; public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView) { setLayout(null); final String substring = type.substring(0, 1); // "存" 或 "取" JLabel label = new JLabel("请输入" + substring + "款金额,ATM 机" + substring + "款必须是整百的"); final JTextField moneyText = new JTextField(); JButton enterButton = new JButton(type); final JButton backButton = new JButton("返回"); backgroundImage = new ImageIcon( Objects.requireNonNull(getClass().getResource("/images/9602.gif_wh860.gif")) ).getImage(); enterButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { if (type.equals("存入")) { HandleServiceImpl handleService = new HandleServiceImpl(); Result<Boolean> r = handleService.addMoney( mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, r.massage, "提示", JOptionPane.INFORMATION_MESSAGE); if (r.data) backButton.doClick(); } else { HandleServiceImpl handleService = new HandleServiceImpl(); Result<Boolean> r = handleService.minusMoney( mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, r.massage, "提示", JOptionPane.INFORMATION_MESSAGE); if (r.data) backButton.doClick(); } } }); backButton.addActionListener(e -> { mainView.setTitle("菜单"); cardLayout.show(contentPane, "菜单"); moneyText.setText(""); }); add(label); add(moneyText); add(enterButton); add(backButton); } @Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (backgroundImage != null) { g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this); } } }

逐段看:type.substring(0, 1)取「存」或「取」一个字,用来拼提示文案,省掉一套 if 判断文案;mainView.currentId是登录成功后存下的当前卡号,业务方法靠它定位账户;Result<Boolean>的data表示成功与否,massage是给用户看的提示,成功才触发backButton.doClick()回菜单,失败就停在当前页让用户重输。paintComponent里重绘背景图,保证窗口缩放时图片跟着拉伸。参数上要注意moneyText.getText().trim()先去掉首尾空格,否则用户手滑敲个空格,后面的数字校验会直接判非法。

3.2 业务层校验:金额合法性、整百限制与余额判断

界面只负责收数,真正的规则在HandleServiceImpl里。存款和取款的校验逻辑几乎对称,但取款多一步余额比较:

@Override public Result<Boolean> addMoney(Long id, String money) { if (money.equals("")) { return new Result<Boolean>("不能为空", false); } if (!StringUtils.isStrictlyNumeric(money)) { return new Result<Boolean>("含有非法字符", false); } if (Double.parseDouble(money) % 100 != 0) { return new Result<Boolean>("ATM 中只支持百元大钞,零钱请前往柜台领取", false); } SqlSession sqlSession = sqlSessionFactory.openSession(); MysqlOperateMapper userMapper = sqlSession.getMapper(MysqlOperateMapper.class); userMapper.addMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new Result<Boolean>("存款成功", true); } @Override public Result<Boolean> minusMoney(Long id, String money) { if (!StringUtils.isStrictlyNumeric(money)) { return new Result<Boolean>("含有非法字符", false); } if (money.equals("")) { return new Result<Boolean>("不能为空", false); } if (Double.parseDouble(money) % 100 != 0) { return new Result<Boolean>("ATM 中只支持百元大钞,零钱请前往柜台领取", false); } SqlSession sqlSession = sqlSessionFactory.openSession(); MysqlOperateMapper userMapper = sqlSession.getMapper(MysqlOperateMapper.class); User user = userMapper.selectById(id); if (user.getMoney().compareTo(new BigDecimal(money)) < 0) { return new Result<Boolean>("余额不足", false); } userMapper.minusMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new Result<Boolean>("取款成功", true); }

校验顺序有讲究:先判空、再判非法字符、最后判整百,任何一步不过都直接返回,不碰数据库。StringUtils.isStrictlyNumeric是自定义工具方法,只允许纯数字,挡住12a、1.5.2这类输入。整百限制用% 100 != 0判断,模拟真实 ATM 只出百元钞。取款时user.getMoney().compareTo(new BigDecimal(money)) < 0是BigDecimal的标准比较方式,返回负数说明余额小于取款额,直接拦下。注意这里new BigDecimal(money)用的是字符串构造,别用double构造,否则又会引入精度问题。

3.3 事务与资源释放:commit 和 close 一个都不能少

上面两段代码里,openSession()之后必须commit()再close(),这是 MyBatis 手动事务的固定套路。openSession()默认不自动提交,SQL 执行完只是挂在 session 里,不commit数据库里看不到变化;不close连接池很快被占满,程序跑一会儿就卡死。常见做法是用 try-with-resources 或 try-finally 包起来,保证异常时也能释放:

SqlSession sqlSession = sqlSessionFactory.openSession(); try { MysqlOperateMapper mapper = sqlSession.getMapper(MysqlOperateMapper.class); mapper.minusMoneyById(id, new BigDecimal(money)); sqlSession.commit(); } catch (Exception ex) { sqlSession.rollback(); return new Result<Boolean>("操作失败:" + ex.getMessage(), false); } finally { sqlSession.close(); }

rollback()是后悔药,更新中途抛异常时把这次事务撤掉,避免出现「钱扣了但没记上」的半截状态。课程设计原版没写异常分支,二次开发时补上这一段,稳定性会明显不一样。

4. 跑起来:环境准备、建库建表与启动步骤

4.1 环境与依赖清单

跑这个项目需要 JDK 8 及以上、MySQL 5.7 或 8.0、MyBatis 和 MySQL 驱动两个 jar。如果项目是 Maven 结构,pom.xml里至少要有:

<dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.13</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>

版本号按本地仓库实际有的填,别照抄。非 Maven 项目就把这两个 jar 手动加到Build Path里,缺驱动时启动会报No suitable driver,这是最常见的第一个坑。

4.2 建库建表与初始数据

先建库建表,再插一条测试账户,否则登录界面输什么都进不去:

CREATE DATABASE atm_db DEFAULT CHARACTER SET utf8mb4; USE atm_db; CREATE TABLE tb_user ( id BIGINT PRIMARY KEY, password VARCHAR(64) NOT NULL, money DECIMAL(12,2) DEFAULT 0.00, name VARCHAR(32) ); INSERT INTO tb_user (id, password, money, name) VALUES (1001, '123456', 5000.00, '测试用户');

DECIMAL(12,2)表示总共 12 位、小数 2 位,够存到亿级。初始余额给 5000,方便测试取款和余额不足两种分支。密码这里存的是明文,课程设计够用,真要较真可以换成 MD5 或 BCrypt,改的时候记得登录校验同步改。

4.3 配置数据源与启动入口

mybatis-config.xml里配数据源和 Mapper 映射:

<configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <property name="driver" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/atm_db?useSSL=false&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> </dataSource> </environment> </environments> <mappers> <mapper resource="mapper/MysqlOperateMapper.xml"/> </mappers> </configuration>

serverTimezone=Asia/Shanghai不加的话,MySQL 8 驱动可能报时区错误;useSSL=false是本地开发省掉证书握手。启动入口是MainView的main方法,SwingUtilities.invokeLater里 new 出主窗口,先显示登录面板。跑通的标准是:输入 1001 / 123456 能进菜单,查余额显示 5000,存 100 后变 5100,取 200 后变 4900,改密后旧密码失效。

5. 避坑与排查:五个真跑起来才会遇到的问题

5.1 登录一直失败,但数据库里明明有这条记录

现象是输入正确卡号密码仍提示错误。原因多半是id字段类型不匹配——界面用Integer.parseInt解析卡号,而表里id是BIGINT,或者登录 SQL 里where id = #{id}传的是字符串,MySQL 隐式转换时对不上。解决是把卡号统一按Long处理,登录方法参数改成Long id,界面解析用Long.parseLong,SQL 参数类型保持一致。

5.2 存取款提示成功,但重新登录余额没变

现象是弹窗说「存款成功」,退出再进余额还是原数。原因是openSession()之后漏了commit(),SQL 执行了但事务没提交,连接一关就回滚。排查时在close()前打一行日志确认有没有走到commit,或者临时把openSession(true)开自动提交验证。正式代码还是手动提交加 try-catch 回滚更稳。

5.3 背景图不显示,界面一片灰

现象是窗口能开,但背景图位置是空白。原因是getResource("/images/xxx.gif")返回 null,Objects.requireNonNull直接抛异常或者图片路径没打进 classpath。检查resources目录是否被标记为资源根,图片是否真的在images下,文件名大小写是否一致——Linux 下大小写敏感,Windows 下不敏感,换环境就翻车。稳妥做法是加载前判空,给个纯色兜底。

5.4 输入金额带小数点或字母,程序直接崩

现象是输入100.5或abc点确认,控制台抛NumberFormatException。原因是校验方法没拦住,Double.parseDouble直接炸。解决是把isStrictlyNumeric做成只认纯数字的正则校验,先拦非法字符再转类型;同时把Double.parseDouble(money) % 100换成new BigDecimal(money).remainder(new BigDecimal("100")),避免浮点取模的精度偏差。

5.5 连续操作几次后界面卡死无响应

现象是存取款来回点几次,按钮点了没反应。原因是每次操作都openSession()但异常路径下没close(),连接池被占满。排查看控制台有没有连接超时警告,解决是把 session 的关闭放进finally,或者干脆用连接池配置里的maxActive调大做临时缓解,根治还是补全释放逻辑。

6. 二次开发:把课程设计改成能拿得出手的作品

这个项目的骨架已经够支撑一次像样的改造,关键是想清楚往哪个方向加。最省力的第一步是把明文密码换成哈希存储:登录时对输入做一次MessageDigest.getInstance("SHA-256")再比对,改密时同样处理,改动集中在登录和改密两个 service 方法,半小时能完成。第二步是补交易流水表,建一张tb_transaction记录卡号、类型、金额、时间,每次存取款成功后插一条,查询界面就能从「只看余额」升级成「看最近十笔记录」,这一步会让项目完整度明显上一个台阶。

第三步是并发安全。现在的取款逻辑是「先查余额、再扣减」,两个线程同时取款可能都通过余额检查,最后扣成负数。改法有两种:一是把扣减 SQL 改成带条件的原子更新update tb_user set money = money - #{money} where id = #{id} and money >= #{money},根据返回的影响行数判断是否成功;二是在 service 方法上加synchronized,简单但只适合单机。我一般选第一种,数据库层面兜底更可靠。

改造方向改动位置难度收益
密码哈希登录/改密 service低安全性
交易流水新建表 + 查询界面中功能完整度
原子扣减Mapper XML + service中并发正确性
界面美化view 包低演示效果

验证改造是否成功,别只看界面提示,直接开两个客户端同时取同一账户,或者用mysql命令行查tb_user的余额和流水表条数是否对得上。我吃过一次亏:界面提示成功、余额也对,但流水少记了一条,原因是插入流水和扣减余额不在同一个事务里。从那以后我每次改涉及多表写入的逻辑,都强制把两条 SQL 放进同一个SqlSession里一起commit,再手动制造一次异常确认能整体回滚。这套习惯比任何框架都管用,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询