nest.js 项目快速入门(万字总结)

start

正常来说,我们一般是在 controller 中书写我们的处理请求代码,但是在 Nest 中 controller 不能写处理请求的代码,而是在 service 中书写我们的代码,故 controller 中只负责调用对应的 service 中的处理函数(有点像路由层~)

# 创建项目
nest new project-name

cd nest

yarn run start

初始化文件说明

src
├── app.controller.spec.ts // app.controller.ts 的测试文件
├── app.controller.ts // 描述路由过程的文件
├── app.module.ts // 通过导入要在项目中使用的模块来构建应用程序
├── app.service.ts // app.controller.ts 中使用的业务逻辑
└── main.ts // 首次执行代码的入口
nest 以模块为单位管理每个功能。

hot load(webpack hmr)

引入 Webpack 相关依赖

yarn add -D  webpack-node-externals run-script-webpack-plugin webpack

创建 webpack.config.js


const nodeExternals = require('webpack-node-externals');
const { RunScriptWebpackPlugin } = require('run-script-webpack-plugin');

module.exports = function (options, webpack) {
  return {
    ...options,
    entry: ['webpack/hot/poll?100', options.entry],
    externals: [
      nodeExternals({
        allowlist: ['webpack/hot/poll?100'],
      }),
    ],
    plugins: [
      ...options.plugins,
      new webpack.HotModuleReplacementPlugin(),
      new webpack.WatchIgnorePlugin({
        paths: [/\.js$/, /\.d\.ts$/],
      }),
      new RunScriptWebpackPlugin({ name: options.output.filename, autoRestart: false }),
    ],
  };
};

将脚本添加到 package.json

{
	"start:dev": "webpack --config webpack.config.js --watch",
}

最后,在 main 中添加 webpack 相关命令

import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

declare const module: any;

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);

  if (module.hot) {
    module.hot.accept();
    module.hot.dispose(() => app.close());
  }
}
bootstrap();

集成 swagger

引入依赖

yarn add @nestjs/swagger

修改主入口文件

import { NestFactory } from "@nestjs/core";
import { SwaggerModule, DocumentBuilder } from "@nestjs/swagger";
import { AppModule } from "./app.module";

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  const config = new DocumentBuilder()
    .setTitle("Swagger example")
    .setDescription("The swagger API description")
    .setVersion("1.0.0")
    .addTag("example")
    .build();
  const document = SwaggerModule.createDocument(app, config);
  SwaggerModule.setup("docs", app, document);

  await app.listen(3000);
}
bootstrap();

这样可以算是基础的集成了 Swagger,但是要想有更清晰明了的展示方式还需要对 controllerdto 等进行一些数据声明

类型和参数

controller:

// app.controller
import { CreateUserDto } from "@/dto/app.dto";
import { ApiTags, ApiOperation } from "@nestjs/swagger";
@ApiTags("示例")
@Controller("/example")
export class AppController {
  constructor(private readonly appService: AppService) {}

  @ApiOperation({ summary: "示例请求" })
  @Get()
  getHello(@I18nLang() lang: string): string {
    return this.appService.getHello(lang);
  }

  @ApiOperation({ summary: "创建用户" })
  @Post("create")
  @UseInterceptors(FileInterceptor("file"))
  createUser(@Body() userData: CreateUserDto) {
    return this.appService.createUser(userData);
  }
}

dto:

// app.dto
import { ApiProperty } from "@nestjs/swagger";
export class CreateUserDto {
  @ApiProperty({ description: "用户名" })
  readonly name: string;
  @ApiProperty({ description: "性别" })
  readonly gender?: string;
  @ApiProperty({ description: "密码" })
  readonly pwd: string;
}

现在 swagger 文档最很清晰明了了

使用 Prisma(ora) 进行数据库管理

yarn add prisma -D

# 初始化
yarn prisma init

连接 mysql

datasource db {
  // 改成 mysql
  provider = "mysql"
  url      = env("DATABASE_URL")
}

进入 .env 文件

# 初始化
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"

