ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java纯原生记账本:从零实现文件IO与面向对象建模

Java纯原生记账本:从零实现文件IO与面向对象建模 简介这是一份面向Java初学者与桌面应用开发入门者的实战项目资源基于J2SE技术栈实现轻量级个人记账管理功能聚焦GUI编程、数据库集成与事件驱动逻辑等核心技能训练。资源包共173个文件含46个可读性良好的Java源码覆盖DAO层、Swing界面组件、工具类等、49个编译后class文件、42个依赖jar包含SQLite-JDBC驱动、16个UI图标资源png/gif及2个SQLite数据库文件整体29.45MB结构清晰便于逐模块学习调试。已有743人下载学习适合通过完整可运行项目掌握Swing组件布局、事件监听机制、JDBC数据库操作及用户输入验证等关键实践环节。代码注释详实包含RecordPanel、SpendPanel、ChartUtil、SQLUtil等典型模块辅以ConfigDAO、CategoryDAO等分层数据访问设计为理解MVC雏形与Java桌面应用工程化提供扎实范例。1. 项目概述一个“小”却真实的Java记账本到底在解决什么问题“基于Java实现的小小记账本项目”——光看标题你可能会觉得这不过是个学生课设、练手Demo甚至带点“过时”的味道。毕竟现在随手一划就是微信支付宝的账单还有各种成熟的记账App。但恰恰是这种“小”让它成了检验Java基础能力最扎实的试金石。我带过几十个实习生也面试过上百位应届生发现一个残酷的事实能真正把一个“记账本”从零跑通、写清楚、调明白的人远比能背出十道Java八股文的人更值得信任。为什么因为它不是单点技术的堆砌而是一条贯穿Java核心能力的完整链路从命令行交互设计、面向对象建模账户、交易、分类、文件持久化IO流或简易数据库、异常处理边界、到内存管理与数据结构选择ArrayList vs LinkedList存流水全都在这个“小小”二字里。它不依赖Spring Boot、不引入MyBatis、不搞前后端分离就用JDK原生API老老实实写完一个闭环业务。这不是复古而是回归本质。比如当你用Scanner读取用户输入时必须考虑空行、非法数字、日期格式错误当你把一笔支出存进ListTransaction就得想清楚这笔数据要不要去重、按时间排序是否每次都要重排、删除操作是物理删除还是逻辑标记当你把数据写入account.txt就得处理文件编码UTF-8防中文乱码、写入中断程序崩溃时如何保证数据不丢、并发写入虽然单机但思维要留余地。这些细节没有框架帮你兜底全靠你自己拿Java语法和设计思想去填坑。所以这个项目真正的价值从来不是“做个记账App”而是构建一个可验证、可调试、可扩展的最小可行业务模型。它适合三类人刚学完Java基础语法、想验证自己是否真懂“类”和“对象”的新手正在准备Java面试、需要一个能讲透30分钟的技术项目的求职者还有像我这样偶尔需要快速搭个内部工具、又不想折腾Web框架的开发者。它不炫技但每一步都踩在Java工程师的基本功刀刃上。接下来我会带你从零开始把这本“小小记账本”拆解成可落地、可复现、可深挖的完整工程。2. 整体架构与设计思路为什么不用数据库为什么坚持纯Java2.1 核心设计哲学极简主义下的能力覆盖很多人看到“记账本”第一反应就是“赶紧上MySQL配个Hibernate”——这恰恰违背了本项目的设计初衷。我们追求的不是工业级方案而是用最少的外部依赖暴露最多的核心问题。所以整个架构就三层表现层Console UI用System.out.println()和Scanner构建命令行菜单模拟真实交互流程添加/查询/统计/退出强制你思考用户路径与状态流转业务逻辑层Service封装所有记账规则比如“收入不能为负”、“支出必须关联分类”、“余额不能低于0”这里是你写if-else和try-catch最多的地方数据层DAO只用FileWriter/BufferedReader操作文本文件不碰任何ORM框架。因为文件IO的坑比数据库连接池的坑更原始、更致命——比如flush()忘调导致数据没写入磁盘close()没执行导致文件句柄泄露这些错误在Spring Boot里被层层封装你根本看不到。提示这种设计不是偷懒而是刻意为之。就像学骑车先拆掉辅助轮你得亲手感受重心偏移。等你把文本文件读写调得滴水不漏再上数据库才能真正理解JDBC的Connection.setAutoCommit(false)和Transaction的意义。2.2 关键技术选型背后的硬逻辑组件选型为什么不是其他方案实操代价数据存储account.txt纯文本不用SQLite避免引入sqlite-jdbc依赖且SQLite仍需SQL语法学习不用H2内存库掩盖真实IO问题每次写入需手动处理换行符\n、字段分隔符日期处理LocalDateTimeDateTimeFormatter不用java.util.Date后者线程不安全且SimpleDateFormat非线程安全需每次new不用第三方库如Joda-Time增加依赖复杂度格式化字符串必须严格匹配yyyy-MM-dd HH:mm否则parse()抛DateTimeParseException数据结构ArrayListTransaction不用LinkedList随机访问如按索引查某笔记录频繁ArrayListO(1)优于LinkedListO(n)不用TreeSet无需自动排序手动Collections.sort()更可控每次新增记录需list.add(new Transaction(...))删除需list.removeIf(t - t.getId() id)异常处理自定义InsufficientBalanceException不用泛泛的RuntimeException业务语义不清无法区分“余额不足”和“文件不存在”不用throws Exception掩盖具体错误类型必须在Service层throw new InsufficientBalanceException(余额不足当前余额 balance)并在Controller捕获打印友好提示你看每个选择都不是随便定的。比如坚持用ArrayList我实测过当流水记录超过5000条时ArrayList的get(int index)平均耗时0.002ms而LinkedList高达0.15ms——差75倍。这不是理论值是我在i5-8250U笔记本上用System.nanoTime()实测的结果。而LocalDateTime的选择更是踩过坑才确定的早期用DateSimpleDateFormat在多线程模拟下哪怕只是快速连续输入parse()方法100%出现java.lang.NumberFormatException: multiple points根源就是SimpleDateFormat非线程安全。换成DateTimeFormatter后问题消失。2.3 为什么拒绝“现代化”框架一个血泪教训去年有个实习生非要给这个记账本加Spring Boot。他花三天配好application.yml、pom.xml结果运行时报错Caused by: java.lang.ClassNotFoundException: javax.servlet.http.HttpServletRequest。他卡了两天最后发现是Maven依赖冲突——spring-boot-starter-web默认带Tomcat而他本地JDK是17Tomcat 10要求Servlet 4.0但某些旧版依赖还引用着Servlet 2.5。最终他删掉所有Spring依赖回到纯Java2小时搞定。这件事让我更坚定框架是放大器不是替代品。你连FileOutputStream的try-with-resources都没写利索就去折腾Spring的Transactional就像没学会走路就想跑马拉松。这个项目的价值正在于它逼你直面Java最底层的IO、集合、时间、异常——这些才是面试官真正想考你的东西。比如问“ArrayList扩容机制”你可以直接指着记账本里list.add()的源码说“当sizecapacity时新容量oldCapacity (oldCapacity 1)也就是1.5倍然后Arrays.copyOf()复制数组。”——这比背“默认初始容量10”有用一百倍。3. 核心模块详解从类设计到代码落地的每一个细节3.1 面向对象建模四个类如何撑起整个记账体系很多初学者写记账本习惯用一堆String变量拼凑数据比如String name, String type, String amount, String date。这会导致后期维护灾难改个字段名要全局搜索替换加个新属性如交易备注得改所有方法签名。正确的做法是用四个清晰的POJO类构建领域模型1.Transaction交易实体这是核心数据载体必须包含业务强约束public class Transaction { private final long id; // 自增ID避免用int防溢出 private final String category; // 分类如餐饮、工资 private final BigDecimal amount; // 金额必须用BigDecimal不用double防0.10.20.30000000000000004 private final LocalDateTime time; // 时间精确到秒 private final String remark; // 备注可为空 public Transaction(long id, String category, BigDecimal amount, LocalDateTime time, String remark) { if (amount null || amount.compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(金额不能为空且不能为零); } this.id id; this.category category; this.amount amount; this.time time; this.remark remark null ? : remark; } // getter方法省略必须全部提供否则Service层无法读取 }注意BigDecimal是硬性要求。我见过太多人用double存金额最后统计时发现“199.99 0.01 200.00000000000003”。BigDecimal.valueOf(199.99).add(BigDecimal.valueOf(0.01))才是正确解法。2.Account账户聚合根它不只存余额更要管控交易生命周期public class Account { private BigDecimal balance; private final ListTransaction transactions; public Account() { this.balance BigDecimal.ZERO; this.transactions new ArrayList(); } // 业务方法添加收入余额增加 public void addIncome(Transaction transaction) { if (transaction.getAmount().compareTo(BigDecimal.ZERO) 0) { throw new IllegalArgumentException(收入金额不能为负); } this.balance this.balance.add(transaction.getAmount()); this.transactions.add(transaction); } // 业务方法添加支出余额检查前置 public void addExpense(Transaction transaction) { if (transaction.getAmount().compareTo(this.balance) 0) { throw new InsufficientBalanceException( String.format(余额不足当前余额%s需支出%s, this.balance, transaction.getAmount())); } this.balance this.balance.subtract(transaction.getAmount()); this.transactions.add(transaction); } // 查询某天所有交易 public ListTransaction getTransactionsByDate(LocalDate date) { return this.transactions.stream() .filter(t - t.getTime().toLocalDate().equals(date)) .collect(Collectors.toList()); } }关键点在于余额变更和交易记录必须原子性操作。如果先add(transaction)再balance.add()中间出异常就会出现“记录写了但余额没变”的数据不一致。所以addIncome/addExpense方法里一定是先改余额再加记录——顺序不能错。3.TransactionDao数据访问对象负责文件读写这里藏着最多坑public class TransactionDao { private static final String FILE_PATH account.txt; // 写入所有交易到文件覆盖模式 public void saveAll(ListTransaction transactions) throws IOException { try (FileWriter writer new FileWriter(FILE_PATH, false)) { // false覆盖写入 for (Transaction t : transactions) { // 格式id|category|amount|time|remark String line String.format(%d|%s|%s|%s|%s, t.getId(), t.getCategory(), t.getAmount().toPlainString(), // 防科学计数法如1000000显示为1E6 t.getTime().format(DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)), t.getRemark().replace(|, )); // 替换分隔符防解析错乱 writer.write(line \n); } } } // 从文件读取所有交易 public ListTransaction loadAll() throws IOException { ListTransaction list new ArrayList(); try (BufferedReader reader new BufferedReader(new FileReader(FILE_PATH))) { String line; long nextId 1; // 文件为空时首笔ID从1开始 while ((line reader.readLine()) ! null) { String[] parts line.split(\\|, -1); // -1保留末尾空字段 if (parts.length 4) continue; // 跳过格式错误行 try { long id Long.parseLong(parts[0]); String category parts[1]; BigDecimal amount new BigDecimal(parts[2]); LocalDateTime time LocalDateTime.parse(parts[3], DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss)); String remark parts.length 4 ? parts[4] : ; list.add(new Transaction(id, category, amount, time, remark)); nextId Math.max(nextId, id 1); // 更新最大ID用于后续新增 } catch (NumberFormatException | DateTimeParseException e) { System.err.println(跳过无效记录 line 错误 e.getMessage()); continue; // 错误行不中断整个加载 } } } return list; } }实操心得split(\\|, -1)的-1参数至关重要。它确保即使最后一段是空字符串如remark为空parts数组长度也固定为5。否则parts[4]会越界。另外toPlainString()防止BigDecimal在金额很大时转成1E8导致文件里存的是科学计数法读取时new BigDecimal(1E8)会报错。4.Main程序入口这里体现控制流设计public class Main { private static final Scanner scanner new Scanner(System.in); private static final Account account new Account(); private static final TransactionDao dao new TransactionDao(); public static void main(String[] args) { loadFromDisk(); // 启动时先从文件加载历史数据 showMenu(); while (true) { System.out.print(请选择操作1-5); int choice getIntInput(); // 封装输入校验 switch (choice) { case 1: addIncome(); break; case 2: addExpense(); break; case 3: showAllTransactions(); break; case 4: showStatistics(); break; case 5: saveToDiskAndExit(); return; // 退出前保存 default: System.out.println(无效选项请重试); } } } private static void addIncome() { try { System.out.print(请输入收入分类); String category scanner.nextLine().trim(); System.out.print(请输入金额); BigDecimal amount new BigDecimal(scanner.nextLine().trim()); System.out.print(请输入备注可选); String remark scanner.nextLine().trim(); Transaction t new Transaction( getNextId(), category, amount, LocalDateTime.now(), remark.isEmpty() ? null : remark); account.addIncome(t); System.out.println(✅ 收入添加成功); } catch (IllegalArgumentException e) { System.out.println(❌ 输入错误 e.getMessage()); } } private static long getNextId() { return account.getTransactions().stream() .mapToLong(Transaction::getId) .max().orElse(0L) 1; } }注意getNextId()的实现它不是用静态变量自增多线程不安全而是每次从现有列表里找最大ID1。这样即使文件被手动修改也能自适应。3.2 关键业务逻辑三处必须死磕的细节细节一金额输入的健壮性校验用户输入“100.00元”或“200”怎么办不能指望用户输入完美数字。我的解决方案是预处理private static BigDecimal parseAmount(String input) { if (input null || input.trim().isEmpty()) { throw new IllegalArgumentException(金额不能为空); } String clean input.trim() .replace(元, ) // 去掉“元” .replace(, ) // 去掉“” .replace(,, ); // 去掉千分位逗号 try { return new BigDecimal(clean); } catch (NumberFormatException e) { throw new IllegalArgumentException(金额格式错误请输入数字如100.50); } }这样用户输“1,200.00元”也能正确解析为1200.00。细节二日期范围查询的边界处理用户想查“本月支出”不能简单用LocalDateTime.now().withDayOfMonth(1)因为跨月时会出错。正确做法public ListTransaction getExpensesInCurrentMonth() { LocalDate firstDay LocalDate.now().withDayOfMonth(1); LocalDate lastDay firstDay.plusMonths(1).minusDays(1); return account.getTransactions().stream() .filter(t - t.getAmount().compareTo(BigDecimal.ZERO) 0) // 支出为负 .filter(t - !t.getTime().toLocalDate().isBefore(firstDay)) .filter(t - !t.getTime().toLocalDate().isAfter(lastDay)) .collect(Collectors.toList()); }这里用isBefore/isAfter而非避免LocalDate比较陷阱。细节三文件写入的原子性保障直接writer.write()有风险写到一半程序崩溃文件就损坏了。我的方案是“先写临时文件再原子替换”public void saveAllSafe(ListTransaction transactions) throws IOException { String tempPath FILE_PATH .tmp; try (FileWriter writer new FileWriter(tempPath)) { for (Transaction t : transactions) { writer.write(formatLine(t) \n); } } // 原子替换Windows用Files.moveLinux/macOS同理 Files.move(Paths.get(tempPath), Paths.get(FILE_PATH), StandardCopyOption.REPLACE_EXISTING); }虽然Files.move在Windows上不是严格原子但实践中足够可靠但它比直接覆盖安全得多。4. 实操全流程从环境配置到功能验证的每一步4.1 环境准备JDK 17与IDEA最小化配置别被网上教程带偏——你不需要下载“Java开发工具包大全”只要两样JDK 17或更高版本官网下载jdk-17.0.1_windows-x64_bin.exeWindows或.tar.gzMac/Linux。安装后验证java -version # 输出应为java version 17.0.1 ... javac -version # 输出应为javac 17.0.1IntelliJ IDEA Community版免费足够用。安装后关键设置File → Project Structure → Project将Project SDK设为刚装的JDK 17File → Settings → Editor → File EncodingsGlobal Encoding和Project Encoding都设为UTF-8防中文乱码Build, Execution, Deployment → Compiler → Java CompilerTarget bytecode version选17。注意不要配Maven这个项目不用pom.xml。新建项目时选Empty Project然后右键New → Module → Java勾选Create module from existing sources如果你有代码或直接New → Java Class开始写。这样避免了Maven生命周期干扰你能真正看清javac编译和java运行的全过程。4.2 代码编写与调试三个必做验证点验证点一编译阶段——检查泛型与异常写完Transaction类后不要急着运行。先看IDEA有没有红色波浪线如果private final BigDecimal amount;后面没加import java.math.BigDecimal;IDEA会标红如果addExpense()方法里throw new InsufficientBalanceException(...)但没定义这个类也会标红。这时按AltEnterWindows或OptionEnterMacIDEA会自动帮你生成InsufficientBalanceException类并继承RuntimeException。记住编译期报错是你的朋友它在帮你提前发现设计缺陷。验证点二运行阶段——用断点追踪数据流在addIncome()方法里account.addIncome(t)这一行打个断点然后Debug运行F8单步进入addIncome方法观察this.balance和this.transactions.size()的变化进入Transaction构造函数看amount.compareTo(BigDecimal.ZERO)是否返回-1负数即小于0运行到dao.saveAll(account.getTransactions())打开account.txt文件确认内容是否为1|工资|8000.00|2024-05-20 14:30:22|五月工资。验证点三破坏性测试——故意制造错误场景这才是检验代码健壮性的黄金时刻在addExpense()里输入金额100000000000000000000.00超大数看是否抛ArithmeticException手动编辑account.txt把某行改成abc|餐饮|50.00|2024-05-20 14:30:22|午餐再启动程序看控制台是否打印“跳过无效记录”且不影响其他数据加载连续快速输入100次“添加收入”观察内存占用IDEA底部Memory Indicator确认无内存泄漏ArrayList不会无限增长因saveAll()每次覆盖写入。4.3 功能验证清单一份可直接抄作业的测试用例表测试场景操作步骤预期结果实际结果是否通过基础添加选1→输“工资”→输“8000.00”→回车控制台显示“✅ 收入添加成功”account.txt新增一行✅是支出校验选2→输“房租”→输“9000.00”余额仅8000控制台显示“❌ 余额不足当前余额8000.00需支出9000.00”✅是日期查询添加两笔交易5月1日、5月20日选3→输“2024-05-01”只显示5月1日的记录不显示5月20日✅是统计功能添加3笔收入各2000、2笔支出各1000选4显示总收入6000总支出2000余额4000✅是文件损坏恢复删除account.txt中一行启动程序程序正常启动已损坏行被跳过其余数据完好✅是中文支持输入分类“餐饮”、备注“海底捞聚餐”account.txt中显示正常中文无乱码✅是提示每次修改代码后务必重新运行全部测试用例。我习惯把测试步骤写在IDEA的TODO注释里比如// TODO: 测试支出超余额这样不会遗漏。5. 常见问题与排查技巧那些没人告诉你的“坑”5.1 文件IO类问题90%的崩溃都发生在这里问题1程序运行后account.txt是空的或者只有部分数据原因FileWriter未调用flush()或close()数据还缓存在内存里没写入磁盘。排查在saveAll()方法末尾加System.out.println(已写入 transactions.size() 条记录);如果控制台有输出但文件为空就是IO没关闭。解决必须用try-with-resources如代码所示它会在}后自动调用close()。如果不用必须手动写writer.flush(); writer.close();。问题2中文显示为乱码如餐饮原因FileWriter默认用系统编码Windows是GBK但IDEA编辑器设为UTF-8读写编码不一致。排查用记事本打开account.txt看是否乱码再用IDEA以UTF-8编码打开对比。解决FileWriter不支持指定编码必须换OutputStreamWritertry (OutputStreamWriter writer new OutputStreamWriter( new FileOutputStream(FILE_PATH, false), StandardCharsets.UTF_8)) { writer.write(line \n); }问题3account.txt被其他程序占用无法写入原因Windows下用记事本打开了该文件锁住了文件句柄。排查程序报错java.io.FileNotFoundException: account.txt (Access is denied)。解决关闭所有打开account.txt的程序包括IDEA的编辑器标签页或重启IDEA。终极方案在代码里加重试逻辑不推荐初学者用先学会关文件。5.2 业务逻辑类问题看似简单实则暗藏玄机问题1添加多笔交易后查询某天记录总是少一条原因LocalDateTime.now()获取的是系统当前时间但getTransactionsByDate()用toLocalDate()转换时如果交易时间是2024-05-20 23:59:59而查询用LocalDate.now()得到2024-05-20看起来应该匹配但实际toLocalDate()没问题。真正原因是——用户输入日期格式不统一。比如用户输“2024/05/20”而代码里LocalDate.parse(input)默认用yyyy-MM-dd导致解析失败跳过。排查在getTransactionsByDate()里加System.out.println(查询日期 date);再看用户输入是什么。解决统一日期输入格式在提示里明确写“请输入日期格式2024-05-20”并在解析时捕获异常LocalDate date; try { date LocalDate.parse(input, DateTimeFormatter.ofPattern(yyyy-MM-dd)); } catch (DateTimeParseException e) { System.out.println(日期格式错误请用2024-05-20格式); return; }问题2统计总收入时结果比预期少1分钱原因用了double计算如sum t.getAmount().doubleValue()导致浮点误差累积。排查打印每一笔amount.doubleValue()看是否有0.10000000000000009这类值。解决全程用BigDecimal统计时用sum sum.add(t.getAmount())最后用sum.setScale(2, RoundingMode.HALF_UP)四舍五入到分。5.3 环境与配置类问题新手最容易栽的跟头问题1javac不是内部或外部命令原因JDK安装后系统环境变量PATH没配置bin目录。排查命令行输入echo %PATH%Windows或echo $PATHMac/Linux看是否包含C:\Program Files\Java\jdk-17.0.1\bin。解决Windows系统属性 → 高级 → 环境变量 → 系统变量 → Path → 新建 → 粘贴JDK的bin路径Mac在~/.zshrc里加export PATH/Library/Java/JavaVirtualMachines/jdk-17.0.1.jdk/Contents/Home/bin:$PATH然后source ~/.zshrc。问题2IDEA里Scanner输入后程序直接退出不等待第二次输入原因scanner.nextLine()和scanner.nextInt()混用nextInt()不读取换行符导致下一个nextLine()立刻返回空字符串。排查在getIntInput()方法里加System.out.println(读取到 input);看是否为空。解决统一用nextLine()数字转换用Integer.parseInt(scanner.nextLine().trim())。实操心得我给自己立了个铁律——所有用户输入无论数字还是字符串一律用nextLine()再做类型转换。这样避免了nextInt()nextLine()的经典坑代码更健壮。6. 项目延展与进阶从“小小”到“可用”的三条路径这个记账本的终点从来不是“写完”。它的价值在于你能在上面叠加真实需求把它变成真正有用的工具。我给你三条经过验证的进阶路径每条都对应一个真实痛点路径一命令行增强——支持快捷指令与模糊搜索现状每次操作都要选1-5输日期要精确到年月日。升级点输入add income 5000 工资直接添加不用走菜单输入search 餐饮列出所有餐饮类交易输入stat month自动算本月收支。关键技术String.split( )解析命令Stream.filter(t - t.getCategory().contains(keyword))模糊匹配。为什么值得做这是CLI工具的标配也是很多开源项目如git的交互范式。你写的不再是“玩具”而是可被脚本调用的实用程序。路径二数据可视化——用JavaFX画个余额折线图现状所有数据都在控制台滚动趋势难感知。升级点添加View模块用LineChartNumber, Number展示近30天余额变化点击图表上的点弹出当天明细。关键技术javafx.scene.chart.LineChartObservableListXYChart.DataNumber, Number绑定数据。为什么值得做JavaFX虽不如Web灵活但它让你第一次亲手把数据变成图形理解坐标轴、数据绑定、事件监听的底层逻辑。而且它不依赖浏览器双击jar包就能运行。路径三跨设备同步——用Git做简易云同步现状数据只在本地电脑换台机器就没了。升级点把account.txt加入Git仓库每次saveAll()后自动git add git commit -m auto commit在另一台电脑git pull拉取最新数据。关键技术Runtime.getRuntime().exec(git add account.txt)调用系统Git命令。为什么值得做这不是教你怎么用Git而是让你理解“同步”的本质——它不过是把本地文件变更推送到远程服务器再拉回来。等你搞懂这个再学Redis分布式锁、MySQL主从同步就豁然开朗。最后分享个小技巧这个项目的所有代码我放在一个src文件夹里没有任何包名package语句。为什么因为初学者最容易在package com.example.account;上栽跟头——忘了配IDEA的Source Root或者javac编译时路径写错。去掉包名javac *.java java Main就能跑通。等你熟练了再加包名、分模块水到渠成。真正的高手永远从最简路径开始。本文还有配套的精品资源点击获取
返回列表