JAVA学习之SpringCloud

1、需要有一个注册中心,用来暴露服务

首先添加依赖,代码入下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.13.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-eureka</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-cloud-eureka</name>
<description>eureka注册中心</description>

<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>Camden.SR7</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

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

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

然后在启动类中增加注解,代表启动为注册中心,代码入下:

@SpringBootApplication
@EnableEurekaServer
public class EurekaApplication {

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

}

配置文件代码如下:

server:
  ## 该服务的端口号
  port: 8000

eureka:
  instance:
    ## 主机地址
    hostname: loaclhost
  client:
    ## 指示此实例是否应将其信息注册到eureka服务器以供其他服务发现,默认为false
    registerWithEureka: false
    ## 客户端是否获取eureka服务器注册表上的注册信息,默认为true
    fetchRegistry: false
    serviceUrl:
      ## 可用区域映射到与eureka服务器通信的完全限定URL列表。每个值可以是单个URL或逗号分隔的备用位置列表
      defaultZone: http://${eureka.instance.hostname}:${server.port}/eureka/

然后启动服务,访问http://localhost:8000, 就可以看到注册中心相关的信息了

2、我们增加一个生产者,用于提供服务的工程,命名为spring-cloud-provide

接着,增加依赖,代码入下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.13.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-provide</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-cloud-provide</name>
<description>provide生产者</description>

<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-services.version>1.5.0.RELEASE</spring-cloud-services.version>
    <spring-cloud.version>Edgware.RELEASE</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.springcloud.learning</groupId>
        <artifactId>spring-cloud-api</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Edgware.SR3</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

然后在启动类中增加注解,表示这是一个客户端,也就是我们俗称的服务,代码入下:

@SpringBootApplication
@EnableDiscoveryClient
public class ProvideServer {

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

}

然后,增加配置,代码如下:

server:
    port: 8001

spring:
    application:
        ## 这个服务的名称
        name: spring-cloud-provide

eureka:
    client:
        ## 是否暴露服务给注册中心
        register-with-eureka: true
        ## 是否从注册中心获取服务
        fetch-registry: false
        service-url:
            ## 此为注册中心的地址
            defaultZone: http://localhost:8000/eureka/

然后我们写一个功能controller,用来提供服务:

@RestController
public class ProvideController implements FeignService {

    @GetMapping("/test/{id}")
    public String testRest(@PathVariable String id) {
       return "rest: hello" + id;
    }

    @Override
    public String test(@PathVariable String id){
        return "spring-cloud-provide: hello" + id;
    }

    @GetMapping("/getway")
    public String testGetWay() {
        return "My name is spring-cloud-provide";
    }
}

先不管其他的东西,只看testRest这个方法,启动程序之后,我们发现注册中心中出现一个名字SPRING-CLOUD-PROVIDE的服务。

3、接下来我们去使用SPRING-CLOUD-PROVIDE这个服务提供的功能,testRest这个方法

首先,新建一个消费者工程spring-cloud-comsume来模拟服务使用者,加入依赖,代码如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.13.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-comsume</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-cloud-comsume</name>
<description>rest消费者</description>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR7</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-ribbon</artifactId>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

然后在启动类中去加入配置,代码如下:

@SpringBootApplication
@EnableDiscoveryClient
public class ComsumeServer {

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

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

此时我们发现,消费者的启动注解和生产者的启动注解是一样的,那么说明生产者可以提供服务,也可以注册服务,而消费者同理,可调用别人也可被别人调用
而下边的restTemplate是指我们需要使用一个叫rest的模板或者实例去注册中心注册服务,其实这个我觉的就类似与spring在xml中通过bean来管理一个类的实例一样。
loadBalanced这个注解是负载均衡的配置,后期再说

然后我们来看配置文件,代码如下:

spring:
    application:
        name: spring-cloud-comsume

server:
    port: 8002

eureka:
    client:
        serviceUrl:
            defaultZone: http://localhost:8000/eureka/

可以看到这个配置很简单,因为我们默认的配置就是只从注册中心注册服务,而不暴露服务,接下来我们看怎么去注册生产者提供的那个服务:

@RestController
public class ComsumeController {