# 修改为(大写部分需要替换成本地的数据库)
mysql://USER:PASSWORD@HOST:PORT/DATABASE

验证是否配置成功

yarn prisma validate

生成 SQL 迁移文件并针对数据库运行它们

# init 为迁移名
yarn prisma migrate dev --name init

安装并生成 Prisma 客户端。

在安装过程中,Prisma 会自动为您调用 prisma generate 命令
在每次更改 Prisma 模型后运行此命令来更新生成的 Prisma 客户端。

yarn add @prisma/client

# generate
yarn prisma generate

controller 中使用

// 基础使用
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();

这里我们将 Prisma 引入 nest

// src/db/mysql.service.ts
import { Injectable, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

// OnModuleInit 是可选的 - 如果省略它,Prisma 将在第一次调用数据库时延迟连接
@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit {
  async onModuleInit() {
    await this.$connect()
      .then(() => {
        console.log("[MYSQL]连接成功...");
      })
      .catch((err) => {
        console.log("[MYSQL]连接失败..."), err);
      });
  }
}

// 延迟连接
@Injectable()
export class PrismaService extends PrismaClient {
  async onModuleInit() {
    await this.$connect()
      .then(() => {
        console.log("[MYSQL]连接成功...");
      })
      .catch((err) => {
        console.log("[MYSQL]连接失败..."), err);
      });
  }
}

配置路径别名

使用 module-alias 进行配置

# 引入库
yarn add module-alias

package.json 进行配置

{
  "_moduleAliases": {
    "@": "dist"
  }
}

这里的 src 需要修改为打包后的目录,比如 dist

然后在入口文件中引入这个库

import "module-alias/register";

到这里我们的路径别名已经配置完成了,但是如果使用的是 ts,还需要在 tsconfig.json 文件中配置

"baseUrl": "./",
"paths": {
  "@/*": ["./src/*"]
}

loggerMiddler

局部中间件

首先,在 common/middleware 中新建 logger.middler.ts 文件

这里先使用 class 方法实现简单的中间件功能

import { Injectable, NestMiddleware } from "@nestjs/common";
import { Request, Response, NextFunction } from "express";
import { cyan } from "kolorist";

@Injectable()
export class LoggerMiddleware implements NestMiddleware {
  async use(req: Request, res: Response, next: NextFunction) {
    const start = Date.now();
    await next();
    const ms = Date.now() - start;

    // example: GET //v1/example/find 200 0ms
    console.log(
      cyan(`${req.method} ${req.baseUrl}/${req.url} ${res.statusCode} ${ms}ms`),
    );
  }
}

接着,我们在 app.module 中应用中间件

export class AppModule {
  configure(consumer: MiddlewareConsumer) {
    consumer.apply(LoggerMiddleware).forRoutes("example");
  }
}

到此,我们的局部中间件已经完成了。当然,由于这个中间件功能比较简单(它没有成员,没有其他方法,也没有依赖项),
可以使用简单的函数实现–功能性中间件

import { Request, Response, NextFunction } from "express";
import { cyan } from "kolorist";

export async function LoggerMiddleware(
  req: Request,
  res: Response,
  next: NextFunction,
) {
  const start = Date.now();
  await next();
  const ms = Date.now() - start;

  // example: GET //v1/example/find 200 0ms
  console.log(
    cyan(`${req.method} ${req.baseUrl}/${req.url} ${res.statusCode} ${ms}ms`),
  );
}
全局中间件

要注册中间件,就把上面在 app.module 中应用中间件改为在入口文件中使用

import { LoggerMiddleware } from "@/common/middleware/logger.middleware";

const app = await NestFactory.create(AppModule);
app.use(LoggerMiddleware);
await app.listen(3000);

异常过滤

要使用 filter,我们需要在 common 新建一个 http-exception.filter

import {
  ArgumentsHost,
  Catch,
  ExceptionFilter,
  HttpException,
} from "@nestjs/common";

