关于SpringCloud的知识点总结(基础版)

Eureka:

1. 注册中心

注册中心与生产者,消费者之间的原理图:

  • Eureka:就是服务注册中心(可以是一个集群),对外暴露自己的地址

  • 提供者:启动后向Eureka注册自己信息(地址,提供什么服务)

  • 消费者:向Eureka订阅服务,Eureka会将对应服务的所有提供者地址列表发送给消费者,并且定期更新

  • 心跳(续约):提供者定期通过http方式向Eureka刷新自己的状态

2. 服务端:
2.1 依赖:
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
        <spring-cloud.version>Finchley.SR2</spring-cloud.version>
    </properties>
    
    <!-- SpringCloud依赖管理 -->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
        </dependency>
    </dependencies>
2.2配置文件:
server:
  port: 10087 # 端口
spring:
  application:
    name: eureka-server # 应用名称,会在Eureka中显示
eureka:
  client:
    service-url: # EurekaServer的地址,现在是自己的地址,如果是集群,需要加上其它Server的地址。
      defaultZone: http://127.0.0.1:${server.port}/eureka
      
  # 不注册自己
  # register-with-eureka: false
  # 不拉取服务
  # fetch-registry: false

(1)eureka.client.registerWithEureka :表示是否将自己注册到Eureka Server,默认为true。由于当前这个应用就是Eureka Server,故而设为false。

(2)eureka.client.fetchRegistry :表示是否从Eureka Server获取注册信息,默认为true。因为这是一个单点的Eureka Server,不需要同步其他的Eureka Server节点的数据,故而设为false。  

2.3 修改引导类,在类上添加@EnableEurekaServer注解:

@SpringBootApplication
@EnableEurekaServer // 声明当前springboot应用是一个eureka服务中心
public class YhEurekaApplication {

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

3.注册到Eureka:

概念:注册服务,就是在服务上添加Eureka的客户端依赖,客户端代码会自动把服务注册到EurekaServer中。

其中还有一个重要的知识点,暂时先不讲:RestTemplate

流程:

  1. 在pom.xml中,添加springcloud的相关依赖。

  2. 在application.yml中,添加springcloud的相关依赖。

  3. 在引导类上添加注解,把服务注入到eureka注册中心。

3.1 依赖 
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.0.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <java.version>1.8</java.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Finchley.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>1.3.2</version>
        </dependency>
<--  上面是springboot的基本依赖  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
3.2 配置文件
server:
  port: 8081
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/lxw
    username: root
    password: root
    driverClassName: com.mysql.jdbc.Driver
  application:
    name: service-provider # 应用名称,注册到eureka后的服务名称
mybatis:
  type-aliases-package: cn.yh.service.pojo
eureka:
  client:
    service-url: # EurekaServer地址
      defaultZone: http://127.0.0.1:10087/eureka
3.3 修改引导类

通过添加@EnableDiscoveryClient来开启Eureka客户端功能,@EnableDiscoveryClient该注解是netflix公司提供的,后期推荐使用springcloud提供的注解@EnableDiscoveryClient,功能相同。

@SpringBootApplication
@EnableDiscoveryClient
public class YhServiceProviderApplication {

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

4. 基本架构

Eureka架构中的三个核心角色:

  • 服务注册中心

    Eureka的服务端应用,提供服务注册和发现功能,就是刚刚我们建立的yh-eureka。

  • 服务提供者

    提供服务的应用,可以是SpringBoot应用,也可以是其它任意技术实现,只要对外提供的是Rest风格服务即可。本例中就是我们实现的yh-service-provider。

  • 服务消费者

    消费应用从注册中心获取服务列表,从而得知每个服务方的信息,知道去哪里调用服务方。本例中就是我们实现的yh-service-consumer。

5. 高可用的Eureka Server

        Eureka Server即服务的注册中心,在刚才的案例中,我们只有一个EurekaServer,事实上EurekaServer也可以是一个集群,形成高可用的Eureka中心。

5.1 服务同步

        多个Eureka Server之间也会互相注册为服务,当服务提供者注册到Eureka Server集群中的某个节点时,该节点会把服务的信息同步给集群中的每个节点,从而实现数据同步。因此,无论客户端访问到Eureka Server集群中的任意一个节点,都可以获取到完整的服务列表信息。

5.2 动手搭建高可用的EurekaServer

        操作流程:

