php 容器源码分析,Lumen框架“服务容器”源码解析

1.服务容器

“服务容器”是Lumen框架整个系统功能调度配置的核心,它提供了整个框架运行过程中的一系列服务。“服务容器”就是提供服务(服务可以理解为系统运行中需要的东西,如:对象、文件路径、系统配置等)的载体,在系统运行的过程中动态的为系统提供这些服务。下边是服务容器工作示意图:

bVbrjXC?w=1000&h=1000

1.1、服务容器的产生

Lumen框架中,服务容器是由illuminate/container/Container.php中Container类完成的,该类实现了服务容器的核心功能。laravel/lumen-framework/src/Application.php中Application类继承了该类,实现了服务容器初始化配置和功能拓展。源码中生成服务容器的代码是在bootstrap/app.php中:

$app = new Laravel\Lumen\Application(

dirname(__DIR__)

);

也就是Lumen框架在处理每一个请求的时候,都会首先为这个请求生成一个服务容器,用于容纳请求处理需要的服务。

1.2、服务绑定

服务容器生成以后,就可以向其中添加服务,服务绑定可以理解为一个服务和一个关键字绑定,看作键值对的形式就是:一个"key" 对应一个服务。要绑定的服务不同,使用的容器中的绑定函数也不同,框架初始化时使用到的是回调函数服务绑定和实例对象服务绑定。回调函数绑定分两种:一种是普通绑定,另外一种是单例绑定,通过bind()函数中的参数$shared进行区分,项目代码中的singleton()绑定单例就是bind()函数中$shared参数为true的情况。源码如下:

public function singleton($abstract, $concrete = null)

{

$this->bind($abstract, $concrete, true);

}

单例绑定在整个Lumen生命周期中只会生成并使用一个实例对象。绑定一个实例对象到容器中使用的是instance()函数,绑定之后生成的实例对象会在$instance属性中记录。回调函数的绑定在$bindings属性中记录。

有一种情况是绑定具体类名称,实际上也是绑定回调函数的方式,只是回调函数是服务容器根据提供的参数自动生成的,下面章节我们会详细讲解。源码中有如下代码:

$app->singleton(

Illuminate\Contracts\Debug\ExceptionHandler::class,

App\Exceptions\Handler::class

);

$app->singleton(

Illuminate\Contracts\Console\Kernel::class,

App\Console\Kernel::class

);

在服务绑定过程中,尽量使用接口名称和服务进行绑定,这样可以使得一个具体的功能仅仅和接口实现了耦合,当应用需求变化时可以修改具体类,只要这个类还符合接口规范,程序依然可以健壮的运行。这种“面向接口”编程是一种新的,更有效的解决依赖的编程模式。Lumen框架的接口定义规范都放在/learnLumen/vendor/illuminate/contracts 文件夹下。

1.3、服务解析

服务绑定到容器之后,运行程序就可以随时从容器中取出服务,这个过程称为“服务解析”。服务解析的步骤就是运行程序先获取到容器对象,然后使用容器对象解析相应的服务。服务解析有常用几种方式:

使用保存服务容器成员属性,调用make函数进行解析

$this->app->make(App\Service\ExampleService::class);

通过全局函数app()来获取

app(App\Service\ExampleService::class);

如果程序使用了Facades外观,还可以通过静态方法来解析

\App::make(App\Service\ExampleService::class);

服务容器类Container实现了ArrayAccess接口,可以使用数组的方式进行服务解析

app[App\Service\ExampleService::class];

ArrayAccess(数组式访问)接口非常有用,提供了像访问数组一样访问对象的能力的接口。

使用依赖注入的方式也可以实现服务的自动解析。即在类的构造函数中,使用相应的类提示符,容器会利用自身的反射机制自动解析依赖并实现注入。需要注意的是:在服务注册以后使用依赖注入功能,则该服务名称和服务是要遵循一定规范的。即服务名称一般为服务生成的类名称或者接口名称,只有这样当服务根据依赖限制查找到服务后生成的实例对象才能满足这个限制,否则就会报错。

并不是Lumen框架中所有的类都能实现自动依赖注入的功能,只有“服务容器”创建的类实例才能实现依赖自动注入。

2.控制反转(Ioc)和依赖注入(DI)

