Spring Cloud Gateway学习

API网关

系统的统一入口,封装了应用程序的内部结构,为客户端提供统一服务,一些与业务本身功能无关的公共逻辑可以在这里实现,例如认证鉴权监控路由转发等。

网关 = 路由转发 + 过滤器(编写额外功能)

作用就是把各个服务对外提供的API汇集起来,让外界看起来是一个统一的接口。

稳定与安全

  • 全局性流控
  • 日志统计
  • 防止SQL注入
  • 防止Web攻击
  • 屏蔽工具扫描
  • 黑白IP名单
  • 证书/加解密处理

提供更好的服务

  • 服务级别流控
  • 服务降级与熔断
  • 路由与负载均衡、灰度策略
  • 服务过滤、聚合与发现
  • 权限验证与用户等级策略
  • 业务规则与参数校验
  • 多级缓存策略

什么是Spring Cloud Gateway?

网关作为流量的入口,常用的功能包括路由转发,权限校验,限流等。

Spring Cloud Gateway 是Spring Cloud官方推出的第二代网关框架,提供了微服务网关功能。定位于取代Netflix Zuul。相比 Zuul 来说,Spring Cloud Gateway提供更优秀的性能,更强大的有功能。

Spring Cloud Gateway 是由 WebFlux+Netty+Reactor实现的响应式的API 网关。它不能在传统的 servlet 容器中工作,也不能构建成 war 包。

Spring Cloud Gateway 旨在为微服务架构提供一种简单且有效的API路由的管理方式,并基于Filter 的方式提供网关的基本功能,例如说安全认证、监控、限流等等。

Spring Cloud Gateway 功能特性:

  • 动态路由:能够匹配任何请求属性
  • 支持路径重写
  • 继承Spring Cloud服务发现功能(Nacos、Eureka)
  • 可以集成流控降级功能(Sentinel、Hystrix)
  • 可以对路由指定易于编写的Predicate(断言)和Filter(过滤器)

Spring Cloud Gateway主要包含:

Route:路由,一个路由包含ID、URI、Predicate(附加条件和内容,如当满足某种条件再进行路由转发)集合、Filter(在Gateway运行过程中Filter负责在代理服务『之前』或『之后』去做一些事情 )集合

在这里插入图片描述

网关客户端访问Gateway网关,Gateway中Handler Mapping对请求URL进行处理。处理完成后交换Web Handler,Web Handler会被Filter进行过滤。Filter中前半部分代码是处理请求的代码。处理完成后调用真实被代理的服务。被代理服务响应结果,结果会被Filter中后半部分代码进行操作,操作完成后把结果返回给Web Handler,再返回给Handler Mapping,最终响应给客户端。

核心概念
  • 路由(route)

路由是网关中最基础的部分,路由信息包括一个ID、一个目的URI、一组断言工厂、一组Filter组成。如果断言为真,则说明请求的URL和配置的路由匹配。Route规则参考:org.springframework.cloud.gateway.route.RouteDefinition

  • 断言(predicates)

Java8中的断言函数,Spring Cloud Gateway中的断言函数类型是Spring5.0框架中的ServerWebExchange。断言函数允许开发者去定义匹配Http request中的任何信息,比如请求头和参数等。

  • 过滤器(Filter)

SpringCloud Gateway中的filter分为Gateway Filler和Global Filter。Filter可以对请求和响应进行处理。

示例:

先引入依赖:

        <!-- 引入Spring Cloud Gateway -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>

