php整合apollo配置中心——k8s从入门到高并发系列教程(十六)

 ads:

关注以下公众号查看更多文章

安装apollo

mysql导入apollo相关数据库

apolloportaldb.sql

apolloconfigdb.sql

helm 导入apollo仓库源

helm repo add apollo https://charts.apolloconfig.com

部署 configService 和 adminService 

helm install apollo-service-dev \
    --set configdb.host=192.168.205.1 \
    --set configdb.dbName=ApolloConfigDB \
    --set configdb.userName=apollo \
    --set configdb.password=apollo \
    --set configdb.connectionStringProperties="characterEncoding=utf8&useSSL=false" \
    --set configdb.service.enabled=true \
    --set configService.replicaCount=1 \
    --set adminService.replicaCount=1 \
    -n apollo \
    apollo/apollo-service

执行完后可以在控制台看到如下输出,可以看到config service地址是 http://apollo-service-dev-apollo-configservice.apollo:8080

Meta service url for current release:
  echo http://apollo-service-dev-apollo-configservice.apollo:8080

For local test use:
  export POD_NAME=$(kubectl get pods --namespace apollo -l "app=apollo-service-dev-apollo-configservice" -o jsonpath="{.items[0].metadata.name}")
  echo http://127.0.0.1:8080
  kubectl --namespace apollo port-forward $POD_NAME 8080:8080

Urls registered to meta service:
Config service: http://apollo-service-dev-apollo-configservice.apollo:8080
Admin service: http://apollo-service-dev-apollo-adminservice.apollo:8090

部署portal服务 

helm install apollo-portal \
    --set portaldb.host=192.168.205.1 \
    --set portaldb.dbName=ApolloPortalDB \
    --set portaldb.userName=apollo \
    --set portaldb.password=apollo \
    --set portaldb.connectionStringProperties="characterEncoding=utf8&useSSL=false" \
    --set portaldb.service.enabled=true \
    --set config.envs="dev" \
    --set config.metaServers.dev=http://apollo-service-dev-apollo-configservice.apollo:8080 \
    --set replicaCount=1 \
    -n apollo \
    apollo/apollo-portal

执行完后可以在控制台看到如下输出

Portal url for current release:
  export POD_NAME=$(kubectl get pods --namespace apollo -l "app=apollo-portal" -o jsonpath="{.items[0].metadata.name}")
  echo "Visit http://127.0.0.1:8070 to use your application"
  kubectl --namespace apollo port-forward $POD_NAME 8070:8070

打开如下地址,可以看到apollo管理界面

http://apollo-portal.apollo.svc.cluster.local:8070/signin

输入用户名apollo,密码admin后登录

添加了两个项目,一个作为基础命名空间,另一个继承这个命名空间 

开放api

拿对应appid的多个命名空间的消息更新

【config service 地址】/notifications/v2?appId=【appid】&cluster=【集群地址】&notifications=%5B%7B%22namespaceName%22%3A%22【命名空间1】%22%2C%22notificationId%22%3A-1%7D%2C%7B%22namespaceName%22%3A%22【命名空间2】%22%2C%22notificationId%22%3A-1%7D%5D

返回如下

[{
	"namespaceName": "application",
	"notificationId": 4618,
	"messages": {
		"details": {
			"ruby-live+default+application": 4618
		}
	}
}, {
	"namespaceName": "ruby.app",
	"notificationId": 4606,
	"messages": {
		"details": {
			"ruby+default+ruby.app": 4606,
			"ruby-live+default+ruby.app": 2323
		}
	}
}]

拉取配置信息

【config service 地址】/configs/【appid】/【集群地址】/【命名空间1】?ip=127.0.0.1&releaseKey=%

返回格式如下

{
	"appId": "ruby-live",
	"cluster": "default",
	"namespaceName": "application",
	"configurations": {

	},
	"releaseKey": "20220929203203-026c76296579b6ac"
}

php集成apollo配置中心

ApolloClient类负责和apollo上述两个接口打交道,把拉取的apollo配置信息写到本地文件,用到了并发curl请求特性
<?php

namespace Ruby\Config;


