Java课程设计实战:基于Swing+MyBatis+MySQL的ATM柜员机模拟系统
简介:一份适合Java课程设计参考的ATM柜员机模拟系统项目报告,面向正在完成Java课程设计、或希望系统练习面向对象编程与Java Swing界面开发的学生。报告围绕用户登录、账户查询、存款、取款、修改密码等核心功能,从需求分析、功能图、界面设计到功能实现逐步展开,详细说明了窗口切换与友好界面设计,并重点讲解了取款/存款代码、逻辑校验和SQL数据库操作;同时涉及数据管理、安全机制、异常处理等重难点,可帮助读者快速掌握ATM系统的模块划分与编码要点。整个资料为1个docx格式的课程设计报告文档,压缩包仅1个文件、大小约1.21MB,文档结构清晰,包含完整设计过程、关键代码、测试思路与项目总结,适合作为课程设计报告模板或开发蓝本。目前已有387人学习/浏览,值得推荐给需要完成Java课设或入门GUI编程的同学。
1. 需求立住:ATM柜员机模拟系统到底要解决什么问题
JAVA课程设计年年都有新题目,但ATM柜员机模拟系统始终是那个"看着简单、做起来全是细节"的经典选题。很多同学拿到这个题目第一反应是:不就是做个界面加几个按钮吗?等真正动手才发现,登录验证、余额查询、存取款、改密、转账,每一块都要跟数据库打交道,还要处理各种输入异常和业务规则。如果你正在为这门课设发愁,或者想找一份能直接参考的完整方案,这套基于Java Swing + MyBatis + MySQL的ATM柜员机模拟系统就是一个很合适的样板。它的定位很明确:用桌面GUI模拟真实ATM的操作流程,让学生把Java面向对象、GUI事件处理、JDBC/数据库操作这些知识点串起来,同时把"需求分析→界面设计→功能实现→测试验收"的完整开发流程走一遍。本文会从需求、设计、实现、避坑、验收五个维度把这份方案的每个细节拆开讲清楚,你可以直接照着搭,也可以在自己的项目里改。
2. 总体设计:界面层、服务层与数据层的三层结构
2.1 需求边界:哪些功能必须做,哪些可以不做
ATM柜员机模拟系统最怕的就是功能边界没划清楚,做着做着发现工作量失控。参考这份课设的方案,核心功能锁定在六个模块:用户登录、账户查询、存款、取款、转账、修改密码。登录是入口,查询和存取款是主业务,改密属于账户安全操作,退出系统做状态收尾。这六个模块对应真实ATM的基本操作,既覆盖了教学要求的知识点,又不至于把项目撑得太大。
有些功能是可以有取舍的。比如原文的界面窗口只有1个主窗口加5个功能窗口,转账窗口独立存在,但并没有把转账做成单独的复杂流程,而是复用"输入对方卡号+金额"的简单模式。这种设计的好处是每个窗口职责单一,代码结构清晰,答辩的时候也能把"模块化设计"讲明白。至于真实ATM会有的凭条打印、吞卡处理、超时退出等,属于加分项,课设阶段不做完全合理——把核心六个功能做扎实,比堆一堆花哨但没实现的按钮更有说服力。
另外一个容易被忽略的点是角色定位。这个系统是单角色系统,只有普通用户视角,没有管理员后台。所以不需要做用户管理界面、交易流水导出这类后台功能。数据层面只需要一张用户表存卡号、密码、余额,再加一张可选的交易记录表就能支撑所有业务。把范围控制在这个程度,一个小组分工协作刚好合适。
2.2 分层结构:Swing界面、业务校验、数据操作各管各的
这套方案虽然是一个课设规模的程序,但代码组织并没有写成"所有逻辑堆在一个类里"的败笔。从源码结构能看出清晰的职责划分:界面层在Panel和View类里,负责窗口、按钮、输入框、事件监听;服务层由HandleServiceImpl承担,负责空值校验、非法字符检查、金额规则校验、余额比对这些业务逻辑;数据层借助MyBatis的Mapper接口和XML映射文件操作MySQL数据库。
这种分层的直接好处是改界面不会碰数据库代码,换数据库也不用改界面逻辑。比如存取款界面里的按钮点击事件,做的事情只是把金额字符串传给HandleServiceImpl,然后弹窗显示返回的Result对象里的提示信息;至于金额是不是数字、是不是整百、余额够不够,全部交给服务层判断。Result这个自定义返回类型值得留意,它封装了data和massage两个字段,前者表示操作成功与否,后者表示给用户看的提示文字,这样界面层不需要自己拼错误信息,所有业务反馈都统一走这一个出口。
对于答辩来说,这个结构非常好讲:界面层负责交互,服务层负责规则,数据层负责持久化,每一层都有明确的职责边界。如果导师追问"为什么用Result而不是直接返回boolean",你可以说boolean只能表达成功失败,但用户需要知道失败原因,比如"余额不足"和"含有非法字符"是两种完全不同的失败,Result把成功状态和提示消息打包返回,界面层拿到后直接弹窗展示就行。
2.3 窗口切换方案:CardLayout与按钮事件的配合
界面切换是ATM模拟系统的一个隐藏考点。真实ATM的流程是:登录→主菜单→选择功能→返回主菜单,每一步都是全屏切换。这个方案用的是CardLayout布局管理器,把所有Panel放进同一个容器里,通过cardLayout.show(contentPane,"菜单")这样的语句在面板之间切换。
主菜单界面放多个JButton按钮,每个按钮绑定一个ActionListener,点击后切换到对应功能面板。比如点"存款"按钮,就调用cardLayout.show(contentPane,"存款"),窗口标题同步改成"存款";点"返回"按钮,就切回"菜单"面板,并且把输入框清空,避免下次进来还残留上一次的输入。这个清空操作看着小,实际很容易漏:如果取款失败后直接返回,金额输入框里还留着刚才输入的数字,下次进来用户会以为金额被保留了,容易产生误解。所以backButton的监听器里除了切面板,一定记得moneyText.setText("")。
面板切换的时候还涉及当前登录用户信息的传递。mainView.currentId就是用来记录当前登录用户ID的字段,登录成功后赋值,后续存钱取钱都靠它定位数据库里的记录。这种用单窗口共享状态的方式简单直接,课设阶段够用,不用引入全局变量管理器之类的复杂方案。
3. 功能实现:存款、取款的业务逻辑与界面事件绑定
3.1 存取款界面:ChangeMoneyPanel的复用设计
存取款界面最值得借鉴的是它的复用思路。方案里没有分别写存款Panel和取款Panel,而是在ChangeMoneyPanel里通过type参数区分——type为"存入"时进入存款逻辑,type为"取钱"时进入取款逻辑。界面标题、确认按钮文字、提示语都用这个type动态拼接,一套界面代码同时服务两个功能,减少了大量重复代码。
public class ChangeMoneyPanel extends JPanel { private final Image backgroundImage; public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView) { setLayout(null); String substring = type.substring(0, 1); JLabel label = new JLabel("请输入" + substring + "款金额, ATM机" + substring + "款必须是整百的"); JTextField moneyText = new JTextField(); JButton enterButton = new JButton(type); JButton backButton = new JButton("返回"); enterButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { HandleServiceImpl handleService = new HandleServiceImpl(); Result<Boolean> result; if (type.equals("存入")) { result = handleService.addMoney(mainView.currentId, moneyText.getText().trim()); } else { result = handleService.minusMoney(mainView.currentId, moneyText.getText().trim()); } JOptionPane.showMessageDialog(mainView, result.massage, "提示", JOptionPane.INFORMATION_MESSAGE); if (result.data) { backButton.doClick(); } } }); backButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { mainView.setTitle("菜单"); cardLayout.show(contentPane, "菜单"); moneyText.setText(""); } }); } }这段代码里有几个细节值得说明。第一个是moneyText.getText().trim(),用户输入金额时很可能带前后空格,不trim的话" 100 "这样的字符串会被判定为非法字符,这个踩坑点很隐蔽但很常见。第二个是操作成功后才触发backButton.doClick(),失败则停留在当前界面,用户可以修改金额再试,这个交互逻辑跟真实ATM一致。第三个是所有提示都用JOptionPane.showMessageDialog弹出,参数里的"提示"是窗口标题,INFORMATION_MESSAGE是消息类型,这些细节不用特别记忆,但答辩时被问到"你如何处理操作反馈"可以顺口回答。
3.2 服务层业务校验:加钱与扣钱的对称逻辑
存款和取款的服务层方法是对称设计的,但两者有一个关键差异:存款只需保证金额合法,取款还要额外验证余额充足。这段逻辑校验代码是整个项目的核心知识点之一,课程设计报告里单独辟了一节写它。
@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 (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); 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这个工具方法检查字符串是否全由数字组成,比手动正则更省事,效果等同。金额运算全程用BigDecimal而不是double,这是为了避免浮点数精度问题——0.1 + 0.2在double里不等于0.3,银行系统里这种误差是致命的。
余额判断那里,compareTo返回负数说明账户余额小于取款金额,直接返回"余额不足"且不执行扣款SQL。这里有个容易被忽略的细节:提前return避免了无效的数据库写操作,也保证了对用户余额的安全校验。扣款SQL只有在前面的校验全部通过后才会执行,并且执行后必须commit(),否则事务不会真正落库。
3.3 其他功能模块:登录验证、改密与查询
登录界面的验证逻辑走的是"输入卡号和密码→查数据库→比对密文"的流程。课设阶段通常不会做真正的密码加密存储,直接明文或简单比对即可,但如果你的报告里写了"安全性需求",答辩时导师可能会追问密码存储方式。我一般会建议在方案里加上MD5加盐或SHA-256的说明,哪怕代码里没实现,至少表达你意识到了这个问题。
改密界面包含原密码、新密码、确认新密码三个输入框,点击确认修改后先校验原密码是否正确,再校验新密码和确认密码是否一致,最后执行update语句。这里同样有一条隐藏规则:新密码不能和原密码相同,这条校验加不加都不影响跑分,但加了以后答辩时能多讲一层业务思考。查询界面最简单,查询当前账户余额和最近的交易记录,直接select出来显示在界面上即可,不需要复杂的格式化。
转账功能在原文里描述为"包含需要给转账对象的卡号和金额,输入对应的数字,即可转账成功",实现思路是:输入对方卡号→查对方账户是否存在→扣减自己余额→增加对方余额。这个操作涉及两个账户的余额变更,必须放在同一个事务里,否则可能出现"自己扣了钱、对方没收到"的数据不一致问题。
4. 数据持久化:tb_user表结构、Mapper映射与事务管理
4.1 数据库设计:一张用户表如何支撑全部业务
整套系统的数据存储核心是tb_user表。字段设计遵循最小化原则:id作为主键,login_id或卡号字段做登录标识,password存密码,money存余额。课设阶段不需要设计单独的交易记录表,因为核心功能只要把余额改对就算完成;如果想把"+交易记录查询"做成完整功能,再增加一张tb_transaction表记录时间、类型、金额、操作前后余额即可。
建表语句通常长这样:
CREATE TABLE tb_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(20) NOT NULL UNIQUE COMMENT '卡号', password VARCHAR(32) NOT NULL COMMENT '密码', money DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '账户余额' ); CREATE TABLE tb_transaction ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, type VARCHAR(10) NOT NULL COMMENT '存取/取款/转账', amount DECIMAL(10,2) NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES tb_user(id) );money字段用DECIMAL(10,2)而不是FLOAT或DOUBLE,这是银行系统的标准做法,理由跟上一章说的一样:浮点数有精度损失。DECIMAL在MySQL内部以字符串形式存储,计算时按十进制精确处理,不会出现0.1+0.2不等于0.3的问题。车到山前必有路,但做金融相关模拟系统,精度问题从一开始就别走弯路。
4.2 MyBatis映射:update语句的简洁写法
数据操作层用了MyBatis的Mapper接口加XML映射文件方式。接口里定义addMoneyById和minusMoneyById两个方法,XML里对应两条update语句。这种写法的核心技巧是直接在SQL表达式里做加减运算,而不是先把余额查出来在Java里算好再update回去。
<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>这两条SQL看着简单,但有一个隐含的并发安全设计:余额的增减在数据库层面完成,天然是原子操作。如果先select余额→Java里做加减→再update回去,在高并发场景下会丢更新——两个线程同时读到余额1000,都存100,最后余额变成1100而不是1200。直接在update语句里写money + #{money},数据库的行锁会保证同一时刻只有一个事务能改这一行的余额,从机制上避免了并发覆盖问题。虽然ATM课设没有真实并发,但这个知识点写进报告里会让导师眼前一亮。
4.3 事务边界与资源回收
MyBatis的SqlSession默认不会自动提交事务,必须手动commit()才能让改动落库。原文的代码里,每次操作都是openSession()→getMapper()→执行SQL→commit()→close()。这里有两个关键点。第一个是commit的时机:必须在所有SQL执行成功后调用,如果中途抛出异常,应该调用rollback()回滚,否则会出现部分更新;第二个是close的时机:无论成功还是失败,SqlSession都要关闭,否则数据库连接池会被耗尽。
课设项目里最常见的资源泄漏就是忘了close()。系统跑起来先正常,测了几十次之后突然报连接超时,十有八九是SqlSession没关。规范做法是放在finally块里关闭,但考虑到课设代码的简洁性要求,至少要在成功路径上close。如果你想让方案更完整,可以用try-with-resources或者ThreadLocal绑定SqlSession,这些进阶做法不需要全部实现,但可以在报告的"改进方向"里提一句。
异常处理也是这一层该考虑的事。比如数据库连接失败、SQL执行异常,如果不在服务层catch住,异常会直接抛到界面层,弹出一个红色的错误堆栈给用户看,非常不友好。我一般会在服务层包一层try-catch,捕获Exception后返回一个Result(false,"系统繁忙,请稍后再试"),既保证用户看到的是友好提示,又避免了堆栈信息直接暴露SQL细节。
5. 避坑指南:ATM课设里最容易翻车的六个问题
5.1 空值校验顺序错误导致用户看到诡异报错
现象:用户在取款界面不输入金额直接点确定,弹窗提示的不是"请输入金额",而是"含有非法字符"。 原因:校验代码先执行了StringUtils.isStrictlyNumeric(money),空串"".isStrictlyNumeric返回false,直接走了非法字符分支,根本没走到空值判断。 解决:调整校验顺序,先判断money.equals("")并返回"不能为空",再做字符类型校验。这个问题看代码非常不起眼,但实测必现,答辩演示的时候很影响观感。
5.2 余额不足判断放在扣款SQL之后
现象:取款金额大于余额时,数据库余额变成负数,用户还收到了"取款成功"的提示。 原因:代码直接执行minusMoneyById,没有先查询余额做比较,或者查询了余额但没有拦截。 解决:必须在执行扣款SQL之前,先selectById查出当前余额,用compareTo和取款金额比较,余额不足直接return,不执行update。这一条是金融逻辑的安全底线,测试用例里必须覆盖。
5.3 SqlSession未关闭导致数据库连接耗尽
现象:系统连续操作二三十次后,界面卡死或报"Too many connections"。 原因:每次操作new出SqlSession执行完就扔了,没有close。MyBatis默认从连接池取连接,不close就还不上,连接池满了后续操作全部阻塞。 解决:在完成数据库操作后立即调用sqlSession.close()。更稳妥的做法是放在finally块里,或者用try-with-resources让JVM帮你关。
5.4 金额用double类型导致精度偏差
现象:存款100次0.1元,账户余额显示不是10.00而是9.9999999。 原因:代码里用Double.parseDouble处理金额,double在计算机中是二进制浮点数,无法精确表示0.1。 解决:金额字符串解析用new BigDecimal(money),数据库字段类型用DECIMAL,运算全部交给BigDecimal的compareTo和加减方法。ATM里的"整百校验"用BigDecimal也一样支持,可以用remainder方法取模判断。
5.5 转账不做事务控制导致两边余额不一致
现象:A向B转账时,A余额扣了但B余额没到账。 原因:扣A和加B是两条update SQL,中间没有事务包裹;或者两条SQL在一个方法里执行了但其中一个失败后没有回滚。 解决:把转账的"扣自己+加对方"两步放进同一个事务,要么都成功,要么都回滚。实现上可以在SqlSession里手动控制commit和rollback。
5.6 CardLayout切换后输入框残留数据
现象:用户查询失败返回菜单页,再进入取款界面,金额输入框还留着上一次输入的"200"。 原因:切换面板时只执行了cardLayout.show(),没有把输入框内容清空。 解决:在返回按钮的事件里,除了show面板,还要对界面上的JTextField执行setText("")清空操作。这个细节不难,但很多同学的方案都翻过车。
6. 验收与答辩:怎么测、怎么讲、怎么拿高分
6.1 功能测试用例表
课程设计的验收看的是流程能不能完整走通、边界情况处理得干不干净。下面这组测试用例基本覆盖了ATM模拟系统的日常场景和异常场景,答辩前用这组用例过一遍,能兜住绝大多数提问。
| 编号 | 操作步骤 | 预期结果 | 是否通过 |
|---|---|---|---|
| TC01 | 正确卡号+密码登录 | 进入主菜单 | 必测 |
| TC02 | 错误密码登录 | 提示密码错误,不进入系统 | 必测 |
| TC03 | 卡号不存在登录 | 提示卡号不存在 | 可测 |
| TC04 | 查询余额 | 显示数据库中的当前余额 | 必测 |
| TC05 | 存入100元 | 余额增加100,提示存款成功 | 必测 |
| TC06 | 存入空值 | 提示不能为空 | 必测 |
| TC07 | 存入abc | 提示含有非法字符 | 必测 |
| TC08 | 存入150元 | 提示只支持整百 | 必测 |
| TC09 | 取款200元且余额充足 | 余额扣减200,提示取款成功 | 必测 |
| TC10 | 取款2000元且余额不足 | 提示余额不足 | 必测 |
| TC11 | 修改密码,原密码正确 | 密码更新成功,下次用新密码登录 | 必测 |
| TC12 | 修改密码,原密码错误 | 提示原密码错误 | 可测 |
测试时注意一点:先查询余额做基准,再执行存取款,再查询验证结果是否准确,每一个用例比对修改前后的数据库记录。不要只看界面弹窗。
6.2 答辩讲法:从点讲到面,把踩坑变成亮点
答辩最忌讳的是只讲"我能干什么",不讲"我为什么这么设计"。走进答辩教室前,把这四句话在脑子里过一遍:第一句讲分层,界面、服务、数据各管各的,改一个模块不影响另外两个;第二句讲校验,所有输入先过空值和非法字符,再过业务规则,最后才碰数据库;第三句讲安全,金额用BigDecimal保证精确,余额判断在更新之前,转账操作纳入事务;第四句讲教训,哪几个问题是在测试中发现的、怎么修好的。
这个结构的好处是:每一个点都能对应到课程要求的知识点。分层对应面向对象设计和模块化,校验对应异常处理和健壮性,事务对应数据库知识,测试对应软件工程流程。哪怕代码有一些小瑕疵,只要你能把设计思路和踩坑教训讲清楚,导师不会太为难你。面试官式的追问(比如"你的系统怎么防止并发下余额出错")也可以用"数据库行锁保证原子性、金额计算用BigDecimal"直接接住。
6.3 答辩前的强制流程
答辩前那个晚上,我习惯强制自己走一遍完整流程:登录→查余额→存100→查余额→取200→查余额→转账→查对方余额→改密→退出→用新密码重新登录。这一套场景走完,功能层面的问题基本都暴露了。如果时间宽裕,再把异常场景过一遍:空输入、非法字符、非整百、余额不足、错误原密码、重复新密码,每条确认弹窗提示文字和预期一致。从那以后,每次交课设前我都强制自己把这条主流程和异常分支各走一遍,宁可多花十分钟测试,也不要在验收现场翻车。希望这套方案的内容能帮到正在为课设头大的你。
本文还有配套的精品资源,点击获取