application.yml配置:

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    # Gateway配置
    gateway:
      # 路由规则
      routes:
        - id: order_route # 路由的唯一标识,此示例理由到订单服务
          uri: http://127.0.0.1:8082 # 需要转发的地址
          # 断言规则,用于路由规则的匹配
          predicates:
            - Path=/order-service/** # http://localhost:8088/order-service/order 会路由到 http://127.0.0.1:8082/order-service/order
          filters:
            - StripPrefix=1 # 转发之前去掉第一层路径,去掉后变成http://127.0.0.1:8082/order

集成Nacos注册中心:

        <!-- 集成Nacos -->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

修改配置文件

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # Gateway配置
    gateway:
      # 路由规则
      routes:
        - id: order_route # 路由的唯一标识,此示例理由到订单服务
          uri: lb://order-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡器
          # 断言规则,用于路由规则的匹配
          predicates:
            - Path=/order-service/** # http://localhost:8088/order-service/order 会路由到 http://127.0.0.1:8082/order-service/order
          filters:
            - StripPrefix=1 # 转发之前去掉第一层路径,去掉后变成http://127.0.0.1:8082/order

或使用:

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # Gateway配置
    gateway:
      discovery:
        locator:
          enabled: true # 是否启动自动识别nacos服务,约定大于配置
          lower-case-service-id: true # 把服务名称转换为小写,Eureka中默认都是大写

路由断言工厂(Route Predicate Factories)配置

当请求gateway的时候,使用断言对请求进行匹配,如果匹配成功就路由转发,如果匹配失败就返回404。

内置路由断言工厂

断言Predicate:在Spring Cloud Gateway中断言实现org.springframework.cloud.gateway.handler.predicate.GatewayPredicate接口。其中类名符合XXXRoutePredicateFactory,其中XXX就是在配置文件中断言名称。Path=/server/book/** 实际使用的就是PathRoutePredicateFactory。

参考示例:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gateway-request-predicates-factories

  • 基于时间类型的断言工厂

BeforeRoutePredicateFactory:接收一个日期参数,判断请求日期是否早于指定日期。

AfterRoutePredicateFactory:设置在指定时间点之后。

BetweenRoutePredicateFactory:请求时必须在设定的时间范围内容,才进行路由转发。

spring:
  cloud:
    gateway:
      routes:
      - id: between_route
        uri: https://example.org
        predicates:
        - Between=2017-01-20T17:42:47.789-07:00[America/Denver], 2017-01-21T17:42:47.789-07:00[America/Denver]
  • 基于远程地址的断言工厂

RemoteAddrRoutePredicateFactory:设置允许访问的客户端地址。接收一个IP地址端,判断请求主机地址是否在地址段中。

- RemoteAddr=192.168.1.1/24
  • 基于Cookie的断言工厂

CookieRoutePredicateFactory:接收两个参数,cookie名字和一个正则表达式,判断请求cookie是否具有给定名称且值与正则表达式匹配。设置请求中包含指定Cookie名和满足特定正则要求的值(Cookie必须有两个值,第一个Cookie包含的参数名,第二个表示参数对应的值(可以正则表达式))。

- Cookie=chocolate, ch.p
  • 基于Header的断言工厂

HeaderRoutePredicateFactory:接收两个参数,标题名称和正确表达式。判断请求Header是否具有给定名称且值与正则表达式匹配。设置请求头中必须包含的内容(参数、参数值)。

- Header=X-Request-Id, \d+
  • 基于Host的断言工厂

HostRoutePredicateFactory:接收一个参数,主机名模式,判断请求的Host是否满足匹配规则。设置匹配请求参数中Host参数的值。(支持?匹配一个字符,*匹配0个或多个字符,**匹配0个或多个目录)。

- Host=**.somehost.org,**.anotherhost.org
  • 基于Method请求方法的断言工厂

MethodRoutePredicateFactory:接收一个参数,判断请求类型是否跟指定的类型匹配。设置允许的请求方式。

- Method=GET,POST
  • 基于Path请求路径的断言工厂

PathRoutePredicateFactory:接收一个参数,判断请求的URI部分是否满足路径规则。

spring:
  cloud:
    gateway:
      routes:
      - id: path_route
        uri: https://example.org
        predicates:
        - Path=/red/{segment},/blue/{segment}
  • 基于Query请求参数的断言工厂

QueryRoutePredicateFactory:接收两个参数,请求param和正则表达式,判断请求参数是否具有指定名称且值与正则表达式匹配。设置必须包含的参数名。

spring:
  cloud:
    gateway:
      routes:
      - id: query_route
        uri: https://example.org
        predicates:
        - Query=green
  • 基于路由权重的断言工厂

WeightRoutePredicateFactory:接收一个[组名,权重],然后对于同一个组内的路由按照权重转发。设置负载均衡中权重。同一组中URI进行负载均衡。(语法:Weight=组名,负责均衡权重),默认条件是标准轮询。分组名称必须相同。

spring:
  cloud:
    gateway:
      routes:
      - id: weight_high
        uri: https://weighthigh.org
        predicates:
        - Weight=group1, 8
      - id: weight_low
        uri: https://weightlow.org
        predicates:
        - Weight=group1, 2
自定义路由断言工厂

自定义路由断言工厂需要继承AbstractRoutePredicateFactory类,重写apply方法的逻辑,在apply方法中可以通过exchange.getRequest()拿到ServletHttpRequest对象,从而可以获取到请求的参数、请求方式、请求头等信息。

  1. 必须Spring组件Bean
  2. 类必须加上RoutePredicateFactory作为结尾
  3. 必须继承AbstractRoutePredicateFactory类
  4. 必须在类中实现一个静态内部类,声明属性来接收配置文件中对应的断言信息
  5. 需要结合shortcutFieldOrder进行绑定
  6. 通过apply方法进行逻辑判断,true就是匹配成功,false表示匹配失败
  7. 泛型为声明的静态内部类
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.gateway.handler.predicate.AbstractRoutePredicateFactory;
import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;

@Slf4j
@Component
public class CheckAuthRoutePredicateFactory extends AbstractRoutePredicateFactory<CheckAuthRoutePredicateFactory.Config> {
    // 构造函数
    public CheckAuthRoutePredicateFactory() {
        super(CheckAuthRoutePredicateFactory.Config.class);
    }

    // 接收的参数需要结合shortcutFieldOrder进行绑定
    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("name");
    }

    @Override
    public Predicate<ServerWebExchange> apply(Config config) {
        return new GatewayPredicate() {
            @Override
            public boolean test(ServerWebExchange serverWebExchange) {
                String name = config.getName();
                if (Objects.equals(name, "zhangsan")) {
                    return true;
                }
                return false;
            }
        };
    }

    // 用于接收配置文件中断言的信息
    @Validated
    public static class Config {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

过滤器工厂(GatewayFilter Factories)配置

在路由转发到代理服务之前和代理服务返回结果之后额外做的事情。Filter执行了说明断言条件通过了。

在Spring Cloud Gateway的路由中Filter分为:

内置Filter工厂,都是org.springframework.cloud.gateway.filter.GatewayFilter实现类

  • 自定义GlobalFilter,所有的路由都会运行
  • 自定义GatewayFilter,在特定的路由上运行

Gateway内置了很多的过滤器工厂,我们通过一些过滤器工厂可以进行一些业务逻辑处理器,比如添加、剔除响应头,添加、去除参数等。

https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#gatewayfilter-factories

内置Filter工厂
过滤器工厂作用参数
AddRequestHeader为原始请求添加HeaderHeader的名称及值,参数和值之间使用逗号分隔
AddRequestParameter为原始请求添加请求参数添加请求表单参数,多个参数需要有多个过滤器
AddResponseHeader添加响应头参数和值之间使用逗号分隔
DedupeResponseHeader剔除响应头中重复的值DedupeResponseHeader=响应头参数,策略(RETAIN_FIRST:默认值,保留第一个;RETAIN_LAST:保留最后一个;RETAIN_UNIQUE:保留唯一的)
CircuitBreaker实现熔断时使用支持Resilience4J
FallbackHeaders为FallbackUri的请求头中添加具体的异常信息,降级的异常信息Header的名称
PrefixPath为满足条件的原始请求路径添加前缀前缀路径
PreserveHostHeader为请求添加一个preserveHostHeader=true的属性,路油过滤器会检查该属性以决定是否要发送原始Host
RequestRateLimiter限流过滤器,对于请求限流,限流算法为令牌桶参考文档
RedirectTo将原始请求重定向到指定的URL有两个参数,status(http状态码)和url(重定向url)。其中status应该是300系列重定向状态码
RemoveHopByHopHeadersFilter为原始请求删除IETF组织规定的一系列Header默认就会启用,可以通过配置指定近删除哪些Header
RemoveRequestHeader为原始请求删除某个HeaderHeader名称
RemoveResponseHeader为原始响应删除某个HeaderHeader名称
RewritePath重写原始的请求路径原始路径正则表达式以及重写后路径的正则表达式
RewriteResponseHeader重写原始响应中的某个HeaderHeader名称,值的正则表达式,重写后的值
SaveSession在转发请求之前,强制执行WebSession::save操作
SecureHeaders为原始响应添加一系列起安全作用的响应头无,支持修改这些安全响应头的值
SetPath修改原始的请求路径,功能与StripPrefix类似,语法更贴近restful(SetPath=/{segment})修改后的路径
SetResponseHeader修改原始响应中某个Header的值Header名称,修改后的值
SetStatus修改原始响应的状态码Http状态码,可以是数字,也可以是字符串
StripPrefix用于截断原始请求的路径,跳过路由URI中前几段后发送给下游使用数字表示要截断的路径的数量,(StripPrefix=1,跳过第一段URI )
Retry针对不同的响应进行重试retries、statuses、methods、senes
RequestSize设置允许接收最大请求包的大小。如果请求包大小超过设置的值,则返回413 Payload Too Large请求包大小,单位为字节,默认值为5M
ModifyRequestBody在转发请求之前修改原始请求体内容修改后的请求体内容
ModifyResponseBody修改原始响应体的内容修改后的响应体内容

AddRequestHeader示例,在order-service的Controller中添加hello方法

    /**
     * 通过@RequestHeader注解来接收请求头参数
     *
     * @param color X-Request-color请求头参数的值
     * @return X-Request-color请求头参数的值
     */
    @GetMapping("/hello")
    public String hello(@RequestHeader("X-Request-color") String color) {
        log.info("请求头X-Request-color的值为:{}", color);
        return color;
    }