                1. 启动第一个eurekaServer,我们修改原来的EurekaServer配置中的端口号,

                2. 复制一个启动器,然后在启动服务,就会有两个端口的客户端(都是生产者/消费者)

5.3客户端注册服务到集群

        因为EurekaServer不止一个,因此注册服务的时候,service-url参数需要变化:

eureka:
  client:
    service-url: # EurekaServer地址,多个地址以','隔开
      defaultZone: http://127.0.0.1:10086/eureka,http://127.0.0.1:10087/eureka
5.3 服务提供者

        服务提供者要向EurekaServer注册服务,并且完成服务续约等工作。

5.3.1 服务注册

        服务提供者在启动时,会检测配置属性中的:eureka.client.register-with-eureka=true参数是否正确,事实上默认就是true。如果值确实为true,则会向EurekaServer发起一个Rest请求,并携带自己的元数据信息,Eureka Server会把这些信息保存到一个双层Map结构中。

Map<serviceId,Map<服务实例名,实例对象instance>>

  • 第一层Map的Key就是服务id,一般是配置中的spring.application.name属性

  • 第二层Map的key是服务的实例id。一般host+ serviceId + port,例如:locahost:service-provider:8081

  • 值则是服务的实例对象,也就是说一个服务,可以同时启动多个不同实例,形成集群。

5.3.2 服务续约

        在注册服务完成以后,服务提供者会维持一个心跳(定时向EurekaServer发起Rest请求),告诉EurekaServer:“我还活着”。这个我们称为服务的续约(renew);

        有两个重要参数可以修改服务续约的行为:

eureka:
  instance:
    lease-expiration-duration-in-seconds: 90
    lease-renewal-interval-in-seconds: 30
  • lease-renewal-interval-in-seconds:服务续约(renew)的间隔,默认为30秒

  • lease-expiration-duration-in-seconds:服务失效时间,默认值90秒

也就是说,默认情况下每个30秒服务会向注册中心发送一次心跳,证明自己还活着。如果超过90秒没有发送心跳,EurekaServer就会认为该服务宕机,会从服务列表中移除,这两个值在生产环境不要修改,默认即可。

但是在开发时,这个值有点太长了,经常我们关掉一个服务,会发现Eureka依然认为服务在活着。所以我们在开发阶段可以适当调小。

eureka:
  instance:
    lease-expiration-duration-in-seconds: 10 # 10秒即过期
    lease-renewal-interval-in-seconds: 5 # 5秒一次心跳
5.4 服务消费者

获取服务列表

        当服务消费者启动时,会检测eureka.client.fetch-registry=true参数的值,如果为true,则会拉取Eureka Server服务的列表只读备份,然后缓存在本地。并且每隔30秒会重新获取并更新数据。我们可以通过下面的参数来修改:

eureka:
  client: 
    registry-fetch-interval-seconds: 5  #默认每隔30秒会从注册中心服务列表中重新拉取一次

生产环境中,我们不需要修改这个值。

但是为了开发环境下,能够快速得到服务的最新状态,我们可以将其设置小一点。

6. 失效剔除和自我保护

6.1 服务下线

        当服务进行正常关闭操作时,它会触发一个服务下线的REST请求给Eureka Server,告诉服务注册中心:“我要下线了”。服务中心接受到请求之后,将该服务置为下线状态。

6.2 失效剔除

        有些时候,我们的服务提供方并不一定会正常下线,可能因为内存溢出、网络故障等原因导致服务无法正常工作。Eureka Server需要将这样的服务剔除出服务列表。因此它会开启一个定时任务,每隔60秒对所有失效的服务(超过90秒未响应)进行剔除。

        可以通过eureka.server.eviction-interval-timer-in-ms参数对其进行修改,单位是毫秒,生产环境不要修改。

        这个会对我们开发带来极大的不便,你对服务重启,隔了60秒Eureka才反应过来。开发阶段可以适当调整,比如:10秒

6.3 自我保护

        我们关停一个服务,就会在Eureka面板看到一条警告:

         这是触发了Eureka的自我保护机制。当一个服务未按时进行心跳续约时,Eureka会统计最近15分钟心跳失败的服务实例的比例是否超过了85%。在生产环境下,因为网络延迟等原因,心跳失败实例的比例很有可能超标,但是此时就把服务剔除列表并不妥当,因为服务可能没有宕机。Eureka就会把当前实例的注册信息保护起来,不予剔除。生产环境下这很有效,保证了大多数服务依然可用。

