nest学习记录 一 基础概念

学习小册:nest通关秘籍

Nest基础概念

controller 路由模块

controller 的方法叫做 handler, 是处理路由和解析请求参数的。

service 业务逻辑处理模块

service 里做业务逻辑的具体实现,比如操作数据库等

Module 模块

像userController和userService,跟bookController,bookSerivce都是属于不同的模块,所以引入了模块的概念,通过@Module声明模块,再通过imports,controllers,providers,exports指明依赖等信息。

dto:data transfer object,数据传输对象,用于封装请求体里数据的对象
entity:对应数据库表的实体
MVC 架构

在这里插入图片描述

nest 应用跑起来后,会从 AppModule 开始解析,初始化 IoC 容器,加载所有的 service 到容器里,
然后解析 controller 里的路由,接下来就可以接收请求了。这种架构叫做 MVC 模式,也就是 model、view、controller
controller 接收请求参数,交给 model 处理(model 就是处理 service 业务逻辑,处理 repository 数据库访问),然后返回 view,也就是响应。

AOP 面向切面编程,在多个请求响应流程中可以复用的逻辑,比如日志记录等

通用逻辑的处理,如中间件,guide,Interceptor,pipe,filter

IOC 反转控制或者叫依赖注入,只要声明依赖,运行时 Nest 会自动注入依赖的实例

IOC之前学习java的时候就有了解。

将手动创建对象实例的操作交给工具去,我们只需要声明对应的依赖关系。
程序启动的时候,工具就会扫描依赖关系,将需要的实例创建放到一个容器中。这种依赖注入就是Dependency Injection,简称DI

从主动创建到被动等待依赖注入,就是Inverse of Controler(IOC)反转控制

  • @Injectable
    比如Service类上面的@Injectable,就是声明这个类可注入到别的类,也可被注入别的类。比如AService中可以注入BService。
  • @Controller,也表示这个类可以被注入,nest也会把它放到ioc里面。

使用IOC容器的时候,可以通过@Inject(xxService)属性声明的方式,也可以使用构造函数参数注入的方式
constructor(private radobly xxService xxx){}

  • @Module声明模块,如
    在这里插入图片描述

  • imports是 需要引入的模块,比如AModule依赖BModule中的某些exports

  • controller是声明路由

  • providers是声明以被注入,也可以注入到其他对象的Service可,providers声明后,便可以在当前module下注入,比如上述AppModule声明了JenkinsService后,就可以在AppService中注入该依赖。

  • exports: 导出的Service(提供给imports[AppModule]的模块用),比如上述的AppService,这时候有BModule中的imports引入AppModule的时候,就不需要在provider中声明AppService,也可以在BService中使用AppService

  • 此外还有全局Service,这样不用声明依赖也可以在任何地方注入。

provider

Nest实现了IOC容器,从入口模块开始扫描,分析Module的依赖关系,自动把对应的Provider注入到目标对象。如
在这里插入图片描述
另一种写法
在这里插入图片描述
provide声明该对象特殊的token,useClass表示创建实例使用的类。
此外可以用字符串,这样的话在生命的时候也需要使用对应的字符串token

在这里插入图片描述
在这里插入图片描述
除此之外还跟java一样可以自定义Bean容器
在这里插入图片描述
使用的时候同注入其他依赖一样即可。
在这里插入图片描述
此外还支持使用useFactory来注入其他的provider
在这里插入图片描述
这其实就是实现一个Person3的类,然后里面依赖了persion和AppService。
此外还支持异步,指定别名,比如TypeOrmModule就是通过useFactory根据传入的options来动态创建数据库连接对象

AOP架构

