symfony 服务配置_了解Symfony捆绑软件配置和服务容器

symfony 服务配置

In this post we’ll cover different ways on how to configure Bundles in Symfony2 and how the dependency injection container works with the configuration. The Bundle configuration and Symfony dependency injection container (also known as service container) can be difficult concepts to grasp when first starting development with Symfony2, especially if dependency injection is not a familiar concept beforehand. Bundle configuration can also be a little bit confusing, since there are multiple ways to do it and the best approach depends on the situation.

在本文中,我们将介绍如何在Symfony2中配置Bundle以及依赖注入容器如何与配置一起工作的不同方法。 Bundle配置和Symfony依赖项注入容器(也称为服务容器)在首次使用Symfony2开始开发时可能很难理解,特别是如果依赖项注入不是预先熟悉的概念时。 捆绑包配置也可能会造成一些混乱,因为这样做的方式有多种,而最佳方法则取决于具体情况。

All of the configuration examples in this post are in YAML. Symfony also supports other configuration formats (XML and PHP array) and they are valid options. I am used to working with YAML because I think it’s more readable than XML, but you do get the benefit of schema validation when using XML. The choice of the configuration format is up to you (or your project team) and there is no right or wrong option here. Just use the one you feel most comfortable with.

这篇文章中的所有配置示例都在YAML中。 Symfony还支持其他配置格式(XML和PHP数组),它们是有效的选项。 我习惯使用YAML,因为我认为它比XML更具可读性,但是当您使用XML时,您确实会受益于模式验证。 配置格式的选择由您(或您的项目团队)决定,这里没有对或错的选择。 只需使用您最喜欢的那种即可。

捆绑创建 (Bundle creation)

A Bundle is a directory containing a set of files (PHP files, stylesheets, JavaScripts, images, …) that implement a single feature (a blog, a forum, etc). In Symfony2, (almost) everything lives inside a bundle.

捆绑包是一个目录,其中包含一组实现单个功能(博客,论坛等)的文件(PHP文件,样式表,JavaScript,图像等)。 在Symfony2中,(几乎)所有内容都生活在一个捆绑包中。

or, in other words, from the docs

或者换句话说,来自文档

A bundle is similar to a plugin in other software, but even better. The key difference is that everything is a bundle in Symfony2, including both the core framework functionality and the code written for your application. Bundles are first-class citizens in Symfony2. This gives you the flexibility to use pre-built features packaged in third-party bundles or to distribute your own bundles. It makes it easy to pick and choose which features to enable in your application and to optimize them the way you want.

捆绑软件类似于其他软件中的插件,但更好。 关键区别在于,所有内容都是Symfony2中的一个捆绑包,包括核心框架功能和为您的应用程序编写的代码。 捆绑包是Symfony2中的一等公民。 这使您可以灵活地使用打包在第三方捆绑软件中的内置功能或分发自己的捆绑软件。 它使您可以轻松地选择和选择要在应用程序中启用的功能,并以所需的方式对其进行优化。

When you create a new bundle, either by auto-generating it (php app/console generate:bundle --namespace=Acme/TestBundle) or manually, you need the BundleNameBundle.php file at the root directory of the bundle. The class in this file, while mostly empty, extends the Symfony core Symfony\Component\HttpKernel\Bundle\Bundle class and it is what you need to register in the AppKernel registerBundles method. When the kernel boots, it creates instances of each bundle and loads the container extension of each bundle (using methods in the parent Bundle class). The container extension is the class in BundleNameExtension.php file inside the DependencyInjection folder of the bundle. The container extension class loads and manages the bundle configuration. The whole extension class is optional and the bundle will work without it, but it’s good to know how things work to better understand the configuration system.

创建新捆绑包时,通过自动生成新捆绑包( php app/console generate:bundle --namespace=Acme/TestBundle )或手动创建,您需要捆绑包根目录中的BundleNameBundle.php文件。 该文件中的类尽管大部分为空,但扩展了Symfony核心Symfony\Component\HttpKernel\Bundle\Bundle类,这是您需要在AppKernel registerBundles方法中registerBundles 。 内核启动时,它将创建每个捆绑包的实例并加载每个捆绑包的容器扩展 (使用父Bundle类中的方法)。 容器扩展名是捆绑软件的DependencyInjection文件夹内BundleNameExtension.php文件中的类。 容器扩展类加载和管理捆绑软件配置。 整个扩展类都是可选的,没有该扩展包,捆绑包也可以工作,但是最好了解事物的工作方式以更好地理解配置系统。

