Apollo 做配置中心

  Apollo(阿波罗)是携程框架部门研发的开源配置管理中心,能够集中化管理应用不同环境、不同集群的配置,配置修改后能够实时推送到应用端,并且具备规范的权限、流程治理等特性。

  官网:https://github.com/ctripcorp/apollo/wiki

  码云地址:https://gitee.com/nobodyiam/apollo

  关于架构以及设计参考码云地址。

========基于Quick Start安装包构建======

1.apollo服务器搭建

1.下载Quick Start安装包(补充,通过这种方式不支持增加环境,只有DEV环境)

通过网盘链接https://pan.baidu.com/s/1Ieelw6y3adECgktO0ea0Gg下载,提取码: 9wwe
下载到本地后,在本地解压apollo-quick-start.zip

2.解压后安装

(1)Apollo服务端共需要两个数据库:ApolloPortalDB和ApolloConfigDB,只需要导入数据库即可。

apollo-quick-start\sql 下面的两个SQL脚本:

 (2)配置数据库连接信息:修改demo.sh(从该脚本也可以看出只支持dev环境)

Apollo服务端需要知道如何连接到你前面创建的数据库,所以需要编辑demo.sh,修改ApolloPortalDB和ApolloConfigDB相关的数据库连接串信息。

补充:由于启动失败我还修改了eureka的启动时间:

(3)启动eureka服务,启动eureka服务7001端口。

(4)启动apollo服务

$ ./demo.sh start

启动后日志如下:

liqiang@root MINGW64 ~/Desktop/apollo-quick-start
$ ./demo.sh start
Windows new JAVA_HOME is: /c/PROGRA~1/Java8/JDK18~1.0_1
==== starting service ====
Service logging file is ./service/apollo-service.log
Started [14708]
Waiting for config service startup.................................
Config service started. You may visit http://localhost:8080 for service status now!
Waiting for admin service startup........
Admin service started
==== starting portal ====
Portal logging file is ./portal/apollo-portal.log
rm: cannot remove './portal/apollo-portal.jar': Device or resource busy
ln: failed to create hard link './portal/apollo-portal.jar': File exists
Started [13524]
Waiting for portal startup......................
Portal started. You can visit http://localhost:8070 now!

(5)访问8070端口后登陆,用户名apollo,密码admin后登录

 

(6)访问8080查看eureka服务:

 2.Springcloud项目中集成apollo

1.apollo中创建项目并发布个配置文件

1.新建项目

 

2.如下新增一个key

 

3.点击发布按钮进行发布

4.查看列表如下:

 1.IDEA新建项目获取上面配置