切面,之前学习java的时候也有了解。就是对通用逻辑的一些封装,使他可以拦截某些handle,比如给参数对象注入page,pageSize,给Insert update的handle的参数对象注入updateUser,updateTime等通用逻辑
nest也实现了一套AOP逻辑。

  • Nest AOP 对一些通用逻辑进行封装,而不影响业务逻辑
    • middleware guide interceptor ExceotionFilter pipe管道
    • middleware express的中间件,可以在请求前后拦截。
    • Interceptro 拦截器, 可以在请求前后拦截,与middleware不同的是,middleware拿不到调用的controller和对应的方法,而拦截器可以,可以通过这个给对应的controller和handle加一些元数据。
    • guide 守卫,在请求前拦截,一般用于鉴权
    • pipe 管道 主要对前端传入参数进行验证,转换等。
    • ExceptionFilter 异常处理,对整个服务抛出的异常进行俘获,已友好的方式返回前端。
    • 上述五种都可以作用于全局或者单个路由,这就是AOP的好处,他们的调用顺序:请求->guide->Interceptor(多个拦截器会依次执行) -> pipe -> handle -> interceptor -> ExceptionFilter(全局异常兜底)
    • middleware是express的概念,nest只是继承了一下,他在最外层调用,所以最终的顺序是:middleware->guide->Interceptro->pipe->处理请求->interceptor->middleware

在这里插入图片描述

  • Nest基于express做了一层封装,实现了IOC,AOP,MVC等架构思想
  • MVC 就是 Model、View Controller 的划分,请求先经过 Controller,然后调用 Model 层的 Service、Repository 完成业务逻辑,最后返回对应的 View。
  • IOC 是指 Nest 会自动扫描带有 @Controller、@Injectable 装饰器的类,创建它们的对象,并根据依赖关系自动注入它依赖的对象,免去了手动创建和组装对象的麻烦。
  • AOP 则是把通用逻辑抽离出来,通过切面的方式添加到某个地方,可以复用和动态增删切面逻辑。
  • Nest 的Middleware、Guard、Interceptor、Pipe、ExceptionFilter 都是 AOP 思想的实现,只不过是不同位置的切面,它们都可以灵活的作用在某个路由或者全部路由,这就是 AOP 的优势。
Nest所有装饰器

在java中叫注解,在Nest中叫装饰器,比如java的@RestController,Nest的@Controller。

Nest提供了一套模块系统,通过@Module声明模块
通过@Controller和@Injectable声明其中的controller和provider。

其中,@Injectable可以作用于任何的class

依赖注入@Inject(xxx),这个xx是制定注入的token名

如果遇到没有容器中没有的对象,注入就会报错,这时候可以再使用一个装饰器@Optional来声明是可选注入。就不会报错
在这里插入图片描述
@Global声明是全局module,这样他生命的provider就可以在全局任何地方注入。

@Catch和@UseFilters是一套可以用来拦截异常报错的组合装饰器。

此外还有@UseGuards,@UseInterceptors @UsePipes等装饰器。

还有@Post @Get @Put @Delete等声明方法方式,@Body @Param @Query @Header获取前端参数,请求头等

此外,handler和class可以通过@SetMetadata指定原数据

在这里插入图片描述
然后可以在guide或者intercleptor中取出来,从而进行一些判断。
在这里插入图片描述
此外可以通过@Req @Request @Res @Responser拿到对应的req, res对象。

@Rediret 在这里插入图片描述
重定向

nest装饰器总结

@Module: 声明 Nest 模块
@Controller:声明模块里的 controller
@Injectable:声明模块里可以注入的 provider
@Inject:通过 token 手动指定注入的 provider,token 可以是 class 或者 string
@Optional:声明注入的 provider 是可选的,可以为空
@Global:声明全局模块
@Catch:声明 exception filter 处理的 exception 类型
@UseFilters:路由级别使用 exception filter
@UsePipes:路由级别使用 pipe
@UseInterceptors:路由级别使用 interceptor
@SetMetadata:在 class 或者 handler 上添加 metadata
@Get、@Post、@Put、@Delete、@Patch、@Options、@Head:声明 get、post、put、delete、patch、options、head 的请求方式
@Param:取出 url 中的参数,比如 /aaa/:id 中的 id
@Query: 取出 query 部分的参数,比如 /aaa?name=xx 中的 name
@Body:取出请求 body,通过 dto class 来接收
@Headers:取出某个或全部请求头
@Session:取出 session 对象,需要启用 express-session 中间件
@HostParm: 取出 host 里的参数
@Req、@Request:注入 request 对象
@Res、@Response:注入 response 对象,一旦注入了这个 Nest 就不会把返回值作为响应了,除非指定 passthrough 为true
@Next:注入调用下一个 handler 的 next 方法
@HttpCode: 修改响应的状态码
@Header:修改响应头
@Redirect:指定重定向的 url
@Render:指定渲染用的模版引擎

