资讯动态

Java课程设计ATM模拟系统:BigDecimal精度与多线程并发实战

发布时间:2026/10/15 5:33:11 来源:尧图企业网站定制
简介这是一份Java课程设计完整方案课题为ATM柜员机模拟系统面向计算机专业学生、课程设计参与者及需要巩固Java面向对象编程的开发者。资源围绕银行业务自助化场景完整覆盖用户登录、账户查询、存款、取款、修改密码等核心功能从需求分析到功能实现均有详细阐述。资源为docx格式文档一份压缩包大小1.21MB包含报告正文、功能图、核心代码与SQL语句等内容已有387人学习/下载。报告结构清晰需求分析、界面设计、功能实现、项目总结等章节一应俱全不仅给出了取钱与存钱功能的具体Java代码实现如账户余额校验、交易记录处理还解释了界面设计中的窗口切换、登录与主菜单布局以及异常处理和数据管理要点可帮助读者快速掌握Java Swing GUI开发和软件工程基本流程既能作为课程设计文档模板也是复习Java编程的实用参考。1. JAVA课程设计ATM柜员机模拟系统为什么说这个选题是答辩加分的稳妥选择每年课程设计答辩被问最多的不是「功能能不能跑」而是「你那台ATM怎么保证两个窗口同时取款不会乱」。JAVA课程设计ATM柜员机模拟系统这个选题把语法、面向对象、集合、异常、文件读写和多线程串成一条线比图书管理、宿舍管理这类重复增删改查更有区分度又不至于像电商系统那样一个月都写不完。这篇文章按我自己的落地顺序讲需求边界、账户模型、服务层骨架、交互层写法再到金额精度、Scanner阻塞、文件持久化这些能毁掉答辩的坑。代码可复制、有注释、带参数说明你改包名就能跑也可以照骨架重写。看完第2章的边界清单你会对这个题目值不值得投入有一个准确的判断。2. 需求边界与数据模型先定清楚「ATM到底让用户干什么」2.1 功能清单与边界条件老师追问的重点全在这里很多同学拿到题目直接写类、写界面写到一半才发现规则没定清楚取款上限、锁定次数、流水保留条数全是拍脑袋。这个现象我见过太多次。课程设计里「ATM柜员机模拟系统」的消息需求通常只写四个字「存取款转账」没有告诉你单笔取款上限是多少、密码错几次锁卡、转账要不要手续费。这些边界参数恰恰是模拟系统与玩具Demo的分水岭也是答辩老师最愿意追问的地方。我先把一份推荐的功能边界清单列出来你直接抄。功能核心规则推荐边界参数插卡登录卡号存在并且密码匹配连续错误3次锁卡查询余额只读、不产生流水不限制次数取款余额足够且不超过限额单笔2000日累计20000存款面额与张数校验模拟面额100/50单次最多100张转账对方卡号存在且余额足够同行免手续费日累计50000改密旧密码验证、两次新密码一致新密码必须6位数字不能与旧密码相同流水查询显示最近N笔交易保留最近50笔这份边界表的每一行背后都有实际意义。比如「连续错误3次锁卡」不是让你判断完后打印一句「密码错误」就结束而是要在Account对象里维护一个连续失败计数器失败满3次置锁。这涉及状态字段设计、锁定状态如何在下次启动时仍然生效第3章的代码会体现这一点。而「流水保留50笔」则意味着交易记录要做环形缓冲而不是无脑往List里加。把边界想清楚写代码的节奏会完全不同。我通常建议把这组参数用常量定义在一个名为AtmConstants的类里而不是散落在各种方法里。原因很简单答辩老师一定会问「如果我想把单笔取款上限改成5000改哪一行」你能在三秒钟内指给他看这就是加分项。后续第6章我还会讲怎么把这组参数挪到配置文件里更进一步。另外一个容易被问住的点既然叫「模拟系统」存款时要不要真的校验纸质面额我的答案是要但用枚举或白名单模拟即可例如只接收100与50面额金额必须是面额的整数倍。这样「边界」就不是嘴上说说而是代码里有真实分支。2.2 账户模型设计余额为什么必须用BigDecimal而不是double课程设计文档里最常见的一个坑是把余额字段设计成double balance。这个写法在演示阶段看不出问题因为数字小、操作少一旦连续存取转账余额就会出现长尾小数比如8000.0变成7999.999999999999。原因是浮点数在二进制里没办法精确表示0.1、0.2这样的十进制小数0.1加0.2算出来是0.30000000000000004这是 IEEE 754 的必然结果而不是Java的bug。你可以用下面这段代码在课程设计报告里直接演示这个现象public class FloatDemo { public static void main(String[] args) { double a 0.1; double b 0.2; System.out.println(double: (a b)); // 0.30000000000000004 BigDecimal x new BigDecimal(0.1); BigDecimal y new BigDecimal(0.2); System.out.println(BigDecimal: x.add(y)); // 0.3 } }逻辑说明BigDecimal直接用十进制字符串构造所以不会引入二进制浮点误差而new BigDecimal(0.1)仍然会把已经被浪费掉的double值传进去所以构造参数必须用字符串。参数说明add返回的是新对象原xy不变后续余额更新时必须把返回值重新赋给余额字段。常用做法是全局统一保留两位小数所有加减运算结束后调用setScale(2, RoundingMode.HALF_UP)这也是银行系统里的标准取整策略。账户实体类的字段设计我建议这样字段类型说明cardNoString卡号建议6到19位数字用正则校验encryptedPinString加密后的PIN不存明文密码balanceBigDecimal余额统一两位小数lockedboolean是否锁定failCountint连续失败次数lockReentrantLock账户级并发锁转账时用为什么放一个ReentrantLock字段而不是等到写转账时再塞一个全局锁原因在并发场景同一张卡在两个ATM窗口同时取款如果不锁余额会变负。如果全局锁不同账户之间的转账也会互相等待吞吐量难看。更合理的设计是锁粒度落在「账户对象」上同一账户的所有交易自然串行化不同账户还能并行。这个设计在答辩时讲出来会明显区别于那些把业务逻辑全写在按钮监听器里的作业。2.3 数据存储选型内存集合、文件序列化还是嵌入式数据库账户数据不能只活在HashMap里否则程序一重启演示时开的户就全没了。课程设计常见的存储方案有四种我按推荐程度排个序方案优点缺点适合场景文件序列化 ObjectOutputStream实现简单代码量小Java版本升级后可能读不了旧文件时间紧、报告以IO为主JSON文件 Gson文本可读、可手工编辑实体类结构变更时要迁移想展示「数据文件」的同学CSV/Properties直接用文本编辑器维护复杂对象表达力弱只存账户列表SQLite JDBC能写事务、能讲SQL驱动打包、路径匹配要额外处理冲击高分的项目常见的做法是「文件序列化」因为它和ObjectOutputStream搭配只要几十行代码。我的个人习惯更偏向JSON数据文件可以用文本编辑器打开演示前能直接改余额造场景出了问题也知道原始数据长什么样。用JSON存储时有一个非常隐蔽的坑Account里有ReentrantLock字段直接把Account对象扔给Gson序列化会报错或产生无意义的内容。解决办法是在Account的lock字段上打Expose(serialize false, deserialize false)或者做一个AccountRecord转换对象只用卡号、密码哈希、余额、锁定状态这些纯数据字段来做持久化。请记住这条路是存在的别等程序启动反序列化抛异常才回头。无论选哪种存储启动时的加载逻辑都要做保护文件不存在就创建默认数据文件损坏就备份并重建。我通常会写一个AtmStore类把load()和save()收口业务层完全不关心数据来自文件还是数据库。这个抽象在课程设计里属于「看着就很工程」的做法后面第3章的服务层会继续沿用这种分层思路。3. 核心代码骨架从账户类到ATM服务层的落地顺序3.1 账户实体类把余额、锁和校验规则放进同一个对象写实体类时最容易犯的错误是把balance当成 public 字段裸奔界面层直接account.balance newBalance。一旦发生这种情况所有业务校验都形同虚设。我给的方案是把余额操作收进账户自己外部只能通过方法请求扣款和入账。import java.math.BigDecimal; import java.math.RoundingMode; import java.util.concurrent.locks.ReentrantLock; public class Account { private final String cardNo; private final String encryptedPin; private BigDecimal balance; private boolean locked; private int failCount; private final ReentrantLock lock new ReentrantLock(); public Account(String cardNo, String encryptedPin, BigDecimal initialBalance) { this.cardNo cardNo; this.encryptedPin encryptedPin; this.balance initialBalance.setScale(2, RoundingMode.HALF_UP); this.locked false; this.failCount 0; } public synchronized void credit(BigDecimal amount) { this.balance this.balance.add(amount).setScale(2, RoundingMode.HALF_UP); } public synchronized BigDecimal debit(BigDecimal amount) { if (amount.compareTo(this.balance) 0) { throw new IllegalArgumentException(余额不足, 可用余额: this.balance); } this.balance this.balance.subtract(amount).setScale(2, RoundingMode.HALF_UP); return amount; } public synchronized boolean verifyPin(String inputPin) { if (this.locked) { return false; } if (this.encryptedPin.equals(inputPin)) { this.failCount 0; return true; } this.failCount; if (this.failCount 3) { this.locked true; } return false; } public ReentrantLock getLock() { return lock; } public String getCardNo() { return cardNo; } public BigDecimal getBalance() { return balance; } public boolean isLocked() { return locked; } }逻辑说明credit和debit都加了synchronized保证同一个账户对象内部操作互斥debit先用compareTo判断余额是否足够而不是用比较。因为compareTo在这个场景里规避了BigDecimal的equals行为差异精确匹配业务语义。verifyPin内部维护failCount连续错3次直接锁卡同时把locked置位。参数说明构造函数的initialBalance必须传字符串或BigDecimal比如new BigDecimal(8000.00)传double小数会有精度隐患。setScale(2, RoundingMode.HALF_UP)表示结果保留两位小数、四舍五入。这里我把密码设计成encryptedPin意思是外部传入的是哈希结果而不是说这个类负责做哈希。3.2 服务层把「业务动作」从「界面操作」里剥离很多课程设计把取款逻辑直接写在控制台菜单的case 2分支里这样程序能跑但答辩时老师问「如果以后加一个手机App端业务逻辑能复用吗」你会很难圆回来。我坚持的原则是交互层只负责拿输入、展示结果真正的余额校验、转账、改密全部放在AtmService类里。这个类不知道用户是用键盘还是按钮它只接受参数、抛异常、返回结果。import java.math.BigDecimal; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; public class AtmService { private final MapString, Account store; public AtmService(MapString, Account store) { this.store store; } public BigDecimal queryBalance(String cardNo) { return requireAccount(cardNo).getBalance(); } public void withdraw(String cardNo, BigDecimal amount) { Account account requireAccount(cardNo); if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(取款金额必须大于0); } if (amount.compareTo(new BigDecimal(2000.00)) 0) { throw new BusinessException(单笔取款不能超过2000元); } account.debit(amount); } public void deposit(String cardNo, BigDecimal amount) { Account account requireAccount(cardNo); if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(存款金额必须大于0); } if (!isValidBanknote(amount)) { throw new BusinessException(只支持100元与50元面额且面额组合必须正确); } account.credit(amount); } public void transfer(String fromCard, String toCard, BigDecimal amount) { if (fromCard.equals(toCard)) { throw new BusinessException(不能给自己转账); } Account from requireAccount(fromCard); Account to requireAccount(toCard); if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new BusinessException(转账金额必须大于0); } // 按卡号字典序决定加锁顺序避免两个线程互相等待造成死锁 Account first fromCard.compareTo(toCard) 0 ? from : to; Account second fromCard.compareTo(toCard) 0 ? to : from; ReentrantLock firstLock first.getLock(); ReentrantLock secondLock second.getLock(); firstLock.lock(); try { secondLock.lock(); try { from.debit(amount); to.credit(amount); } finally { secondLock.unlock(); } } finally { firstLock.unlock(); } } private Account requireAccount(String cardNo) { Account account store.get(cardNo); if (account null) { throw new BusinessException(卡号不存在或已注销); } if (account.isLocked()) { throw new BusinessException(该账户已锁定请联系柜台); } return account; } private boolean isValidBanknote(BigDecimal amount) { // 模拟只接收100与50面额任意张数组合能凑出的金额都合法 try { BigDecimal[] parts amount.divideAndRemainder(new BigDecimal(50.00)); return parts[1].compareTo(BigDecimal.ZERO) 0; } catch (ArithmeticException e) { return false; } } }这里需要一个自定义异常类放在同一个包下即可public class BusinessException extends RuntimeException { public BusinessException(String message) { super(message); } }逻辑说明withdraw先做金额有效性校验再调debitdebit内部还会再判断一次余额双层校验不是冗余而是为了防止界面层漏检后直接穿过服务层。transfer的锁顺序用compareTo比较卡号字典序小的先加锁这能有效避免两个线程同时转账时互相等待对方释放锁的死锁问题。requireAccount统一处理卡号不存在与账户锁定两类情况让交互层只用catch BusinessException就能拿到全部业务错误。参数说明单笔取款上限、存款面额组合这些我暂时写死在方法里第6章会改成从配置文件读取。锁的范围是账户对象不是整个store所以不同账户之间的转账在锁层面可以并行。这里有一点需要特别说明credit和debit拆成两步之间不是严格事务严谨做法是预检查双方余额后再依次更新课程设计到这个深度已经足够你可以把事务性写进报告作为可扩展方向。3.3 控制台主循环登录流程与菜单驱动方式控制台交互层最容易被忽略的是输入方式。很多人用scanner.nextInt()读选项结果用户输了一个字母程序直接抛InputMismatchException这是非常难看的翻车现场。我建议全部用nextLine()读取原始字符串再手工解析整个交互流程会稳定很多。下面这段是主程序和控制台菜单的骨架。import java.util.Scanner; public class AtmConsoleApp { private final AtmService service; public AtmConsoleApp(AtmService service) { this.service service; } public void start() { Scanner scanner new Scanner(System.in); String cardNo login(scanner); if (cardNo null) { System.out.println(密码错误次数过多账户已被锁定。); return; } boolean running true; while (running) { printMenu(); String option scanner.nextLine().trim(); switch (option) { case 1: System.out.println(当前余额: service.queryBalance(cardNo)); break; case 2: withdraw(scanner, cardNo); break; case 3: deposit(scanner, cardNo); break; case 4: transfer(scanner, cardNo); break; case 5: changePin(scanner, cardNo); break; case 0: running false; System.out.println(欢迎下次使用卡片已退出。); break; default: System.out.println(无效选项请重新输入数字 0-5。); } } scanner.close(); } private String login(Scanner scanner) { System.out.print(请输入卡号: ); String cardNo scanner.nextLine().trim(); System.out.print(请输入密码: ); String inputPin scanner.nextLine().trim(); Account account storeAccount(cardNo); if (account ! null account.verifyPin(hashInput(inputPin))) { System.out.println(登录成功欢迎使用ATM柜员机模拟系统。); return cardNo; } System.out.println(卡号或密码错误。); return null; } }说明一下代码里的空缺storeAccount和hashInput分别对接数据层与加密工具类我建议你把它们单独放在AtmStore和PinHasher两个类里。case分支里的withdraw、deposit这些私有方法负责用Scanner读金额、调用service、打印结果实际代码很短逻辑是统一的连续读两次输入第一次读不到就提示重新输入捕获BusinessException后打印getMessage()。这样做的要点是让每一个case都保持「读输入、调服务、打印结果」三段式不掺业务判断。参数说明卡片退出用running false而不是break是为了在主循环结束后统一做收尾保存比如把流水日志写入文件。scanner.nextLine().trim()同时解决了用户输入带空格、输入了非法字符串两个问题。这里的传统switch写法兼容 JDK 8如果你的课程环境是 JDK 14 以上可以换成箭头语法但要注意答辩用的机器可能版本较老传统写法最保险。4. 人机交互层与演示前置准备怎么让答辩演示一次过4.1 控制台菜单的输入容错写一个你自己的readInt方法我发现很多同学是自己测试时没输错过一上台就输错。数字键按成字母是高频事故程序直接抛异常会让演示非常尴尬。所以任何读数字的地方都不要直接Integer.parseInt(scanner.nextLine())而是包一层循环重试。下面这个readInt方法会一直问直到用户输入合法的整数。private int readInt(Scanner scanner, String prompt) { while (true) { System.out.print(prompt); String line scanner.nextLine().trim(); try { return Integer.parseInt(line); } catch (NumberFormatException e) { System.out.println(请输入纯数字比如 2000 或 5000。); } } }逻辑说明try捕获NumberFormatException后继续循环不会让程序崩溃。readInt拿到的结果还可以再叠加业务校验比如取款金额必须大于0那是服务层的事交互层只负责保证「输入的是整数」。读金额的场景建议单独写一个readAmount内部用new BigDecimal(line)并捕获NumberFormatException原因很简单整数能解析成int但金额可能是100.50用整数读会丢掉小数。到这里交互层的框架就完整了start()负责主循环login()负责登录readInt负责输入容错各case分支只做转调。控制台版本适合快速完成课程设计但如果老师明确要求有图形界面那就不要在这个骨架上叠窗口而是换一层壳下面一节讲换壳的原则。4.2 如果必须做图形界面Swing换皮但不换骨架课程设计里比例很大一部分要求带图形界面。我的态度是不要为图形界面重写业务Swing只做一层壳AtmService原封不动复用。这样报告里可以写成「控制台与 Swing 共用同一套服务层」老师会认为你有分层意识。下面是一段极简Swing界面的取款按钮逻辑。JButton withdrawButton new JButton(取款); withdrawButton.addActionListener(e - { String text amountField.getText().trim(); try { BigDecimal amount new BigDecimal(text); service.withdraw(currentCardNo, amount); balanceLabel.setText(余额: service.queryBalance(currentCardNo)); } catch (NumberFormatException ex) { statusLabel.setText(金额格式错误请输入数字。); } catch (BusinessException ex) { statusLabel.setText(ex.getMessage()); } });逻辑说明按钮监听器只做三件事取文本、调服务、更新界面。BigDecimal构造失败时捕获NumberFormatException业务错误统一捕获BusinessException。这样界面上不会出现一行try-catch里套十层if的代码以后想改成JavaFX或者其他框架按钮回调以外的代码都不用动。这里有一个决定性细节绝对不要在Swing的事件回调里调用Scanner.nextLine()。事件线程被阻塞窗口会假死看起来像程序卡住实际是等键盘输入。这个坑在第5章我会再专门展开。另一个建议是把登录后的currentCardNo放在一个Session对象里而不是每个界面传来传去第6章会给出一个精简版本。4.3 演示数据与演示脚本把答辩的每分钟都安排明白答辩现场最怕的是临时创建账号暴露出输入法问题、卡号输错、密码记混。我的习惯是提前在代码里构造一组固定测试账户启动即存在演示也按固定脚本走。下面是一段初始化数据的方法。private static MapString, Account buildInitialData() { MapString, Account map new ConcurrentHashMap(); map.put(100001, new Account(100001, PinHasher.hash(111111), new BigDecimal(8000.00))); map.put(100002, new Account(100002, PinHasher.hash(222222), new BigDecimal(12000.00))); map.put(100003, new Account(100003, PinHasher.hash(333333), new BigDecimal(500.00))); return map; }逻辑说明三个账户覆盖三种演示场景余额充足、余额中等、低余额易触发不足提示。ConcurrentHashMap是并发安全的Map实现多线程演示时更稳。PinHasher.hash是一个独立的工具方法内部用SHA-256加随机盐生成哈希不要为了图方便存明文密码。推荐演示脚本登录100001查余额取款3000触发「单笔超限」取款1000成功转账给100002再转账给100003触发「余额不足」最后查流水看到完整记录。这个脚本故意包含了一个失败场景比一路顺风更能体现边界判断存在。5. 避坑手册金额精度、Scanner阻塞与文件持久化的典型翻车现场5.1 现象余额出现长尾小数0.1加0.2不等于0.3同学A在连续存款、取款三次后发现余额变成了7999.999999999999怎么都理不顺。原因就是用了double存余额。二进制浮点数无法精确表示所有十进制小数任何涉及金融计算的系统里这都是大忌。解决方式是换成BigDecimal并且所有构造参数都用字符串所有结果统一setScale(2, RoundingMode.HALF_UP)。如果你已经写完了大部分代码替换成本也不高把字段类型改掉改成add-改成subtract比较大小用compareTo。花一小时解决换来的是一次不会在答辩现场被数字打脸的演示。5.2 现象界面点击按钮后整个窗口假死控制台程序读到一半不往下走某开发者在Swing里调用了Scanner.nextLine()后窗口点按钮没反应另一个同学在控制台程序里先nextInt()再nextLine()第二次输入直接读到空字符串。原因有两个层面Swing事件线程被阻塞在键盘输入上界面自然假死nextInt()不会消费换行符残留的\n会被下一次nextLine()读掉。解决方式是统一原则图形界面里不用Scanner全部用文本框控制台里全部用nextLine()读原始字符串再自己解析成int或BigDecimal。这样两条规则互相独立不会再出现输入类型错乱导致的卡流程。5.3 现象程序在IDE里跑数据正常打包成jar后找不到数据文件这是最典型的「IDE里好好的换台机器就翻车」。原因是在代码里写了相对路径data.jsonJava的相对路径取决于启动时的工作目录IDE启动时工作目录是项目根目录java -jar启动时工作目录是jar包所在目录两处不一致就会找不到文件。解决方式是不要用相对路径把数据文件放到用户主目录下的固定子目录。private static final Path DATA_DIR Paths.get( System.getProperty(user.home), .atm-system); private static final Path DATA_FILE DATA_DIR.resolve(accounts.json); ublic void save(MapString, Account accounts) { Files.createDirectories(DATA_DIR); // 序列化账户列表到DATA_FILE }逻辑说明user.home在不同操作系统中都能解析到正确位置Linux是/home/用户名Windows是C:\Users\用户名macOS是/Users/用户名。.atm-system是一个隐藏目录名的惯例不会与别的软件冲突。DATA_FILE每次启动固定就不存在工作目录漂移问题。如果程序里还用了Lock字段序列化会报错的问题用第2.3节提到的Expose(serialize false)或转换对象解决。5.4 现象输了一个负数金额居然能存款超长卡号直接让程序崩溃原因很直接交互层读入字符串后转成数字没有检查正负卡号识别直接store.get(cardNo)超长字符串在正则匹配之前就占用了一大段内存。解决方式分两层。交互层负责格式金额必须是非负数字卡号必须匹配^\d{6,19}$服务层负责业务amount.compareTo(BigDecimal.ZERO) 0直接抛BusinessException。这两层校验互相补充就算界面层绕过格式检查直连服务层服务层也能兜底。我在第3.2节的代码里已经演示了这个写法如果你已经写完但没有这段请务必补上。5.5 现象开两个线程同时给同一个账户转账余额被扣成负数某同学的模拟系统开了两个窗口同时操作同一张卡取款最终余额出现负数。原因是两个线程同时进入debit方法检查余额时都通过扣款时都执行了减法。解决方式是我在第3.2节里写得比较完整账户内部的credit、debit用synchronized保证同一账户操作互斥跨账户的transfer再加锁排序防死锁。还有一个额外步骤是转账完成后写流水不要在锁内写文件先把锁释放再落盘否则文件IO慢的时候会阻塞所有转账。6. 把课程设计做成工程交易日志、参数配置化与会话收口6.1 交易日志让每一笔钱都有据可查答辩时展示「这台ATM记录了每一笔交易」比单纯展示余额数字更有说服力。实现方式不需要引入日志框架一个ListString加一个flush方法就够了。每次service.withdraw成功后追加一行格式为时间 卡号 交易类型 金额 剩余余额程序退出时把整份列表写入DATA_DIR/logs/下的日期文件。注意不要把锁和IO耦合先完成业务操作再写日志。6.2 参数配置化把「写着死」的常量挪到properties文件第3.2节里单笔取款上限写死在代码里答辩时被问到「怎么改成5000」只能回答「改代码重新编译」。更好的做法是把这些参数放进atm.properties启动时用Properties类加载服务层从配置对象里取值。改动成本约半小时收益是答辩时现场演示改配置重启这个动作比任何口头解释都直观。6.3 会话对象把当前登录卡号收口而不是到处传String控制台版本里cardNo在start和各方法间来回传递Swing版本里它被存在某个类的字段里。更稳妥的做法是定义一个Session包含当前登录卡号、登录时间、最近操作时间。每次操作前验证Session仍有效超时自动退出。这个对象往小了说是减少参数传递往大了说是为以后做多用户登录打基础。三个改进加起来工作量很小但能明显提升整个项目的完成度。我印象最深的一次是某同学把整个系统跑通后把单笔取款上限、锁定次数、流水保留条数全部改成配置文件驱动。答辩老师真的问了「日取款上限怎么改」他现场打开properties、改数字、重启、演示整个过程不到一分钟。我的习惯是凡是会变化的数字不写死凡是结算不用double凡是权限边界都在服务层收口凡是文件路径都全局收敛。这三件套花的时间不超过半小时收益是从「写完了」变成「能扛追问」。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