@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
  catch(exception: HttpException, host: ArgumentsHost) {
    // 获取请求上下文
    const ctx = host.switchToHttp();
    // 获取请求上下文中的 response 对象
    const response = ctx.getResponse();
    // 获取异常状态码
    const status = exception.getStatus();

    const exceptionMsg = exception.getResponse() as any;
    const msgLength = exceptionMsg.message.length;
    const messageLen = exceptionMsg.message[0].length;
    const finalMsg =
      msgLength > messageLen ? exceptionMsg.message : exceptionMsg.message[0];

    const message = finalMsg
      ? finalMsg
      : `${status >= 500 ? "Service Error" : "Client Error"}`;

    const errorResponse = {
      data: [],
      message: message,
      code: -1,
    };

    // 设置返回的状态码, 请求头,发送错误信息
    response.status(status);
    response.header("Content-Type", "application/json; charset=utf-8");
    response.send(errorResponse);
  }
}

在全局注册使用

import { HttpExceptionFilter } from "@/common/filters/http-exception.filter";

app.useGlobalFilters(new HttpExceptionFilter());

守卫

pipe

数据校验比较好的一点就是先对请求的数据进行校验,如果不符规则就直接丢出错误。
这样能够让我们能够更好的专注于逻辑层面,而不是要先对数据进行 if 判断其类型

数据校验主要由 Nest 内置的管道 ValidationPipe 完成

先全局注册管道

yarn add class-validator class-transformer
// main
import { ValidationPipe } from "@nestjs/common";
app.useGlobalPipes(new ValidationPipe());

接着在 controller 中使用,注意我们在这里使用到了 DTO(一种用于描述数据模型和数据传输的对象)

使用 DTO 可以将这些不同的数据格式和方式转换成一种统一的、标准化的方式

import { createUserDto } from "@/common/dto/app.dto";

@Controller("/example")
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Post("create")
  @ApiOperation({ summary: "创建用户" })
  @UseInterceptors(FileInterceptor("file"))
  createUser(@Body() userData: createUserDto) {
    return this.appService.createUser(userData);
  }
}

接着,我们来看看那 dto

import { ApiProperty } from "@nestjs/swagger";
import { IsNotEmpty, Length } from "class-validator";

export class createUserDto {
  @ApiProperty({ description: "用户名" })
  @Length(5, 10, {
    message: "用户名在 5 个到 10 个之间",
  })
  readonly name: string;

  readonly gender?: string;

  @ApiProperty({ description: "密码" })
  @IsNotEmpty({ message: "用户密码必填" })
  readonly pwd: string;
}

这样就完成了我们的数据校验了

集成 i18n

引入依赖

yarn add nestjs-i18n

添加翻译文件,目录结构如下

src
└── i18n
    ├── en
    │   └── test.json
    └── zh
        └── test.json

app.module 中引入

import { Module } from '@nestjs/common';
import {join} from 'path';
import {
  I18nModule,
  QueryResolver,
  HeaderResolver,
  AcceptLanguageResolver,
} from "nestjs-i18n";

@Module({
  imports: [
    I18nModule.forRoot({
      fallbackLanguage: "en",
      loaderOptions: {
        path: join(__dirname, "/i18n/"),
        watch: true,
      },
      resolvers: [
        { use: QueryResolver, options: ["lang"] },
        AcceptLanguageResolver,
        new HeaderResolver(["x-lang"]),
      ],
    }),
  ],
})

接下来就可以使用了

controller

import { Controller, Get } from "@nestjs/common";
import { AppService } from "@/services/app.service";
import { I18nLang } from "nestjs-i18n";

@Controller("/example")
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(@I18nLang() lang: string): string {
    // 需要一个语言参数:zh|en
    return this.appService.getHello(lang);
  }
}

service

import { Injectable } from "@nestjs/common";
import { PrismaService } from "@/db/mysql.service";
import { UserInter } from "@/interface/user.interface";
import { I18nLang, I18nService } from "nestjs-i18n";

@Injectable()
export class AppService {
  constructor(
    private prisma: PrismaService,
    private readonly i18n: I18nService,
  ) {}
  private readonly users: UserInter[];