    @Autowired
    private RestTemplate restTemplate;

    @GetMapping("/test/{id}")
    public String test(@PathVariable String id){
        return this.restTemplate.getForObject("http://spring-cloud-provide/test/" + id,String.class);
    }

    @GetMapping("/getway")
    public String testGetWay() {
        return "My name is spring-cloud-comsume";
    }

}

那么我们可以看到rest注册服务的方式就是这样子,通过restTemplante来去调用这个url,但是呢,我们一般都是接口啊,这个写起来不是太麻烦了,那么接下来我们就看怎么用接口来调用

4、我们再来创建一个使用接口注册服务的应用,给他起名字为spring-cloud-feign,从名字就能看出来,他的方式就是feign方式,首先我们看依赖,代码如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.1.4.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-feign</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-cloud-feign</name>
<description>feign消费者</description>

<properties>
    <java.version>1.8</java.version>
    <spring-cloud.version>Greenwich.SR1</spring-cloud.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-feign</artifactId>
        <version>1.4.4.RELEASE</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>com.springcloud.learning</groupId>
        <artifactId>spring-cloud-api</artifactId>
        <version>1.0-SNAPSHOT</version>
        <scope>compile</scope>
    </dependency>
</dependencies>

<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>

然后我们需要在启动类中加入对应的注解来说明这个工程是要干啥,代码如下:

@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class FeignApplication {

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

}

我们可以看到,这个服务比rest服务的启动类多了一个注解,多出来的这个注解就表示这个服务是一个feign服务

接着,我们来看配置文件,代码如下:

spring:
  application:
    name: spring-cloud-feign

server:
  port: 8003

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:8000/eureka/

配置文件依然是这么的简单

然后我们看怎么去调用生产者提供的那个接口,首先我们既然是调用接口,为了方便,肯定会有一个公用的api,也就是一个jar包,不然如果有参数类,那提供者和被提供者要写俩次了,增加了代码的冗余

5、那我们增加一个api工程,先看依赖,代码如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.13.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-api</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-cloud-api</name>
<description>Demo project for Spring Boot</description>

<properties>
    <java.version>1.8</java.version>
</properties>

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

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

从依赖可以看出,这只是个普通的springboot工程,而且不需要启动,其他类依赖了他他就会直接打成一个jar包以jar包的形式存在,所以不需要启动,不需要配置

然后我们在api中增加一个接口,代码如下:

/**
* 功能描述: 接口调用,注解的value是指调用哪一个服务,即服务在配置文件中的名字
* @author renyf24249
* @version $ID: FeignTestService.java, v  2019/4/24 17:25 Exp $
*/
@RequestMapping("/api")
public interface FeignService {

    /**
     * 功能描述: 通过spring mvc 注解来绑定服务提供的Rest接口
     * 此处的url是生产者提供的服务的url
     * @author renyf24249
     */
    @RequestMapping("/test/{id}")
    String test(@PathVariable("id") String id);
}

其中的接口上的/api好像没作用,尝试着去弄了一下也没发现有啥不同

然后我们往上翻,看生产者的controller类(我把代码拷了下来),这个类实现了这个接口,并实现了test这个方法,因为再接口中我们已经写明了url地址,所以这里也就不需要再写了,只需要实现就好了

@RestController
public class ProvideController implements FeignService {

    @GetMapping("/test/{id}")
    public String testRest(@PathVariable String id) {
       return "rest: hello" + id;
    }

    @Override
    public String test(@PathVariable String id){
        return "spring-cloud-provide: hello" + id;
    }

    @GetMapping("/getway")
    public String testGetWay() {
        return "My name is spring-cloud-provide";
    }
}

做完这一步之后我们提供服务这边就算是完成了,接下来我们要看怎么调用了,首先我们需要调用的地方写一个接口来继承api中的这个接口,并重写他的方法,代码如下:

/**
* 功能描述: 接口调用,注解的value是指调用哪一个服务,即服务在配置文件中的名字
* @author renyf24249
* @version $ID: FeignTestService.java, v  2019/4/24 17:25 Exp $
*/
@FeignClient("spring-cloud-provide")
public interface FeignTestService extends FeignService {