控制反转是框架设计的一种原则,在很大程度上降低了代码模块之间的耦合度,有利于框架维护和拓展。实现控制反转最常见的方法是“依赖注入”,还有一种方法叫“依赖查找”。控制反转将框架中解决依赖的逻辑从实现代码类库的内部提取到了外部来管理实现。

我们用简单代码模拟一下Lumen处理用户请求的逻辑,框架中要使用到最简单的Request请求模块、Response请求模块,我们使用单例模式简单实现一下:

//Request模块实现

class Request

{

static private $instance = null;

private function __construct()

{

}

private function __clone()

{

}

static function getInstance()

{

if (self::$instance == null) self::$instance = new self();

return self::$instance;

}

public function get($key)

{

return $_GET[$key] ? $_GET[$key] : '';

}

public function post($key)

{

return $_POST[$key] ? $_POST[$key] : '';

}

}

//Response模块实现

class Response

{

static private $instance = null;

private function __construct()

{

}

private function __clone()

{

}

static function getInstance()

{

if (self::$instance == null) self::$instance = new self();

return self::$instance;

}

public function json($data)

{

return json_encode($data);

}

}

我们先来使用“依赖查找”的工厂模式来实现控制反转,我们需要一个工厂,简单实现一下:

include_once 'Request.php';

include_once 'Response.php';

include_once 'ExceptionHandler.php';

abstract class Factory

{

static function Create($type, array $params = [])

{

//根据接收到的参数确定要生产的对象

switch ($type) {

case 'request':

return Request::getInstance();

break;

case 'response':

return Response::getInstance();

break;

case 'exception':

return new ExceptionHandler();

break;

}

}

}

接下来就开始实现用户逻辑,我们首先加入错误处理的简单实现:

//开启报告代码中的错误处理

class ExceptionHandler

{

public function __construct()

{

error_reporting(-1);

ini_set('display_errors', true);

}

}

我们模拟一个请求用户列表的逻辑:

include_once 'Factory.php';

Factory::Create('exception');

//用户逻辑

class UserLogic

{

private $modules = [];

public function __construct(array $modules)

{

foreach ($modules as $key => $module) {

$this->modules[$key] = Factory::Create($module);

}

}

public function getUserList()

{

if ($this->modules['request']->get('path') == 'userlist') {

$userList = [

['name' => '张三', 'age' => 18],

['name' => '李四', 'age' => 22]

];

return $this->modules['response']->json($userList);

}

}

}

try {

$userLogic = new UserLogic(['request' => 'request', 'response' => 'response']);

echo $userLogic->getUserList();

} catch (\Error $e) {

var_dump($e);

exit();

}

可以看到我们使用工厂模式管理依赖的时候,可以在处理业务逻辑外部根据处理请求需要依赖的模块自行进行注入。比如例子中就注入了request、response模块。这种模式虽然解决了我们处理逻辑对外部模块的依赖管理问题,但是并不是太完美,我们的程序只是将原来逻辑对一个个实例子对象的依赖转换成了工厂对这些实例子对象的依赖,工厂和这些实例子对象之间的耦合还存在,随着工厂越来越大,用户逻辑实现越来越复杂,这种“依赖查找”实现控制反转的模式对于用户来讲依然很痛苦。

接下来我们使用Ioc服务容器来实现依赖注入,下边先实现一个简单的服务容器:

class Container