class ApolloClient
{
    protected $configServer; //apollo服务端地址
    protected $appId; //apollo配置项目的appid
    protected $cluster = 'default';
    protected $clientIp = '127.0.0.1'; //绑定IP做灰度发布用
    protected $notifications = [];
    protected $pullTimeout = 10; //获取某个namespace配置的请求超时时间
    protected $intervalTimeout = 60; //每次请求获取apollo配置变更时的超时时间
    public $save_dir; //配置保存目录

    /**
     * ApolloClient constructor.
     * @param string $configServer apollo服务端地址
     * @param string $appId apollo配置项目的appid
     * @param array $namespaces apollo配置项目的namespace
     */
    public function __construct($configServer, $appId, array $namespaces)
    {
        $this->configServer = $configServer;
        $this->appId = $appId;
        foreach ($namespaces as $namespace) {
            $this->notifications[$namespace] = ['namespaceName' => $namespace, 'notificationId' => -1];
        }
        $this->save_dir = dirname($_SERVER['SCRIPT_FILENAME']);
    }

    public function setCluster($cluster)
    {
        $this->cluster = $cluster;
    }

    public function setClientIp($ip)
    {
        $this->clientIp = $ip;
    }

    public function setPullTimeout($pullTimeout)
    {
        $pullTimeout = intval($pullTimeout);
        if ($pullTimeout < 1 || $pullTimeout > 300) {
            return;
        }
        $this->pullTimeout = $pullTimeout;
    }

    public function setIntervalTimeout($intervalTimeout)
    {
        $intervalTimeout = intval($intervalTimeout);
        if ($intervalTimeout < 1 || $intervalTimeout > 300) {
            return;
        }
        $this->intervalTimeout = $intervalTimeout;
    }

    private function _getReleaseKey($config_file)
    {
        $releaseKey = '';
        if (file_exists($config_file)) {
            $last_config = require $config_file;
            is_array($last_config) && isset($last_config['releaseKey']) && $releaseKey = $last_config['releaseKey'];
        }
        return $releaseKey;
    }

    //获取单个namespace的配置文件路径
    public function getConfigFile($namespaceName)
    {
        return $this->save_dir . DIRECTORY_SEPARATOR . 'apolloConfig.' . $namespaceName . '.php';
    }

    //获取单个namespace的配置-无缓存的方式
    public function pullConfig($namespaceName)
    {
        $base_api = rtrim($this->configServer, '/') . '/configs/' . $this->appId . '/' . $this->cluster . '/';
        $api = $base_api . $namespaceName;

        $args = [];
        $args['ip'] = $this->clientIp;
        $config_file = $this->getConfigFile($namespaceName);
        $args['releaseKey'] = $this->_getReleaseKey($config_file);

        $api .= '?' . http_build_query($args);

        $ch = curl_init($api);
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);

        $body = curl_exec($ch);
        $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        $error = curl_error($ch);
        curl_close($ch);