    /**
     * 功能描述: 通过spring mvc 注解来绑定服务提供的Rest接口
     * 此处的url是生产者提供的服务的url
     * @author renyf24249
     */
    @Override
    @RequestMapping("/test/{id}")
    String test(@PathVariable("id") String id);
}

其中@FeignClient(“spring-cloud-provide”)是指我们要通过feign调用哪一个服务,其他的不用解释了

然后我们看我们的controller怎么使用,代码如下:

/**
* @author renyf24249
* @version $ID: FeignTestController.java, v  2019/4/24 17:27 Exp $
*/
@RestController
public class FeignTestController {

    @Autowired private FeignTestService feignTestService;

    @RequestMapping(value = "/feign/{id}", method = RequestMethod.GET)
    public String helloConsumer(@PathVariable("id") String id){
        return  feignTestService.test(id);
    }

    @GetMapping("/getway")
    public String testGetWay() {
        return "My name is spring-cloud-feign";
    }
}

其实和我们正常的使用方式一样,自动注入接口的实例,然后调用方法,这样我们通过接口调用就完成了。

然后我们尝试一下api的接口中不写注解会怎么样… 经过验证,不写注解不可以,他还是通过url的方式来调用的,只不过封装了,更方便了,可以将api接口中类上的注解不写是可以的,但是方法上的注解不能不写,不过写在接口中或者写在实现中都是可以的。

那么这样子验证完是不是觉得那里不对劲,总是把所有的controller的url暴露出去是不是不好,毕竟还是要保密的,此时我们就引入了路由这个工程,路由就是给你统一管理转发的,所有的请求都去走路由然后由路由统一去转发到某个服务中,接下来我们就看怎么实现这个路由

6、我们新建一个作为路由的工程,命名为spring-cloud-getway,首先我们看依赖,代码如下:

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>1.5.13.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.springcloud.learning</groupId>
<artifactId>spring-cloud-getway</artifactId>
<version>1.0-SNAPSHOT</version>
<name>spring-cloud-getway</name>
<description>Zuul网关</description>

<properties>
    <java.version>1.8</java.version>
</properties>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter</artifactId>
    </dependency>
    <!-- 引入zuul依赖 , 它依赖了spring-boot-starter-actuator/spring-boot-starter-hystrix/spring-boot-starter-ribbon-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-zuul</artifactId>
    </dependency>
    <!-- 引入eureka依赖 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-eureka</artifactId>
    </dependency>
</dependencies>

<!-- 使用dependencyManagement进行版本管理 -->
<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Camden.SR6</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

接着我们再来看启动类,代码如下:

/**
* @author renyf24249
* @EnableZuulProxy 开启Zuul 的API网关服务功能
*
*/
@EnableZuulProxy
@SpringBootApplication
public class GetwayApplication {

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

    @Bean
    public AccessFilter accessFilter() {
        return new AccessFilter();
    }
}

先不管下边八个bean,红色的注解是指这是一个网关工程

然后我们再看路由规则是怎么样的,再看配置文件,代码如下:

server.port=8004
spring.application.name=spring-cloud-gateway

#增加路由规则的配置(普通路由配置)
#通过zuul.routes.<route>.path和zuul.routes.<route>.url进行配置
#<route>为路由的名字,可以任意指定,但是一组path和url的路由名要相同
#如下面的例子:所有满足/api-a/** 规则的访问都会被路由转发到//localhost:9001的地址
#也就是说,我们访问http://localhost:5555/api-a/hello的时候,API网关服务就会将该请#求路由到http://localhost:9001/hello提供的微服务接口上
zuul.routes.spring-cloud-provide.path=/spring-cloud-provide/**
zuul.routes.spring-cloud-provide.url=http://localhost:8001

zuul.routes.spring-cloud-comsume.path=/spring-cloud-comsume/**
zuul.routes.spring-cloud-comsume.url=http://localhost:8002

zuul.routes.spring-cloud-feign.path=/spring-cloud-feign/**
zuul.routes.spring-cloud-feign.service-id=spring-cloud-feign

## 如果不使用url的方式,而使用service-id的方式,就需要加入的eureka依赖,并加入注册中心的地址
eureka.client.service-url.defaultZone=http://localhost:8000/eureka

这个配置文件我是用的springboot另一种配置方式实现的,首先配置中关于provide和comsume这俩个服务的转发是通过path和url来定义的,即,如果你要访问的是/spring-cloud-provide/**等路径,我就给你转发到http://localhost:8001台服务器上,有点像nginx,下边feign的是通过path和service-id来实现的,即如果你请求的是/spring-cloud-feign/**等路径,我就给你转发到service-id对应的叫spring-cloud-feign这台服务上

此时我们就会想,那我们这个时候不是还是可以请求到所有暴露出来的服务的么?不要着急,接着我们来看上文中启动类中配置的那个bean,代码如下:

package com.springcloud.learning.filter;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.servlet.http.HttpServletRequest;

/**
* 功能描述: 继承ZuulFilter,并且实现其4个接口 用来进行请求过滤
* @author renyf24249
* @version $ID: AccessFilter.java, v  2019/4/25 09:46 Exp $
*/
public class AccessFilter extends ZuulFilter {

