【SpringCloud】Gateway

在这里插入图片描述

前言

前面我们使用了 Eureka 和 Nacos 实现了服务注册和服务发现,使用 Spring Cloud LoadBalance 解决了负载均衡的问题,使用 OpenFeign 解决了如何优雅的实现服务和服务之间的调用关系。

但是有一个问题,就是我们前面实现的这些微服务的接口都是直接对外暴露的,可以直接通过外部访问。那么这样的话,各个微服务就会显得非常的不安全,那么为了技能对外提供服务,又能够保证微服务的安全,就需要服务端实现的微服接口带有一定的权限校验机制,但是由于使用了微服务,原本一个应用的多个模块拆分成了多个应用,我们就需要实现多次校验逻辑,当这套逻辑需要修改的时候,我们就需要修改多个应用,那么这就会显得非常的麻烦。

那么针对上面的问题,一个常用的解决方案是使用 API 网关。跟银行的接待人员差不多,当我们进入银行的时候,首先接待人员会问我们需要办理什么业务,如果觉得你的行为不正常的话,就不会允许你进行业务的办理,如果你的请求被准许了话,接待人员就会告诉你去哪个窗口办理。

什么是 API 网关

API网关(API Gateway)是分布式系统架构中常用的一种设计模式,用于充当客户端和后端服务之间的中介。它的主要功能是集中管理和处理来自客户端的请求,并将这些请求路由到适当的后端服务。

它的定义类似前面学习的设计模式中的门面模式,而 API 网关就相当于整个微服务架构的门面,所有的外部客户端访问,都需要经过他来进行调度和过滤。

在这里插入图片描述
API 网关的主要功能:

  1. 请求路由(Routing)
  • 根据客户端请求的URL、HTTP方法或其他信息,将请求转发到适当的后端服务。
  1. 负载均衡(Load Balancing)
  • 在后端服务之间分配请求流量,确保服务的高可用性和性能。
  1. 认证与授权(Authentication and Authorization)
  • 验证用户身份(如OAuth、JWT等)并检查用户是否有权限访问某些资源。
  1. 协议转换(Protocol Translation)
  • 支持将HTTP/HTTPS请求转换为后端支持的协议(如gRPC、WebSocket等)。
  1. 请求聚合(Request Aggregation)
  • 将来自多个后端服务的数据合并为一个响应,减少客户端的请求次数。
  1. 安全性(Security)
  • 阻止恶意请求(如防止SQL注入、跨站脚本攻击等)。
  • 实现速率限制和流量控制,避免资源滥用。
  1. 监控与日志记录(Monitoring and Logging)
  • 记录请求和响应的详细信息,提供性能监控和故障诊断功能。
  1. 缓存(Caching)
  • 为常用的请求和响应设置缓存,减少后端服务的负载。

常见的 API 网关实现

Zuul

Zuul 是 Netflix 开发的一款开源网关服务,主要用于微服务架构中的请求路由和过滤。作为 Spring Cloud 生态系统中的早期网关解决方案之一,Zuul 在微服务之间扮演了客户端与服务端通信的中介角色。

Spring Cloud Gateway

Spring Cloud Gateway 是 Spring Cloud 生态系统中的一个关键组件,用于构建基于 Java 的现代 API 网关。它是 Spring 官方推荐的用于微服务架构中的 API 网关解决方案,旨在取代 Netflix Zuul 1.x,提供更高性能、更灵活的功能。

Spring Cloud Gateway 与 Zuul 的比较:

特性Spring Cloud GatewayZuul 1.x
性能非阻塞,基于 Netty阻塞,基于 Servlet
编程模型Reactive 编程基于传统 Servlet 模型
扩展性简单,支持自定义过滤器复杂,自定义性较弱
服务发现深度集成 Spring Cloud支持但集成较弱
推荐使用场景新项目,现代微服务架构传统项目迁移

看了 Zuul 和 Spring Cloud Gateway 的比较,我们发现 Spring Cloud Gateway 更适合当今时代的发展,所以我们接下来学习 Spring Cloud Gateway 的使用。

Spring Cloud Gateway

快速上手

我们还是通过父子项目来实现 gateway。

在这里插入图片描述

引入网关依赖:

<!--基于nacos实现服务发现依赖-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!--负载均衡-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!--⽹关-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

配置启动类:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GateWayApplication {
    public static void main(String[] args) {
        SpringApplication.run(GateWayApplication.class,args);
    }
}