加载捆绑包配置的简便方法 (Loading the bundle configuration, the easy way)

As I stated above, the extension class is optional and there are other ways to configure a bundle. The simplest form of bundle configuration is to configure the parameters and services inside the application’s main configuration file (app/config/config.yml). This is a totally valid option, although you need to understand the implications of this to better understand when this really is appropriate. Having bundle configuration in the main config file makes your bundle tightly coupled to the current application, and therefore not very portable. This might be okay, if the bundle doesn’t have many services or parameters and you know it will be used only in the current application. This said, I really don’t recommend using this approach even then, because things tend to change and more configuration might be needed later on and other developers will probably look for the configuration inside the bundle.

如前所述,扩展类是可选的,还有其他配置包的方法。 捆绑包配置的最简单形式是在应用程序的主配置文件( app/config/config.yml )中配置参数和服务。 这是一个完全有效的选项,尽管您需要了解其含义以更好地了解何时确实合适。 在主配置文件中具有捆绑软件配置可以使您的捆绑软件与当前应用程序紧密耦合,因此移植性不强。 如果捆绑包没有很多服务或参数,并且您知道它将仅在当前应用程序中使用,则可能没问题。 这就是说,我什至不建议使用这种方法,因为情况会发生变化,并且以后可能需要更多配置,其他开发人员可能会在捆绑包中寻找配置。

Another simple method is to have a separate configuration file inside the bundle (Resources/config/services.yml for example) and import it in the main configuration. At the beginning of config.yml there are usually some imports already, so all you need to do is add one more import to the list that points to your bundle configuration file. Note, the path is relative to the main config file. Here is a small example:

另一个简单的方法是在捆绑包中有一个单独的配置文件(例如, Resources/config/services.yml )并将其导入主配置中。 在config.yml的开头,通常已经有一些导入,因此您需要做的就是向列表中添加另一个导入,该列表指向您的捆绑软件配置文件。 注意,该路径是相对于主配置文件的。 这是一个小例子:

imports:
    - { resource: parameters.yml }
    - { resource: security.yml }
    - { resource: ../../src/Cvuorinen/ExampleBundle/Resources/config/services.yml }

加载包配置,语义方式 (Loading bundle configuration, the semantic way)

As noted above, the extension class handles loading the bundle configuration. It creates an instance of a Configuration class, from the Configuration.php file also located in the DependencyInjection folder. The Configuration class is used to validate and process the main configuration (from config files in app/config/) related to the bundle. If there is no configuration related to your bundle in the main config, the Configuration class can be left as is (more on this after the next paragraph).

如上所述,扩展类处理加载捆绑软件配置。 它从DependencyInjection文件夹中的Configuration.php文件创建一个Configuration类的实例。 Configuration类用于验证和处理与捆绑包相关的主要配置(来自app/config/配置文件)。 如果在主配置中没有与您的软件包相关的配置,则可以保留Configuration类(在下一段之后对此进行更多说明)。

After processing the main configuration, the extension class loads the bundle specific configuration from Resources/config folder of the bundle using a loader appropriate for the config file type (YamlFileLoader when using YAML). Any services defined in the bundles configuration file can use the parameters from the applications main config file (and global parameters) and will be added to the service container. After this the services are available for your controllers, CLI commands or any other “container aware” part of the application. You just need to call the get method of the container with the service name specified in the service configuration.

处理完主要配置后,扩展类使用适合于配置文件类型(使用YAML时为YamlFileLoader )的加载器,从捆绑软件的Resources/config文件夹中加载捆绑软件特定的配置 。 捆绑软件配置文件中定义的任何服务都可以使用应用程序主配置文件中的参数 (以及全局参数 ),并将其添加到服务容器中 。 此后,服务可用于您的控制器,CLI命令或应用程序的任何其他“容器感知”部分。 您只需要使用服务配置中指定的服务名称来调用容器的get方法。