    private Logger logger = LoggerFactory.getLogger(AccessFilter.class);

    private static final String TOKEN = "token";

    /**
     * shouldFilter 判断该过滤器是否需要被执行
     * 这里直接返回true,表示该过滤器对所有请求都会生效。
     * 实际运用中我们可以利用该函数指定过滤器的有效范围
     */
    @Override
    public boolean shouldFilter() {
        return true;
    }

    /**
     * 过滤器的具体逻辑
     * 这里我们通过ctx.setSendZuulResponse(false)让zuul过来请求,不对其进行路由
     * 然后通过ctx.setResponseStatusCode(401)设置了返回的错误码
     */
    @Override
    public Object run() {
        RequestContext context = RequestContext.getCurrentContext();
        HttpServletRequest request = context.getRequest();
        Object accessToken = request.getParameter("accessToken");
        logger.info("send {} request to {}", request.getMethod(),request.getRequestURL().toString());
        if(!TOKEN.equals(accessToken)) {
            context.setSendZuulResponse(false);
            context.setResponseStatusCode(401);
        }
        return null;
    }

    /**
     * filterType 返回过滤器类型
     * 他决定了过滤器在请求的哪个生命周期中执行。这里定义为pre,代表会在请求被路由前执行。
     * pre: 请求执行之前filter
     * route: 处理请求,进行路由
     * post: 请求处理完成后执行的filter
     * error:出现错误时执行的filter
     */
    @Override
    public String filterType() {
        return "pre";
    }


    /**
     * filterOrder 返回过滤器的执行顺序
     * 当请求在一个阶段有多个过滤器是,需要根据该方法的返回值来依次执行
     */
    @Override
    public int filterOrder() {
        return 0;
    }

}

可以看到run方法中判断了这个请求中是否存在或token是否正确,这样就增加了一定的保密性

7、springcloud还增加了服务降级处理的方式,即如果某一个服务注册不成功时可以设置去请求另一个服务或者输出对应的信息,而不是导致服务停止

首先需要增加依赖,代码如下:

<!-- 引入hystrix 依赖 ,用来实现服务容错保护-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-hystrix</artifactId>
</dependency>

然后去要在启动类加入容错保护开启的注解@EnableCircuitBreaker,然后看具体怎么实现容错机制,代码如下:

/**
* @author renyf24249
* @version $ID: RestServiceImpl.java, v  2019/4/26 13:58 Exp $
*/
@Service
public class RestServiceImpl implements RestService {

    @Autowired
    private RestTemplate restTemplate;

    @Override
    @HystrixCommand(fallbackMethod = "errorMsg")
    public String test(String id){
        return this.restTemplate.getForObject("http://spring-cloud-provide/testRest/" + id,String.class);
    }

    public String errorMsg(String id) {
        return "生产者挂掉了!" + id;
    }
}

其中@HystrixCommand(fallbackMethod = “errorMsg”)这个注解就表示,如果发生服务注册不成功,则去执行errorMsg这个方法
注意:errorMsg必须与test方法的参数、参数类型、数量相同,不然就报找不到errorMsg这个方法

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值