  getHello(@I18nLang() lang: string): string {
    return this.i18n.translate("test.Hello", { lang });
  }
}

集成了 i18n 后,可以在 apifox 中使用 param(lang)、headers(Accept-Language)返回相应的 i18n 内容

日志

  • 基础使用

在 main 中可以对 logger 一些基础的配置,比如是否启用,日志是否需要颜色,日志等级等等

const app = await NestFactory.create<NestExpressApplication>(AppModule, {
  rawBody: true,
  // 是否禁用日志记录,以及启用等级
  // logger: false,
  // logger: ["error", "warn",],
  // logger: console,
});

如果日志打印时不需要颜色,只需在 env 中修改变量即可,只要是非空就不会有颜色

# NO_COLOR="1"
  • 自定义日志

如果想要使用自定义日志,那么我们就必须要实现 LoggerService 接口

自定义我们的日志系统

import { LoggerService } from "@nestjs/common";

export class MyLogger implements LoggerService {
  /**
   * 写log'等级日志.
   */
  log(message: any, ...optionalParams: any[]) {
    console.log(message, optionalParams);
  }

  /**
   * 写'error'等级日志.
   */
  error(message: any, ...optionalParams: any[]) {
    console.log(message, optionalParams);
  }
  ...
}

这里实例化我们自己的日志

import { MyLogger } from "@/common/utils/logger";

const app = await NestFactory.create<NestExpressApplication>(AppModule, {
  rawBody: new MyLogger(),
});

当然,这里我们需要重写我们的日志,所以我们也可以选择通过扩展内置的ConsoleLogger类并覆盖默认实现的选定行为

import { ConsoleLogger } from "@nestjs/common";

export class MyLogger extends ConsoleLogger {
  // log、error...
  log(message: any, stack?: string, context?: string) {
    // add your tailored logic here
    console.log(message, stack, context);
    super.error(...arguments);
  }
}
  • 依赖注入
  • 使用记录器进行应用程序日志记录
  • 使用扩展 logger
yarn add winston nest-winston winston-daily-rotate-file

使用 minio 作为存储图片

上传图片

使用这个方法上传的时候,需要对已有的文件进行判断是否有相同文件名(或者使用时间戳),如果有重名的文件会对其进行覆盖操作

// minio.service
import { Injectable } from "@nestjs/common";
import * as Minio from "minio";

@Injectable()
export class MinioService {
  private readonly minioClient: Minio.Client;

  constructor() {
    this.minioClient = new Minio.Client({
      // 服务ip
      endPoint: "192.168.18.24",
      // 服务端口
      port: 9000,
      useSSL: false,
      accessKey: "minioadmin",
      secretKey: "minioadmin",
    });
  }

  async uploadFile(bucketName: string, objectName: string, stream: Buffer) {
    const res = await this.minioClient.bucketExists(bucketName);
    console.log(res);
    console.log(objectName, stream);
    await this.minioClient.putObject(
      bucketName,
      objectName,
      stream,
      function (e) {
        if (e) {
          console.log(e);
          return "error";
        } else {
          // 如果重名也会成功
          return "Successfully uploaded the buffer";
        }
      },
    );
  }
}

在 controller 中调用文件 minio 的文件上传接口

// minio.controller
import {
  Controller,
  Post,
  UseInterceptors,
  UploadedFile,
} from "@nestjs/common";
import { MinioService } from "@/services/minio.service";
import { FileInterceptor } from "@nestjs/platform-express";

@Controller("minio")
export class MinioController {
  constructor(private readonly minioService: MinioService) {}

  @Post("upload")
  @UseInterceptors(FileInterceptor("file"))
  uploadFile(@UploadedFile() file: any) {
    return this.minioService.uploadFile(
      "images",
      file.originalname,
      file.buffer,
    );
  }
}

上传文件到本地

引入依赖

yarn add -D @types/multer

app.module 中应用 multer 中间件