{

//用于装提供实例的回调函数,真正的容器还会装实例等其他内容

protected $bindings = [];

//容器共享实例数组(单例)

protected $instances = [];

public function bind($abstract, $concrete = null, $shared = false)

{

if (! $concrete instanceof Closure) {

//如果提供的参数不是回调函数,则产生默认的回调函数

$concrete = $this->getClosure($abstract, $concrete);

}

$this->bindings[$abstract] = compact('concrete', 'shared');

}

public function getBuildings()

{

return $this->bindings;

}

//默认生成实例的回调函数

protected function getClosure($abstract, $concrete)

{

return function ($c) use ($abstract, $concrete)

{

$method = ($abstract == $concrete) ? 'build' : 'make';

//调用的是容器的build或make方法生成实例

return $c->$method($concrete);

};

}

//生成实例对象,首先解决接口和要实例化类之间的依赖关系

public function make($abstract)

{

$concrete = $this->getConcrete($abstract);

if ($this->isBuildable($concrete, $abstract)) {

$object = $this->build($this->build($concrete));

} else {

$object = $this->make($concrete);

}

return $object;

}

protected function isBuildable($concrete, $abstract)

{

return $concrete === $abstract || $concrete instanceof Closure;

}

//获取绑定的回调函数

protected function getConcrete($abstract)

{

if (!isset($this->bindings[$abstract]))

{

return $abstract;

}

return $this->bindings[$abstract]['concrete'];

}

//实例化一个对象

public function build($concrete)

{

if ($concrete instanceof Closure) {

return $concrete($this);

}

$reflector = new ReflectionClass($concrete);

if (! $reflector->isInstantiable()) {

echo $message = "Target [$concrete] is not instantiable.";

}

$constructor = $reflector->getConstructor();

if(is_null($constructor)) {

return new $concrete;

}

$dependencies = $constructor->getParameters();

$instances = $this->getDependencies($dependencies);

return $reflector->newInstanceArgs($instances);

}

//通过反射机制实例化对象时的依赖

protected function getDependencies($parameters)

{

$dependencies = [];

foreach($parameters as $parameter)

{

$dependency = $parameter->getClass();

if(is_null($dependency)) {

$dependencies[] = NULL;

} else {

$dependencies[] = $this->resolveClass($parameter);

}

}

return (array) $dependencies;

}

protected function resolveClass(ReflectionParameter $parameter)

{

return $this->make($parameter->getClass()->name);

}

//注册一个实例并绑定到容器中

public function singleton($abstract, $concrete = null){

$this->bind($abstract, $concrete, true);

}

}

该服务容器可以称为Lumen服务容器的简化版,但是它实现的功能和Lumen服务容器是一样的,虽然只有一百多行的代码,但是理解起来有难度,这里就详细讲解清楚简化版容器的代码和原理,接下来章节对Lumen服务容器源码分析时就仅仅只对方法做简单介绍。

根据对服务容器介绍章节所讲:容器中有两个关键属性$bindings和$instance,其中$bindings中存在加入到容器中的回调函数,而$instance存放的是容器中绑定的实例对象。我们还知道$singleton方法用来绑定单例对象,其底层只是调用了bind方法而已,只不过$shared属性为true,意为容器中全局共享:

//注册一个实例并绑定到容器中

public function singleton($abstract, $concrete = null){

$this->bind($abstract, $concrete, true);

}

bind方法的实现也很简单,只是将用户指定的服务解析好之后存放入相应的属性当中:

public function bind($abstract, $concrete = null, $shared = false)

{

if (! $concrete instanceof Closure) {

//如果提供的参数不是回调函数,则产生默认的回调函数

$concrete = $this->getClosure($abstract, $concrete);

}

$this->bindings[$abstract] = compact('concrete', 'shared');

}

Closure是php中的匿名函数类类型。$abstract和$concrete可以抽象理解为KV键值对,K就是$abstract,是服务名;V是$concrete,是服务的具体实现。我们理解容器,首先要将思维从平常的业务逻辑代码中转换回来。业务逻辑中操作的一般是用户数据,而容器中,我们操作的是对象、类、接口之类的,在框架中可称为“服务”。如果用户要绑定的具体实现$concrete不是匿名函数,则调用getClosure方法生成一个匿名函数:

//获取绑定的回调函数

//默认生成实例的回调函数

protected function getClosure($abstract, $concrete)

{

return function ($c) use ($abstract, $concrete)

{

$method = ($abstract == $concrete) ? 'build' : 'make';

//调用的是容器的build或make方法生成实例

return $c->$method($concrete);

};

}

getClosure是根据用户传入的参数来决定调用系统的build和make方法。其中build方法就是构建匿名函数和类实例的关键实现,使用了php中的反射机制,解析出类实例:

//实例化一个对象

public function build($concrete)

{

if ($concrete instanceof Closure) {

return $concrete($this);

}

$reflector = new ReflectionClass($concrete);

if (! $reflector->isInstantiable()) {

echo $message = "Target [$concrete] is not instantiable.";

}

$constructor = $reflector->getConstructor();

if(is_null($constructor)) {

return new $concrete;

}

$dependencies = $constructor->getParameters();

$instances = $this->getDependencies($dependencies);

return $reflector->newInstanceArgs($instances);

}