        if ($httpCode == 200) {
            $result = json_decode($body, true);
            $content = '<?php return ' . var_export($result, true) . ';';
            file_put_contents($config_file, $content);
            echo 'get content' . $content . "\r\n";

        } elseif ($httpCode != 304) {
            echo $body ?: $error . "\n";
            return false;
        }
        return true;
    }

    //获取多个namespace的配置-无缓存的方式
    public function pullConfigBatch(array $namespaceNames)
    {
        if (!$namespaceNames) return [];
        $multi_ch = curl_multi_init();
        $request_list = [];
        $base_url = rtrim($this->configServer, '/') . '/configs/' . $this->appId . '/' . $this->cluster . '/';
        $query_args = [];
        $query_args['ip'] = $this->clientIp;
        foreach ($namespaceNames as $namespaceName) {
            $request = [];
            $config_file = $this->getConfigFile($namespaceName);
            $request_url = $base_url . $namespaceName;
            $query_args['releaseKey'] = $this->_getReleaseKey($config_file);
            $query_string = '?' . http_build_query($query_args);
            $request_url .= $query_string;
            $ch = curl_init($request_url);
            curl_setopt($ch, CURLOPT_TIMEOUT, $this->pullTimeout);
            curl_setopt($ch, CURLOPT_HEADER, false);
            curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
            $request['ch'] = $ch;
            $request['config_file'] = $config_file;
            $request_list[$namespaceName] = $request;
            curl_multi_add_handle($multi_ch, $ch);
        }

        $active = null;
        // 执行批处理句柄
        do {
            $mrc = curl_multi_exec($multi_ch, $active);
        } while ($mrc == CURLM_CALL_MULTI_PERFORM);

        while ($active && $mrc == CURLM_OK) {
            if (curl_multi_select($multi_ch) == -1) {
                usleep(100);
            }
            do {
                $mrc = curl_multi_exec($multi_ch, $active);
            } while ($mrc == CURLM_CALL_MULTI_PERFORM);

        }

        // 获取结果
        $response_list = [];
        foreach ($request_list as $namespaceName => $req) {
            $response_list[$namespaceName] = true;
            $result = curl_multi_getcontent($req['ch']);
            $code = curl_getinfo($req['ch'], CURLINFO_HTTP_CODE);
            $error = curl_error($req['ch']);
            curl_multi_remove_handle($multi_ch, $req['ch']);
            curl_close($req['ch']);
            if ($code == 200) {
                $result = json_decode($result, true);
                $content = '<?php return ' . var_export($result, true) . ';';
                file_put_contents($req['config_file'], $content);
                echo 'get content' . $content . "\r\n";
            } elseif ($code != 304) {
                echo 'pull config of namespace[' . $namespaceName . '] error:' . ($result ?: $error) . "\n";
                $response_list[$namespaceName] = false;
            }
        }
        curl_multi_close($multi_ch);
        return $response_list;
    }

    protected function _listenChange(&$ch, $callback = null)
    {
        $base_url = rtrim($this->configServer, '/') . '/notifications/v2?';
        $params = [];
        $params['appId'] = $this->appId;
        $params['cluster'] = $this->cluster;
        do {
            $params['notifications'] = json_encode(array_values($this->notifications));
            $query = http_build_query($params);
            curl_setopt($ch, CURLOPT_URL, $base_url . $query);
            $response = curl_exec($ch);
            $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
            $error = curl_error($ch);
            if ($httpCode == 200) {
                $res = json_decode($response, true);
                $change_list = [];
                foreach ($res as $r) {
                    if ($r['notificationId'] != $this->notifications[$r['namespaceName']]['notificationId']) {
                        $change_list[$r['namespaceName']] = $r['notificationId'];
                    }
                }
                echo "config change:" . $response . "\r\n";
                $response_list = $this->pullConfigBatch(array_keys($change_list));
                foreach ($response_list as $namespaceName => $result) {
                    $result && ($this->notifications[$namespaceName]['notificationId'] = $change_list[$namespaceName]);
                }
                //如果定义了配置变更的回调,比如重新整合配置,则执行回调
                ($callback instanceof \Closure) && call_user_func($callback);
            } elseif ($httpCode != 304) {
                throw new \Exception($response ?: $error);
            }
        } while (true);
    }

    /**
     * @param $callback 监听到配置变更时的回调处理
     * @return mixed
     */
    public function start($callback = null)
    {
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_TIMEOUT, $this->intervalTimeout);
        curl_setopt($ch, CURLOPT_HEADER, false);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        try {
            $this->_listenChange($ch, $callback);
        } catch (\Exception $e) {
            curl_close($ch);
            return $e->getMessage();
        }
    }
}

Config类对 illuminate/config 基础上,把apollo的配置信息整合到自己管理的配置数组中,进行读取和设置

<?php

namespace Ruby\Config;

class Config extends \Illuminate\Config\Repository
{
    private $configServer;

    private $appId;

    private $namespaces;

    private $clientIp;

    private $cluster;