The real power of the semantic configuration comes from the way it can be used to process, merge and validate configuration options from the application’s main configuration and pass those on to the bundles service configuration. This is especially useful when creating bundles that are intended for wider audiences, like open source bundles, but might also be good for in-house distribution inside companies. This way you can provide a few simple and well documented configuration options (with default values where appropriate), that users of the bundle can set in the main configuration file and then the bundles extension class can process and validate them so that the user of the bundle does not need to touch any configuration inside the bundle.

语义配置的真正威力来自于可用于处理,合并和验证应用程序主配置中的配置选项并将其传递给捆绑服务配置的方式。 当创建面向更广泛受众的捆绑包(例如开源捆绑包)时,这特别有用,但对于在公司内部进行内部分发也可能是好的。 这样,您可以提供一些简单且有据可查的配置选项(适当时使用默认值),该捆绑包的用户可以在主配置文件中进行设置,然后捆绑包扩展类可以对其进行处理和验证,以便捆绑包无需触摸捆绑包内的任何配置。

加载捆绑包配置,正确的方法? (Loading bundle configuration, the right way?)

Of course, there is no one right way to do it. But most of the time, the easy way is not the way to go since the whole idea of bundles is to make them reusable across different applications. And it’s also quite easy to auto-generate all the required files for the semantic configuration using the Symfony CLI command:

当然,没有正确的方法来做到这一点。 但是在大多数情况下,简单的方法不是可行的方法,因为捆绑软件的全部思想是使捆绑软件可在不同的应用程序之间重用。 而且,使用Symfony CLI命令自动生成语义配置所需的所有文件也非常容易:

$ app/console generate:bundle

When first starting development with Symfony, I would say go with the easy way or the (auto-generated) semantic way without any configuration processing. When you get more familiar with the bundle system and start creating bundles that you plan on distributing to other people, then learn more about the validation and merging of the main configuration.

第一次开始使用Symfony进行开发时,我想说的是简单的方法或(自动生成的)语义方法,无需任何配置处理。 当您更加熟悉捆绑软件系统并开始创建计划分发给其他人的捆绑软件时,请进一步了解主要配置验证和合并

配置文件结构 (Configuration file structure)

Ok, now that we know how to load a configuration file, let’s figure out what to put in there. The configuration consists mainly of two types of information, parameters and services.

好的,现在我们知道了如何加载配置文件,让我们找出放置在其中的内容。 该配置主要包括两种类型的信息, 参数服务

Parameters are any static values that your bundle requires. Things like login credentials, API keys, host names or URLs of external services, you probably get the idea. Parameters can be any scalar values, (e.g. strings, numbers, booleans) and arrays and they are specified under the parameters key in the configuration. It is also good practice to specify service class names as parameters, as this allows extending the bundle and, for example, overriding certain services from another bundle. You can retrieve the parameters from the container by calling the getParameter method with the parameter name as an argument, but most of the time you will be passing the parameters as arguments to services in the configuration.

参数是捆绑软件需要的任何静态值。 诸如登录凭据,API密钥,主机名或外部服务的URL之类的东西,您可能会明白。 参数可以是任何标量值(例如字符串,数字,布尔值)和数组,它们可以在配置中的parameters键下指定。 最好将服务类名称指定为参数,因为这样可以扩展捆绑包,例如,从另一个捆绑包覆盖某些服务。 您可以通过使用参数名称作为参数调用getParameter方法来从容器中检索参数,但是大多数情况下,您会将参数作为参数传递给配置中的服务。

Services are the classes that hold the business logic of your bundle. By defining them in the configuration file, you can harness the power of the dependency injection container, i.e. inject parameters and other services into them automatically, and later retrieve them from the container fully operational with a single line of code. If you are not very familiar with dependency injection, I suggest you read about it a little bit first, there are many good explanations about it elsewhere so I will not cover it in detail here.

服务是保存捆绑软件业务逻辑的类。 通过在配置文件中定义它们,您可以利用依赖项注入容器的功能,即自动将参数和其他服务注入到它们中,然后从容器中检索它们,并且只需一行代码即可完全操作。 如果您对依赖注入不是很熟悉,建议您先阅读一些有关它的内容,在其他地方对此有很多 很好的 解释 ,因此在此不做详细介绍。