build首先判断参数$concrete是一个匿名函数,就返回调用匿名函数的一个闭包。否则$concrete是一个类,利用反射机制解析类的信息,首先判断类是否能够被实例化(例如单例就不能被实例化,容器中的单例是通过属性$shared来区分的);确保了类能够被实例化以后,使用getConstructor()判断类是否定义了构造函数,如果没有定义构造函数,直接实例化得到一个类的实例。否则就再次调用getParameters获取构造函数中都传入了哪些参数(也就是判断$concrete类都有哪些依赖),getDependencies方法就是来生成$concrete依赖的函数:

//通过反射机制实例化对象时的依赖

protected function getDependencies($parameters)

{

$dependencies = [];

foreach($parameters as $parameter)

{

$dependency = $parameter->getClass();

if(is_null($dependency)) {

$dependencies[] = NULL;

} else {

$dependencies[] = $this->resolveClass($parameter);

}

}

return (array) $dependencies;

}

得到了类依赖的实例以后,就调用newInstanceArgs($instances)来生成类的实例。

服务解析函数make主要由build函数实现:

//生成实例对象,首先解决接口和要实例化类之间的依赖关系

public function make($abstract)

{

$concrete = $this->getConcrete($abstract);

if ($this->isBuildable($concrete, $abstract)) {

$object = $this->build($this->build($concrete));

} else {

$object = $this->make($concrete);

}

return $object;

}

有了服务容器以后,我们就可以使用服务容器来存储处理请求中需要的服务,并实现服务中的依赖自动注入。不过首先我们需要将Request、Response单例做修改,因为服务容器对单例的管理,是通过$shared属性进行设置的。所以Request、Response要能够被实例化,才能保存到容器的$bindings数组中:

class Request

{

public function __construct()

{

}

public function get($key)

{

return $_GET[$key] ? $_GET[$key] : '';

}

public function post($key)

{

return $_POST[$key] ? $_POST[$key] : '';

}

}

class Response

{

public function __construct()

{

}

public function json($data)

{

return json_encode($data);

}

}

我们再来看使用容器后处理用户请求的源代码:

include_once 'Container.php';

include_once 'Request.php';

include_once 'Response.php';

include_once 'ExceptionHandler.php';

$app = new Container();

//绑定错误处理

$app->bind('exception', 'ExceptionHandler');

//将请求、响应单例组件添加到容器中

$app->singleton('request', 'Request');

$app->singleton('response', 'Response');

//解析错误处理

$app->make('exception');

//用户逻辑

class UserLogic

{

public $app = null;

public function __construct(Container $app)

{

$this->app = $app;

}

public function getUserList()

{

if ($this->app->make('request')->get('path') == 'userlist') {

$userList = [

['name' => '张三', 'age' => 18],

['name' => '李四', 'age' => 22]

];

return $this->app->make('response')->json($userList);

}

}

}

try {

$userLogic = new UserLogic($app);

echo $userLogic->getUserList();

} catch (\Error $e) {

var_dump($e);

exit();

}

我们还是按照之前的步骤,使用容器将错误处理类绑定到容器中,然后解析出来使用。使用singleton方法将Request和Response类绑定到容器中,类型是单例。这样我们管理服务模块、实现依赖注入这些问题全都交给容器来做就好了。我们想要什么样的服务,就向容器中添加,在需要使用的时候,就利用容器解析使用就可以了。lumen框架中的服务容器是全局的,不需要像例子中一样,手动注入到逻辑代码中使用。

3.源码解析

对于lumen框架来讲,服务容器相当于发动机,绑定与解析框架启动和运行生命周期中所有的服务。它的大致架构如下所示:

bVbrjXy?w=2361&h=1045

3.1、服务容器绑定的方法

bind绑定

bindif绑定

singleton绑定

instance绑定

context绑定

数组绑定

标签绑定

extend拓展

Rebounds与Rebinding

源码中bind实现代码如下:

public function bind($abstract, $concrete = null, $shared = false)