        但是这给我们的开发带来了麻烦, 因此开发阶段我们都会关闭自我保护模式:(yh-eureka)

eureka:
  server:
    enable-self-preservation: false # 关闭自我保护模式(缺省为打开)
    eviction-interval-timer-in-ms: 1000 # 扫描失效服务的间隔时间(缺省为60*1000ms)

负载均衡

1. 概念:

        Ribbon是Netflix发布的负载均衡器,它有助于控制HTTP和TCP客户端的行为。为Ribbon配置服务提供者提供地址列表后,Ribbon就可基于某种负载均衡算法,自助地帮助服务消费者去请求。Ribbon默认为我们提供了很多的负载均衡算法,例如轮询,随机等。当然,我们也可为Ribbon实现自定义的负载均衡算法。

2. 开启负载均衡

因为Eureka中已经集成了Ribbon,所以我们无需引入新的依赖,直接修改代码。

修改yh-service-consumer的引导类,在RestTemplate的配置方法上添加@LoadBalanced注解:

@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}

3. 修改调用方式,不再手动获取ip和端口,而是直接通过服务名称调用:

@Controller
@RequestMapping("consumer/user")
public class UserController {

    @Autowired
    private RestTemplate restTemplate;

    //@Autowired
    //private DiscoveryClient discoveryClient; // 注入discoveryClient,通过该客户端获取服务列表

    @GetMapping
    @ResponseBody
    public User queryUserById(@RequestParam("id") Long id){
        // 通过client获取服务提供方的服务列表,这里我们只有一个
        // ServiceInstance instance = discoveryClient.getInstances("service-provider").get(0);
        String baseUrl = "http://service-provider/user/" + id;
        User user = this.restTemplate.getForObject(baseUrl, User.class);
        return user;
    }

}

Hystrix

1. 概念:

1. 原理:

Hystrix为每个依赖服务调用分配一个小的线程池,如果线程池已满调用将被立即拒绝,默认不采用排队.加速失败判定时间。
用户的请求将不再直接访问服务,而是通过线程池中的空闲线程来访问服务,如果线程池已满,或者请求超时,则会进行降级处理。
服务降级虽然会导致请求失败,但是不会导致阻塞,而且最多会影响这个依赖服务对应的线程池中的资源,对其它服务没有响应。

2.触发Hystix服务降级的情况:
- 线程池已满
- 请求超时
3. 依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
4. 注解:

注解:@EnableCircuitBreaker   --->@SpringCloudApplication=@SpringBootApplication+@EnableDiscoveryClient+@EnableCircuitBreaker

5. 操作

1. 实现单个降级方法:@HystixCommond(fallbackMethod = "降级要执行的方法名")注到要降级的方法上,然后编写要降级的方法,参数和返回值要一致,方法名没有要求。
2. 
    2.1 实现多个降级方法:在Controller类上添加@DefaultProperties(defaultFallback = "降级要执行的方法名") // 指定一个类的全局熔断方法
    2.2 接着在要降级的方法上添加 @HystixCommond注解,然后再编写要降级的方法,参数和返回值要一致,方法名没有要求

- @DefaultProperties(defaultFallback = "defaultFallBack"):在类上指明统一的失败降级方法
- @HystrixCommand:在方法上直接使用该注解,使用默认的降级方法。
- defaultFallback:默认降级方法,不用任何参数,以匹配更多方法,但是返回值一定一致

设置超时时间:请求在超过这个时间之后会报错,默认是1秒
我们可以通过`hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds`来设置Hystrix超时时间:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 6000 # 设置hystrix的超时时间为6000ms

2. 熔断

2.1 概念:

熔断器,也叫断路器,其英文单词为:Circuit Breaker         
熔断是降级下的一个小的模块,当至少访问20次,且有一般的概率出错时,会触发熔断,此时熔断处于断开状态,不论是正确的请求还是错误的请求都无法通过,
默认会休眠5秒,5秒之后,熔断会进入半开状态,此时会关闭一部分请求路径,如果请求正确,那么就可以通过,拿去数据,之后就可以进入关闭状态,所有正确的请求都可以通过。

2.2 熔断状态机3个状态:

可以通过抛异常让熔断进入打开状态
- Closed:关闭状态,所有请求都正常访问。
- Open:打开状态,所有请求都会被降级。Hystix会对请求情况计数,当一定时间内失败请求百分比达到阈值,则触发熔断,断路器会完全打开。默认失败比例的阈值是50%,请求次数最少不低于20次。
- Half Open:半开状态,open状态不是永久的,打开后会进入休眠时间(默认是5S)。随后断路器会自动进入半开状态。此时会释放部分请求通过,若这些请求都是健康的,
则会完全关闭断路器,否则继续保持打开,再次进行休眠计时

- requestVolumeThreshold:触发熔断的最小请求次数,默认20
- errorThresholdPercentage:触发熔断的失败请求最小占比,默认50%
- sleepWindowInMilliseconds:休眠时长,默认是5000毫秒

Feign

1. 概念

Feign可以把Rest的请求进行隐藏,伪装成类似SpringMVC的Controller一样。你不用再自己拼接url,拼接参数等等操作,一切都交给Feign去做。

2. 操作流程

0. 在启动器application类中添加@EnableFeignClients注解
1. 创建一个feign在feign目录下的UserClient接口,接着添加@FeignClient(value = "service-provider")注解,value是生产者的服务名。
2. 定义抽象方法,返回值类型,参数类型与生产者的方法一致,@PathVariable注解也不能省略,方法名可以任意,RequestMapping与生产者的controller的uri一致

@FeignClient(value = "service-provider") // 标注该类是一个feign接口
public interface UserClient {
    @GetMapping("user/{id}")
    User queryById(@PathVariable("id") Long id);
}

- 首先这是一个接口,Feign会通过动态代理,帮我们生成实现类。这点跟mybatis的mapper很像
- `@FeignClient`,声明这是一个Feign客户端,类似`@Mapper`注解。同时通过`value`属性指定服务名称
- 接口中的定义方法,完全采用SpringMVC的注解,Feign会根据注解帮我们生成URL,并访问获取结果

然后在Controller里面