网关配置如下:

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # Gateway配置
    gateway:
      # 路由规则
      routes:
        - id: order_route # 路由的唯一标识,此示例理由到订单服务
          uri: lb://order-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡器
          # 断言规则,用于路由规则的匹配
          predicates:
            - Path=/**
          filters:
            - AddRequestHeader=X-Request-color, red # 添加请求头

PrefixPath示例:

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # Gateway配置
    gateway:
      # 路由规则
      routes:
        - id: order_route # 路由的唯一标识,此示例理由到订单服务
          uri: lb://order-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡器
          # 断言规则,用于路由规则的匹配
          predicates:
            - Path=/**
          filters:
            - AddRequestHeader=X-Request-color, red # 添加请求头
            - PrefixPath=/myorder # 添加前缀,对应微服务需要配置context-path
自定义Filter工厂

继承AbstractNameValueGatewayFilterFactory且我们的自定义名称必须要以GatewayFilterFactory结尾并交给Spring管理。

server:
  port: 8088
spring:
  application:
    name: api-gateway
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    # Gateway配置
    gateway:
      # 路由规则
      routes:
        - id: order_route # 路由的唯一标识,此示例理由到订单服务
          uri: lb://order-service # lb指的是从nacos中按照名称获取微服务,并遵循负载均衡器
          # 断言规则,用于路由规则的匹配
          predicates:
            - Path=/**
          filters:
            - PrefixPath=/myorder
            - CheckAuth=zhangsan

自定义CheckAuthGatewayFilterFactory:

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.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

import java.util.Arrays;
import java.util.List;
import java.util.Objects;

@Component
public class CheckAuthGatewayFilterFactory
        extends AbstractGatewayFilterFactory<CheckAuthGatewayFilterFactory.Config> {

    public CheckAuthGatewayFilterFactory() {
        super(CheckAuthGatewayFilterFactory.Config.class);
    }

    @Override
    public List<String> shortcutFieldOrder() {
        return Arrays.asList("name");
    }

    /**
     * 最好使用断言,name必须为zhangsan才能正常访问,如果没有带参数也可以正常访问
     */
    @Override
    public GatewayFilter apply(Config config) {
        return new GatewayFilter() {
            @Override
            public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
                // 获取name参数
                String name = exchange.getRequest().getQueryParams().getFirst("name");
                if (StringUtils.hasText(name)) {
                    if (Objects.equals(name, config.getName())) {
                        // 正常访问
                        return chain.filter(exchange);
                    } else {
                        // 如果不等于zhangsan就失败返回404
                        exchange.getResponse().setStatusCode(HttpStatus.NOT_FOUND);
                        return exchange.getResponse().setComplete();
                    }
                }
                // 正常请求
                return chain.filter(exchange);
            }
        };
    }

    @Validated
    public static class Config {
        private String name;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }

}
全局过滤器(Global Filters)配置