    /**
     * Config constructor.
     * @param $configServer
     * @param $appId
     * @param $namespaces
     * @param $clientIp
     * @param $savePath
     * @throws \Exception
     */
    public function __construct($configServer, $appId, $namespaces, $savePath = null, $clientIp = null, $cluster = 'default')
    {
        parent::__construct();
        $this->setConfigServer($configServer);
        $this->setAppId($appId);
        $this->setNamespaces($namespaces);
        $this->setSavePath($savePath);
        $this->setClientIp($clientIp);
        $this->loadLocalConfig();
        $this->cluster = $cluster;
    }

    private $savePath;


    /**
     * 读取apollo配置
     * @param null $callback
     */
    public function run($callback = null)
    {
        if (is_null($callback)) {
            $callback = function () {
                echo "config changed sync config \r\n";
                $this->loadLocalConfig();
                reload_jobs();
            };
        } else {
            $callback = function () use ($callback) {
                $this->loadLocalConfig();
                $callback();
            };
        }
        $apollo = new ApolloClient($this->getConfigServer(), $this->getAppId(), $this->getNamespaces());

        if ($this->getClientIp()) {
            $apollo->setClientIp($this->getClientIp());
        }
        $cluster = $this->getCluster();
        $apollo->setCluster($cluster);
        $apollo->save_dir = $this->getSavePath();
        ini_set('memory_limit', '128M');
        $pid = getmypid();
        echo 'cluster is :' . $cluster . "\r\n";
        echo 'save_path is :' . $this->getSavePath() . "\r\n";
        echo 'config server is :' . $this->getConfigServer() . "\r\n";
        echo 'app_id is :' . $this->getAppId() . "\r\n";
        echo 'namespaces is :' . implode(',', $this->getNamespaces()) . "\r\n";
        echo "start [$pid]\r\n";
        $restart = true; //auto start if failed
        do {
            $error = $apollo->start($callback);
            if ($error) {
                echo('error:' . $error . "\r\n");
            } else {
                echo "get config success\r\n";
            }
            sleep(1);
            echo "restarting apollo config client daemon\r\n";
        } while ($restart);
    }

    public function loadLocalConfig()
    {
        $pattern = $this->getSavePath() . DIRECTORY_SEPARATOR . 'apolloConfig.*';
        $list = glob($pattern);
        foreach ($list as $l) {
            $config = require $l;
            if (is_array($config) && isset($config['configurations'])) {
                foreach ($config['configurations'] as $name => $value) {
                    if (is_json($value)) {
                        $value = json_decode($value, true);
                    }
                    data_set($this->items, $name, $value);
                }
            }
        }
        return $this->all();
    }

    public function merge($config)
    {
        $this->items = array_merge_recursive($config, $this->items);
    }

    /**
     * @return mixed
     */
    public function getClientIp()
    {
        return $this->clientIp;
    }

    /**
     * @param mixed $clientIp
     */
    public function setClientIp($clientIp)
    {
        $this->clientIp = $clientIp ? $clientIp : null;
    }

    /**
     * @return mixed
     */
    public function getNamespaces()
    {
        return $this->namespaces;
    }

    /**
     * @param mixed $namespaces
     */
    public function setNamespaces($namespaces)
    {
        !is_array($namespaces) && $namespaces = explode(',', $namespaces);
        $this->namespaces = $namespaces;
    }

    /**
     * @return mixed
     */
    public function getAppId()
    {
        return $this->appId;
    }

    /**
     * @param mixed $appId
     */
    public function setAppId($appId)
    {
        $this->appId = $appId;
    }

    /**
     * @return mixed
     */
    public function getConfigServer()
    {
        return $this->configServer;
    }

    /**
     * @param mixed $configServer
     */
    public function setConfigServer($configServer)
    {
        $this->configServer = $configServer;
    }

    /**
     * @return mixed
     */
    public function getSavePath()
    {
        return $this->savePath;
    }

    /**
     * @param mixed $savePath
     */
    public function setSavePath($savePath)
    {
        if (empty($savePath)) {
            $savePath = '/home/config/' . $this->getAppId();
        }
        if (!is_dir($savePath)) {
            @mkdir($savePath, 0777, true);
        }
        $this->savePath = $savePath;
    }

