Java课程设计避坑指南:ATM模拟系统中的金额精度与事务边界

发布时间:2026/11/2 18:05:04
Java课程设计避坑指南:ATM模拟系统中的金额精度与事务边界 简介这份资源是面向高校计算机专业学生与Java初学者的课程设计完整文档围绕ATM柜员机模拟系统展开帮助读者在掌握面向对象编程思想的同时完成一次贴近真实银行业务的项目实践。压缩包内共1个docx文件约1.21MB内容涵盖需求分析、界面设计、功能实现与项目总结四大模块具体包括用户登录、账户查询、存款、取款、更改密码等功能设计以及取款存款核心代码、逻辑校验思路、SQL语句示例和功能结构图并配有目录与参考文献结构完整、层次清晰。已有387人学习下载适合需要参考课程设计写作框架、梳理Swing界面开发流程或对照代码理解业务逻辑的读者可直接借鉴其分析思路与文档组织方式用于自己的课程设计或项目练习。1. ATM 模拟系统为什么“余额不足”这四个字最难写对很多人第一次做 Java 课程设计看到“ATM 柜员机模拟系统”会觉得简单登录、查询、存取款、改密五个按钮五个窗口Swing 拖一拖就完事。真正动手才发现界面反而是最不费脑子的部分真正让人翻车的是那句“余额不足”——它背后牵扯的是金额精度、并发更新、校验顺序和事务边界。一个能跑起来的 Demo 和一个“敢让人乱点”的系统差距全在这些看不见的地方。这份课程设计资源的价值不在于它用了多少高级框架而在于它把一个金融类业务的最小闭环走完了Swing 做界面、CardLayout 做窗口切换、Service 层做逻辑校验、MyBatis 映射 SQL 落库。它把“界面事件如何驱动业务逻辑、业务逻辑如何安全地改数据库”这条链路完整暴露出来正好适合拿来拆解底层机制。搞懂它你就明白为什么金额不能用 double、为什么校验要分先后、为什么一次取款必须包在一个会话里。2. 分层与窗口切换Swing 的事件模型到底在替谁干活2.1 CardLayout 的本质是“同一容器里的多张卡片”这个系统用 1 个主窗口加 5 个功能窗口主窗口上排按钮点击后切换内容。常见的实现思路不是开 6 个 JFrame 互相跳转而是用一个 JFrame 装一个 CardLayout 容器每个功能面板作为一张“卡片”塞进去切换时只换显示的那张。这样做的好处很直接窗口状态、背景、标题都在同一个容器里管理不会出现“点了三次取款弹出三个窗口”的经典 bug。从源码里能看到ChangeMoneyPanel的构造函数签名它接收Container contentPane、CardLayout cardLayout、MainView mainView三个参数。这不是随便传的——contentPane是卡片容器本体cardLayout是切换器mainView是持有当前登录用户currentId的上下文对象。面板自己不存用户身份而是回头问主视图要这就是最朴素的“上下文传递”。// 面板构造时注入容器、布局管理器和主视图上下文 public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView) { setLayout(null); // 绝对定位配合 setLocation/setSize final String substring type.substring(0, 1); // 存入-存取出-取 // ... 组件创建与定位 ... backButton.addActionListener(e - { mainView.setTitle(菜单); cardLayout.show(contentPane, 菜单); // 按卡片名切回主菜单 moneyText.setText(); // 清空输入避免残留 }); }这里有个容易被忽略的细节type.substring(0, 1)把“存入/取出”截成“存/取”用来动态拼提示语和按钮文字。这种“一个面板类复用两种业务”的写法省了代码但也埋了坑——后面校验逻辑里type.equals(存入)一旦写错一个字存钱就变成取钱。参数type实际承担了“模式开关”的角色属于用字符串当枚举用的典型课程设计手法。2.2 事件监听器里为什么不能直接写 SQL看enterButton的监听器它没有直接操作数据库而是new HandleServiceImpl()然后调addMoney/minusMoney拿回一个ResultBoolean根据data决定是否backButton.doClick()返回菜单。这个结构值得说透Swing 的事件分发线程EDT是单线程的所有界面重绘和事件回调都排在这一个线程上。如果在监听器里直接跑一条慢 SQL整个界面会卡死用户以为程序崩了。所以合格的写法是把“界面响应”和“业务执行”分开监听器只负责收集输入、调用服务、根据结果弹提示。Result这个返回类型是这套代码的“契约”——它同时携带massage原文拼写如此和data界面层不需要知道失败原因是余额不足还是字符非法只管把massage弹出来。这种“服务层返回结构化结果、界面层只做展示”的分工是分层最实在的收益。提示JOptionPane.showMessageDialog是模态的弹窗期间 EDT 被占用但用户无法再点其他按钮反而避免了重复提交。这是课程设计里“歪打正着”的并发保护。2.3 绝对定位布局的取舍代码里满屏setLocation/setSizesetLayout(null)关掉了布局管理器。教学项目爱这么写因为所见即所得、不用理解 BoxLayout 和 GridBagLayout 的约束规则。代价是窗口一拉伸组件就错位换个分辨率字体就挤在一起。这不是不能接受——课程设计的窗口尺寸固定绝对定位反而让“背景图 文字 输入框”的叠放关系一目了然。但如果要把它改成可缩放界面第一件事就是把null布局换成GridBagLayout否则血泪经验马上就来。3. 金额校验链为什么 double 是金融系统的原罪3.1 校验顺序本身就是设计addMoney和minusMoney的校验顺序几乎一模一样先判空、再判是否纯数字、再判是否整百。这个顺序不是随手排的。判空放最前是因为money.equals()对 null 会抛空指针而输入框getText().trim()至少返回空串所以先挡空串最安全。判纯数字放第二是因为Double.parseDouble遇到字母直接抛NumberFormatException必须在解析前拦住。整百判断放最后因为它依赖解析后的数值。Override public ResultBoolean addMoney(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) { // ATM 只收百元整钞 return new Result(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(存款成功, true); }注意isStrictlyNumeric这个自定义工具方法它比Double.parseDouble更严格能挡住1e3、 12 、0x10这类解析器能吞但业务不该接受的输入。参数money是字符串而非数值正是为了“先校验、后转换”把非法输入挡在类型转换之前。3.2 BigDecimal 与 double 的分水岭校验里用Double.parseDouble(money) % 100做整百判断落库时却用new BigDecimal(money)。这个混用暴露了一个关键认知double是二进制浮点0.1 0.2不等于0.3用它存钱几次存取之后余额就会出现999.9999999999999这种鬼数字。BigDecimal是十进制精确表示配合数据库的DECIMAL字段才能保证“一分不多一分不少”。但BigDecimal也有讲究new BigDecimal(100.00)和new BigDecimal(100.00)结果不同后者会把 double 的误差原样带进来。所以正确姿势永远是从字符串构造。这套代码在落库处用了字符串构造算是踩对了点整百判断用 double 只是因为% 100对整数部分够用属于“局部妥协”。3.3 取款的余额比较compareTo 而不是 equalsminusMoney里最关键的一行是user.getMoney().compareTo(new BigDecimal(money)) 0。为什么不用equals因为BigDecimal的equals连精度scale都比100和100.00用equals判不相等但业务上它们是同一个金额。compareTo只比数值大小返回负数表示前者小于后者正好对应“余额不足”。User user userMapper.selectById(id); if (user.getMoney().compareTo(new BigDecimal(money)) 0) { return new Result(余额不足, false); // 先查后改读到的余额是判断依据 } userMapper.minusMoneyById(id, new BigDecimal(money)); sqlSession.commit();这里有个隐蔽的时序问题先selectById读出余额再minusMoneyById做减法两步之间如果另一个请求也取款就可能双双通过余额检查造成透支。课程设计单用户操作碰不到但这是理解“为什么真实系统要用update ... where money ?原子扣减”的最佳入口。SQL 层写成money money - #{money}本身是原子的问题出在“检查”和“扣减”被拆成了两条语句。4. SQL 与持久化一条 update 背后的会话生命周期4.1 手写 MyBatis 映射的极简形态持久层没有用 Spring 托管而是手动openSession→getMapper→ 执行 →commit→close。这套流程把 MyBatis 的会话生命周期完整摊开SqlSession是一次数据库对话commit()之前所有写操作都在事务里没落地close()释放连接。漏掉commit是最常见的翻车点——界面提示“存款成功”重启程序余额没变因为事务回滚了。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这两条 SQL 用的是“在数据库端做增减”而不是“查出来加完再写回去”。前者是原子操作后者在并发下会丢更新。#{money}是预编译占位符MyBatis 会把它转成?并绑定参数天然防 SQL 注入如果写成${money}就是字符串拼接输入框里敲个1; drop table就热闹了。这个区别是安全审计的必考点。4.2 为什么每次操作都新开会话代码里每个 Service 方法都openSession一次、close一次没有复用。教学项目这么写没毛病逻辑简单、不会出现会话泄漏、每次操作都是独立事务。但真实系统里频繁开关连接开销大通常用连接池加会话复用。理解这一点你就明白“连接池”不是玄学它解决的就是“开一次会话成本太高”的问题。参数上openSession()默认不自动提交所以必须显式commit()如果改成openSession(true)每条语句自动提交反而失去了“多条语句一个事务”的能力。4.3 转账缺失的那一半功能列表里写了转账但正文的代码只给了存取款。从机制上推转账必然是“扣一方、加一方”两条 update且必须包在同一个SqlSession里先扣款、再入账、一起commit。如果分两个会话各自提交扣款成功而入账失败钱就凭空消失了。这正是事务 ACID 里“原子性”要解决的问题也是课程设计从“能跑”迈向“可信”的分水岭。常见的实现思路是把两条 mapper 调用放进同一个 session任何一步抛异常就rollback()。5. 避坑与排查课程设计里最容易翻车的五个点5.1 提示成功但余额没变现象点“存入”弹出“存款成功”回菜单查询余额还是原值。原因SqlSession忘了commit()或者commit之后又开了新会话查询。解决确认写操作后紧跟commit()查询用同一份已提交的数据调试时在commit前后各打一条日志看事务边界到底在哪。5.2 输入“100.5”被拒但提示词不对现象输入带小数的金额弹出“含有非法字符”而不是“只支持百元大钞”。原因isStrictlyNumeric把小数点也当非法字符挡掉了校验顺序导致提示不精确。解决明确业务规则——ATM 只收整百那小数点本就该拒但提示语应改成“请输入整百金额”若允许角分则校验函数要放行小数点并改用BigDecimal解析。5.3 取款后余额变成负数现象连续快速点击取款按钮余额被扣成负数。原因余额检查和扣减是两条语句中间没有原子约束或者按钮未禁用导致重复提交。解决SQL 改成update tb_user set money money - #{money} where id #{id} and money #{money}用受影响行数判断是否成功界面层在请求返回前禁用按钮。5.4 改密后旧密码还能登录现象修改密码提示成功退出后用旧密码仍能进。原因改密只更新了内存对象没落库或更新语句的where条件写错导致更新了 0 行却返回成功。解决检查update的受影响行数为 0 就提示“原密码错误”确认commit执行。5.5 背景图在打包后消失现象IDE 里背景正常打成 jar 后一片空白。原因用getClass().getResource(/images/xxx.gif)读资源路径大小写或打包配置不对返回 null 后Objects.requireNonNull直接抛异常。解决确认资源在src/main/resources下、路径大小写一致对getResource的返回值做空判断缺图时降级为纯色背景而不是崩溃。6. 从“能跑”到“可信”一个课程设计该留什么后手这套代码最值得琢磨的地方不是它写得多好而是它把“教学项目”和“生产系统”的鸿沟标得清清楚楚。界面层用绝对定位、服务层用字符串当模式开关、持久层手动管会话——每一条在课程设计里都合理因为目标是“用最小认知成本把闭环跑通”。但只要你多想一步就能看到每条捷径背后的代价绝对定位换不来自适应字符串开关换不来编译期检查手动会话换不来连接复用。我的判断是这类项目的正确用法不是照抄而是拿它当“反例基线”。比如把type.equals(存入)改成枚举OperationType.DEPOSIT编译器立刻帮你挡住拼写错误把余额检查挪进 SQL 的where条件并发透支问题当场消失把openSession包进 try-with-resources会话泄漏不再靠自觉。这些改动都不大但每改一处你对“为什么框架要这么设计”的理解就深一层。教学写法生产等价物解决的核心问题字符串当模式开关枚举 / 策略模式编译期校验、可扩展先查后改条件更新 受影响行数并发下的原子性手动开关会话连接池 声明式事务资源复用、事务边界清晰double 判整百BigDecimal 全程金额精度绝对定位布局管理器界面自适应验证方法也很朴素写一个并发测试两个线程同时对同一账户各取 80 元、余额 100 元看最终余额是 -60 还是 20。跑一次你就永远记住“检查与扣减必须原子”。再写一个精度测试连续存 0.1 元一百次假设放开角分看余额是不是 10.00 而不是 9.999999。这两个测试比任何理论都管用。从那以后我每次碰金额相关的代码都强制走一遍“字符串入参 → BigDecimal 解析 → 条件更新 → 查受影响行数”这条链少一步心里就不踏实。这份 ATM 课程设计资源把这条链的雏形完整摆在你面前界面、校验、SQL、事务一个不缺正好拿来当练手的底稿——把上面那几处“教学妥协”逐个替换掉你对 Java 分层和金融业务底层的理解会比读十篇教程都扎实。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询