By convention, the parameters and services are namespaced with the bundle name (in snake case), but this is not enforced in any way so you are free to name them as you like. Although I would recommend using the bundle name namespacing to avoid any collisions with other bundles.

按照约定, 参数服务使用捆绑包名称(在snake情况下 )命名空间,但是并没有以任何方式强制实施,因此您可以随意命名它们。 尽管我建议使用捆绑软件名称命名空间以避免与其他捆绑软件发生任何冲突。

Here is a small example of a services.yml file from an example bundle:

这是示例捆绑包中的services.yml文件的一个小示例:

parameters:
    cvuorinen_example.greeter.class: Cvuorinen\ExampleBundle\Service\Greeter
    cvuorinen_example.greeter.greeting: "Hello"

services:
    cvuorinen_example.greeter:
        class: %cvuorinen_example.greeter.class%
        arguments: [%cvuorinen_example.greeter.greeting%]

Here we define one service (called cvuorinen_example.greeter). We have two parameters, the first one holds the class name of the Greeter service and is set as the class parameter of the service, and the other one is a string value that will be passed to the service as a constructor argument using the arguments parameter array of the service.

在这里,我们定义了一项服务(称为cvuorinen_example.greeter )。 我们有两个参数,第一个参数保存Greeter服务的类名,并设置为该服务的class参数,另一个参数是一个字符串值,将使用arguments参数将其作为构造函数参数传递给该服务服务数组。

As you can see, the defined parameters can be referenced in the configuration by wrapping them with %-characters. You can also reference parameters from the main configuration, and also from other bundles (although be aware that this again makes your bundle coupled to the other bundle). You can read more about parameters in the Symfony documentation.

如您所见,可以通过在配置中使用%字符包装来引用已定义的参数。 您还可以从主要配置以及其他捆绑软件中引用参数(尽管请注意,这又使捆绑软件与另一个捆绑软件耦合)。 您可以在Symfony文档中阅读有关参数的更多信息。

You can test and debug your configuration with the CLI command:

您可以使用CLI命令测试和调试配置:

$ app/console container:debug

This will construct and merge the bundle configurations and print out all the registered services, or an error message in case there is something wrong with the configuration. There is also a --parameters option to print out all the parameters.

这将构造并合并捆绑软件配置,并打印出所有已注册的服务,如果配置有问题,则会显示一条错误消息。 还有一个--parameters选项可以打印出所有参数。

放在一起 (Putting it all together)

Now that we have a working service configuration, let’s see how this can be used in a controller.

现在我们有了一个有效的服务配置,让我们看看如何在控制器中使用它。

First, here is our very simple Greeter service:

首先,这是我们非常简单的Greeter服务:

namespace Cvuorinen\ExampleBundle\Service;

class Greeter
{
    public function __construct($greeting)
    {
        $this->greeting = $greeting;
    }

    public function greet($name)
    {
        return $this->greeting . ' ' . $name;
    }
}

In this post I will focus on the service configuration, so I will not cover router configuration in detail. You can read more about it in the official documentation if you are not familiar with it. But suppose we have a route with pattern /hello/{name}, that points to a DefaultController inside our example bundle. Then our controller might look something like this:

在本文中,我将重点介绍服务配置,因此,我将不详细介绍路由器配置。 如果您不熟悉它,可以在官方文档中阅读有关它的更多信息。 但是假设我们有一条路由,其模式为/hello/{name} ,它指向示例捆绑包中的DefaultController 。 然后我们的控制器可能看起来像这样:

namespace Cvuorinen\ExampleBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Response;

class DefaultController extends Controller
{
    public function indexAction($name)
    {
        $greeter = $this->get('cvuorinen_example.greeter');

        return new Response(
            $greeter->greet($name)
        );
    }
}

As you can see, there is no logic in the controller, it only retrieves a service from the container and calls a method on that service and passes along a parameter from the request. The controller does not need to know how to construct the Greeter service, the dependency injection container does all the work for it. This allows us to keep the controllers “skinny” and all the business logic and dependency management elsewhere.