自定义装饰器

比如SetMetadata

import { SetMetadata } from '@nestjs/common';

export const TestAA = (...args: string[]) => {
  return SetMetadata('role', args);
};

使用

 @Get()
  @Version('1')
  @TestAA('admin')
  getHello(): string {
    return this.appService.getHello();
  }


在这里插入图片描述
效果一样

组装装饰器,

export const TestBB = (path, args: string[]) => {
  return applyDecorators(Get(path), SetMetadata('role', args), UseGuards());
};

效果跟

@Get('')
@SetMetadata('')
@UseGuards()
xxx(){}

一样

实现@Queyr @Headers装饰器。
本质就是通过createParamDecorator创建一个函数,通过ctx获取req对象,然后通过req获取里面的值,如

export const MyHeaders = createParamDecorator(
  (key: string, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    return key ? req.headers[key.toLowerCase()] : req.headers;
  },
);

export const MyQuery = createParamDecorator(
  (key: string, ctx: ExecutionContext) => {
    const req = ctx.switchToHttp().getRequest();
    return req.query[key];
  },
);

效果跟@Headers和@Query是一样的。

Metadata 和 Reflector

对类,方法,属性,方法参数我们可以设置元数据,
Reflect支持defineMetadata和getMetadata设置和获取某个类的元数据。

Reflect.defineMetadata(metadataKey, metadataValue, class1) //给类加元数据
Reflect.defineMetadata(metadataKey, metadataValue, class1, method) // 给类的method属性加元数据

let result = Reflect.getMetadata(metadataKey, class1);//取出元数据
let result = Reflect.getMetadata(metadataKey, class1, method); //取出元数据

也支持装饰器的使用
比如

@Reflect.metadata(metadataKey, metadataValue)
class C {

  @Reflect.metadata(metadataKey, metadataValue)
  method() {
  }
}

如上,元数据一般存在类上,如果给类的实例属性加元数据,元数据就存在实例上,用类似[[metadata]]的key来存。

metadata也可以再封装一层,本质上是一个函数。

function Type(type){
return Reflect.metadata("design: type", type) //定义类型
}
function ParamTypes(...types) {
    return Reflect.metadata("design:paramtypes", types); // 定义参数类型
}
function ReturnType(type) {
    return Reflect.metadata("design:returntype", type); //定义返回类型
}
class Test1 {
@Type(String)
get name(){}
}

  @Type(Function)
  @ParamTypes(Number, Number)
  @ReturnType(Number)
  add(x, y) {
    return x + y;
  }

接着就可以通过getMetadata取出定义的数据

let obj = new Test1();
let paramTypes = Reflect.getMetadata("design:paramtypes", obj, "add"); 
// [Number, Number]

如上,可以取出参数类型,nest也是基于这个原理,通过Reflect.getMetadata获取定义的参数(注入的类)

@Module @Controller @Injectable

@Module
@Module的基础使用

@Module({
  imports: [
    // 引用依赖的模块,通过imports引用后,对应模块的exports可以在当前module中的Service中注入,而不用声明providers
    CacheModule.register({
      isGlobal: true,
    }),
    // 动态模块
    TypeOrmModule.forRoot({
      type: 'mysql',
      host: Context.mySql.host,
      port: Context.mySql.port,
      username: Context.mySql.username,
      password: Context.mySql.password,
      database: Context.mySql.database,
      entities: [__dirname + '/**/*.entity{.ts,.js}'],
      synchronize: false,
    }),
    UserModule,
    GithubModule,
    WeichatModule,
    ProjectModule,
  ],
  controllers: [AppController, JenkinsController], //只能被注入的对象
  providers: [{ provide: 'AppService', useClass: AppService }, JenkinsService], //可以被注入,也可以注入到其他对象,providers声明后,便可以在当前module下注入
  //通过exports声明的话,UserModule中的exports: [UserService],
  //那么,在import中如果引用了UserModule,比如当前的AppModule,在AppService中也可以注入UserService,而不用在providers声明
  exports: [{ provide: 'AppService', useClass: AppService }],
})
export class AppModule {}