    public function getCluster()
    {
        $cluster = getenv('APOLLO_CLUSTER');
        if ($cluster !== false) {
            return $cluster;
        }
        return $this->cluster;
    }
}
Container类用单例模式保证Config类被唯一实例化,并且允许使用环境变量来覆盖apollo的配置信息
<?php

namespace Ruby\Config;

use Illuminate\Support\Str;

class Container
{
    private static $config;

    private function __construct()
    {
        throw new \Exception('not allowed construct');
    }

    /**
     * 调用实例化
     * @param $configServer
     * @param $appId
     * @param $namespaces
     * @param null $savePath
     * @param null $clientIp
     * @return Config
     * @throws \Exception
     */
    public static function register($configServer, $appId, $namespaces, $savePath = null, $clientIp = null, $cluster = 'default')
    {
        self::$config = new Config($configServer, $appId, $namespaces, $savePath, $clientIp, $cluster);
        self::registerOsEnv();
        return self::$config;
    }

    private static function registerOsEnv()
    {
        //register os env
        $osEnvs = getenv();
        if (!empty($osEnvs)) {
            foreach ($osEnvs as $key => $item) {
                if (Str::startsWith($key, 'RUBY_')) {
                    $key = str_replace('_', '.', strtolower(ltrim($key, 'MELO_')));
                    if (Str::contains($key, '\.')) {
                        $key = str_replace('\.', '_', $key);
                    }
                    self::$config->set($key, $item);
                }
            }
        }
    }

    public static function instance()
    {
        if (self::$config) {
            return self::$config;
        }
        throw new \Exception("config is not registered, call register before ");
    }

    private function __clone()
    {
        throw new \Exception('not allowed clone');
    }
}
ConfigCommand 封装了 illuminate/console 的一个命令,然后注册到lumen框架中去 
<?php

namespace Ruby\Config\Lumen;

use Illuminate\Console\Command;
use Melo\Config\Container;

class ConfigCommand extends Command
{
    protected $signature = 'config:sync';

    protected $description = 'sync config from apollo';

    public function handle()
    {
        $config = Container::instance();
        $callback = null;
        $config->run($callback);
    }
}

helpers.php封装一个小助手函数来读取apollo配置中心的内容(也有可能是覆盖apollo的环境变量)

<?php

if (!function_exists('conf')) {
    /**
     * Get / set the specified configuration value.
     *
     * If an array is passed as the key, we will assume you want to set an array of values.
     *
     * @param  array|string|null $key
     * @param  mixed $default
     * @return mixed
     * @throws Exception
     */
    function conf($key = null, $default = null)
    {
        if (is_null($key)) {
            return \Melo\Config\Container::instance()->all();
        }

        if (is_array($key)) {
            return \Melo\Config\Container::instance()->set($key);
        }

        return \Melo\Config\Container::instance()->get($key, $default);
    }
}

用php写一个可执行文件来长期运行拉取apollo配置信息到本地并及时更新的重要任务

#!/usr/bin/env php
<?php

use Ruby\Config\Container;

$dir = __DIR__ . '/src';

$dir = __DIR__ . '/..';

if (!file_exists($dir . '/autoload.php')) {
    $dir = __DIR__ . '/../vendor';
}

if (!file_exists($dir . '/autoload.php')) {
    $dir = __DIR__ . '/../../..';
}

if (!file_exists($dir . '/autoload.php')) {
    echo 'Autoload not found.';
    exit(1);
}

require $dir . '/autoload.php';

$config = [
    'config_server' => getenv('APOLLO_CONFIG_SERVER'),
    'app_id' => getenv('APOLLO_APP_ID'),
    'namespaces' => getenv('APOLLO_NAMESPACES'),
    'save_path' => getenv('APOLLO_SAVE_PATH') ?? null,
    'client_ip' => getenv('APOLLO_CLIENT_IP') ?? null,
    'cluster' => getenv('APOLLO_CLUSTER') ?? null,
];

$config = array_merge($config, parse_cli_params($argv));

if (empty($config['config_server'])) {
    error('config_server is not passed');
}
if (empty($config['app_id'])) {
    error('app_id is not passed');
}
if (empty($config['namespaces'])) {
    error('namespaces is not passed');
} else {
    $config['namespaces'] = explode(',', $config['namespaces']);
}

