JavaFX+SSM架构实战:桌面客户端与数据服务端的高效整合 简介这是一份融合 JavaFX 与 SSM 框架的 ERP 系统完整项目面向需要将桌面 GUI 与企业级后端整合开发的 Java 学习者、毕业设计或项目实战人群既能学习界面设计也能掌握经典后端框架的落地方式。资源共包含 2 个文件一个主压缩包为 7z 格式内含工程源码及 FXML、CSS 等界面资源另附一份 SQL 数据库脚本用于快速初始化业务表整体约 75.26MB已有 3415 人学习/下载。项目以典型 ERP 业务为背景覆盖 Spring 的依赖注入与事务管理、SpringMVC 的请求调度、Mybatis 的注解与 XML 映射以及 JavaFX 的 FXML 布局、CSS 美化、TableView 实时刷新与表单双向绑定等关键技术点能直观看到各层之间的调用关系。7z 压缩包内工程目录组织清晰SQL 脚本包含初始化数据导入 IDE 后即可还原演示环境通过实际代码可以学到桌面端与后端服务如何分层协作界面设计与持久层映射也能直接复用是一份适合进阶开发或课题设计的完整参考。1. 这到底是个什么项目为什么有人会用 JavaFX 去套 SSM第一次见到这个组合的时候我也愣了一下JavaFX 是桌面客户端技术SSM 是 Web 后端三板斧这俩怎么能凑到一个项目里跑起来才明白这个项目把 JavaFX 当成「界面渲染器」把 SSM 当成「数据服务层」两者通过 HTTP 接口通信。换句话说这是一个典型的「瘦客户端 数据服务端」架构——界面负责展示和交互业务逻辑和数据访问全部下沉到 SpringMVC 的 Controller 和 Mybatis 的 Mapper 里。这种结构最适合谁一类是刚学完 SSM 想做点东西却不知道往哪放的同学另一类是想把老 Web 系统改造成桌面工具的内部开发者。JavaFX 负责把数据变成表格、表单、图表SSM 负责把 MySQL 里的数据洗干净再吐出来分工干净测试也方便。如果你正愁「SSM 学完了只会写网页」这个项目能给你另一种落地姿势。2. 架构与技术栈JavaFX 和 SSM 是怎么各司其职的2.1 分层设计从 FXML 到 Mapper 的数据流整个项目按「两端三层」的方式组织。两端指 JavaFX 客户端和 SSM 服务端三层指服务端的 Controller、Service、Mapper 三层。客户端不直接连数据库所有数据操作都通过 HTTP 请求打到 SpringMVC 的接口上返回 JSON 再渲染到界面。这种设计有一个明显的好处JavaFX 端的代码只关心「怎么把数据画出来」服务端只关心「数据从哪来、怎么算」。职责边界清楚了两个人同时改同一个功能也不会打架。我在二次开发时甚至能把 JavaFX 端整个扔掉换成 Flutter 或 Vue 的 Web 页面后端一行不用动——接口定了前端随便换。服务端的目录结构是这样的src/main/java/com/example/ssm/ ├── controller/ # SpringMVC 控制器唯一对外的入口 │ ├── UserController.java │ ├── OrderController.java │ └── StatsController.java ├── service/ │ ├── UserService.java │ ├── OrderService.java │ └── impl/ │ ├── UserServiceImpl.java │ └── OrderServiceImpl.java ├── mapper/ # Mybatis 的 Mapper 接口 │ ├── UserMapper.java │ └── OrderMapper.java ├── entity/ # 数据库实体类 │ ├── User.java │ └── Order.java └── config/ ├── SpringConfig.java └── SpringMvcConfig.java服务端是标准的 SSM 三层。客户端这边JavaFX 按界面拆分成 FXML 文件和对应的 Controller 类一个 FXML 对应一个界面模板一个 Controller 负责一个界面的交互逻辑。2.2 技术选型为什么用 Mybatis 而不是 JPA这个项目选了 Mybatis 而不是 JPA老开发一看就懂——Mybatis 对 SQL 的控制是粒度的尤其是统计类报表写起来比 JPA 的 Criteria 舒服得多。项目中有一个订单统计模块要吃聚合函数、日期分组、多表关联Mybatis 里几行 XML 就搞定了JPA 那边要折腾半天。Spring 的容器负责把所有 Bean 串起来。SpringMvc 的 DispatcherServlet 拦截 /api 路径的请求分发到 ControllerService 层做事务管理Mapper 层通过 Mybatis 的 SqlSession 操作数据库。集成方式用的是全注解 少量 XML数据源交给 Druid 管理连接池参数在 jdbc.properties 里调。客户端到服务端的通信项目里用的是 Spring 自带的 RestTemplate。每个 JavaFX Controller 在初始化时注入一个 RestTemplate 实例调用后端接口拿数据。这里有个小设计值得学他把所有接口地址集中放到了一个 ApiUrls 常量类里前端代码里不会散落一堆字符串 URL后期接口地址调整一处改完。2.3 关键取舍JavaFX 端的线程模型处理JavaFX 的界面线程FX Application Thread是不能被阻塞的否则界面直接卡死。这个项目在客户端封装了一个简单的异步工具类把每一条 HTTP 调用丢到线程池里执行拿到结果后再通过 Platform.runLater() 切回界面线程刷新数据。这个封装虽然简单但是是 JavaFX 客户端能不能「用起来」的关键后面核心代码部分会专门拆开讲。3. 核心模块拆解一条订单数据从前端到数据库的完整链路3.1 入口与配置Spring 容器如何与 JavaFX 共存这个项目的启动方式比较特殊它不是 Web 项目那种丢进 Tomcat 就能跑也不是纯 JavaFX 的 main 方法直接 launch。它的启动分两步先初始化 Spring 容器再启动 JavaFX 的 Application 类。public class MainLauncher { public static void main(String[] args) { // 第一步初始化 Spring 容器加载所有 Bean ApplicationContext context new AnnotationConfigApplicationContext( SpringConfig.class, SpringMvcConfig.class); // 第二步把 Spring 容器挂到全局供 JavaFX 的 Controller 使用 SpringContextHolder.setContext(context); // 第三步启动 JavaFX 界面 Application.launch(FxApplication.class, args); } }这里的逻辑说明两点。第一Spring 容器必须先生成否则 JavaFX 界面加载时拿不到 Service 层的 Bean第二用 SpringContextHolder 这个静态类做全局桥接让 FXML 对应的 Controller 类可以在界面的 initialize() 方法里直接取需要的 Bean这是一种常见的「非 Spring 管理的类里使用 Spring Bean」的做法在不引入额外框架的前提下最干净。参数说明SpringConfig.class 是纯注解配置类负责扫描 service 和 mapper 包配置数据源与事务管理器SpringMvcConfig.class 负责启用 SpringMVC 注解驱动和设置 JSON 序列化器。如果你的项目还需要集成其他组件往这两个 Configuration 类里继续添加 Bean 即可。3.2 Controller 层设计REST 接口是前后端的分界线Controller 层是两面胶。对前端来说它是数据接口对后端来说它是忙碌的入口。这个项目里 Controller 写得很克制每个方法都很薄主要做三件事收参数、调 Service、封装返回结果。RestController RequestMapping(/api/order) public class OrderController { Autowired private OrderService orderService; GetMapping(/list) public Result list(RequestParam(name page, defaultValue 1) int page, RequestParam(name limit, defaultValue 10) int limit, RequestParam(name keyword, required false) String keyword) { // 分页查询订单keyword 是模糊搜索条件 PageResultOrder pageResult orderService.queryPage(page, limit, keyword); return Result.success(pageResult); } GetMapping(/detail/{id}) public Result detail(PathVariable(id) Integer id) { // 查询订单详情包含订单项和关联的用户信息 OrderDetailVO detail orderService.getDetailById(id); return Result.success(detail); } PostMapping(/update) public Result update(RequestBody Order order) { // 更新订单参数直接是 JSON 体SpringMVC 自动反序列化 orderService.update(order); return Result.success(null); } }Result 是项目里封装的一个统一返回体包含 code、message、data 三个字段。这个设计非常有用JavaFX 端只用判断 code 是否为 200不用每次解析不同的返回结构。接口路径上的 /api 前缀让前端一眼就知道哪些请求走服务端如果需要做权限拦截在 SpringMvcConfig 里配置拦截器只拦 /api/** 就行。这里有个细节值得二次开发时注意update 方法用的是 RequestBodyJavaFX 端发请求时必须设置 Content-Type 为 application/json而且 Order 实体类必须有对应的 setter 方法Jackson 才能把 JSON 字段塞进去。很多新手在这里踩坑明明接口签名没问题就是 400多半是实体类缺了无参构造器或 setter。3.3 Service 层与事务管理别把业务逻辑写进 ControllerController 薄Service 层就得厚一点。这个项目的 Service 层做得比较讲究几个关键场景都有体现比如订单下单时同时扣库存、写流水这些操作必须放在同一个事务里。Service public class OrderServiceImpl implements OrderService { Autowired private OrderMapper orderMapper; Autowired private StockMapper stockMapper; Autowired private FlowMapper flowMapper; Override Transactional(rollbackFor Exception.class) public void createOrder(Order order, ListOrderItem items) { // 1. 插入订单主表 orderMapper.insertOrder(order); // 2. 插入订单明细 for (OrderItem item : items) { item.setOrderId(order.getId()); orderMapper.insertItem(item); // 3. 扣减库存注意这里要开启行锁 int rows stockMapper.deductStock(item.getProductId(), item.getQuantity()); if (rows 0) { throw new RuntimeException(库存不足 item.getProductId()); } } // 4. 写资金流水 flowMapper.insertFlow(buildFlowFromOrder(order)); } }扣库存那步是关键。stockMapper.deductStock 的 SQL 用了 UPDATE ... SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}返回值是受影响的行数如果为 0 说明库存不够直接抛出异常触发事务回滚。这种「乐观锁 原子扣减」的写法在库存类业务里是标准做法避免先查再改带来的并发超卖问题——这也是 SSM 三件套做业务系统时最有含金量的部分面试时能把这套链路讲清楚比背一百道八股文管用。事务注解用了 rollbackFor Exception.class把运行时异常和受检异常都纳入回滚。我见过不少项目只写 Transactional 不写 rollbackFor结果业务抛出 IOException 时事务静默提交数据就花了。3.4 Mapper 层与动态 SQLMybatis 的 XML 怎么写才不乱Mapper 接口和 XML 文件是这个项目的持久层核心。接口定义方法签名XML 写 SQL 和结果映射。看一段订单分页查询的动态 SQLselect idqueryPage resultTypecom.example.ssm.entity.Order SELECT id, order_no, user_id, total_amount, status, create_time FROM t_order where if testkeyword ! null and keyword ! AND (order_no LIKE CONCAT(%, #{keyword}, %) OR user_id IN (SELECT id FROM t_user WHERE name LIKE CONCAT(%, #{keyword}, %))) /if /where ORDER BY create_time DESC LIMIT #{offset}, #{limit} /select标签是 Mybatis 动态 SQL 的常用技巧它会自动处理条件前缀如果关键字为空就只查全部不为空才会追加 AND 子句省去手动写 WHERE 11 这种不那么体面的写法。LIMIT #{offset}, #{limit} 是 MySQL 的分页语法#{} 是预编译占位符Mybatis 会生成 PreparedStatement能有效防止 SQL 注入。这里有一个项目里没用但值得补的建议如果数据量上来了不要用 LIMIT 大偏移量分页改成基于上一页最后一条订单时间或 ID 的键集分页Keyset Pagination性能会好一个量级。这属于二次开发时的优化方向后面会再提到。4. JavaFX 客户端核心逻辑界面怎么与服务端稳定对接4.1 FXML 与 Controller 绑定画界面和写逻辑分离JavaFX 端的界面文件用 FXML 编写类似 HTML 之于网页。FXML 里写控件布局Controller 类里写交互逻辑通过 fx:id 和 FXML 注解关联起来。看一个订单列表界面的 FXML 片段VBox xmlnshttp://javafx.com/javafx xmlns:fxhttp://javafx.com/fxml fx:controllercom.example.fx.controller.OrderListController HBox spacing10 padding10 TextField fx:idkeywordField promptText输入订单号或客户姓名/ Button text查询 onAction#handleSearch/ Button text刷新 onAction#handleRefresh/ /HBox TableView fx:idorderTable columns TableColumn text订单号 fx:idorderNoCol/ TableColumn text金额 fx:idamountCol/ TableColumn text状态 fx:idstatusCol/ TableColumn text下单时间 fx:idtimeCol/ /columns /TableView /VBoxfx:controller 属性把 FXML 文件绑定到 OrderListController 类fx:id 和 Controller 里标注 FXML 的字段一一对应。onAction#handleSearch 表示查询按钮点击后调用 Controller 类里的 handleSearch 方法。这样设计的好处是美工调样式改 FXML开发调逻辑改 Controller互不干扰。4.2 异步 HTTP 调用不让界面线程卡死的封装JavaFX 单一界面线程的约束是所有桌面端项目最难受的点也是这个项目的核心处理逻辑所在。我见过有人直接用 HttpURLConnection 同步调用一点查询按钮整个界面冻住好几秒像是程序崩溃了其实只是网络请求阻塞了 UI 线程。这个项目封装了一个 AsyncHttp 工具类思路值得借鉴public class AsyncHttp { private static final RestTemplate restTemplate new RestTemplate(); // 在后台线程中发送 GET 请求结束后回调 onSuccess 方法 public static T void get(String url, ParameterizedTypeReferenceT type, ConsumerT onSuccess, ConsumerException onError) { executor.execute(() - { try { ResponseEntityT response restTemplate.exchange( url, HttpMethod.GET, null, type); // 切回 JavaFX 界面线程执行回调 Platform.runLater(() - { if (response.getStatusCode() HttpStatus.OK) { onSuccess.accept(response.getBody()); } }); } catch (Exception e) { Platform.runLater(() - onError.accept(e)); } }); } private static final ExecutorService executor Executors.newFixedThreadPool(4); }这个工具类的逻辑说明executor 是固定大小为 4 的线程池所有 HTTP 请求在线程池里执行不占用 JavaFX 的界面线程拿到响应后用 Platform.runLater() 把回调丢回界面线程保证 UI 操作安全。RestTemplate.exchange 方法接收一个 ParameterizedTypeReference这是为了处理泛型返回类型——比如 ResultPageResult 这种嵌套泛型如果不传这个参数Jackson 反序列化时会把泛型信息丢掉这是很多同学遇到「接口返回正常但前端一直拿不到 List 数据」的根因之一。在多线程代码里有一个常见的坑是在回调函数里直接更新 TableView 的 items。JavaFX 要求对界面控件的修改必须在 FX 线程上执行所以项目把 onSuccess 包装在 Platform.runLater() 里再调用。这个封装跨项目通用我在做其他 JavaFX 项目时也直接复制过。4.3 TableView 数据绑定把 Service 拿到的列表灌进表格TableView 是 JavaFX 中最常用的数据表格控件但它的数据绑定方式比 Web 端的表格要绕一些。TableView 本身不直接接收 List而是接收 ObservableList这样才能监听数据变化并自动刷新界面。FXML private TableViewOrderVO orderTable; FXML private TableColumnOrderVO, String orderNoCol; FXML private TableColumnOrderVO, BigDecimal amountCol; // 数据集合必须是 ObservableListTableView 会自动监听并刷新 private final ObservableListOrderVO dataList FXCollections.observableArrayList(); FXML private void handleSearch() { String keyword keywordField.getText(); String url ApiUrls.ORDER_LIST ?page1limit10keyword URLEncoder.encode(keyword, StandardCharsets.UTF_8); // 清空旧数据避免界面残留上一条查询结果 dataList.clear(); AsyncHttp.get(url, new ParameterizedTypeReferenceResultPageResultOrderVO() {}, result - { // 从服务端返回的分页对象中取出列表数据 dataList.addAll(result.getData().getRecords()); }, error - { error.printStackTrace(); // 生产环境应该在这里弹出 Alert 提示用户 }); } FXML private void initialize() { // 把每个列绑定到 OrderVO 的属性名 orderNoCol.setCellValueFactory(new PropertyValueFactory(orderNo)); amountCol.setCellValueFactory(new PropertyValueFactory(totalAmount)); statusCol.setCellValueFactory(new PropertyValueFactory(status)); orderTable.setItems(dataList); // 启动后自动加载一次数据 handleRefresh(); }这段代码有两个需要留意的节点。第一PropertyValueFactory 通过反射调用 OrderVO 的 getOrderNo() 等方法取数所以 OrderVO 属性名要和 FXML 里的字符串对应而且必须有 getter否则表格列会一直空白不报错这种情况排查起来很头疼从这入手能很快定位第二URL 中的中文关键字必须先 URLEncoder.encode否则服务端接收到的参数是乱码有时候能查到值有时候查不到就是因为编码不一致这是个经典翻车点。5. 运行环境与避坑把这套项目拉起来需要踩过哪些坑5.1 从零启动数据库初始化到界面弹窗的完整步骤先把运行环境讲清楚。JDK 推荐 8 或 11JavaFX 在 JDK 8 里是自带组件到 JDK 11 之后变成了独立模块需要额外引入 org.openjfx:javafx-controls 依赖。Maven 项目直接在 pom.xml 里声明即可。数据库用 MySQL 5.7 以上版本项目带了一个 init.sql 初始化脚本建库建表另外配好 Druid 连接池的连接信息。# 第一步初始化数据库 mysql -uroot -p123456 init.sql # 第二步修改数据库连接配置 vim src/main/resources/jdbc.properties # 第三步启动服务端先启动 Spring 容器 mvn spring-boot:run # 第四步启动客户端Launch MainLauncher 即可 mvn javafx:run服务端如果用的是内嵌 Tomcat启动后默认监听 8080 端口。JavaFX 客户端的 ApiUrls 常量类里 BaseUrl 默认是 http://localhost:8080如果服务端部署在远程机器只需要改这一处常量不用动任何其他代码。这是我比较喜欢这个项目的原因之一——接口集中定义了运行环境的差异被压到了最小改动成本变得很低。5.2 常见问题排查从端口冲突到中文乱码到泛型丢失这个项目能跑通不算难真正恼人的是那些「环境没问题但就是跑不出数据」的场景我把在调试中遇到过的问题整理成了排查清单。现象一服务端日志显示请求进来了但 JavaFX 界面一直转圈没有数据。原因多半是 RestTemplate 请求线程抛了异常但异常被吞掉没有反馈到界面。解决方法是先看后台线程的 error 回调有没有被调用在 onError 里加日志输出。这是我见过最多的一个隐蔽问题排查时要强制走通这条回调链路再谈其他。现象二窗口打开了Table 列表数据加载不出来但后端 Postman 测接口有返回。这个现象让人摸不着头脑。原因基本出在泛型反序列化丢失上。如果直接用 RestTemplate.getForObject(url, Result.class) 接收返回值Result 里的 data 字段会被转成 LinkedHashMap取不到 Order 对象。解决方法是必须用 ParameterizedTypeReference 带完整的泛型信息去请求或者对 Result 的 data 字段做手动 TypeReference 转换。写 RestTemplate 的时候要养成「带泛型信息请求」的习惯可以避免很多类似问题。现象三前端提交中文数据服务端接收乱码。原因有两层第一层是 JavaFX 端 URL 参数没有编码第二层是 SpringMVC 的字符集过滤器没有配置。解决方法是前端用 URLEncoder.encode 编码后端在 web.xml 或 SpringBoot 的配置类里加 CharacterEncodingFilter强制 UTF-8。我遇到过前后端都检查了还是乱码的情况最后发现是 MySQL 表字段的 collation 是 latin1 开头建表时指定 ENGINEInnoDB DEFAULT CHARSETutf8mb4 才能彻底解决。现象四Maven 编译报错 JavaFX 类找不到。原因是在 JDK 11 及以上版本没有引入 JavaFX 依赖时代码里 import javafx.* 会直接编译失败。解决办法是 pom.xml 增加 org.openjfx 依赖并配置 maven-compiler-plugin同时注意 Windows/Mac/Linux 的 classifier 不同。当然有的项目为了省事直接退回 JDK 8倒也是个可行的方案但长期维护还是建议按标准方式配好依赖。5.3 开发阶段的调试技巧日志和接口文档两手都要硬开发这套项目时建立一个简单的调试习惯能省不少时间。服务端接口用 Postman 先测过一轮确认返回 JSON 结构再动前端前端调不通的时候不要盲目猜先看服务端控制台有没有报错再看返回的 JSON 结构是否符合前端 parse 的预期最后才怀疑是不是界面线程的问题。6. 二次开发思路围绕这套骨架扩展新功能的具体做法6.1 从订单模块扩展到报表模块这个项目最值得改的切入点是把现有订单数据变成统计报表。服务端新增一个报表 ControllerJavaFX 端使用 BarChart 控件展示柱状图整条链路走的是项目里已经跑通的分层套路不需要引入额外框架。RestController RequestMapping(/api/stats) public class StatsController { Autowired private StatsService statsService; GetMapping(/monthly) public Result monthly(RequestParam(year) Integer year) { // 返回 1 到 12 月订单总额供前端图表展示 ListMapString, Object data statsService.monthlySales(year); return Result.success(data); } }Service 层的实现用一句 SQL 就能聚合出月度销售数据比 Java 内存里循环统计高效得多这正是 Mybatis 适合干的事。JavaFX 端把 AsyncHttp.get() 请求的结果灌进 XYChart.Series再把 Series 加到 BarChart 上十几行代码就能做出一张月度营收趋势图。如果业务上还需要导出 Excel可以在服务端集成一个 EasyExcel 依赖新增一个导出接口复用现成的查询逻辑。6.2 给服务端加上权限控制项目当前的接口是裸奔的谁拿到地址就能调。如果要把这套骨架用于内部工具或生产环境建议第一件事就是加登录认证。技术路线上最轻量的是拦截器加 Token。public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头中取 Token校验通过才放行 String token request.getHeader(Authorization); if (StringUtils.isEmpty(token) || !TokenStore.validate(token)) { response.setStatus(HttpStatus.UNAUTHORIZED.value()); return false; } return true; } }TokenStore 可以是 Redis 的封装或内存 Map取决于部署规模。JavaFX 客户端那边只需要在发送请求时从一个全局登录会话里取 Token塞进请求头。两个端改动各集中在一处因为这属于跨切面的横切关注点可以放到已有的工具类中集中处理不需要每个界面改一遍这也体现了项目分层合理的优势。6.3 替换 RestTemplate 为轻量级 HttpClient如果客户端请求量比较大或者需要做连接复用、超时控制可以考虑把 RestTemplate 换成 java.net.http.HttpClient 或 OkHttp。项目里所有 HTTP 调用都走了 AsyncHttp 工具类所以替换时只需要改这一个类——这也说明当初的封装给二次开发留了后路。private static final java.net.http.HttpClient httpClient java.net.http.HttpClient.newBuilder() .connectTimeout(Duration.ofSeconds(5)) .build(); private static T void getHttpClient(String url, TypeReferenceT type, ConsumerT onSuccess, ConsumerException onError) { HttpRequest request HttpRequest.newBuilder() .uri(URI.create(url)) .timeout(Duration.ofSeconds(15)) .GET() .build(); httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString()) .thenApply(HttpResponse::body) .thenApply(json - JsonUtils.parse(json, type)) .whenComplete((data, throwable) - { Platform.runLater(() - { if (throwable ! null) { onError.accept(throwable); } else { onSuccess.accept(data); } }); }); }JDK 自带的 HttpClient 在 JDK 11 以后 API 已经是稳定的了配合同步转异步的 CompletableFuture代码甚至比 RestTemplate 还要干净一些。注意 JavaFX 的界面刷新仍然要包在 Platform.runLater() 里这是绕不开的原则。换完以后原有的调用方代码一行都不用动全链路也就十分钟的事。6.4 改造后的验证方法改动完成后先按「查询订单 → 打开详情 → 修改金额 → 刷新列表」这条主链路回归一遍。如果改了通信层用抓包工具看一眼请求格式确认 Content-Type 和参数名没变。如果改了数据库表结构重点看 Mapper XML 里 SQL 的字段引用是否同步更新了。这套项目骨架的可贵之处在于服务端和客户端之间隔了一条清晰的 HTTP 接口线。我后来再做桌面工具项目时始终保持着这条线的纪律——接口先定、双边各自 mock、联调阶段再对齐边界。从那以后我每次启动新功能都会强制走一遍「Controller 接口签名先行Service 实现跟进JavaFX 端最后填充」的流程翻车次数就肉眼可见地少了。希望这次的拆解也能帮你把这套代码顺顺当当跑起来改出你自己想要的效果。本文还有配套的精品资源点击获取