import { diskStorage } from "multer";
import { Module } from "@nestjs/common";
import { join, extname } from "node:path";
import { PrismaService } from "@/db/mysql.service";
import { AppService } from "@/services/app.service";
import { MinioService } from "@/services/minio.service";
import { MulterModule } from "@nestjs/platform-express";
import { AppController } from "@/controllers/app.controller";
import { MinioController } from "@/controllers/minio.controller";

@Module({
  imports: [
    MulterModule.register({
      storage: diskStorage({
        // 指定文件的存储目录
        destination: join(__dirname, "/uploads"),
        // 时间戳来重命名
        filename: (req, file, callback) => {
          const fileName = `${
            new Date().getTime() + extname(file.originalname)
          }`;
          return callback(null, fileName);
        },
      }),
    }),
  ]
})

app.controller 中书写调用上传文件 service,这里需要注意的是 UploadedFile 后面不能跟 s(这是单文件上传),否则打印出来的文件将是 undefined

import { FileInterceptor } from "@nestjs/platform-express";

  @Post("upload")
  @UseInterceptors(FileInterceptor("file"))
  upload(@UploadedFile() file: Express.Multer.File) {
    return this.appService.upload(file);
  }

app.service 中返回上传成功逻辑

  async upload(file: Express.Multer.File) {
    // 到这里文件已经上传到本地了,有后续的存储需求
    // 比如要上传到云存储服务中,在这里可以继续处理
    console.log("上传成功");
    // 返回文件URL
    return `http://localhost:9001/uploads/${file.filename}`;
  }

全局 config 配置

创建一个 ConfigModule 公开加载相应 .env 文件的公开文件 ConfigService

引入依赖

yarn add @nestjs/config

在顶层模块中导入 ConfigModule

// app.module
import { ConfigModule } from "@nestjs/config";
@Module({
  imports: [
    ConfigModule.forRoot({
      // 从 env 文件中读取常量值
      envFilePath: [".env"],
      // 开启常量缓存
      cache: true,
    }),
  ]
})

main 文件读取值

import { ConfigService } from "@nestjs/config";

const configService = app.get(ConfigService);
const port = configService.get("NEST_PORT");

当然,我们也可以使用自定义的配置文件进行加载。创建 config 文件,将需要读取的变量都从这里导出

// config.ts
export default {
  port: parseInt(process.env.NEST_PORT, 10) || 3000,
};

JWT

创建 token

首先,引入相关依赖

yarn add  @nestjs/jwt

接着在 app.service 中引入

import { JwtService } from "@nestjs/jwt";

// 在构造函数中引入
  constructor(
    private prisma: PrismaService,
    private jwtService: JwtService,
    private readonly i18n: I18nService,
  ) {}

async signIn(data: UserInter) {
  let result: string = "";
  const { id, pwd } = data;
  const userInfo = await this.prisma.user.findUnique({
    where: { id: Number(id) },
  });

  const payload = { sub: id, username: userInfo.name };
  if (userInfo.pwd === pwd) {
    result = "登录成功";
  }
  const token = await this.jwtService.signAsync(payload);
  // https://docs.nestjs.com/recipes/passport
  return { result, userInfo, token };
}

然后,创建 secret 文件并导出

// auth.constants
export const jwtConstants = {
  secret: "studTWork",
};

最后,在 app.module 中注册

import { JwtModule } from "@nestjs/jwt";
@Module({
  imports: [
    JwtModule.register({
      global: true,
      secret: jwtConstants.secret,
      // 过期时间
      signOptions: { expiresIn: "60s" },
    }),
  ]
})

验证 token

首先,要先创建守卫 auth.guard

import {
  CanActivate,
  ExecutionContext,
  Injectable,
  UnauthorizedException,
} from "@nestjs/common";
import { Request } from "express";
import { JwtService } from "@nestjs/jwt";
import { jwtConstants } from "../utils/auth.constants";

