简介:这份资源是面向高校Java课程设计场景的ATM柜员机模拟系统完整项目文档,适合正在完成课程设计或希望以真实项目巩固面向对象编程的学生参考。文档围绕用户登录、账户查询、存款、取款、更改密码等核心功能展开,涵盖需求分析、功能图设计、窗口切换与登录主菜单界面布局,并给出存取款功能代码、逻辑校验思路及SQL语句示例,还涉及数据管理、安全机制与异常处理等重难点分析。压缩包共1个docx文件,约1.21MB,内容按需求分析、界面设计、功能实现、项目总结与附录组织,目录清晰,便于按模块查阅和对照撰写报告。目前已有387人学习,适合需要完整设计思路、代码片段与文档框架的读者参考借鉴。
1. 拆开这份 JAVA 课程设计:ATM 柜员机模拟系统到底能跑通什么
课程设计季一到,后台总有人问同一个问题:有没有一份能直接跑起来、结构还算干净的 Java 小系统,拿来改改就能交差,顺便把 Swing 和 JDBC 这两块补一补。这份 ATM 柜员机模拟系统就是冲着这个场景来的——登录、查询、存款、取款、转账、改密、退出,七个动作全走图形界面,数据落在 MySQL 里,不是那种内存里塞个 HashMap 糊弄人的玩具。它适合两类人:一是刚学完 Java 语法、需要一次完整 GUI 加数据库串联练手的学生;二是想找个现成骨架,把业务逻辑换成自己题目的开发者。下面按我实际拆包的顺序讲,从工程结构到关键代码,再到几个必踩的坑,能抄的地方直接给代码。
2. 工程结构与技术选型:为什么是 Swing 加 MyBatis 而不是纯 JDBC
拿到一个课程设计包,我第一件事不是看界面,是看目录和依赖。这份项目的分层比多数同类作业清楚:视图层用 Swing 的CardLayout做窗口切换,业务层有HandleServiceImpl统一收口,持久层走 MyBatis 的 Mapper 接口,实体是User,返回值统一包了一个Result<T>。这个结构决定了它可改、可测、可扩展,而不是所有逻辑堆在一个main方法里。
2.1 目录分层与各层职责
拆包后大致是这么几块,理解了这个,后面改代码才不会乱:
| 层级 | 典型类/文件 | 职责 |
|---|---|---|
| 视图层 | MainView、ChangeMoneyPanel、登录面板 | 组件布局、事件绑定、窗口切换 |
| 业务层 | HandleServiceImpl | 参数校验、余额判断、事务提交 |
| 持久层 | MysqlOperateMapper接口 + XML | SQL 映射、增删改查 |
| 实体/工具 | User、Result<T>、StringUtils | 数据载体、统一返回、字符串校验 |
| 资源 | images/背景图、mybatis-config.xml | 界面素材、数据库连接配置 |
视图层不直接碰 SQL,业务层不直接碰JButton,这条线守住了,后面加一个「交易流水查询」功能只需要动 Mapper 和业务层,界面加个面板就行。很多课程设计翻车就翻在视图里直接new SqlSession(),改一个字段要翻五个文件。
2.2 选 Swing 加 MyBatis 的理由与边界
Swing 是 JDK 自带的,不用装额外 UI 框架,课程设计环境里最稳;CardLayout天然适合 ATM 这种「一个主窗口、多个功能页来回切」的交互模型,比开一堆JFrame干净得多。持久层选 MyBatis 而不是纯 JDBC,核心原因是 SQL 和 Java 代码分离——存款取款的 SQL 写在 XML 里,改语句不用重新编译业务类,而且#{}预编译天然挡掉一部分注入。
但边界也要说清楚:这套东西是单机模拟,没有并发控制,没有真正的密码加密(原文提到加密存储是需求,实现里是明文比对),也没有事务隔离级别的考量。拿它当学习骨架没问题,直接往生产环境搬就是给自己挖坑。我一般会跟人说:把它当「能跑通全链路的教学样本」,别当「金融系统雏形」。
2.3 环境准备与首次运行
跑起来之前,环境对齐是第一步,版本错位是新手最常见的卡点:
# 1. 确认 JDK 版本(项目用 Swing,JDK 8 及以上均可) java -version # 2. 建库建表,字段名要和 Mapper XML 对齐 mysql -u root -p -e "CREATE DATABASE atm_db DEFAULT CHARSET utf8mb4;" # 3. 导入依赖:mybatis、mysql-connector-java、junit(测试用) # Maven 项目直接在 pom.xml 里加,非 Maven 项目把 jar 丢进 lib 并 Add to Build Path建表语句按实体字段来,核心就一张用户表:
CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE, -- 卡号,登录用 password VARCHAR(50) NOT NULL, -- 密码,教学项目明文存储 money DECIMAL(12,2) DEFAULT 0.00 -- 余额,用 DECIMAL 不用 DOUBLE );这里有个细节值得单独说:余额字段用DECIMAL(12,2)而不是DOUBLE。浮点数做金额运算会出现0.1 + 0.2 != 0.3这类玄学问题,取款几次之后余额对不上账,排查半天以为是 SQL 写错了。金额一律用BigDecimal对应DECIMAL,这是血泪经验,不是洁癖。
数据库连接配置在mybatis-config.xml里,改url、username、password三处即可,url记得带上useSSL=false&serverTimezone=Asia/Shanghai,否则高版本驱动连不上会报时区错误。
3. 核心功能实现:存取款代码逐段拆解与参数说明
界面能点开只是第一步,真正决定这份代码能不能改的是业务层。原文给的ChangeMoneyPanel和HandleServiceImpl是全文最有价值的部分,我按「界面事件 → 业务校验 → SQL 执行」这条链路拆开讲,每段代码后面说清楚它在干什么、参数怎么传、哪里容易改错。
3.1 存取款面板的事件绑定逻辑
ChangeMoneyPanel用了一个很讨巧的设计:存款和取款共用一个面板类,靠构造参数type区分。传「存入」就是存款页,传「取出」就是取款页,标题、按钮文案、调用的业务方法全由这个字符串分流。
public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView) { setLayout(null); // 取 type 首字:"存入"->"存","取出"->"取",用于拼接提示文案 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(e -> { HandleServiceImpl service = new HandleServiceImpl(); Result<Boolean> r; if (type.equals("存入")) { // currentId 是登录成功后挂在 MainView 上的当前用户主键 r = service.addMoney(mainView.currentId, moneyText.getText().trim()); } else { r = service.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 与坐标设置略,见原文 }几个参数要盯住:mainView.currentId是当前登录用户的主键,登录成功时必须赋值,否则存取款会往null上操作;moneyText.getText().trim()一定要trim,用户复制粘贴金额时常带空格,不 trim 会在数字校验那步报「含有非法字符」,让人一头雾水;r.data为true才跳回菜单,这个「成功才返回」的约定贯穿所有功能页,改代码时别破坏它。
3.2 业务层的四道校验与 BigDecimal 比较
HandleServiceImpl里的addMoney和minusMoney是整份代码的「黑匣子」,所有规则都在这。校验顺序有讲究:先判空、再判非法字符、再判整百、最后判余额,顺序错了提示信息就会误导用户。
@Override public Result<Boolean> minusMoney(Long id, String money) { if (money.equals("")) { return new Result<>(false, "不能为空"); } if (!StringUtils.isStrictlyNumeric(money)) { return new Result<>(false, "含有非法字符"); } if (Double.parseDouble(money) % 100 != 0) { return new Result<>(false, "ATM 中只支持百元大钞,零钱请前往柜台领取"); } SqlSession session = sqlSessionFactory.openSession(); try { MysqlOperateMapper mapper = session.getMapper(MysqlOperateMapper.class); User user = mapper.selectById(id); // BigDecimal 比较必须用 compareTo,不能用 equals if (user.getMoney().compareTo(new BigDecimal(money)) < 0) { return new Result<>(false, "余额不足"); } mapper.minusMoneyById(id, new BigDecimal(money)); session.commit(); return new Result<>(true, "取款成功"); } finally { session.close(); // 无论成功失败都关会话,防止连接泄漏 } }参数说明:id是用户主键,money是界面传进来的字符串,全程不转double做金额运算,只在整百判断时临时用一下。compareTo返回负数表示余额小于取款额,这个比较方式必须记住——BigDecimal的equals会连精度一起比,100.0和100.00判不相等,用equals判余额是经典翻车点。
存款方法结构一样,少了余额判断,多了一步addMoneyById。两个方法都遵循「开会话 → 拿 Mapper → 执行 → commit → close」的固定套路,commit漏了数据不落库,close漏了连接池很快耗尽,这两个是新手改代码时最容易删掉的两行。
3.3 Mapper 与 SQL:两条 update 语句的写法差异
持久层就两条核心 SQL,看着简单,但写法直接决定并发下会不会算错账:
<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>这里用的是「在数据库端做加减」而不是「查出来、Java 里算、再写回去」。前者一条语句原子完成,后者在单机教学项目里看不出问题,一旦两个人同时取款就可能出现覆盖写。#{money}是预编译占位符,传BigDecimal会自动映射到DECIMAL列,别改成${money}字符串拼接,那是注入漏洞的入口。
selectById的查询语句对应User实体,字段名和列名不一致时要么起别名要么配resultMap,课程设计里字段少,直接select id, card_no as cardNo, password, money from tb_user where id = #{id}起别名最省事。
4. 避坑与排查:五个真实会卡住你的问题
这份代码我照着跑过一遍,也在别人的机器上远程排过几次,问题高度集中。下面五条按「现象 → 原因 → 解决」写,遇到对得上的直接照方抓药。
4.1 登录后存取款报空指针
现象:登录界面能进主菜单,点存款输入金额确认,控制台抛NullPointerException,定位到addMoney里的id为null。原因是登录成功时只做了密码比对,忘了把查出来的用户主键赋给mainView.currentId。解决:登录校验通过后立刻mainView.currentId = user.getId();,并在主菜单加载时断言它非空,早失败早发现。
4.2 存款成功但重启后余额没变
现象:界面提示「存款成功」,退出重进余额还是原值。原因基本是session.commit()被漏掉或写在close()之后。MyBatis 默认不自动提交,openSession()不带true参数就必须手动 commit。解决:把commit放在 SQL 执行之后、close之前,用try-finally保证close一定执行,别把commit塞进finally,失败时也提交就麻烦了。
4.3 输入 100.00 提示「含有非法字符」
现象:用户输入100.00或带空格的100,被isStrictlyNumeric判为非法。原因是校验工具只认纯数字串,小数点、负号、空格都不放行。解决:界面层先trim(),校验工具按需扩展成允许一位小数点的正则,或者干脆在输入框加DocumentFilter只允许数字,从源头堵住。改校验工具时注意别把整百判断的顺序打乱。
4.4 余额比较结果反了
现象:余额 500,取 300 提示「余额不足」;余额 100,取 500 反而成功。原因是用了equals或把compareTo的返回值方向记反。a.compareTo(b) < 0表示 a 小于 b,取款判断应该是「余额小于取款额就拒绝」。解决:统一封装一个boolean enough(BigDecimal balance, BigDecimal amount)方法,全项目只调它,别在每个方法里手写比较。
4.5 背景图加载报 NPE 或图片不显示
现象:new ImageIcon(getClass().getResource("/images/xxx.gif"))抛空指针,或界面一片灰。原因是资源路径以/开头表示从 classpath 根找,图片没被打进输出目录,或者文件名大小写和代码不一致。解决:确认images目录在源码根下且被标记为资源目录,打包后检查target/classes/images里有没有文件;getResource返回null时先打印路径排查,别直接Objects.requireNonNull把异常吞成一行看不懂的堆栈。
5. 从能跑到能改:把这份骨架扩成自己的课程设计
跑通只是及格线,课程设计要拿得出手,得在骨架上加东西。我给一个改动成本最低、又能明显拉开差距的方向:加一张交易流水表,把每次存取款记下来,查询页顺带展示最近五条。改动只涉及三处,不动现有界面结构。
先建流水表,字段和用户表通过user_id关联:
CREATE TABLE tb_trade ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, type VARCHAR(10) NOT NULL, -- DEPOSIT / WITHDRAW / TRANSFER amount DECIMAL(12,2) NOT NULL, trade_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, trade_time) );然后在HandleServiceImpl的存取款方法里,SQL 执行成功后补一条插入,注意和余额更新放在同一个SqlSession里,保证要么都成功要么都回滚:
mapper.minusMoneyById(id, new BigDecimal(money)); mapper.insertTrade(id, "WITHDRAW", new BigDecimal(money)); // 同一会话内 session.commit();最后在查询面板加一个JTable,数据源调selectRecentTrades(userId, 5),SQL 用order by trade_time desc limit 5。这一步做完,你的系统就从「功能演示」变成了「有审计痕迹的业务闭环」,答辩时能讲的东西立刻多一层。
验证方法也简单:连续做三笔操作(存 500、取 200、转 100),重启程序,查询页应显示三条流水且余额为 200;再手动把流水插入语句注释掉重跑,确认余额更新和流水记录是同一事务——如果余额变了流水没记,说明你的事务边界没圈对。这个反向验证比正向点一遍更能暴露问题。
从那以后我每次拿到这类课程设计包,都强制先跑一遍「存—取—重启—查流水」这条最小闭环,再动任何一行代码。骨架跑不通就改业务,等于在流沙上盖楼。希望这份拆解能帮你少走几个晚上。
本文还有配套的精品资源,点击获取