如上,我们通过@Module定义了配置。
{imports:[], controllers: [], providers: [], exports: []}
看下@Module的源码

// Nest 的实现原理就是通过装饰器给 class 或者对象添加元数据,然后初始化的时候取出这些元数据,进行依赖的分析,然后创建对应的实例对象就可以了。
export function Module(metadata: ModuleMetadata): ClassDecorator {
  const propsKeys = Object.keys(metadata);
  validateModuleKeys(propsKeys);

  
  return (target: Function) => {
    for (const property in metadata) {
      if (metadata.hasOwnProperty(property)) {
        // 给类上面绑定元数据,imports, exports, controllers, providers等
        Reflect.defineMetadata(property, (metadata as any)[property], target);
      }
    }
  };
}

Module本质就是一个函数,他将传入的imports,exports等,作为元数据定义在了对应的AppModule这个上面。后面启动服务创建IOC容器的时候,就会拿出这些定义的元数据使用。
类似的@Controller和@Injectable都是同性质的。

 */
export function Controller(
  prefixOrOptions?: string | string[] | ControllerOptions,
): ClassDecorator {
  const defaultPath = '/';

  const [path, host, scopeOptions, versionOptions] = isUndefined(
    prefixOrOptions,
  )
    ? [defaultPath, undefined, undefined, undefined]
    : isString(prefixOrOptions) || Array.isArray(prefixOrOptions)
      ? [prefixOrOptions, undefined, undefined, undefined]
      : [
          prefixOrOptions.path || defaultPath,
          prefixOrOptions.host,
          { scope: prefixOrOptions.scope, durable: prefixOrOptions.durable },
          Array.isArray(prefixOrOptions.version)
            ? Array.from(new Set(prefixOrOptions.version))
            : prefixOrOptions.version,
        ];

  return (target: object) => {
    Reflect.defineMetadata(CONTROLLER_WATERMARK, true, target); // __controller__标识controller
    Reflect.defineMetadata(PATH_METADATA, path, target); // path
    Reflect.defineMetadata(HOST_METADATA, host, target); // host
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, scopeOptions, target);
    Reflect.defineMetadata(VERSION_METADATA, versionOptions, target); //version
  };
}

@Injectable是定义Service的

export function Injectable(options?: InjectableOptions): ClassDecorator {
  return (target: object) => {
    Reflect.defineMetadata(INJECTABLE_WATERMARK, true, target);
    Reflect.defineMetadata(SCOPE_OPTIONS_METADATA, options, target);
  };
}

nest IoC的实现原理是通过给类加装饰器,或者给对象家元数据,初始化的时候取出这些元数据,进行依赖分析,然后创建对应的实例对象就可以了。(声明配置依赖,自动创建)

这里通过Reflect.getMetadata可以知道需要创建哪些实例到IOC容器,但像@Controller这些,我们并没有配置对应的参数元数据,那么nest是如何知道该Controller需要注入哪些Service的呢?
其实上面已经解答了,通过
Reflect.getMetadata("design:paramtypes", obj, "add");,我们可以得到add这个属性的参数类型是[Number Number]
那么对应的,我们也可以通过Reflect.getMetadata("design:paramtypes", AppController,)获取AppController的依赖参数。
这是Typescript的优势,ts支持编译时自动添加metadata元数据。
ts 有一个编译选项叫做 emitDecoratorMetadata,开启它会自动添加一些元数据
(design:type, design:paramtypes, design:returntype)

  • design:type 是 Function,很明显,这个是描述装饰目标的元数据,比如函数,字符串等
  • design:paramtypes 是 [Number],很容易理解,就是参数的类型
  • design:returntype 是 String,也很容易理解,就是返回值的类型