网关的实现方式基本上都是通过配置文件来进行配置的,所以我们创建出 application.yml 配置文件并且添加配置:

server:
  port: 10030
spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: http://x.x.x.x:8848
    gateway:
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
        - id: order-service
          uri: lb://order/service
          predicates:
            - Path=/order/**
  • id:自定义路由 ID,保持唯一
  • uri:目标服务地址,支持普通 uri 及 lb://应用注册服务名称。lb 表示负载均衡,使用 lb://方式表示从注册中心获取服务地址
  • predicates:路由条件,根据匹配解决决定是否执行该请求路由,上述代码中,我们将符合 Path 规则的一切请求,都代理到 uri 参数指定的地址

然后我们输入正确的 url 看是否可以访问:

在这里插入图片描述
url 符合配置文件中配置的 /order/**,然后路由转发到order-service,然后我们修改一下网关:

在这里插入图片描述

在这里插入图片描述

我们输入的 url order 不匹配 yml 配置文件中配置的路由,所以该请求就被拦截了。

我们知道 gateway 可以拦截非法的请求,对请求进行权限校验,那么 gateway 是如何进行校验的呢?

Route Predicate Factories

Predicate

Predicate 是 Java 8 提供的一个函数式编程接口,它接受一个参数并返回布尔值,通常用于条件过滤,请求参数的校验:

在这里插入图片描述
我们来自己实现一个 Predicate:

public class StringPredicate implements Predicate<String> {
    @Override
    public boolean test(String s) {
        return s.isEmpty();
    }
}

测试我们写的这个 Predicate 类:

public class PredicateTest {
    @Test
    public void test() {
        StringPredicate predicate = new StringPredicate();
        System.out.println(predicate.test("nihao"));
        System.out.println(predicate.test(""));
    }
}

在这里插入图片描述
我们也可以直接使用匿名内部类或者 lambda 表达式来写这个 Predicate 类:

//匿名内部类
@Test
public void test2() {
    Predicate<String> predicate = new Predicate<String>() {
        @Override
        public boolean test(String s) {
            return s.isEmpty();
        }
    };
    System.out.println(predicate.test("nihao"));
    System.out.println(predicate.test(""));
}
//lambda表达式
@Test
public void test3() {
    Predicate<String> predicate = s -> s.isEmpty();
    System.out.println(predicate.test("nihao"));
    System.out.println(predicate.test(""));
}

Predicate 不只有 test() 方法,还有:

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • and(Predicate<? super T> other):短路与操作,返回一个组成 Predicate
  • negate():返回此 Predicate 逻辑否定的 Predicate
  • or(Predicate<? super T> other):短语或操作,返回一个组成 Predicate
  • isEqual(Object targetRef):比较两个对象是否相等,参数可以是null
  • not(Predicate<? super T> target):逻辑跟negate类似,可以看到not中调用了negate方法
@Test
public void test4() {
    Predicate<String> predicate1 = s -> "aa".equals(s);
    Predicate<String> predicate2 = s -> "bb".equals(s);
    //只有当传入的参数既是aa又是bb才会返回true
    System.out.println(predicate1.and(predicate2).test("aa")); //false
    System.out.println(predicate1.and(predicate2).test("")); //false
}

剩下的就不给大家逐一展示了,用法都差不多。

Route Predicate Factories

Route Predicate Factories(路由断言工厂),也称为路由谓词工厂,在Spring Cloud Gateway中起到了定义和控制路由行为的重要作用。

在上面的演示中我们使用的是 Path,那么当请求传来经过 gateway 的时候,就会将请求的 URL 和 Path 中配置的 URL 进行匹配。

Spring Cloud Gateway 不止提供了这一个属性,还提供了很多的 Route Predicate Factory,这些 Predicate 会分别匹配 HTTP 请求的不同属性,并且多个 Predicate 可以通过 and 逻辑进行组合。

名称说明
After这个工厂需要一个日期时间(Java的ZonedDateTime对象),匹配指定日期之后的请求
Before匹配指定日期之前的请求
Between匹配两个指定时间之内的请求,datetime2的参数必须在datetime1之后
Cookie请求中包含指定的Cookie,且该Cookie值符合指定的正则表达式
Header请求中包含指定的Header,且该Header值符合指定的正则表达式
Host请求中必须是访问某个Host(根据请求中的Host字段进行匹配)
Method匹配指定的请求方式
Path匹配指定规则的路径
Remote Addr请求者的IP必须为指定范围

更多可以参考 Spring Cloud Gateway 官方文档:https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/request-predicates-factories.html

测试其他的路由谓词工厂:

我们可以通过使用 ZonedDateTime.now() 获取我们当前所处的时区信息:

public void test5() {
    System.out.println(ZonedDateTime.now());
}

在这里插入图片描述
我们将配置文件中的时间设置为 2025 年之后,看看是否能够访问到服务:

spring:
  cloud:
    gateway:
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
            - After=2025-12-02T16:37:12.097918500+08:00[Asia/Shanghai]

在这里插入图片描述

Gateway Filter Factories

Predicate 决定了请求由哪个路由处理,如果在请求处理的前后需要加一些逻辑,这就是 Filter(过滤器)的作用范围了。过滤器分为 Pre 类型和 Post 类型。

  • Pre 类型:路由处理之前执行(请求转发到后端服务器之前执行),在 Pre 类型过滤器中可以做鉴权,限流等
  • Post 类型:请求执行完成后,将结果返回给客户端之前执行

在这里插入图片描述
Spring Cloud Gateway 内置了很多 Filter,用于拦截和链式处理 web 请求,比如权限校验,访问超时等设定。Spring Cloud Gateway 从作用范围上,把 Filter 分为 GatewayFilter 和 GlobalFilter。

  • GatewayFilter:应用到单个路由或者一个分组的路由上
  • GlobalFilter:应用到所有的路由上,也就是对所有的请求生效

GatewayFilter

GatewayFilter 如何使用呢?和 Predicate 一样,都是在 application.yml 配置文件中进行相关的配置就可以了。

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: http://x.x.x.x:8848
    gateway:
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
            - After=2025-12-02T16:37:12.097918500+08:00[Asia/Shanghai]
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order1/**
          filters:  # 添加过滤器
            - AddRequestParameter=userName,zhangsan

修改一下 order-service 中的代码:

@Slf4j
@RequestMapping("/order")
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @Autowired
    private ObjectMapper objectMapper;

    @RequestMapping("/{orderId}")
    public OrderInfo getOrderById(@PathVariable("orderId") Integer orderId,String userName) {
        log.info("接收到参数:{}",userName);
        OrderInfo orderInfo = orderService.selectOrderById(orderId);
        return orderInfo;
    }
}

在这里插入图片描述

在这里插入图片描述
可以看到我们请求的时候没有传入 userName 这个参数,但是后端却获取到了 userName,这就说明 GatewayFilter AddRequestParameter 生效了。

下面是 Spring Cloud Gateway 官方提供的 GatewayFilter https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/gatewayfilter-factories.html

在这里插入图片描述
为当前请求添加 Header。

在这里插入图片描述
只有当请求头当中不存在这个设置的属性的时候,才添加进去设置的属性,否则发送属性的原来值。

在这里插入图片描述
为当前请求添加请求参数。

在这里插入图片描述
为响应的结果添加 Header。

在这里插入图片描述
从请求中删除某个 Header。

在这里插入图片描述
从响应结果中删除某个 Header。

在这里插入图片描述
为当前网关的的所有请求执行限流过滤,如果被限流,默认会响应 429-TooManyRequests,默认提供了 RedisRateLimiter 的限流实现,采用令牌桶算法实现限流功能。

限流算法

限流算法主要有四种:

计数器限流算法(固定窗口限流算法)

通过维护一个计数器变量来限制在特定时间间隔内的请求数量。在一段时间间隔内(如1分钟)对请求进行计数,并将计数结果与设置的最大请求数进行比较。如果请求数超过了最大值,则进行限流处理。时间间隔结束时,计数器会被清零,重新开始计数。

这种限流算法实现简单,直观易懂,设置明确的阈值,易于理解和配置。但是存在窗口切换时的突增问题,即在时间窗口的临界点附近,如果请求数突然增加,可能会导致短时间内大量请求通过限流检查,从而对系统造成压力。

在这里插入图片描述
滑动窗口限流算法

滑动窗口限流算法是计数器限流算法的升级版。它将时间窗口分为多个小周期,每个小周期都有自己的计数器。随着时间的滑动,过期的小周期数据被删除,这样可以更精确地控制流量。

这种算法能够更精确地控制流量,尤其是在处理短时间内突发的高请求量时,通过动态调整时间窗口的起始点和结束点,可以有效地平滑流量波动,避免系统因瞬间高负载而崩溃。但是实现相对复杂,需要维护多个时间窗口的计数器,并且需要处理时间窗口的滑动逻辑。

在这里插入图片描述

漏桶限流算法

漏桶限流算法通过模拟一个带有恒定流出速度的漏桶来限制系统的流量。当有请求进来时,如果漏桶还有剩余容量,那么这个请求就可以成功放入漏桶中;如果漏桶已经满了,那么这个请求就被丢弃或者排队等待处理。

这种算法简单易懂,能够平滑突发流量,为网络提供一个稳定的流量;精确严格的限流。但是无法应对突发流量,漏桶的容量和出水速率可能会成为瓶颈;响应时间延长,如果漏桶累积过多可能导致请求等待较长时间才能被处理。

在这里插入图片描述
令牌桶限流算法

令牌桶限流算法通过一个令牌桶来管理请求。桶中的令牌数量表示系统的可用流量,系统按照预设的速度持续不断的生成令牌放入令牌桶中。当有请求进来时,需要从令牌桶中获取一个令牌,如果没有令牌可用,则请求被拒绝。

这种算法简单易懂,能够灵活应对突发流量;精确控制请求处理的数量;可以与其他限流算法结合使用以实现更加灵活和高效的限流效果。但是实现复杂度稍高,需要维护一个令牌桶和令牌的生成速度。但是相较于上面的三种算法,这种方法更能应对流量激增的情况。

在这里插入图片描述

在这里插入图片描述
针对不同的响应进行重试,当后端服务不可用时,网关会根据配置参数来发起重试请求。

filters:
    - AddRequestParameter=userName,zhangsan
    - name: Retry
      args:
        retries: 3
        statuses: BAD_REQUEST

当返回的相应的状态为 BAD_REQUEST 的时候,会进行重试,如果还是返回 BAD_REQUEST,继续重试,最多重试次数为 3 次。

状态码描述对应的状态码我们可以在 org.springframework.http 包中的 HttpStatus 枚举类中查看:

在这里插入图片描述
在这里插入图片描述

@Slf4j
@RequestMapping("/order")
@RestController
public class OrderController {
    @Autowired
    private OrderService orderService;

    @Autowired
    private ObjectMapper objectMapper;

    @RequestMapping("/{orderId}")
    public OrderInfo getOrderById(@PathVariable("orderId") Integer orderId, String userName, HttpServletResponse response) {
        log.info("接收到参数:{}",userName);
        OrderInfo orderInfo = orderService.selectOrderById(orderId);
        response.setStatus(400);
        return orderInfo;
    }
}

在这里插入图片描述
可以看到一共执行了四次,第一次返回 BAD_REQUEST 之后进行了三次重试,虽然 retry 可以重试,但是不会影响我们的最终结果:

在这里插入图片描述

Default-Filter

上面的 Filter 添加在指定的路由下,只对当前路由生效,若需要对全部路由生效,可以使用 default-filters:

spring:
  cloud:
    gateway:
      default-filters:
      - AddResponseHeader=X-Response-Default-Red, Default-Blue
      - PrefixPath=/httpbin

GlobalFilter

GlobalFilter 是 Spring Cloud Gateway 中的全局过滤器,他和 GatewayFilter 的作用是一样的,只不过 GlobalFilter 作用于所有的路由。全局过滤器通常用于实现与安全性,性能监控和日志记录等相关的全局功能。

在这里插入图片描述

使用 Spring Cloud Gateway 内置的 GlobalFilter 也是直接在 application.yml 文件中进行配置,与 GlobalFilter 相关的配置大家可以去官网上看看 https://docs.spring.io/spring-cloud-gateway/reference/spring-cloud-gateway/global-filters.html

我们要想使用的话,还需要添加下面的依赖:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

在 application.yml 文件中添加配置,指定需要显示哪些内容:

management:
  endpoints:
    web:
      exposure:
        include: "*"
  endpoint:
    health:
      show-details: always
    shutdown:
      enabled: true

输入 127.0.0.1:10030/actuator,可以查看监控信息:

在这里插入图片描述
其中展示的 url 我们也是可以再进去看的:

在这里插入图片描述

上面我们了解了 GatewayFilter 和 GlobalFilter,那么如果我们一个项目中同时存在这两种类型的过滤器,那么他们的执行顺序是什么样的呢?

过滤器执行顺序

在一个项目中,当同时存在GatewayFilter(路由过滤器)和GlobalFilter(全局过滤器)时,它们的执行顺序主要由各自的order值决定。

请求进入网关时,会碰到三类过滤器:当前路由的过滤器(GatewayFilter)、DefaultFilter(默认过滤器,也属于GatewayFilter的一种)、GlobalFilter。网关会将这三类过滤器合并到一个过滤器链(集合)中。过滤器链会根据每个过滤器的order值进行排序,order值越小,优先级越高,执行顺序越靠前。排序完成后,依次执行每个过滤器。

每一个过滤器都需要指定一个 order 值,默认值为0,表示过滤的优先级,order 值越小,优先级越高,执行顺序越靠前

那么我们如何设置 order 值呢?

  • Filter 通过实现 Order 接口或者添加 @Order 注解来指定 order 值
  • Spring Cloud Gateway 提供的 Filter 由 Spring 指定。用户也可以自定义 Filter,由用户指定
  • 当过滤器的 order 值一样的时候,会按照 defaultFilter > GatewayFilter > GlobalFilter 的顺序执行

自定义过滤器

自定义GatewayFilter

自定义 GatewayFilter 需要去实现对应的接口 GatewayFilterFactory,但是 Spring Boot 默认帮我们实现的抽象类是 AbstractGatewayFilterFactory,我们可以直接使用:

package org.example.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Service
@Slf4j
public class CustomGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomConfig> implements Ordered {
    public CustomGatewayFilterFactory() {
        super(CustomConfig.class);
    }

    @Override
    public GatewayFilter apply(CustomConfig config) {
        return new GatewayFilter() {
            /**
             * ServerWebExchange:HTTP请求交互契约,提供了对HTTP请求和响应的访问
             * GatewayFilterChain:过滤器链
             * MonoReactor的核心类, 数据流发布者,Mono最多只能触发一个事件.可以把Mono用在异步完成任务时,发出通知
             * chain.filter(exchange)  执行请求
             * Mono.fromRunnable()  创建一个包含Runnable元素的数据流
             */
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                //Pre类型 执行请求 Post类型
                log.info("Pre Filter,config:{}",config);
                return chain.filter(exchange).then(Mono.fromRunnable(() -> {
                    log.info("Post Filter...");
                }));
            }
        };
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