在这里插入图片描述

局部过滤器和全局过滤器:

局部过滤器针对某个路由生效,需要在路由中配置。全局过滤器针对所有路由生效,一旦定义就会投入使用。

自定义全局过滤器:

需要实现GlobalFilter接口。

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

/**
 * 打印记录请求路径
 */
@Component
@Slf4j
public class LogFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        log.info("请求路径:{}", exchange.getRequest().getPath().value());
        return chain.filter(exchange);
    }
}

Reactor Netty 访问日志

要启用Reactor Netty 访问日志,请设置 -Dreactor.netty.http.server.accessLogEnabled=true 它必须是Java系统属性(环境变量),而不是Spring Boot 属性

在logback.xml中配置到打印文件

<appender name="accessLog" class="ch.qos.logback.core.FileAppender">
    <file>access_log.log</file>
    <encoder>
        <pattern>%msg%n</pattern>
    </encoder>
</appender>
<appender name="async" class="ch.qos.logback.classic.AsyncAppender">
    <appender-ref ref="accessLog"/>
</appender>
<logger name="reactor.netty.http.server.AccessLog" level="INFO" additivity="false">
    <appender-ref ref="async"/>
</logger>

Gateway跨域配置(CORS Configuration)

参考文档:https://docs.spring.io/spring-cloud-gateway/docs/current/reference/html/#cors-configuration