{

$this->dropStaleInstances($abstract);

if (is_null($concrete)) {

$concrete = $abstract;

}

if (! $concrete instanceof Closure) {

$concrete = $this->getClosure($abstract, $concrete);

}

$this->bindings[$abstract] = compact('concrete', 'shared');

if ($this->resolved($abstract)) {

$this->rebound($abstract);

}

}

从源码中我们可知:使用bind方法绑定服务,每次都会重新进行绑定(删除原来的绑定,再重新绑定)。我们类比服务容器中服务的绑定为KV健值对。key为接口名称,而value为具体的服务实现,之所以推荐使用接口名称作为key,是因为只要开发者遵循相关的接口约束规范,就可以对服务进行拓展和改进,这也是面向接口编程比较新颖之处。另外我们可以看到bind方法核心实现方法是调用rebound方法。

bindif方法核心是调用bind方法,只不过对容器是否绑定服务做了一个判断:

public function bindIf($abstract, $concrete = null, $shared = false)

{

if (! $this->bound($abstract)) {

$this->bind($abstract, $concrete, $shared);

}

}

singleton是bind方法的一种特例,shared=true表示为单例绑定:

public function singleton($abstract, $concrete = null)

{

$this->bind($abstract, $concrete, true);

}

instance是绑定对象实例到容器中(不用使用make进行解析了):

public function instance($abstract, $instance)

{

$this->removeAbstractAlias($abstract);

$isBound = $this->bound($abstract);

unset($this->aliases[$abstract]);

$this->instances[$abstract] = $instance;

if ($isBound) {

$this->rebound($abstract);

}

return $instance;

}

数组绑定是Container类继承了ArrayAccess接口,在offsetSet中调用了bind方法进行注册:

public function offsetSet($key, $value)

{

$this->bind($key, $value instanceof Closure ? $value : function () use ($value) {

return $value;

});

}

extend方法实现了当原来的类注册或者实例化出来后,对其进行拓展:

public function extend($abstract, Closure $closure)

{

$abstract = $this->getAlias($abstract);

if (isset($this->instances[$abstract])) {

$this->instances[$abstract] = $closure($this->instances[$abstract], $this);

$this->rebound($abstract);

} else {

$this->extenders[$abstract][] = $closure;

if ($this->resolved($abstract)) {

$this->rebound($abstract);

}

}

}

Context绑定是针对于两个类使用同一个接口,但是我们在类中注入了不同的实现,这时候我们就需要使用when方法了:

public function when($concrete)

{

$aliases = [];

foreach (Arr::wrap($concrete) as $c) {

$aliases[] = $this->getAlias($c);

}

return new ContextualBindingBuilder($this, $aliases);

}

继续看ContextualBindingBuilder类的源码我们知道,上下文绑定的基本思路就是$this->app->when()->needs()->give();

比如有几个控制器分别依赖IlluminateContractsFilesystemFilesystem的不同实现:

$this->app->when(StorageController::class)

->needs(Filesystem::class)

->give(function () {

Storage::class

});//提供类名

$this->app->when(PhotoController::class)

->needs(Filesystem::class)

->give(function () {

return new Storage();

});//提供实现方式

$this->app->when(VideoController::class)

->needs(Filesystem::class)

->give(function () {

return new Storage($app->make(Disk::class));

});//需要依赖注入

有一些场景,我们希望当接口改变以后对已实例化的对象重新做一些改变,这就是rebinding 函数的用途:

public function rebinding($abstract, Closure $callback)

{

$this->reboundCallbacks[$abstract = $this->getAlias($abstract)][] = $callback;

if ($this->bound($abstract)) {

return $this->make($abstract);

}

}

3.2、服务别名

在服务容器解析之前,Lumen框架会将常用的服务起一些别名,方便系统Facade方法调用和解析。

public function withAliases($userAliases = [])

{

$defaults = [

'Illuminate\Support\Facades\Auth' => 'Auth',

'Illuminate\Support\Facades\Cache' => 'Cache',

'Illuminate\Support\Facades\DB' => 'DB',

'Illuminate\Support\Facades\Event' => 'Event',

'Illuminate\Support\Facades\Gate' => 'Gate',

'Illuminate\Support\Facades\Log' => 'Log',

'Illuminate\Support\Facades\Queue' => 'Queue',

'Illuminate\Support\Facades\Route' => 'Route',

'Illuminate\Support\Facades\Schema' => 'Schema',

'Illuminate\Support\Facades\Storage' => 'Storage',

'Illuminate\Support\Facades\URL' => 'URL',

'Illuminate\Support\Facades\Validator' => 'Validator',

];

if (! static::$aliasesRegistered) {

static::$aliasesRegistered = true;

$merged = array_merge($defaults, $userAliases);

foreach ($merged as $original => $alias) {

class_alias($original, $alias);

}

}

}