1.新建项目

 2.修改pom

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>cloud</artifactId>
        <groupId>cn.qz.cloud</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>cloud-config-apollo-3366</artifactId>

    <dependencies>
        <!--apollo-->
        <dependency>
            <groupId>com.ctrip.framework.apollo</groupId>
            <artifactId>apollo-client</artifactId>
            <version>1.1.0</version>
        </dependency>
        <!--引入自己抽取的工具包-->
        <dependency>
            <groupId>cn.qz.cloud</groupId>
            <artifactId>cloud-api-commons</artifactId>
            <version>${project.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--监控-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <!--热部署-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

3.修改yml

server:
  port: 3366

app:
  # 与apollo的admin管理界面添加的 appId 一致
  id: cloud-config-apollo
apollo:
  # meta的url
  meta: http://localhost:8080
  bootstrap:
    enabled: true
    eagerLoad:
      enabled: true

4.主启动类:

package cn.qz.cloud;

import com.ctrip.framework.apollo.spring.annotation.EnableApolloConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Author: qlq
 * @Description
 * @Date: 12:41 2020/10/31
 */
@SpringBootApplication
@EnableApolloConfig
public class ApolloConfigMain3366 {
    public static void main(String[] args) {
        SpringApplication.run(ApolloConfigMain3366.class, args);
    }
}

5.测试类:

package cn.qz.cloud.controller;

import cn.qz.cloud.utils.JSONResultUtil;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @Author: qlq
 * @Description
 * @Date: 12:43 2020/10/31
 */
@RestController
@RequestMapping("/config/apollo")
public class ConfigController {

    @Value("${config.info}")
    private String configInfo;

    @GetMapping("/getConfigInfo")
    public JSONResultUtil<String> getConfigInfo() {
        return JSONResultUtil.successWithData(configInfo);
    }
}

6.测试:

liqiang@root MINGW64 /e/IDEAWorkSpace/cloud (master)
$ curl http://localhost:3366//config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    81    0    81    0     0   1306      0 --:--:-- --:--:-- --:--:--  2612{"success":true,"code":"200","msg":"","data":"apollo config info!!! version = 1"}

7.apollo修改后测试: 将version修改为2

liqiang@root MINGW64 /e/IDEAWorkSpace/cloud (master)
$ curl http://localhost:3366//config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    81    0    81    0     0   1306      0 --:--:-- --:--:-- --:--:--  2612{"success":true,"code":"200","msg":"","data":"apollo config info!!! version = 2"}

补充:直接下载的quick start 包不支持多环境,所以只能从8080的dev环境获取信息。

=====分布式部署方式==== 

  上面通过快速入门大概有了个了解。下面详细分析下。

1.简介

Apollo分为客户端和服务端。

服务端基于Springboot和Springcloud开发。打包后可以直接运行,不需要额外tomcat等容器。

Java客户端不依赖任何框架,能够运行于所有Java运行时环境,同时Spring/Springboot环境也有较好的支持。

1.架构图如下:

从下往上看:

Config Service提供配置的读取、推送等功能,服务对象是Apollo客户端

Admin Service提供配置的修改、发布等功能,服务对象是Apollo Portal(管理界面)

Config Service和Admin Service都是多实例、无状态部署,所以需要将自己注册到Eureka中并保持心跳

在Eureka之上我们架了一层Meta Server用于封装Eureka的服务发现接口

Client通过域名访问Meta Server获取Config Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Client侧会做load balance、错误重试

Portal通过域名访问Meta Server获取Admin Service服务列表(IP+Port),而后直接通过IP+Port访问服务,同时在Portal侧会做load balance、错误重试

为了简化部署,我们实际上会把Config Service、Eureka和Meta Server三个逻辑角色部署在同一个JVM进程中

1.四个核心模块及其主要功能

ConfigService:提供配置获取接口、提供配置推送接口、服务于Apollo客户端

AdminService:提供配置管理接口、提供配置修改发布接口、服务于管理界面Portal

Client:为应用获取配置,支持实时更新、通过MetaServer获取ConfigService的服务列表、使用客户端软负载SLB方式调用ConfigService

Portal:配置管理界面、通过MetaServer获取AdminService的服务列表、使用客户端软负载SLB方式调用AdminService

2.三个辅助服务发现模块

Eureka:用于服务发现和注册、Config/AdminService注册实例并定期报心跳、和ConfigService在一起部署

MetaServer:Portal通过域名访问MetaServer获取AdminService的地址列表、Client通过域名访问MetaServer获取ConfigService的地址列表、相当于一个Eureka Proxy逻辑角色,和ConfigService在一起部署

NginxLB:和域名系统配合,协助Portal访问MetaServer获取AdminService地址列表、和域名系统配合,协助Client访问MetaServer获取ConfigService地址列表、和域名系统配合,协助用户访问Portal进行配置管理

2.克隆代码之后执行

 代码从码云克隆之后,查看项目如下:

 用到的三个主要项目:apollo-configservice、apollo-adminservice、apollo-portal。也可以自己从这三个项目入手查看源码。

apollo-configservice 中包含了eurekaServer,主要是为客户端使用。

apollo-adminservice 注册到上面的eurekaServer中。主要为portal管理界面服务

apollo-portal中,从上面eurekaServer中根据服务名称获取到adminService进行服务。

  这里演示两个环境。dev和prod环境。总共支持的环境如下:

public enum Env{
  LOCAL, DEV, FWS, FAT, UAT, LPT, PRO, TOOLS, UNKNOWN;

  public static Env fromString(String env) {
    Env environment = EnvUtils.transformEnv(env);
    Preconditions.checkArgument(environment != UNKNOWN, String.format("Env %s is invalid", env));
    return environment;
  }
}

1.准备数据库,如下:

  项目中scripts\sql\下面有两个脚本。

由于apollo多环境部署的模式是一个portal,每个环境对应一个adminservice和一个configservice。所以准备三个库。

到mysql执行后会创建两个数据库:ApolloConfigDB和ApolloPortalDB

另外加一个数据库:ApolloConfigDB1  (数据和上面ApolloConfigDB一样,作为prod环境的数据库)

注意:修改ApolloConfigDB1  数据库中,serverconfig表中eureka.service.url为http://localhost:8081/eureka/

2.启动服务

前提,下面启动均需要修改启动参数,修改方式点击Edit Configurations:

1.启动两个configservice服务

 (1)修改vm,第一次配置如下:

-Dapollo_profile=github
-Dserver.port=8081
-Dspring.datasource.url=jdbc:mysql://localhost:3306/apolloconfigdb1?characterEncoding=utf8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Deureka.client.service-url.default-zone=http://localhost:8081/eureka/

(2)第二次如下:

-Dapollo_profile=github
-Dserver.port=8080
-Dspring.datasource.url=jdbc:mysql://localhost:3306/apolloconfigdb?characterEncoding=utf8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Deureka.client.service-url.default-zone=http://localhost:8080/eureka/

2.启动两个adminservice

(1)第一次vm参数如下:

-Dapollo_profile=github
-Dserver.port=8090
-Dspring.datasource.url=jdbc:mysql://localhost:3306/apolloconfigdb?characterEncoding=utf8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Deureka.client.service-url.default-zone=http://localhost:8080/eureka/

(2)第二个参数

-Dapollo_profile=github
-Dserver.port=8090
-Dspring.datasource.url=jdbc:mysql://localhost:3306/apolloconfigdb?characterEncoding=utf8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456
-Deureka.client.service-url.default-zone=http://localhost:8080/eureka/

3.查看Eureka服务

8081服务:

 8080服务:

 4.启动portal服务

(1) vm参数为:

-Dapollo_profile=github,auth
-Ddev_meta=http://localhost:8080/
-Dpro_meta=http://localhost:8081/
-Dserver.port=8070
-Dspring.datasource.url=jdbc:mysql://localhost:3306/ApolloPortalDB?characterEncoding=utf8&serverTimezone=UTC
-Dspring.datasource.username=root
-Dspring.datasource.password=123456

(2)修改数据库apolloportaldb的表serverconfig的apollo.portal.envs的值为dev,pro 

5.启动apollo之后访问8070端口创建项目

 6.查看由两个环境

 7.dev环境和prod环境发布一个key

(1)dev

 (2)pro环境

 

补充:.了解到不同你给的环境需要不同的configservice和adminservice。比如再增加一个环境beta需要再增加一个configservice和一个adminservice。

  通过跟代码简单了解下如何如何区分环境。以点击PRO环境接口为例子。大致流程为:获取环境变量Env-》获取到基路径adminService地址-》拼接地址进行访问获取所需信息。也就验证了上面adminService是为portal服务的。

(1)访问dcontroller是http://localhost:8070/apps/cloud-config-apollo/envs/PRO/clusters/default/namespaces

(2)后端接口NamespaceController.findNamespaces方法,方法内部调用NamespaceService.findNamespaceBOs()方法,调用namespaceAPI.findNamespaceByCluster()方法

(3)namespaceAPI.findNamespaceByCluster方法如下:

    public List<NamespaceDTO> findNamespaceByCluster(String appId, Env env, String clusterName) {
      NamespaceDTO[] namespaceDTOs = restTemplate.get(env, "apps/{appId}/clusters/{clusterName}/namespaces",
          NamespaceDTO[].class, appId,
          clusterName);
      return Arrays.asList(namespaceDTOs);
    }

(4)get方法如下:

  public <T> T get(Env env, String path, Class<T> responseType, Object... urlVariables)
      throws RestClientException {
    return execute(HttpMethod.GET, env, path, null, responseType, urlVariables);
  }

(5)execute

  private <T> T execute(HttpMethod method, Env env, String path, Object request, Class<T> responseType,
                        Object... uriVariables) {

    if (path.startsWith("/")) {
      path = path.substring(1);
    }

    String uri = uriTemplateHandler.expand(path, uriVariables).getPath();
    Transaction ct = Tracer.newTransaction("AdminAPI", uri);
    ct.addData("Env", env);

    List<ServiceDTO> services = getAdminServices(env, ct);

(6)getAdminServices

  private List<ServiceDTO> getAdminServices(Env env, Transaction ct) {

    List<ServiceDTO> services = adminServiceAddressLocator.getServiceList(env);

    if (CollectionUtils.isEmpty(services)) {
      ServiceException e = new ServiceException(String.format("No available admin server."
                                                              + " Maybe because of meta server down or all admin server down. "
                                                              + "Meta server address: %s",
              portalMetaDomainService.getDomain(env)));
      ct.setStatus(e);
      ct.complete();
      throw e;
    }

    return services;
  }

(7) getServiceList

  public List<ServiceDTO> getServiceList(Env env) {
    List<ServiceDTO> services = cache.get(env);
    if (CollectionUtils.isEmpty(services)) {
      return Collections.emptyList();
    }
    List<ServiceDTO> randomConfigServices = Lists.newArrayList(services);
    Collections.shuffle(randomConfigServices);
    return randomConfigServices;
  }

跟断点返回的信息:

 (8)根据环境获取到adminservice地址之后拼接URL进行访问:

  private String parseHost(ServiceDTO serviceAddress) {
    return serviceAddress.getHomepageUrl() + "/";
  }

 8.springboot项目中获取配置

  这里用cloud-config-apollo-3366服务进行测试。通过测试也明白了apollo区分不同的环境是通过meta地址来区分,也就是apollo中configService的地址。

(1)bootstrap.yml如下:

server:
  port: 3366

app:
  # 与apollo的admin管理界面添加的 appId 一致
  id: cloud-config-apollo
apollo:
  # meta的url
  meta: http://localhost:8080
  bootstrap:
    enabled: true
    # 从 namespace 中获取配置, 多个以逗号隔开, namespace的配置非properties格式的需要加后缀名
#    namespaces: application,gateway,redis
    eagerLoad:
      enabled: true

测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    63    0    63    0     0   1000      0 --:--:-- --:--:-- --:--:--  3937{"success":true,"code":"200","msg":"","data":"dev version = 1"}

(2)修改meta的地址为8081的pro环境的meta地址:

server:
  port: 3366

app:
  # 与apollo的admin管理界面添加的 appId 一致
  id: cloud-config-apollo
apollo:
  # meta的url
  meta: http://localhost:8081
  bootstrap:
    enabled: true
    # 从 namespace 中获取配置, 多个以逗号隔开, namespace的配置非properties格式的需要加后缀名
#    namespaces: application,gateway,redis
    eagerLoad:
      enabled: true

测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    63    0    63    0     0     52      0 --:--:--  0:00:01 --:--:--    53{"success":true,"code":"200","msg":"","data":"pro version = 1"}

9.补充namespace的用法

(1)创建namespace

 (2)为mynamespace的pro环境创建配置信息并发布:

 (3)修改yml中的namespace测试

server:
  port: 3366

app:
  # 与apollo的admin管理界面添加的 appId 一致
  id: cloud-config-apollo
apollo:
  # meta的url(用于区分apollo中不同的环境)
  meta: http://localhost:8081
  bootstrap:
    enabled: true
    # 从 namespace 中获取配置, 多个以逗号隔开, namespace的配置非properties格式的需要加后缀名
    namespaces: mynamespace
    eagerLoad:
      enabled: true

测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    88    0    88    0     0    104      0 --:--:-- --:--:-- --:--:--   106{"success":true,"code":"200","msg":"","data":"pro version = 1! namespace = mynamespace"}

10.补充集群的用法

(1)创建集群

 

 

 

 (2)新建配置

 

(3)yml中指定集群

server:
  port: 3366

app:
  # 与apollo的admin管理界面添加的 appId 一致
  id: cloud-config-apollo
apollo:
  # meta的url(用于区分apollo中不同的环境)
  meta: http://localhost:8081
  cluster: cluster1
  bootstrap:
    enabled: true
    # 从 namespace 中获取配置, 多个以逗号隔开, namespace的配置非properties格式的需要加后缀名
#    namespaces: mynamespace
    eagerLoad:
      enabled: true

测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getConfigInfo
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    72    0    72    0     0    461      0 --:--:-- --:--:-- --:--:--   576{"success":true,"code":"200","msg":"","data":"cluster1 pro version = 1"}

 10.springboot以配置bean的方式注入属性

(1)apollo新增配置:

 (2)pom加入如下配置:

        <!--使用刷新配置注解-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-context</artifactId>
        </dependency>

(3)新增配置类ApolloConfig,按前缀进行注入

package cn.qz.cloud.config;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.stereotype.Component;

/**
 * @Author: qlq
 * @Description
 * @Date: 22:55 2020/10/31
 */
@Component
@ConfigurationProperties(prefix = "es")
@Getter
@Setter
@RefreshScope
public class ApolloConfig {

    private String host;
}

(4)增加ConfigRefresh允许自动更新

package cn.qz.cloud.config;

import com.ctrip.framework.apollo.model.ConfigChangeEvent;
import com.ctrip.framework.apollo.spring.annotation.ApolloConfigChangeListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.context.scope.refresh.RefreshScope;
import org.springframework.stereotype.Component;

/**
 * bean配置的方式自动刷新
 *
 * @Author: qlq
 * @Description
 * @Date: 23:08 2020/10/31
 */
@Component
public class ConfigRefresh {

    @Autowired
    private RefreshScope refreshScope;

    @ApolloConfigChangeListener
    public void onChange(ConfigChangeEvent event) {
        for (String key : event.changedKeys()) {
            System.out.println("===\t " + key);
            if (key.startsWith("es")) {
                refreshScope.refresh("apolloConfig");
                break;
            }
        }
    }
}

11. 补充:灰度发布的用法。灰度发布实际就是相当于先对部分节点进行测试,然后更新更新到所有节点或者删除灰度数据

(1)点击灰度之后点击新增灰度配置:es.host,值为  127.0.0.1:9200灰度测试

(2)新增灰度规则:实际就是对哪些节点有效

 (3)灰度发布之后测试:取的是灰度的值

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getEsHost
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    74    0    74    0     0     53      0 --:--:--  0:00:01 --:--:--    53{"success":true,"code":"200","msg":"","data":"127.0.0.1:9200灰度测试"}

(4)灰度发布同步到主版本:选择全量发布

 (5)发布成功之后会和主版本保持一致

 (6)修改信息为如下:

 测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getEsHost
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    83    0    83    0     0   1765      0 --:--:-- --:--:-- --:--:--  5187{"success":true,"code":"200","msg":"","data":"127.0.0.1:9200灰度测试222222222"}

(7)放弃灰度:直接点击 放弃灰度 后确认

测试:

liqiang@root MINGW64 /e/IDEAWorkSpace
$ curl http://localhost:3366/config/apollo/getEsHost
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100    74    0    74    0     0   2387      0 --:--:-- --:--:-- --:--:--  4933{"success":true,"code":"200","msg":"","data":"127.0.0.1:9200灰度测试"}

12.补充:apollo构建高可用 

  apollo构建高可用,实际上是通过对configservice和adminservice进行集群。类似于springcloud的应用进行集群部署。  

 

 总结:

 1.apollo四大核心组件:

configservice: 主要作用是提供eureka服务器以及为client服务

adminservice:注册到上面的eureka服务器,主要是为portal管理界面提供服务

portal:主要是为界面提供服务,从eureka服务器中获取adminservice调用对应接口。(不同的环境对应不同的conigservice)

client:从configservice获取配置信息

2.新建一个环境的时候一般是增加configservice和adminservice两个服务,当然要新建数据库。实现不同环境直接的数据隔离。

3.apollo的配置区分是:环境(DEV\PRO)->集群(默认是default)->namespace,和nacos的namespace、grop、以及环境有点区别。

4.灰度测试实际是可以先用配置对部分服务节点产生影响,之后测试没问题可以合并到主分支对所有节点生效,或者有问题删除灰度测试的配置。

 5.Springboor应用client区分环境是通过apollo.meta区分,值是configservice的地址;区分集群通过配置apollo.cluster;区分namespace通过配置apollo.bootstrap.namespaces

 

posted @ 2020-11-01 00:06  QiaoZhi  阅读(1639)  评论(0编辑  收藏  举报