$client = Container::register(
    $config['config_server'],
    $config['app_id'],
    $config['namespaces'],
    $config['save_path'] ?? null,
    $config['client_ip'] ?? null
);
$client->run();

function error($msg)
{
    print_params();
    echo '[ERROR]: ' . $msg;
    exit(-1);
}

function print_params()
{
    echo "usage: config config_server=sample app_id=sample namespaces=sample1,sample2 client_ip=sample save_path=absolute_path \r\n";
    echo "or set os_env APOLLO_CONFIG_SERVER APOLLO_APP_ID APOLLO_NAMESPACES APOLLO_CLIENT_IP APOLLO_SAVE_PATH \r\n";
}

把这些封装成一个php composer包,供内网其他项目使用

{
  "name": "ruby/config",
  "description": "config for projects use apollo",
  "type": "library",
  "keywords": [
    "client",
    "config",
    "apollo"
  ],
  "license": "MIT",
  "authors": [{
    "name": "ruby",
    "email": "rubyruby@gmail.com"
  }],
  "require": {
    "php": ">=7.1",
    "illuminate/config": ">=6.0",
    "illuminate/console": ">=6.0",
    "ext-curl": "*",
    "ext-json": "*"
  },
  "autoload": {
    "files": [
      "src/helpers.php"
    ],
    "psr-4": {
      "RUBY\\Config\\": "src/"
    }
  },
  "bin": [
    "bin/config"
  ]
}

 相关链接

手把手教你部署nginx+php

php和nginx镜像合并 && 代码打包到镜像

nginx-php镜像安装常用软件

yaf && yar微服务/hprose微服务 镜像初始化

常用开发工具:php_codesniffer代码规范检查&修复、phpstan语法检查、phpunit单元测试

.gitlab-ci.yaml自动镜像打包&&互联网企业规范化上线流程(上)

kustomize/kubectl自动镜像部署&&互联网企业规范化上线流程(下)

apisix网关、JMeter压测  

prometheus/grafana监控数据收集与展示

k8s容器网络性能调优

supervisor进程管理

安装opcache和apcu

APM性能监测工具skywalking

链路跟踪工具zipkin

phpfpm和nginx配置

php整合apollo配置中心

php rdkafka操作kafka消息队列

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Apollo on k8s是指在Kubernetes(简称k8s)上部署和运行Apollo配置中心Apollo是携程框架部门开发的一款开源配置管理平台,用于集中管理和动态配置应用程序的配置信息。Kubernetes是一个开源的容器编排平台,用于自动化部署、扩展和管理容器化应用程序。 将Apollo配置中心部署在Kubernetes集群上可以带来以下好处: 1. 弹性伸缩:Kubernetes可以根据应用程序的负载情况自动调整Apollo实例的数量,以满足不同规模的应用需求。 2. 高可用性:Kubernetes提供了故障恢复和自动重启的机制,可以确保Apollo配置中心的高可用性。 3. 灵活性:Kubernetes支持多种部署策略,可以根据需要选择合适的部署方式,如单节点、多节点、分布式等。 4. 简化管理:Kubernetes提供了丰富的管理工具和API,可以方便地进行配置、监控和扩展等操作。 要在Kubernetes上部署Apollo配置中心,通常需要进行以下步骤: 1. 创建Kubernetes集群:可以使用云服务提供商(如AWS、Azure、GCP)或自建集群。 2. 编写Apollo配置文件:根据应用程序的需求,编写Apollo的配置文件,包括应用信息、环境配置、数据库连接等。 3. 创建Kubernetes资源:使用Kubernetes的资源定义文件(如Deployment、Service、ConfigMap)创建Apollo的相关资源。 4. 部署Apollo配置中心使用Kubernetes的命令或管理工具,将Apollo配置中心部署到Kubernetes集群中。 5. 验证和测试:确保Apollo配置中心在Kubernetes上正常运行,并进行相关的验证和测试。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

fanghailiang2016

扔个包子砸我一下吧~

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

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

打赏作者

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

抵扣说明:

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

余额充值