
简介这份资源是面向高校计算机专业学生与Java初学者的课程设计完整文档围绕ATM柜员机模拟系统展开帮助读者在缺乏真实银行环境的情况下完成自助业务系统的设计与编码实践。压缩包内共1个docx文件约1.21MB内容按需求分析、界面设计、功能实现、项目总结等章节组织涵盖用户登录、账户查询、存款、取款、改密等模块并给出取存款核心代码、逻辑校验思路与SQL语句示例还讨论了数据管理、界面布局、安全机制与异常处理等重难点。文档同时梳理了Java Swing图形界面开发、面向对象封装继承等知识点附有功能图与参考文献适合作为课程设计报告模板或期末项目参考。目前已有387人学习可帮助读者快速理清系统结构、对照实现功能并完善自己的设计文档。1. 从一份课程设计说起这个 ATM 模拟系统到底能跑出什么很多人看到“课程设计”四个字就下意识划走觉得无非是控制台打印几个菜单、if-else堆一堆。但我把这份 JAVA ATM 柜员机模拟系统源码拉下来跑了一遍发现它并不是那种交完作业就扔的玩具Swing 图形界面、CardLayout 多窗口切换、MyBatis 持久化到 MySQL、BigDecimal 处理金额、统一的ResultT返回封装这套组合放在学生项目里算是相当完整的工程骨架。它模拟的是真实柜员机的完整业务闭环——插卡登录、查余额、存取款、转账、改密、退出每一步都有界面、有校验、有落库。适合两类人一是刚学完 Java 语法、想找一个能跑通 GUI 数据库全链路的练手项目的新手二是需要一份结构清晰、方便二次改造的课程设计底稿的开发者。下面我按“它是什么 → 怎么跑起来 → 核心代码怎么写的 → 怎么改”的顺序拆给你看。2. 技术栈与工程结构Swing 界面层如何与 MyBatis 数据层对接2.1 技术选型拆解为什么是 Swing MyBatis MySQL这个项目的分层意图很明显虽然代码量不大但职责切得比较干净。界面层用 Java Swing因为课程设计场景下它零额外依赖、JDK 自带、拖控件就能出效果比 JavaFX 省心数据层用 MyBatis通过 Mapper 接口 XML 写 SQL把“余额加多少、减多少”这种操作直接下沉到 SQL 语句里避免在 Java 里读出对象再算再写回带来的并发覆盖问题金额类型统一用BigDecimal这是金融类项目的基本素养double算钱迟早出玄学误差。数据库选 MySQL用户表tb_user存卡号、密码、余额。从源码里的HandleServiceImpl、MysqlOperateMapper、ResultT、MainView这些类名能反推出典型的分层MainView是主窗口容器负责 CardLayout 卡片切换各个XxxPanel是功能面板HandleService/HandleServiceImpl是业务服务层MysqlOperateMapper是 MyBatis 映射接口ResultT是统一返回体带data和massage原文拼写如此两个字段。这种结构对二次开发很友好——加一个新功能无非是加一个 Panel、加一个 Service 方法、加一条 Mapper SQL。2.2 目录结构还原与关键文件职责按源码引用关系工程大致是这样组织的包名以实际下载为准这里给的是职责映射src/ ├── main/ │ ├── java/ │ │ ├── view/ │ │ │ ├── MainView.java # 主窗口持有 CardLayout 和 currentId │ │ │ ├── LoginPanel.java # 登录卡片 │ │ │ ├── MenuPanel.java # 主菜单卡片 │ │ │ ├── ChangeMoneyPanel.java # 存取款共用面板按 type 区分 │ │ │ ├── QueryPanel.java # 余额查询 │ │ │ ├── TransferPanel.java # 转账 │ │ │ └── ChangePwdPanel.java # 改密 │ │ ├── service/ │ │ │ ├── HandleService.java │ │ │ └── impl/HandleServiceImpl.java │ │ ├── mapper/MysqlOperateMapper.java │ │ ├── entity/User.java │ │ └── util/ │ │ ├── Result.java │ │ ├── StringUtils.java # isStrictlyNumeric 等校验 │ │ └── SqlSessionFactoryUtil.java │ └── resources/ │ ├── mybatis-config.xml │ ├── mapper/MysqlOperateMapper.xml │ └── images/ # 背景图资源几个关键点值得注意MainView里维护了一个currentId登录成功后把当前用户主键写进去后续所有业务面板都通过mainView.currentId拿用户身份这是一种“会话状态挂在主窗口上”的简化做法比到处传参省事但也要注意退出登录时清空。ChangeMoneyPanel一个类同时承担存款和取款靠构造参数type区分type.substring(0,1)取出“存”或“取”字拼提示语这种复用思路在小项目里很实用。2.3 环境准备与启动步骤跑起来需要 JDK 8 及以上、MySQL 5.7/8.0、Maven或手动导入 jar。步骤按顺序来第一步建库建表。密码字段建议存明文先跑通后面再改加密CREATE DATABASE atm_db DEFAULT CHARSET utf8mb4; USE atm_db; CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(32) NOT NULL UNIQUE, -- 卡号登录用 password VARCHAR(64) NOT NULL, -- 密码 money DECIMAL(12,2) DEFAULT 0.00 -- 余额用 DECIMAL 别用 double ); INSERT INTO tb_user(card_no, password, money) VALUES (1001, 123456, 5000.00);第二步改mybatis-config.xml里的数据源把 URL、用户名、密码换成你本机的environment iddevelopment transactionManager typeJDBC/ dataSource typePOOLED property namedriver valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/atm_db?useSSLfalseamp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /dataSource /environment第三步确认MysqlOperateMapper.xml已被注册进mappers然后从MainView的main方法启动。启动后应看到登录界面输入1001 / 123456进入主菜单。如果背景图加载报空指针检查images目录是否在 resources 根下、路径是否以/images/开头——源码里用的是getClass().getResource(/images/xxx.gif)斜杠开头表示从 classpath 根找。提示DECIMAL(12,2)和 Java 侧BigDecimal是配套的别图省事把数据库字段改成double否则存取几次之后余额会出现4999.999999这种脏数据。3. 核心功能实现存取款面板与业务校验的完整链路3.1 ChangeMoneyPanel一个面板如何复用存款与取款先看界面层。ChangeMoneyPanel继承JPanel构造时接收type“存入”或“取款”、contentPane、cardLayout、mainView四个参数前两个用于返回时切卡片后两个用于拿当前用户和弹窗父组件。布局用setLayout(null)绝对定位每个控件手动setLocation/setSize这在课程设计里很常见好处是所见即所得坏处是换分辨率会错位后面改造章节会说怎么换布局。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(返回); enterButton.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { if (type.equals(存入)) { HandleServiceImpl handleService new HandleServiceImpl(); ResultBoolean 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(); ResultBoolean r handleService.minusMoney( mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, r.massage, 提示, JOptionPane.INFORMATION_MESSAGE); if (r.data) { backButton.doClick(); } } } }); // ... 控件定位与 add 省略 }这段代码的逻辑说明type决定按钮文案和走哪个 Service 方法mainView.currentId是登录时写入的用户主键作为所有资金操作的定位依据ResultBoolean的data表示业务是否成功massage是给用户看的提示语成功才触发backButton.doClick()回到菜单失败则停留在当前面板让用户改金额。参数上要注意moneyText.getText().trim()先去掉首尾空格再进 Service否则 100 这种输入会在数字校验里翻车。3.2 业务层校验空值、非法字符、整百、余额不足四道关界面只负责收集输入真正的防线在HandleServiceImpl。存款和取款的校验顺序有讲究先判空、再判非法字符、再判是否整百取款还要多一道余额比较。这个顺序不能乱——如果先Double.parseDouble(money)再判空空字符串会直接抛NumberFormatException用户看到的就是一坨堆栈而不是友好提示。Override public ResultBoolean addMoney(Long id, String money) { if (money.equals()) { return new ResultBoolean(不能为空, false); } if (!StringUtils.isStrictlyNumeric(money)) { return new ResultBoolean(含有非法字符, false); } if (Double.parseDouble(money) % 100 ! 0) { return new ResultBoolean(ATM 中只支持百元大钞零钱请前往柜台领取, false); } SqlSession sqlSession sqlSessionFactory.openSession(); MysqlOperateMapper userMapper sqlSession.getMapper(MysqlOperateMapper.class); userMapper.addMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new ResultBoolean(存款成功, true); } Override public ResultBoolean minusMoney(Long id, String money) { if (!StringUtils.isStrictlyNumeric(money)) { return new ResultBoolean(含有非法字符, false); } if (money.equals()) { return new ResultBoolean(不能为空, false); } if (Double.parseDouble(money) % 100 ! 0) { return new ResultBoolean(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 ResultBoolean(余额不足, false); } userMapper.minusMoneyById(id, new BigDecimal(money)); sqlSession.commit(); sqlSession.close(); return new ResultBoolean(取款成功, true); }参数说明id是用户主键money是界面传来的字符串。StringUtils.isStrictlyNumeric是自定义工具方法判断是否全为数字可含小数点比try-catch包parseDouble更干净。BigDecimal.compareTo返回负数表示前者小于后者这里用来判断余额是否够扣注意别用equals比较BigDecimal1.0和1.00用equals会返回 false这是经典坑。取款这里有个小瑕疵minusMoney里先判非法字符再判空顺序和addMoney反了空字符串会先被isStrictlyNumeric拦下返回“含有非法字符”提示不够准确二次开发时统一一下顺序即可。3.3 Mapper 与 SQL把加减法交给数据库而不是 Java资金增减没有走“查出来、加一下、再 update 整个对象”的路子而是直接在 SQL 里做money money #{money}这是这个项目里我最认可的一处设计。原因很实在如果两个窗口同时操作同一账户Java 侧读到的旧余额再写回会互相覆盖而update ... set money money - ?由数据库保证原子性天然避免这类丢失更新。update idaddMoneyById update tb_user set money money #{money} where id #{id} /update update idminusMoneyById update tb_user set money money - #{money} where id #{id} /update对应的 Mapper 接口方法签名是void addMoneyById(Param(id) Long id, Param(money) BigDecimal money)#{money}会以预编译参数形式传入不存在 SQL 注入问题。这里要注意SqlSession的关闭源码里每次操作openSession()后手动commit()再close()没有用 try-with-resources一旦中间抛异常 session 就泄漏了。改造时建议包一层try (SqlSession s factory.openSession()) { ... s.commit(); }这是后面避坑章节会展开的点。4. 避坑与排查跑这个项目最容易翻车的五个地方4.1 现象启动报NullPointerException定位在背景图那行原因getClass().getResource(/images/9602.gif_wh860.gif)返回 null通常是 Maven 工程里images目录没放在src/main/resources下或者 IDE 没把 resources 标记为资源根编译后 classpath 里根本没有这个文件。解决确认目录位置mvn clean package后去target/classes/images/下看文件在不在文件名带下划线和双扩展名这种命名也容易手误复制时核对一遍。实在不行先把paintComponent里的绘制逻辑注释掉让界面先出来。4.2 现象登录一直提示失败但数据库里明明有这条记录原因多半是card_no和password的查询条件写反或者密码字段在插入时带了空格。还有一种情况是 MyBatis 的mapUnderscoreToCamelCase没开card_no映射不到cardNo属性查出来对象字段全是 null。解决在mybatis-config.xml的settings里加setting namemapUnderscoreToCamelCase valuetrue/登录 SQL 用where card_no #{cardNo} and password #{password}并在插入测试数据时用TRIM()检查有没有隐藏空格。4.3 现象取款成功但余额没变或者变了两次原因SqlSession忘了commit()JDBC 默认手动提交不 commit 数据不落库变两次则可能是按钮被重复绑定监听器或者backButton.doClick()之后用户又点了一次。解决检查每个写操作后面是否都有sqlSession.commit()按钮监听在构造里只addActionListener一次别在show事件里重复加。这类问题没有后悔药只能靠日志——在 Service 方法入口打一行System.out.println(minusMoney id id money money)跑一次看调用了几次。4.4 现象输入100.5提示“含有非法字符”输入abc反而没拦住原因isStrictlyNumeric的实现如果只判断Character.isDigit逐字符小数点会被判非法如果用了正则但写错边界字母又漏过去。解决把校验方法改成明确的正则money.matches(\\d(\\.\\d{1,2})?)同时配合整百校验new BigDecimal(money).remainder(new BigDecimal(100)).compareTo(BigDecimal.ZERO) ! 0用BigDecimal做取余比double % 100更稳避免100.000000001这种浮点误差导致的误判。4.5 现象改密后旧密码还能登录或者新密码直接登录不上原因改密 SQL 的where条件只用了id没校验原密码导致任何登录用户都能改或者更新时把password和card_no字段搞混。解决改密必须走“先select校验原密码 → 再update新密码”两步SQL 写成update tb_user set password #{newPwd} where id #{id} and password #{oldPwd}用影响行数判断是否成功返回 0 就提示“原密码错误”。这样即使并发也不会改错人。注意以上五条里第 3 条和第 5 条是真正会让数据出问题的其余多是环境或提示层面的小坑。排查顺序建议永远是“先看控制台异常 → 再看 SQL 日志 → 最后看界面逻辑”别一上来就怀疑 Swing。5. 二次开发改造从课程设计到能拿得出手的工程底稿这份源码作为底稿最值得动手改的有三处改完整个项目的“工程味”会明显不一样。第一处是密码加密。现在密码明文存库登录 SQL 直接比对明文这在任何评审场合都是扣分项。改造思路引入MessageDigest做 SHA-256 加盐注册/改密时存salt hash登录时用同样盐算一遍比对。不用上 Spring Security课程设计场景下一个工具类足够public static String sha256(String raw, String salt) throws Exception { MessageDigest md MessageDigest.getInstance(SHA-256); md.update((raw salt).getBytes(StandardCharsets.UTF_8)); byte[] digest md.digest(); StringBuilder sb new StringBuilder(); for (byte b : digest) sb.append(String.format(%02x, b)); return sb.toString(); }盐可以每个用户随机生成存一列salt登录时先按卡号查出盐再算哈希。这样即使数据库被看到密码也不是一眼可读。第二处是SqlSession的生命周期管理。把每个 Service 方法里的openSession / commit / close抽成一个模板方法或者用 try-with-resources 包起来异常时rollback。这一步改完转账这种“扣 A 加 B”的复合操作才能保证要么全成功要么全回滚——现在的代码里转账如果扣款成功、加款失败钱就凭空消失了这是最该补的洞。第三处是界面布局。绝对定位在 1366×768 上好看换个高分屏就散架。可以把setLayout(null)换成GridBagLayout或BorderLayout 嵌套 JPanel背景图用ImageIcon缩放适配getWidth()/getHeight()。改动量不小但改完这个项目才真正具备“给别人演示”的底气。验证方法很直接改完加密后用旧数据登录应该失败重新注册一个用户能正常登录改完事务后故意在转账的加款 SQL 里写个错表名观察 A 的余额是否回滚改完布局后把窗口拖到不同尺寸看控件是否跟随。我自己的习惯是每改一处就跑一遍“登录 → 查询 → 存 100 → 取 100 → 转账 → 改密 → 退出”这条主链路任何一步提示语不对就停下来查别攒着一起调。从那以后我每次接手这类带数据库的 Swing 项目都强制先把主链路手工走一遍再动代码省下的调试时间远比这一圈多。希望这份拆解能帮你把这个 ATM 模拟系统真正跑起来、改下去。本文还有配套的精品资源点击获取