所以在nest里面,当我们通过@Controller装饰Controller类的时候,编译后的代码,会自动给AppController加上这三个元数据,也就是说
,比如

@Controller()
export class AppController {
  constructor(@Inject('AppService') private readonly appService: AppService) {}

  @Get()
  @Version('1')
  @SetMetadata('role', ['admin'])
  getHello(): string {
    return this.appService.getHello();
  }
}

通过Reflect.getMetadata("design:paramtypes", AppController)就可以获取constructor定义的AppService,拿到后就可以知道这个controller依赖哪些service了。
打印出来得到
[ [class AppService] ]

这就是nest的实现原理:通过装饰器给class配置metadata,开启ts的编译选项,获取对应类的依赖,运行的时候可以通过获取这些元数据(需要创建哪些实例,这些类的依赖是什么都能知道),实现依赖扫描,对象创建,依赖注入等等。

nest提供@SetMetadata,可以给class和method添加一些metadata,本质还是Reflect.defineMetadata

export const SetMetadata = <K = string, V = any>(
  metadataKey: K,
  metadataValue: V,
): CustomDecorator<K> => {
  const decoratorFactory = (target: object, key?: any, descriptor?: any) => {
    if (descriptor) {
      Reflect.defineMetadata(metadataKey, metadataValue, descriptor.value);
      return descriptor;
    }
    Reflect.defineMetadata(metadataKey, metadataValue, target);
    return target;
  };
  decoratorFactory.KEY = metadataKey;
  return decoratorFactory;
};