如您所见,控制器中没有逻辑,它仅从容器中检索服务并在该服务上调用方法,并从请求中传递参数。 控制器不需要知道如何构造Greeter服务,依赖项注入容器会为此做所有工作。 这使我们可以使控制器保持“App.svelte”,并使所有业务逻辑和依赖管理保持在其他位置。

Now, suppose we wanted to change the greeting, all we need to do is change the parameter inside the configuration file. This might not seem like a big deal with this example, but in a real world application you will probably reuse the same services in many places of the application. With this perspective, the benefits are quite obvious.

现在,假设我们要更改问候语,我们要做的就是更改配置文件中的参数。 在这个示例中,这似乎没什么大不了的,但是在实际应用程序中,您可能会在应用程序的许多地方重用相同的服务。 从这个角度来看,好处是显而易见的。

那么依赖项呢? (What about the dependencies!?)

Ok, if you have been paying attention, you might have noticed that our example didn’t really inject any dependencies, only a simple parameter. This is true, I wanted to keep the first example as simple as possible. Now, what if we need to make a multilingual application? We will have to use some kind of translation system and the Greeter service needs it to translate the greeting.

好的,如果您一直在注意,您可能已经注意到我们的示例实际上并没有注入任何依赖关系,而只是注入了一个简单的参数。 的确如此,我想使第一个示例尽可能简单。 现在,如果我们需要进行多语言应用程序怎么办? 我们将不得不使用某种翻译系统,而Greeter服务需要它来翻译问候语。

First, let’s change the service class like this:

首先,让我们像这样更改服务类:

namespace Cvuorinen\ExampleBundle\Service;

use Symfony\Component\Translation\TranslatorInterface;

class Greeter
{
    public function __construct($greeting, TranslatorInterface $translator)
    {
        $this->greeting = $greeting;
        $this->translator = $translator;
    }

    public function greet($name)
    {
        return $this->translator->trans($this->greeting) . ' ' . $name;
    }
}

Then let’s change the configuration to inject a translator service like this:

然后让我们更改配置以注入这样的翻译器服务:

parameters:
    cvuorinen_example.greeter.class: Cvuorinen\ExampleBundle\Service\Greeter
    cvuorinen_example.greeter.greeting: "Hello"

services:
    cvuorinen_example.greeter:
        class: %cvuorinen_example.greeter.class%
        arguments:
            - %cvuorinen_example.greeter.greeting%
            - @translator

Otherwise this is exactly the same as the previous example, the only difference is in the arguments section of the greeter service configuration. Here we have added another constructor argument, this time it is another service. Services can be referenced in the configuration by prefixing them with the @-character. We are using the default Symfony Translator service, which has the name “translator”. Some core services do not have a namespace in their name, but you could just as well inject a custom translator service with @cvuorinen_example.translator for example.

否则,这与前面的示例完全相同,唯一的区别在于greeter服务配置的arguments部分。 在这里,我们添加了另一个构造函数参数,这次是另一个服务。 可以在配置中引用服务,方法是在服务之前添加@字符。 我们正在使用默认的Symfony Translator服务 ,其名称为“ translator”。 一些核心服务的名称中没有名称空间,但是您也可以使用@cvuorinen_example.translator注入自定义翻译器服务。

注入所有东西 (Injecting all the things)

So far I have only covered the “basic” injection method, constructor injection. The Symfony service container can also be used with setter injection and property injection.

到目前为止,我仅介绍了“基本”注入方法,即构造函数注入 。 Symfony服务容器还可以与setter注入属性注入一起使用

Setter injection means that the service class has a separate setter method for a dependency (because it might be optional, for example). This can be configured by adding a calls parameter to the service configuration that is an array of method calls that will be executed after the service has been constructed. Property injection means that the class has public properties and the dependency can be set into it from outside. This can be achieved with properties parameter in the service configuration. More information about the different injection types can be found in the documentation.