CustomConfig类:

import lombok.Data;

@Data
public class CustomConfig {
    private String name;
}
  • 自定义的 GatewayFilter 的类名需要统一以 GatewayFilterFactory 结尾,因为默认情况下,过滤器的 name 会采用该定义类的前缀,这里的 name 会在 yml 配置文件中使用到
  • CustomConfig 是一个自定义的配置类,该类只有一个属性 name,和 yml 的配置对应
  • 自定义的 GatewayFilter 需要交给 Spring 管理,所以需要加上五大注解
  • getOrder 表示过滤器的优先级,值越小,优先级越高

将自定义的 GatewayFilter 在 yml 配置文件中进行配置:

spring:
  application:
    name: gateway
  cloud:
    nacos:
      discovery:
        server-addr: x.x.x.x:8848
    gateway:
      routes:
        - id: product-service
          uri: lb://product-service
          predicates:
            - Path=/product/**
            - After=2024-12-02T16:37:12.097918500+08:00[Asia/Shanghai]
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/order/**
          filters:
            - AddRequestParameter=userName,zhangsan
            - name: Retry
              args:
                retries: 3
                statuses: BAD_REQUEST
            # 配置自定义的GatewayFilter 
            - name: Custom
              args:
                name: test_custom

在这里插入图片描述
上面是我们自定义的 GatewayFilter,那么下面我们来看看如何自定义 GlobalFilter 类型的过滤器:

自定义GlobalFilter

因为 GlobalFilter 的作用范围是所有的请求,所以它不需要在 yml 配置文件中进行配置,只需要实现 GlobalFilter 接口,他就会自动过滤所有的 Filter。

package org.example.filter;

import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Slf4j
@Service
public class CustomGlobalFilter implements GlobalFilter, Ordered {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("Pre Global Filter");
        return chain.filter(exchange).then(Mono.fromRunnable(() -> {
            log.info("Post Global Filter");
        }));
    }

    @Override
    public int getOrder() {
        return Ordered.LOWEST_PRECEDENCE;
    }
}

在这里插入图片描述

可以看到当 GatewayFilter 和 GlobalFilter 同时存在并且两个类型的过滤器的优先级相同的时候的执行顺序为:GatewayFilter的Pre类型的过滤器 -> GlobalFilter的Pre类型的过滤器 -> GlobalFilter的Post类型的过滤器 -> GatewayFilter的Post类型的过滤器

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不能再留遗憾了

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值