如上我们给getHello设置了@SetMetadata('role', ['admin'])
那么在拦截器或者守卫,就可以取出来。

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  async canActivate(context: ExecutionContext): Promise<any> {
    const handler = context.getHandler(); //对应的getHello方法
    this.reflector.get('role', handler); //admin

可以在guide里面通过IOC注入reflector,context.getHandler可以获取处理的函数,这里是getHello ,然后通过reflector.get就可以得到对应的方法上面的元数据。

总结
  • Nest装饰器的实现原理就是Reflect.getMetadata和defineMetadata,通过在class和method上添加metadata,初始化的时候扫描取出metadata来做出相应的处理。
  • 实例化的对象需要构造参数的params,通过开启ts的emitDecoratorMetadata的选项,ts在编译的时候会自动注入三种元数据,分别是design:type、design:paramtypes、design:returntype,这样就可以通过Reflect.getMetadata(‘design:paramtypes’, obj)获取对应的params类型。
  • 理解了 metadata,nest 的实现原理就很容易搞懂了。

循环依赖处理

如果A Module依赖了B Module,然后B Module也依赖了A Module,那么nest在创建A Module的时候会递归创建他的依赖,到创建B的时候又需要去创建A Module,此时A还没创建完,就是undefined,就会报错。
解决办法:

  • 先单独创建两个Module,使用forwardRef
// A Module
imports([forwardRef(() => BModule),])
// B Module
imports([forwardRef(() => AModule),])

这时候nest会先单独创建两个module,在把引用转发过去。

像Service之间也存在循环依赖的问题。也可以通过forwardRef解决。

//A Service
@Injectable()
export class Aservice {
	constructor(@Injet(forwardRef(()=>BService)) private bService: Bservice){}
}

引入Service就不能用默认的注入方法了,得用forwardRef。

动态模块

Nest和Express关系

Nest的request和response都是基于express,也支持express的中间件机制。

express是基于http模块封装的,处理请求关系,响应的库。
通过app.use一个个中间件来处理请求,但express并没有提供组织代码的架构能力,代码可能写的五花八门。

而nest是一个企业级开发的库,提供了AOP,IOC等能力,并且规定代码的组织形式,对对websocket、graphql、orm 等各种方案都提供了开箱即用的支持。

用node写服务端有三种方式

  • 直接用http模块
  • 使用express,koa库
  • 使用nest这种企业级框架。

就像写java一样,我们都是直接用spring。
nest就是node生态的spring。

实现抽象类

nest并没有和express强关联,而是实现了一个抽象接口和抽象类

export interface HttpServer<
  TRequest = any,
  TResponse = any,
  ServerInstance = any,
> {
  use(
    handler:
      | RequestHandler<TRequest, TResponse>
      | ErrorHandler<TRequest, TResponse>,
  ): any;
  use(
    path: string,
    handler:
      | RequestHandler<TRequest, TResponse>
      | ErrorHandler<TRequest, TResponse>,
  ): any;
  useBodyParser?(...args: any[]): any;
  get(handler: RequestHandler<TRequest, TResponse>): any;
  get(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  post(handler: RequestHandler<TRequest, TResponse>): any;
  post(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  head(handler: RequestHandler<TRequest, TResponse>): any;
  head(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  delete(handler: RequestHandler<TRequest, TResponse>): any;
  delete(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  put(handler: RequestHandler<TRequest, TResponse>): any;
  put(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  patch(handler: RequestHandler<TRequest, TResponse>): any;
  patch(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  all(path: string, handler: RequestHandler<TRequest, TResponse>): any;
  all(handler: RequestHandler<TRequest, TResponse>): any;
  options(handle
  ....}

基于httpServer接口实现抽象类

export abstract class AbstractHttpAdapter<
  TServer = any,
  TRequest = any,
  TResponse = any,
> implements HttpServer<TRequest, TResponse>
{
  protected httpServer: TServer;

  constructor(protected instance?: any) {}

  // eslint-disable-next-line @typescript-eslint/no-empty-function
  public async init() {}

  public use(...args: any[]) {
    return this.instance.use(...args);
  }

  public get(handler: RequestHandler);
  public get(path: any, handler: RequestHandler);
  public get(...args: any[]) {
    return this.instance.get(...args);
  }

  public post(handler: RequestHandler);
  public post(path: any, handler: RequestHandler);
  public post(...args: any[]) {
    return this.instance.post(...args);
  }

  public head(handler: RequestHandler);
  public head(path: any, handler: RequestHandler);
  public head(...args: any[]) {
    return this.instance.head(...args);
  }

然后适配了express和fastify的实现。


/**
 * @publicApi
 */
export class ExpressAdapter extends AbstractHttpAdapter<
  http.Server | https.Server
> {
  private readonly routerMethodFactory = new RouterMethodFactory();
  private readonly logger = new Logger(ExpressAdapter.name);
  private readonly openConnections = new Set<Duplex>();

  constructor(instance?: any) {
    super(instance || express());
  }
...


export class FastifyAdapter extends AbstractHttpAdapter....

Adapter是适配器的意思,nest提供这个抽象类并实现了express和fastify的实现。
也就是说,实现这个抽象类的库都能适配nest。

nest的核心还是AOP,IOC这块,基于什么库(express, fastify)只是请求处理的api不同,区别不大,以后有更好的请求库还是直接兼容切换。

Nest的中间件

nest也有中间件的概念,跟express的不太一样

nest的中间件可以是class也可以是函数的形式,用法其实跟express差不多,支持全局,或者指定路由等。但是拿不到对应的处理的路由和hanler信息。

nest的中间件还有个不同的地方,支持class后,就可以注入对应的依赖了。可以通过@Inject或者构造器注入等,注入对应的实例。还可以指定作用于哪些路由

在这里插入图片描述
如果不需要注入依赖,可以写成函数的形式,这样跟express的中间件就没啥区别,如果需要注入依赖,就需要写成class的形式,可以用nest的依赖注入能力。

  • next

middleware中有个next,而nest也提供了一个装饰器@Next
他们的区别就是:中间件的next用来调用下一个中间件,而nest的@Next,用来调用下一个handler。

中间件跟拦截器有什么区别
  • 拦截器可以通过executionContext拿到目标的class和handler,进而通过reflector拿到他的元数据,而middleware就不可以。
  • 拦截器可以通过rxjs来处理流程。
  • 拦截器更适合处理与具体业务相关的逻辑,而中间件则更适用于通用的处理逻辑。

Rxjs和拦截器Interceptor

内置Pipe

自定义Filter

图解串一串Nest的核心概念

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

coderlin_

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值