
简介一套完整的JAVA课程设计报告以ATM柜员机模拟系统为开发对象面向正在学习Java面向对象编程与Swing界面开发的高校学生可作为课程设计或自学练手的参考范本。资源包仅含1个docx文档大小1.21MB尽管体量精简但内容完整覆盖需求分析、界面设计、功能实现与项目总结全流程并附有存钱取钱核心代码、逻辑校验思路及SQL语句示例方便读者按章节对照学习。已有387人学习下载适合希望快速掌握ATM模拟系统开发框架、理解账户查询/存款/取款/改密等业务逻辑并规范撰写课程设计报告的读者。通过该文档读者既能对照梳理自己的代码结构也能学习如何将面向对象设计、异常处理与数据库操作融入实际项目同时了解窗口切换、用户登录验证等功能模块的搭建方法有效提升Java综合应用能力。1. 一个能跑完整个业务闭环的JAVA课设ATM柜员机模拟系统到底在练什么ATM柜员机模拟系统这个名字在JAVA课程设计里出现频率极高但它不是那种「交个作业就完事」的玩具项目。它把登录、查询、存款、取款、转账、改密六个完整业务串在一条链路上前端是Swing图形界面后端走数据库真实读写。也就是说做完这个系统你等于亲手走了一遍「界面层—业务层—持久层」的完整开发流程。对于刚接触Java面向对象、还没碰过GUI开发的同学来说这份资源最值钱的地方不是代码本身而是它把「一个软件怎么从需求变成可运行的程序」这个黑匣子拆开了。适合两类人一类是正在做课设、需要参考完整实现的学生另一类是准备面试、想快速回顾Swing和MyBatis配合方式的开发者。2. 需求与模块边界登录、查询、存取款、改密六条主线的功能拆解课程设计的报告里需求分析往往是最容易被跳过的一章但恰恰是它决定了系统做出来是「能演示」还是「能答辩」。这份ATM柜员机模拟系统的需求写得比较清晰六大功能模块边界明确登录、账户查询、存款、取款、改密、退出。拆开看每一条都可以对应到具体的类、方法和数据库操作。2.1 功能清单与优先级排序先理清功能边界。登录是入口没有登录成功其他所有界面都不应该可达查询是最简单的只读操作适合作为第一个实现的模块存款和取款是核心业务涉及金额变更和校验逻辑改密涉及新旧密码校验退出则是把状态清空、回到登录界面。功能模块与实现要点的对应关系大致是这样功能界面层业务层持久层用户登录登录界面输入卡号密码比对数据库用户信息按卡号查询用户记录账户查询查询面板显示余额读取当前用户余额按ID查询money字段存款存款面板输入金额校验金额合法性并累加UPDATE tb_user SET moneymoney?取款取款面板输入金额校验余额是否充足并扣减UPDATE tb_user SET moneymoney-?改密输入原密码、新密码、确认新密码校验原密码后用新密码更新UPDATE tb_user SET password?退出主菜单退出按钮关闭所有界面无从这里可以看出六个功能实际上是三类操作的变种查询类只读、变更类金额增减、更新类密码替换。我在做类似项目的时候会先把这三类抽成三个公共服务方法而不是一个功能一个方法硬写。原代码里的HandleServiceImpl就是这种思路——addMoney处理存款和转账增加minusMoney处理取款和转账扣减逻辑复用度高。2.2 重难点分析界面、数据持久化与异常处理报告里列了七个重难点实际编码时真正卡人的是三个Swing布局与事件处理、数据持久化方式、异常处理覆盖范围。Swing的难点在于它不像Web前端那样有CSS控制布局所有组件都要手动setLocation和setSize。原代码里可以明显看到这一点每个JLabel、JTextField、JButton都是先设定位置再设定尺寸最后统一add到面板上。这种绝对定位方式写起来繁琐但好处是界面效果可控不会因为窗口缩放导致组件乱跑。如果你是第一次写Swing建议先画一张草图标注每个组件的x、y坐标再动手写代码能少调半天布局。数据持久化这块常见做法是两种文件存储和数据库。原项目用的是MySQL MyBatis属于正经的数据库方案。文件存储比如序列化成文本虽然代码量小但课设答辩时容易被追问「数据存在哪、怎么保证一致性」不如数据库方案好解释。至于异常处理原代码主要在业务层做了空值和格式校验界面层用JOptionPane弹窗提示这种分层处理是合理做法。提示功能图里画的模块划分实际对应的是界面跳转关系不是类继承关系。写代码时不要被图误导Swing界面之间用CardLayout切屏业务逻辑独立到Service层各管各的。这一条主线捋下来你会发现整个系统的技术栈并不复杂Swing画界面、MyBatis操作数据库、Service处理业务规则。真正的难点在「界面事件如何正确触达数据库」也就是第3章要讲的窗口切换和第4章要讲的业务实现。3. Swing界面与CardLayout从登录窗口到五个功能面板的切换实现界面设计这一章核心不是「怎么把界面画好看」而是「多个窗口之间怎么切换」。原项目用的是1个主窗口 5个功能窗口的结构主窗口放菜单按钮点击后跳转到对应功能界面。这个场景用CardLayout最合适它是Swing里专门做多面板切换的布局管理器所有面板叠在一起通过show()方法控制显示哪一张。3.1 窗口切换CardLayout配合按钮事件先说结构。常见做法是在主窗口创建一个JPanel作为容器给它设置成CardLayout然后把登录面板、菜单面板、查询面板、存款面板、取款面板、转账面板、改密面板全部add进去每个面板给一个字符串标识。切换的时候只要调用CardLayout cardLayout (CardLayout) contentPane.getLayout(); cardLayout.show(contentPane, 菜单);这行代码的含义是从当前面板切换到名为「菜单」的面板。第一次看这段代码的人容易懵的是contentPane.getLayout()这步——它先拿到容器现有的布局管理器强转成CardLayout然后才能调show()。如果容器压根没设置过CardLayout这行会直接抛ClassCastException。原代码里每个功能面板的「返回」按钮都有这样一串动作mainView.setTitle(菜单); cardLayout.show(contentPane,菜单); moneyText.setText();前两行是切换界面第三行是清空输入框。这里有个很容易忽略的细节每次返回菜单时把输入框清空。如果不清空下一次进入存款界面上次输入的金额还留在文本框里用户点了「存入」就会把旧数据再提交一次这在演示场景里非常尴尬。参数说明show(Container parent, String name)的第一个参数是卡片所在容器第二个参数是add()时指定的约束字符串必须完全一致多一个空格都匹配不上。3.2 登录界面与主菜单输入校验与页面跳转登录界面是最先展示的窗口它的逻辑是用户输入卡号和密码点击登录后系统拿这两个值和数据库里的tb_user表比对。登录成功后界面跳转到主菜单失败则弹窗提示。这里原代码没有展开我按最常见的实现补全一下思路。登录按钮的事件里通常会做这几件事loginButton.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { String cardId cardText.getText().trim(); String password pwdText.getText().trim(); if (cardId.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(frame, 卡号和密码不能为空, 提示, JOptionPane.WARNING_MESSAGE); return; } User user userService.login(cardId, password); if (user ! null) { mainView.currentId user.getId(); cardLayout.show(contentPane, 菜单); } else { JOptionPane.showMessageDialog(frame, 卡号或密码错误, 提示, JOptionPane.ERROR_MESSAGE); } } });这段代码的逻辑说明trim()去掉首尾空格避免用户手误多打了空格导致比对失败currentId是MainView里一个用来记录当前登录用户ID的字段后续所有查询、存款、取款操作都靠它定位用户登录成功后用cardLayout.show切到菜单面板。参数说明JOptionPane.showMessageDialog的四个参数分别是父窗口、提示文案、标题、消息类型。消息类型有WARNING_MESSAGE、ERROR_MESSAGE、INFORMATION_MESSAGE三种按场景选答辩时老师喜欢问这个。登录成功的前提是数据库里有预设用户。因为这是模拟系统不提供注册功能所以必须在数据库初始化脚本里预置几条用户数据比如卡号1001、密码123456、余额5000.00这样的测试账号。很多同学在演示时栽跟头就是因为数据库里没有预置数据登录永远失败。3.3 功能面板的布局设计绝对定位与图片背景原代码的功能面板有个很显眼的地方——构造函数里设置了一行setLayout(null);这是Swing里最原始的布局方式彻底放弃布局管理器所有组件全靠手动定位。setLayout(null)之后组件的位置和大小都交给setLocation和setSize控制。原代码里每个JLabel都先setLocation(0, 30)再setSize(700, 50)这种写法看着啰嗦但其实是模拟ATM机界面时最稳定的方案因为ATM机界面元素本来就固定不需要响应式调整。图片背景的处理则是重写了paintComponent方法Override protected void paintComponent(Graphics g) { super.paintComponent(g); if (backgroundImage ! null) { g.drawImage(backgroundImage, 0, 0, getWidth(), getHeight(), this); } }这段代码的作用是每当面板需要重绘时先把背景图片画满整个面板再画其他组件。getWidth()和getHeight()是面板当前的实际宽高图片会拉伸填满。注意这里有个坑背景图用的是一张gif动图ImageIcon加载动图时只有第一帧会被画出来动效是看不到的。如果你想让背景真正动起来得用Timer逐帧刷新但课程设计场景下静态背景完全够用别在这上面浪费时间。界面布局的另一个细节是字体设置。原代码反复使用new Font(黑体, Font.BOLD, 30)这是为了在较大分辨率下让界面文字清晰可见。如果是在没有中文字体的系统上跑Swing会回退到默认字体中文可能显示成方块。这个在后面避坑章会详细讲。这一章的核心结论是Swing的多窗口项目用CardLayout做主切换、绝对定位做子面板布局是一套成熟且容易讲解的方案。你不需要学很炫的布局技巧把这两招用熟界面层就能撑起整个项目。4. 存取款核心逻辑按钮事件、金额校验与MyBatis数据库更新第3章把界面层讲透了这一章进入真正的业务核心——存取款。原代码里最有价值的部分也在这里一个ChangeMoneyPanel类同时处理存款和取款两种场景通过构造函数传入的type参数区分业务层addMoney和minusMoney分别实现金额增加和扣减持久层用MyBatis的动态SQL完成数据库更新。三层各司其职代码结构值得细读。4.1 存取款面板ChangeMoneyPanel复用一个类实现两个功能先看界面层的复用设计。构造函数签名是public ChangeMoneyPanel(String type, Container contentPane, CardLayout cardLayout, MainView mainView)type参数只有两个取值「存入」和「取款」。构造函数里有一行很关键final String substring type.substring(0, 1);这是取出type的第一个字也就是「存」或者「取」。后面所有提示语都用这个substring拼接于是同一个类自动适配了两种场景——输入「存入」时提示「请输入存款金额」输入「取款」时提示「请输入取款金额」。按钮事件里也是靠type.equals(存入)来分流的enterButton.addActionListener(new ActionListener() { Override public void actionPerformed(ActionEvent e) { if(type.equals(存入)){ HandleServiceImpl handleService new HandleServiceImpl(); ResultBoolean booleanResult handleService.addMoney(mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, booleanResult.massage, 提示, JOptionPane.INFORMATION_MESSAGE); if(booleanResult.data){ backButton.doClick(); } }else{ HandleServiceImpl handleService new HandleServiceImpl(); ResultBoolean booleanResult handleService.minusMoney(mainView.currentId, moneyText.getText().trim()); JOptionPane.showMessageDialog(mainView, booleanResult.massage, 提示, JOptionPane.INFORMATION_MESSAGE); if(booleanResult.data){ backButton.doClick(); } } } });逻辑说明点击按钮后调用handleService对应的业务方法传入当前登录用户的ID和文本框里输入的金额。业务方法返回一个ResultBoolean对象里面封装了操作是否成功的标记data和提示信息massage。弹窗展示提示后如果操作成功就模拟点击「返回」按钮回到菜单。这种「成功自动返回」的设计很符合ATM机的真实使用逻辑——存完钱退到主界面而不是停在原界面等用户手动返回。参数说明moneyText.getText().trim()拿到的是用户输入的原始字符串trim()去掉两端的空格。注意这里传的是String真正的类型转换和校验发生在Service层界面层不做任何业务判断。这是MVC分层的好习惯界面只负责收集和展示业务规则归Service管。4.2 金额校验空值、非法字符、整百限制业务层的校验逻辑是这一段代码的精髓完整拆开看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); }三个if判断对应三个异常场景我逐条说明第一个if判断money.equals()捕获的是空输入。用户在界面里什么都没敲就点了确认getText()返回空字符串直接提示「不能为空」。这里有个隐藏细节money在调用前已经过了trim()所以全空格输入也在这里被拦截了。第二个if判断StringUtils.isStrictlyNumeric(money)这个方法来自org.apache.commons.lang3.StringUtils作用是判断字符串是否全部由数字组成。它不认小数点、不认正负号、不认空格。所以「12.5」「-100」「1 00」都会被拦下来提示「含有非法字符」。这一步把绝大多数脏数据挡在门外。第三个if是业务规则Double.parseDouble(money)%100!0。ATM机取款通常只支持百元面额所以金额必须是100的倍数。这里用Double.parseDouble把字符串转成double再取模判断。注意能被100整除的余数一定是0所以取反就是「不是整百」就拒绝。关于校验顺序这三个if的顺序是有讲究的。先查空再查格式最后查业务规则。如果把第三个if放前面用户输入「abc」时Double.parseDouble(abc)直接抛NumberFormatException程序就崩了。校验顺序错了代码写得再好也白搭。数据库操作部分是标准的MyBatis使用流程SqlSession sqlSession sqlSessionFactory.openSession(); MysqlOperateMapper userMapper sqlSession.getMapper(MysqlOperateMapper.class); userMapper.addMoneyById(id,new BigDecimal(money)); sqlSession.commit(); sqlSession.close();这四行里最重要的一行是sqlSession.commit()。MyBatis的openSession()默认不自动提交事务如果不手动commit数据变更不会生效。很多同学跑完程序发现余额没变第一个排查点就是这里——是不是忘了commit。最后一行close()释放连接资源如果不关连接池很快耗尽系统运行一段时间后就会卡死。minusMoney取款的逻辑和存款基本对称多了一个余额检查User user userMapper.selectById(id); if(user.getMoney().compareTo(new BigDecimal(money))0){ return new ResultBoolean(余额不足,false); }这里用的是BigDecimal.compareTo而不是equals原因是BigDecimal的equals方法要求精度也一致1.0和1.00用equals比较会返回false但用compareTo就只看数值大小。实际开发中金额比较一律用compareTo这是写财务相关代码的基本常识。4.3 SQL映射与事务提交持久层的SQL写在MyBatis的XML映射文件里原代码给了两个最关键的操作update idaddMoneyById update tb_user set moneymoney#{money} where id #{id} /update update idminusMoneyById update tb_user set moneymoney-#{money} where id #{id} /update这两个SQL的核心技巧是金额更新用「相对更新」而不是「绝对更新」。moneymoney#{money}是在原有余额基础上做加法先查询余额再算出新余额然后更新而是一条SQL直接在数据库端完成运算。这样做的好处有两个一是减少了一次查询的IO开销二是避免了并发场景下的数据覆盖问题——假设账户余额1000同时在两个窗口分别取100和200如果用先查后更新的思路两个操作都读到1000最后结果可能只剩800而用相对更新数据库串行执行两条SQL结果一定是700。参数说明#{money}和#{id}是MyBatis的预编译参数对应方法传入的BigDecimal money和Long id。预编译能防SQL注入——用户输入100; DROP TABLE tb_user这种恶意字符串时预编译把它当成纯数据而不是SQL语句的一部分。做金融模拟系统这个是必须有的安全底线。事务层面SQL是放在Service方法里执行的也就是每执行一次存取款开一个SqlSession提交后关闭。这个写法在单用户场景没问题但如果未来改造成多用户并发,建议把事务提升到Service层统一管理。课程设计阶段保持现状就够。这一章看下来存取款功能的完整链路是界面层收集输入 → Service层三重校验 → Mapper层相对更新SQL → 提交关闭Session。每一层职责单一任何一步失败都能在界面弹窗给到明确提示。这个分层模型是你答辩时讲「系统设计」的最佳素材。5. 常见问题与避坑课设提交前最容易翻车的五个点这一章我从实际拆过的同类项目里挑出五个高频故障记录给你。每一条都是真实会发生的场景按「现象 → 原因 → 解决」写清楚。5.1 登录按钮点了没反应控制台报NullPointerException现象界面正常显示输入卡号和密码点登录按钮界面没跳转控制台输出一串NullPointerException。原因最常见的是cardLayout或contentPane变量为null。写界面代码时如果CardLayout对象是在某个方法里创建的局部变量而按钮事件里又引用这个变量方法执行完变量就被回收了事件触发时找不到对象。解决把CardLayout和主容器定义为Window类比如MainView的成员变量在构造函数里实例化所有内部类的事件处理器通过外层类.变量名访问。我一般会写MainView.this.cardLayout显式指定避免内部类里同名变量遮挡。5.2 运行报错「ClassNotFoundException: com.mysql.cj.jdbc.Driver」现象程序一启动就连不上数据库报找不到JDBC驱动类。原因两个常见诱因。一个是项目里没引入MySQL驱动jar包——用Maven的话是漏了mysql-connector-java依赖用IDEA直接开发的话是遗漏了jar包到lib目录另一个是MySQL 8.x的驱动类名和5.x不一样8.x要用com.mysql.cj.jdbc.Driver5.x才用com.mysql.jdbc.Driver。解决如果你用的是MySQL 8.x在pom.xml里加dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency同时检查连接串加serverTimezoneAsia/Shanghai参数不然MySQL 8.x会报时区错误。一个典型的连接串是jdbc:mysql://localhost:3306/atm_db?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf85.3 存了1000元余额显示只多了10.0现象输入1000点存入提示成功查询余额时数字不对小数点位置诡异。原因类型不一致导致的精度丢失。界面输入是String业务层先Double.parseDouble转成double数据库映射时又转成BigDecimal。double本身就是二进制浮点1000.0的二进制表示不是精确的转成BigDecimal后可能得到999.99999999之类的值。原代码里存款方法在校验时用了Double.parseDouble但真正写入数据库时用的是new BigDecimal(money)——这里money是原始字符串所以写入数据库的其实是精确的1000但如果哪一步把double的值传给数据库精度就丢了。解决持久层统一用BigDecimal字符串直接new BigDecimal(money)传入绝不要经过double中转。校验时可以用Integer.parseInt或者先判断是不是整数再做整除判断避免double误差。这是金融系统的基本红线课设阶段养成好习惯后面做正式项目就少填一个坑。5.4 取款后余额没变但提示「取款成功」现象取款500提示成功回菜单查余额发现一分没扣。原因看一下minusMoney方法里有没有sqlSession.commit()。MyBatis默认openSession()开的是非自动提交事务执行完UPDATE后必须手动提交。如果只在addMoney里写了commitminusMoney里漏了存款生效取款不生效。解决在两个方法里都补上sqlSession.commit()并把sqlSession.close()放进finally块或者直接用MyBatis的工具类让SqlSession自动管理事务。逻辑说明finally里close()能保证即使前面的代码抛异常数据库连接也能被释放不会造成连接泄漏。5.5 中文界面全部变成方块乱码现象界面标题、按钮、提示文字全是「口口口」这种方块。原因Swing默认字体不含中文字形而原代码里字体设为「黑体」如果运行环境没装中文字库就回退失败。这是国产Java课设最常见的玄学问题平时在Windows上跑得好好的换到某些精简版Linux服务器上就翻车。解决字体配置改成兼容性更好的写法优先检测系统可用中文字体Font font new Font(Microsoft YaHei, Font.BOLD, 30);如果还是乱码检查启动时有没有加上文件编码参数java -Dfile.encodingUTF-8 -jar atm_system.jar-Dfile.encodingUTF-8强制JVM用UTF-8读取源文件和资源能在这个场景下兜底解决绝大多数乱码。提示数据库那边也别忘。如果建表用了utf8mb4而JDBC连接串里没写characterEncodingutf8MySQL返回的数据到Java里同样会乱码。连接串、源文件编码、数据库编码三处必须保持一致。这五个坑前两个属于环境问题后三个属于编码习惯问题。它们共同的特点是报错信息不一定直观排查起来费时间。把这条清单截图存在手机里课设提交前对照检查一遍能少熬两个通宵。6. 进阶打磨从演示级代码变成能过评审的完整项目如果只是把原代码跑通课设能及格但答辩时老师会追问很多「为什么」。这一章我按自己拆项目的习惯给你三条进阶改造路线做完之后你的系统就不是「能演示」而是「能讲清楚」。第一条是加密存储。原项目的密码在数据库里是明文答辩时老师大概率会问「密码安全吗」。你可以引入MessageDigest做SHA-256哈希登录时比对密文而不是明文public static String toSHA256(String input) { try { MessageDigest md MessageDigest.getInstance(SHA-256); byte[] hash md.digest(input.getBytes(StandardCharsets.UTF_8)); StringBuilder sb new StringBuilder(); for (byte b : hash) { sb.append(String.format(%02x, b)); } return sb.toString(); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(SHA-256算法不可用, e); } }逻辑说明MessageDigest.getInstance(SHA-256)拿到算法实例digest()做哈希运算得到字节数组再逐字节转成十六进制字符串。这样数据库里存的是哈希值即使库被拖走密码也不会直接暴露。第二条是操作日志。原代码做了存取款、改密的操作记录但没有落库。你可以加一张tb_record表字段包含user_id、operation_type、amount、operate_time每次业务成功后插入一条记录。这样「查看交易记录」功能才有数据支撑系统的业务闭环才算完整。第三条是统一异常处理。现在每个方法里都有if判断弹窗代码重复率高。你可以封装一个业务异常类BizExceptionService层只抛异常不处理界面层统一捕获弹窗。改造后业务方法从十行变三行代码好看很多答辩时还能提「我用的是统一异常处理机制」这是一个很好的加分印象点。说到验证方法我的习惯是每次改完代码强制跑一遍「开户登录 → 查询余额 → 存取款 → 改密 → 退出再登录」的完整链路确认每一步的数据库结果都正确。从那以后我每拿到一份JAVA课程设计的代码都会第一时间把数据库脚本和驱动版本先匹配上再开始看业务代码——这个习惯帮我省掉了不知道多少「明明代码没问题却跑不起来」的深夜。希望帮到你。本文还有配套的精品资源点击获取