...

protected function registerContainerAliases()

{

$this->aliases = [

'Illuminate\Contracts\Foundation\Application' => 'app',

'Illuminate\Contracts\Auth\Factory' => 'auth',

'Illuminate\Contracts\Auth\Guard' => 'auth.driver',

'Illuminate\Contracts\Cache\Factory' => 'cache',

'Illuminate\Contracts\Cache\Repository' => 'cache.store',

'Illuminate\Contracts\Config\Repository' => 'config',

'Illuminate\Container\Container' => 'app',

'Illuminate\Contracts\Container\Container' => 'app',

'Illuminate\Database\ConnectionResolverInterface' => 'db',

'Illuminate\Database\DatabaseManager' => 'db',

'Illuminate\Contracts\Encryption\Encrypter' => 'encrypter',

'Illuminate\Contracts\Events\Dispatcher' => 'events',

'Illuminate\Contracts\Hashing\Hasher' => 'hash',

'log' => 'Psr\Log\LoggerInterface',

'Illuminate\Contracts\Queue\Factory' => 'queue',

'Illuminate\Contracts\Queue\Queue' => 'queue.connection',

'request' => 'Illuminate\Http\Request',

'Laravel\Lumen\Routing\Router' => 'router',

'Illuminate\Contracts\Translation\Translator' => 'translator',

'Laravel\Lumen\Routing\UrlGenerator' => 'url',

'Illuminate\Contracts\Validation\Factory' => 'validator',

'Illuminate\Contracts\View\Factory' => 'view',

];

}

......

lumen服务容器中通过alias方法添加服务别名:

public function alias($abstract, $alias)

{

$this->aliases[$alias] = $abstract;

$this->abstractAliases[$abstract][] = $alias;

}

通过getAlias获得服务的别名:

public function getAlias($abstract)

{

if (! isset($this->aliases[$abstract])) {

return $abstract;

}

if ($this->aliases[$abstract] === $abstract) {

throw new LogicException("[{$abstract}] is aliased to itself.");

}

return $this->getAlias($this->aliases[$abstract]);

通过getAlias我们知道,服务别名是支持递归设置的。

3.3、其他函数简述

服务容器解析一个对象时会触发resolving和afterResolving函数。分别在之前之后触发:

public function resolving($abstract, Closure $callback = null)

{

if (is_string($abstract)) {

$abstract = $this->getAlias($abstract);

}

if (is_null($callback) && $abstract instanceof Closure) {

$this->globalResolvingCallbacks[] = $abstract;

} else {

$this->resolvingCallbacks[$abstract][] = $callback;

}

}

public function afterResolving($abstract, Closure $callback = null)

{

if (is_string($abstract)) {

$abstract = $this->getAlias($abstract);

}

if ($abstract instanceof Closure && is_null($callback)) {

$this->globalAfterResolvingCallbacks[] = $abstract;

} else {

$this->afterResolvingCallbacks[$abstract][] = $callback;

}

}

服务容器中有一些装饰函数,wrap装饰call,factory装饰make:

public function call($callback, array $parameters = [], $defaultMethod = null)

{

return BoundMethod::call($this, $callback, $parameters, $defaultMethod);

}

......

public function wrap(Closure $callback, array $parameters = [])

{

return function () use ($callback, $parameters) {

return $this->call($callback, $parameters);

};

}

服务容器的解析方法和函数之前已经说过,有几种常用的方法,这里就不再一一赘述了。

可以服务容器中flush()方法用于清空容器中所有的服务:

public function flush()

{

$this->aliases = [];

$this->resolved = [];

$this->bindings = [];

$this->instances = [];

$this->abstractAliases = [];

}

Lumen中的服务容器源码实现非常复杂,但是对其工作原理了解清楚之后,看起来也就有些头绪了,每个函数所做的工作也可以结合注释和源码进行理解了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值