    @Autowired
    private UserClient userClient;
	userClient.queryUserById(id);

3. 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

Feign默认也有对Hystrix的集成:
只不过,默认情况下是关闭的。我们需要通过下面的参数来开启:(在Yh-service-consumer工程添加配置内容)

feign:
  hystrix:
    enabled: true # 开启Feign的熔断功能
	这个时候就不可以用@SpringCloudApplication注解了,因为@SpringCloudApplication包含了@EnableCircuitBreaker,这个注解的意思就是开启熔断

首先,我们要定义一个类UserClientFallback,实现刚才编写的UserClient,作为fallback的处理类

@Component
public class UserClientFallback implements UserClient {

    @Override
    public User queryById(Long id) {
        User user = new User();
        user.setUserName("服务器繁忙,请稍后再试!");
        return user;
    }
}

然后在UserFeignClient中,指定刚才编写的实现类

@FeignClient(value = "service-provider", fallback = UserClientFallback.class) // 标注该类是一个feign接口
public interface UserClient {

    @GetMapping("user/{id}")
    User queryUserById(@PathVariable("id") Long id);
}

Zuul网关

1. 概念:

        Zuul是Netflix开源的微服务网关,他可以和Eureka,Ribbon,Hystrix等组件配合使用。Zuul的核心是一系列的过滤器。

这些过滤器可以完成一下功能:

  • 身份认证与安全:识别每个资源的验证要求,并拒绝那些与要求不符的请求。
  • 审查与监控:在边缘位置追踪有意义的数据和统计结果,从而带来精确的生产视图。
  • 动态路由:动态地将请求路由到不同的后端集群。
  • 压力测试:逐渐增加指向集群的流量,以了解性能。
  • 负载分配:为每一种负载类型分配对应容量,并启用超出限定值的请求。
  • 静态相应处理:在边缘位置直接建立部分响应,从而避免其转发到内部集群。
  • 多区域弹性:跨越AWS Region进行请求路由,旨在实现ELB(ElasticSearch Load Balanceing) 使用多样化,以及让系统的边缘更贴近系统的使用者。

2. 依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

3. 配置文件

server:
  port: 10010 #服务端口
spring:
  application:
    name: api-gateway #指定服务名

4. 添加注解

给启动类添加@EnableZuulProxy注解,表示开启网关功能

5. 配置路由规则:

第一种方式:
zuul:
  routes:
    service-provider: # 这里是路由id,随意写
      path: /service-provider/** # 这里是映射路径
      url: http://127.0.0.1:8081 # 映射路径对应的实际url地址
第二种方式
zuul:
  routes:
    service-provider: # 这里是路由id,随意写
      path: /service-provider/**
      url: http://127.0.0.1:8081
      service-id: service-provider #指定服务名称

第三种方式:推荐使用
zuul:
  routes:
    service-provider: /service-provider/**
    service-consumer: /service-consumer/**
  prefix: /api

第四种方式:
不配置,即默认配置,默认情况,每个微服务的服务名,即是访问映射地址

6. 过滤器

        Zuul作为网关的其中一个重要功能,就是实现请求的鉴权。而这个动作我们往往是通过Zuul提供的过滤器来实现的     

6.1 ZuulFilter抽象类

ZuulFilter是过滤器的顶级父类。在这里我们看一下其中定义的4个最重要的方法:

public abstract ZuulFilter implements IZuulFilter{
    
    abstract public String filterType();

    abstract public int filterOrder();
    
    boolean shouldFilter();// 来自IZuulFilter

    Object run() throws ZuulException;// IZuulFilter
}

    

  • shouldFilter:返回一个Boolean值,判断该过滤器是否需要执行。返回true执行,返回false不执行。

  • run:过滤器的具体业务逻辑。

  • filterType:返回字符串,代表过滤器的类型。包含以下4种:

    • pre:请求在被路由之前执行

    • route:在路由请求时调用

    • post:在route和errror过滤器之后调用

    • error:处理请求时发生错误调用

  • filterOrder:通过返回的int值来定义过滤器的执行顺序,数字越小优先级越高。

6.2 过滤器执行生命周期

6.2.1 正常流程

  • 请求到达首先会经过pre类型过滤器,而后到达route类型,进行路由,请求就到达真正的服务提供者,执行请求,返回结果后,会到达post过滤器。而后返回响应。

6.2.2 异常流程

  • 整个过程中,pre或者route过滤器出现异常,都会直接进入error过滤器,在error处理完毕后,会将请求交给POST过滤器,最后返回给用户。

  • 如果是error过滤器自己出现异常,最终也会进入POST过滤器,将最终结果返回给请求客户端。

  • 如果是POST过滤器出现异常,会跳转到error过滤器,但是与pre和route不同的是,请求不会再到达POST过滤器了。

6.3 使用场景
  • 请求鉴权:一般放在pre类型,如果发现没有访问权限,直接就拦截了

  • 异常处理:一般会在error类型和post类型过滤器中结合来处理。

  • 服务调用时长统计:pre和post结合使用。

6.4 自定义过滤器
6.4.1 定义过滤器类

        定义过滤器类,继承ZuulFilter抽象类,并重写抽象方法。

@Component
public class LoginFilter extends ZuulFilter {
    /**
     * 过滤器类型,前置过滤器
     * @return
     */
    @Override
    public String filterType() {
        return "pre";
    }

    /**
     * 过滤器的执行顺序
     * @return
     */
    @Override
    public int filterOrder() {
        return 1;
    }

    /**
     * 该过滤器是否生效
     * @return
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 登陆校验逻辑
     * @return
     * @throws ZuulException
     */
    @Override
    public Object run() throws ZuulException {
        // 获取zuul提供的上下文对象
        RequestContext context = RequestContext.getCurrentContext();
        // 从上下文对象中获取请求对象
        HttpServletRequest request = context.getRequest();
        // 获取token信息
        String token = request.getParameter("access-token");
        // 判断
        if (StringUtils.isBlank(token)) {
            // 过滤该请求,不对其进行路由
            context.setSendZuulResponse(false);
            // 设置响应状态码,401
            context.setResponseStatusCode(HttpStatus.SC_UNAUTHORIZED);
            // 设置响应信息
            context.setResponseBody("{\"status\":\"401\", \"text\":\"request error!\"}");
        }
        // 校验通过,把登陆信息放入上下文信息,继续向后执行
        context.set("token", token);
        return null;
    }
}

7. 负载均衡和熔断

        Zuul中默认就已经集成了Ribbon负载均衡和Hystix熔断机制。但是所有的超时策略都是走的默认值,比如熔断超时时间只有1S,很容易就触发了。因此建议我们手动进行配置:

hystrix:
  command:
    default:
      execution:
        isolation:
          thread:
            timeoutInMilliseconds: 2000 # 设置hystrix的超时时间为6000ms

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值