设置器注入意味着服务类具有用于依赖项的单独的设置器方法(例如,因为它可能是可选的)。 可以通过在服务配置中添加一个calls参数来配置此参数,该参数是在构造服务之后将执行的方法调用的数组。 属性注入意味着该类具有公共属性,并且可以从外部将其设置为依赖项。 这可以通过服务配置中的properties参数来实现。 有关不同注射类型的更多信息,请参见文档

The service configuration can also use another service as a factory to create a service. The factory can be another service inside your own bundle, but you can also use this feature with some existing services. One example is that if your service needs a certain Doctrine repository, instead of injecting the entity manager service, you can use the entity manager service as a factory to define the repository class as a service. This way, your service class only needs to add a dependency to the repository class and not the entire entity manager. This makes the service more portable and more easily testable, since you will only need to mock the repository class.

服务配置还可以将其他服务用作工厂来创建服务。 工厂可以是您自己的捆绑软件中的另一项服务,但是您也可以将此功能与某些现有服务一起使用。 一个示例是,如果您的服务需要某个Doctrine存储库,而不是注入实体管理器服务,则可以将实体管理器服务用作工厂,以将存储库类定义为服务。 这样,您的服务类仅需要向存储库类添加一个依赖项,而不是整个实体管理器。 这使服务更可移植,更易于测试,因为您只需要模拟存储库类。

Services can also be declared private. This means they will not be available from the container using the get method, they can only be used as arguments injected to other services. This can be done by setting public: false in the service configuration.

服务也可以声明为私有。 这意味着使用get方法将无法从容器中使用它们,它们只能用作注入到其他服务的参数。 这可以通过在服务配置中设置public: false来完成。

Here is a final example that shows how we can define an entity repository as a service using the Doctrine entity manager as a factory and injecting it into another service using setter injection. The entity repository service is also declared private, so it will not be available through the container.

这是最后一个示例,展示了如何使用Doctrine实体管理器作为工厂将实体存储库定义为服务,并使用setter注入将其注入到另一个服务中。 实体存储库服务也被声明为私有,因此将无法通过容器使用。

parameters:
    cvuorinen_example.item_repository.class: Doctrine\ORM\EntityRepository
    cvuorinen_example.item_repository.entity: "CvuorinenExampleBundle:Item"
    cvuorinen_example.items.class: Cvuorinen\ExampleBundle\Service\Items

services:
    cvuorinen_example.item_repository:
        class: %cvuorinen_example.item_repository.class%
        public: false
        factory_service: doctrine.orm.entity_manager
        factory_method: getRepository
        arguments: [%cvuorinen_example.item_repository.entity%]
    cvuorinen_example.items:
        class: %cvuorinen_example.items.class%
        calls:
            - [setRepository, [@cvuorinen_example.item_repository]]

结论 (Conclusion)

This turned out to be quite a long post and there is still so much more to the Symfony bundle configuration and dependency injection container that I have not covered. Hopefully this will help you understand the Symfony bundle system a little bit better and encourage you to use dependency injection more. I hope to cover more advanced topics in a future post, such as overriding bundle configuration (bundle inheritance), sharing parameters between bundles and defining controllers as services.

事实证明,这是一篇很长的文章,而且Symfony捆绑包配置和依赖项注入容器还有很多我还没有介绍的内容。 希望这可以帮助您更好地了解Symfony捆绑系统,并鼓励您更多地使用依赖项注入。 我希望在以后的文章中介绍更高级的主题,例如,覆盖捆绑配置(捆绑继承),在捆绑之间共享参数以及将控制器定义为服务。

Mean while, you can learn more about the service container and dependency injection from the Symfony documentation.

同时,您可以从Symfony文档中了解有关服务容器依赖项注入的更多信息。

One last thing. Even though the service container itself is a service that can be referenced in the configuration and injected into other services, don’t do that. That is really the anti-pattern of dependency injection, as the services will then be responsible of their own dependencies again and also the services then become tightly coupled to the container itself.

最后一件事。 即使服务容器本身是可以在配置中引用并注入到其他服务中的服务,也不要这样做。 这实际上是依赖注入的反模式,因为服务将再次负责其自身的依赖,并且服务随后将紧密耦合到容器本身。

翻译自: https://www.sitepoint.com/understanding-symfony-bundle-configuration-service-container/

symfony 服务配置

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值