@Injectable()
export class AuthGuard implements CanActivate {
  constructor(private jwtService: JwtService) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const token = this.extractTokenFromHeader(request);
    if (!token) {
      throw new UnauthorizedException();
    }
    try {
      const payload = await this.jwtService.verifyAsync(token, {
        secret: jwtConstants.secret,
      });
      request["user"] = payload;
    } catch {
      throw new UnauthorizedException();
    }
    return true;
  }

  private extractTokenFromHeader(request: Request): string | undefined {
    const [type, token] = request.headers.authorization?.split(" ") ?? [];
    return type === "Bearer" ? token : undefined;
  }
}

接着,在要验证的 controller 的路由中使用

import { AuthGuard } from "@/common/guards/auth.guard";

@Get("find")
@UseGuards(AuthGuard) //使用 token 验证
@ApiOperation({ summary: "查找用户" })
findUser(@Query() data): Promise<UserInter> {
  return this.appService.findUser(Number(data.id));
}

websocket 集成

yarn add @nestjs/websockets @nestjs/platform-socket.io

疑问

为什么 nest 不能正常解析发送过来的 form-data?

官方示例 stackOverflow csdn 资料

解决:

NestJS提供了一个内置 multipart/form-data 的解析器,可以使用. FileInterceptor

import { Body, Post, UseInterceptors } from "@nestjs/common";
import { FileInterceptor } from "@nestjs/platform-express";

@Post("create")
@UseInterceptors(FileInterceptor("file"))
createUser(@Body() userData) {
  return this.appService.createUser(userData);
}

另外一个方法:使用 nestjs-form-data

获取请求中 param 中的参数

使用 Query 装饰器进行获取

@Get("find")
findUser(@Query() data): Promise<UserInter> {
  return this.appService.findUser(Number(data.id));
}

基础概念

资料

IoC

维基:IoC,控制反转(英语:Inversion of Control,缩写为IoC),是面向对象编程中的一种设计原则,可以用来减低计算机代码之间的耦合度。其中最常见的方式叫做依赖注入(Dependency Injection,简称 DI),还有一种方式叫“依赖查找”(Dependency Lookup)

也就是说 IoC 是开发代码的设计原则,DI 则是实现 IoC 的方案:将控制权交给框架

代码层面来说:

  • Class A 中用到了 Class B 的对象 b,一般情况下,需要在 A 的代码中显式地用 new 创建 B 的对象。
  • 使用 IoC 后,A 的代码只需要定义一个 private 的 B 对象,不需要直接 new 来获得这个对象,而是通过相关的容器控制程序来将 B 对象在外部 new 出来并注入到 A 类里的引用中。
  • IoC 将采用依赖注入依赖查找两种方案去实现

DI

  • 依赖注入是被动的接收对象,在类 A 的实例创建过程中即创建了依赖的 B 对象,通过类型或名称来判断将不同的对象注入到不同的属性中
  • 依赖查找是主动索取相应类型的对象,获得依赖对象的时间也可以在代码中自由控制

AOP

AOP (Aspect Oriented Programming),面向切面编程(装饰器)。

面向切面程序设计是计算机科学中的一种程序设计思想,旨在将横切关注点与业务主体进行进一步分离,以提高程序代码的模块化程度。

AOP 主要的思想是将我们对于业务逻辑无关的一些操作,比如日志记录、性能统计、安全控制、异常处理、错误上报等,将这些操作从业务逻辑中剥离出来,将它们放在一些独立的方法中,使得对这些操作做修改的时候就可以不用影响到业务逻辑相关的代码。它主要体现了对代码的低耦合性的追求。

因为 NestJS 使用的 MVC 架构,所以有 AOP 的能力,其中 Nest 的实现主要包括如下五种

  • Middleware
  • Guard
  • Pipe
  • Interceptor
  • ExceptionFilter

DTO

数据传输对象(DTO - Data Transfer Object),是一种设计模式之间传输数据的软件应用系统。数据传输目标往往是数据访问对象从数据库中检索数据。数据传输对象与数据交互对象或数据访问对象之间的差异是一个以不具有任何行为除了存储和检索的数据(访问和存取器)

DTO 本身更像是一个指南, 在使用API时,方便我们了解请求期望的数据类型以及返回的数据对象。先使用一下,可能更方便理解。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

画一个圆_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值