解决CORS policy:no 'Access-Control-Allow-Origin' header is present on the requested resource问题。

通过yml配置的方式:

spring:
  cloud:
    gateway:
      globalcors:
        cors-configurations:
          '[/**]': # 允许跨域访问的资源
            allowedOrigins: "https://docs.spring.io" # 跨域允许的来源
            allowedMethods:
            - GET
            - POST
            - PUT
            - DELETE

通过Java配置方式

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.reactive.CorsWebFilter;
import org.springframework.web.cors.reactive.UrlBasedCorsConfigurationSource;
import org.springframework.web.util.pattern.PathPatternParser;

@Configuration
public class CorsConfig {

    @Bean
    public CorsWebFilter corsWebFilter() {
        CorsConfiguration config = new CorsConfiguration();
        // 允许的Method
        config.addAllowedMethod("*");
        // 允许的来源
        config.addAllowedOrigin("*");
        // 允许的请求头参数
        config.addAllowedHeader("*");
        // WebFlux,允许访问的资源
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(new PathPatternParser());
        source.registerCorsConfiguration("/**", config);
        return new CorsWebFilter(source);
    }
}

Gateway限流降级

常见的限流算法

  • 计数器算法

以QPS(每秒查询率)为100举例:从第一个请求开始计时,当达到100以后,其它的请求都拒绝。如果1秒钟前200ms的请求数量已经达到了100,后面800ms中多少次的请求都被拒绝了,这种情况称为『突刺现象』。

  • 漏桶算法——可以解决突刺现象

和生活中漏桶一样,有一个水桶,下面有一个『漏眼』往出漏水,不管桶里有多少水,漏水的速率都是一样的。但是既然是一个桶,桶里装的水都是有上限的。当达到了上限,新进来的水就装不了了(主要出现在突然倒进来大量水的情况)。

  • 令牌桶算法

令牌桶算法可以说是对漏桶算法的一种改进。

在桶中放令牌,请求获取令牌后才能继续执行。如果桶中没有令牌,请求可以选择进行等待或者直接拒绝。由于桶中令牌是按照一定速率放置的,所以可以一定程度解决突发访问。如果桶中令牌最多有100个,QPS最大为100。

Gateway整合Sentinel实现限流(推荐)

网关作为内部系统外的一层屏障,对内起到一定的保护作用,限流便是其中之一。网关层的限流可以简单地针对不同路由进行限流,也可以针对业务的接口进行限流,或者根据接口的特征分组限流。

参考文档:https://sentinelguard.io/zh-cn/docs/api-gateway-flow-control.html

添加依赖:

        <!-- Sentinel整合Gateway-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
        </dependency>
        <!-- Sentinel依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
        </dependency>

添加配置:

    # 配置Sentinel
    sentinel:
      transport:
        dashboard: 127.0.0.1:9000
使用 Gateway 中的RequestRateLimiter实现限流

使用 Gateway 中的RequestRateLimiter实现限流,RequestRateLimiter是基于Redis和Lua脚本实现的令牌桶算法。

使用Gateway实现服务降级

Spring Cloud Gateway可以使用Hystrix实现服务降级功能。

当Gateway进行路由转发时,如果发现下游服务连接超时允许进行服务降级。

实现原理:当连接超时时,使用Gateway自己的一个降级接口返回托底数据,保证程序继续运行。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值