在Spring Boot 中使用 JSON Schem实现复杂对象、可变结构参数校验!

1. 为什么要在 Spring Boot 中使用 JSON Schem进行校验

1.1 传统校验方式的局限

  • • 手写 POJO + @Valid:只能校验映射到 Java 对象的字段,面对 深层嵌套可变结构(如数组中对象字段不统一)时往往需要大量的自定义 DTO 与转换代码。

  • • 手动解析 + if 判断:代码冗余、易遗漏、错误信息不统一,维护成本随业务增长呈指数级上升。

1.2 JSON Schema 的优势

优势

说明

声明式

通过 JSON 文档描述结构、约束、默认值,业务代码无需关心细节。

可组合

支持 $ref 引用、allOf/anyOf/oneOf 组合,能够复用公共子结构。

跨语言

同一套 Schema 可在前端、后端、测试脚本等多端共享,保证全链路一致性。

错误定位精准

校验器会返回错误路径(JSON Pointer),便于快速定位问题字段。

可扩展

支持自定义关键字,满足业务特有的校验需求(如唯一性、业务规则校验)。

在微服务架构中,接口契约往往以 JSON Schema 形式保存于 API 文档中心(如 Swagger、OpenAPI),将其直接用于运行时校验是最自然的落地方式。


2. JSON Schema 基础概念与核心关键字

2.1 基本结构

{
  "$schema":"http://json-schema.org/draft-07/schema#",
"title":"用户信息",
"type":"object",
"properties":{
    "id":   {"type":"integer","minimum":1},
    "name":{"type":"string","minLength":1},
    "email":{"type":"string","format":"email"},
    "roles":{
      "type":"array",
      "items":{"type":"string","enum":["ADMIN","USER","GUEST"]},
      "minItems":1,
      "uniqueItems":true
    }
},
"required":["id","name","email"]
}
  • • type:限定数据类型(objectarraystringnumberbooleannull)。

  • • properties:对象属性的子 Schema。

  • • required:必须出现的属性列表。

  • • enum:枚举值集合。

  • • format:预定义格式(emaildate-timeuri 等)。

2.2 常用关键字速查

关键字

作用

示例

minimum

 / maximum

数值范围

"minimum": 0
exclusiveMinimum

 / exclusiveMaximum

开区间

"exclusiveMinimum": 0
minLength

 / maxLength

字符串长度

"minLength": 3
pattern

正则匹配

"pattern": "^[A-Z]{3}\\d{4}$"
minItems

 / maxItems

数组元素个数

"minItems": 1
uniqueItems

数组元素唯一性

"uniqueItems": true
additionalProperties

是否允许未声明属性

"additionalProperties": false
dependencies

属性间依赖

"dependencies": { "creditCard": ["billingAddress"] }
allOf

 / anyOf / oneOf / not

组合约束

"allOf": [{...}, {...}]
$ref

引用外部或内部 Schema

"$ref": "#/definitions/Address"
default

默认值(仅在生成时有意义)

"default": "UNKNOWN"

2.3 版本兼容性

  • • Draft-07 是目前最广泛支持的版本,几乎所有 Java 校验库均兼容。

  • • 若项目需要 2020‑12 或 2023‑09 的新特性(如 unevaluatedProperties),需要确认所选库已实现对应草案。


3. Spring Boot 项目准备与依赖选型

3.1 项目结构(示例)

src/main/java
 └─ com.example.demo
      ├─ controller
      ├─ service
      ├─ validator   // 自定义校验器
      └─ config      // Spring 配置
src/main/resources
 └─ schemas
      └─ user-schema.json

3.2 主流 JSON Schema 校验库对比

Maven 坐标

主要特性

备注

NetworkNT json-schema-validatorcom.networknt:json-schema-validator

完全实现 Draft‑07、支持 $ref、自定义关键字、缓存

社区活跃,文档完整

Everit JSON Schemaorg.everit.json:org.everit.json.schema

轻量、异常信息友好、支持 Draft‑07

依赖 org.json

Justifyorg.leadpony.justify:justify

支持 Draft‑07、流式校验、低内存占用

适合大文件校验

Jackson-module-jsonSchemacom.fasterxml.jackson.module:jackson-module-jsonSchema

与 Jackson 紧耦合、生成 Schema 为主

生成能力强,校验功能相对弱

推荐:在 Spring Boot 项目中使用 NetworkNT,因为它提供了 JsonSchemaFactoryValidatorValidationMessage 等易于集成的 API,并且对 $ref 的解析支持良好。

3.3 Maven 依赖示例

<dependencies>
    <!-- Spring Boot Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- JSON Schema Validator (NetworkNT) -->
    <dependency>
        <groupId>com.networknt</groupId>
        <artifactId>json-schema-validator</artifactId>
        <version>1.0.86</version>
    </dependency>

    <!-- Jackson (已随 Spring Boot 引入) -->
    <dependency>
        <groupId>com.fasterxml.jackson.core</groupId>
        <artifactId>jackson-databind</artifactId>
    </dependency>

    <!-- Lombok(可选) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
</dependencies>

注意json-schema-validator 依赖 jackson-databind 进行 JSON 读取,确保版本兼容。


4. 基于 json-schema-validator(NetworkNT)实现自动校验

4.1 加载 Schema

package com.example.demo.config;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.JsonSchemaFactory;
import com.networknt.schema.SpecVersion;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.io.InputStream;

@Configuration
publicclassJsonSchemaConfig {

    privatefinalObjectMappermapper=newObjectMapper();

    /** 读取 classpath 下的 schema 文件并返回 JsonSchema 实例 */
    @Bean
    public JsonSchema userSchema()throws Exception {
        try (InputStreamis= getClass().getResourceAsStream("/schemas/user-schema.json")) {
            JsonNodeschemaNode= mapper.readTree(is);
            JsonSchemaFactoryfactory= JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
                    .objectMapper(mapper)
                    .build();
            return factory.getSchema(schemaNode);
        }
    }
}
  • • SpecVersion.VersionFlag.V7 指定使用 Draft‑07。

  • • 通过 @Bean 将 JsonSchema 注入 Spring 容器,后续可直接 @Autowired 使用。

4.2 编写校验工具类

package com.example.demo.validator;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import org.springframework.stereotype.Component;

import java.util.Set;

@Component
publicclassJsonValidator {

    privatefinalObjectMappermapper=newObjectMapper();

    /** 校验 JSON 字符串是否符合给定的 Schema,返回错误集合 */
    public Set<ValidationMessage> validate(String json, JsonSchema schema)throws Exception {
        JsonNodenode= mapper.readTree(json);
        return schema.validate(node);
    }

    /** 将错误集合转为统一的错误信息字符串(可自行改造为错误对象) */
    public String formatErrors(Set<ValidationMessage> errors) {
        StringBuildersb=newStringBuilder();
        for (ValidationMessage msg : errors) {
            sb.append("路径 ").append(msg.getPath())
              .append(" : ").append(msg.getMessage())
              .append("; ");
        }
        return sb.toString();
    }
}
  • • ValidationMessage#getPath() 返回 JSON Pointer(如 /roles/0),帮助前端定位。

  • • validate 方法抛出异常仅用于 JSON 解析错误,业务校验错误通过返回集合处理。

4.3 在 Controller 中使用

package com.example.demo.controller;

import com.example.demo.validator.JsonValidator;
import com.networknt.schema.JsonSchema;
import com.networknt.schema.ValidationMessage;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.Set;

@RestController
@RequestMapping("/api/users")
publicclassUserController {

    @Autowired
    private JsonSchema userSchema;          // 注入的 Schema Bean

    @Autowired
    private JsonValidator validator;       // 校验工具

    @PostMapping
    public ResponseEntity<?> createUser(@RequestBody String rawJson) {
        try {
            Set<ValidationMessage> errors = validator.validate(rawJson, userSchema);
            if (!errors.isEmpty()) {
                // 返回 400 并附带错误详情
                return ResponseEntity.badRequest()
                        .body(validator.formatErrors(errors));
            }

            // 业务处理:将 JSON 反序列化为 POJO、持久化等
            // User user = objectMapper.readValue(rawJson, User.class);
            // userService.save(user);

            return ResponseEntity.ok("校验通过,业务处理完成");
        } catch (Exception e) {
            // JSON 解析异常或其他内部错误
            return ResponseEntity.status(500).body("服务器内部错误:" + e.getMessage());
        }
    }
}
  • • 核心思路:把原始请求体保留为字符串,先交给校验器;只有在校验通过后才进行业务层的对象映射与持久化。

  • • 这样可以 避免因反序列化错误导致的异常泄露,并且错误信息直接对应 JSON Schema 定义。


5. 自定义关键字与扩展校验逻辑

5.1 业务场景示例

假设业务要求 用户名在同一租户内唯一,这属于跨记录的业务规则,JSON Schema 本身不提供此类校验。我们可以通过 自定义关键字 uniqueInTenant 来实现。

5.2 实现自定义关键字

package com.example.demo.config;

import com.networknt.schema.*;
import com.networknt.schema.keyword.Keyword;
import com.networknt.schema.keyword.KeywordFactory;
import org.springframework.stereotype.Component;

import java.util.Collections;
import java.util.Set;

/** 自定义关键字工厂 */
@Component
publicclassCustomKeywordFactoryimplementsKeywordFactory {

    @Override
    public Set<String> getKeywords() {
        return Collections.singleton("uniqueInTenant");
    }

    @Override
    public Keyword createKeyword(String keyword, JsonNode node) {
        returnnewUniqueInTenantKeyword(node);
    }

    /** 关键字实现类 */
    staticclassUniqueInTenantKeywordimplementsKeyword {
        privatefinal JsonNode schemaNode;

        UniqueInTenantKeyword(JsonNode schemaNode) {
            this.schemaNode = schemaNode;
        }

        @Override
        public ValidationResult validate(JsonNode node, JsonNode rootNode, String at) {
            // 这里的 node 为待校验的字段值(如 username)
            Stringusername= node.asText();
            // 假设有一个租户 ID 已经在上下文中获取
            StringtenantId= ValidationContext.getCurrentTenantId();

            // 调用业务服务检查唯一性(这里用伪代码演示)
            booleanexists= UserService.isUsernameExistsInTenant(username, tenantId);
            if (exists) {
                return ValidationResult.error(at, "用户名在当前租户内已存在");
            }
            return ValidationResult.ok();
        }

        @Override
        public String getKeyword() {
            return"uniqueInTenant";
        }
    }
}

5.3 将自定义关键字注册到 Factory

@Bean
public JsonSchemaFactory jsonSchemaFactory(ObjectMapper mapper, CustomKeywordFactory customFactory) {
    return JsonSchemaFactory.builder(JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7))
            .objectMapper(mapper)
            .addKeyword(customFactory)   // 注册自定义关键字
            .build();
}

5.4 在 Schema 中使用

{
  "$schema":"http://json-schema.org/draft-07/schema#",
"title":"用户注册",
"type":"object",
"properties":{
    "username":{
      "type":"string",
      "minLength":3,
      "uniqueInTenant":true   // 自定义关键字
    },
    "password":{
      "type":"string",
      "minLength":8
    }
},
"required":["username","password"]
}

注意:自定义关键字的实现必须是 无副作用 的纯函数式校验,否则可能导致并发安全问题。


6. 全局异常处理与错误信息统一返回

6.1 统一错误响应结构

{
  "timestamp":"2025-10-11T14:23:45.123+08:00",
"status":400,
"error":"Bad Request",
"message":"请求参数校验失败",
"details":[
    {"path":"/email","msg":"必须是合法的 email 地址"},
    {"path":"/roles/0","msg":"不允许的枚举值"}
]
}

6.2 实现 @ControllerAdvice

package com.example.demo.exception;

import com.networknt.schema.ValidationMessage;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.ZonedDateTime;
import java.util.List;
import java.util.stream.Collectors;

@RestControllerAdvice
publicclassGlobalExceptionHandler {

    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ValidationException ex) {
        List<FieldError> fieldErrors = ex.getErrors().stream()
                .map(v -> newFieldError(v.getPath(), v.getMessage()))
                .collect(Collectors.toList());

        ErrorResponseresp=newErrorResponse(
                ZonedDateTime.now(),
                HttpStatus.BAD_REQUEST.value(),
                "Bad Request",
                "请求参数校验失败",
                fieldErrors
        );
        returnnewResponseEntity<>(resp, HttpStatus.BAD_REQUEST);
    }

    // 其它异常统一处理...
}

/** 自定义异常包装 ValidationMessage 集合 */
classValidationExceptionextendsRuntimeException {
    privatefinal Set<ValidationMessage> errors;
    publicValidationException(Set<ValidationMessage> errors) {
        this.errors = errors;
    }
    public Set<ValidationMessage> getErrors() { return errors; }
}

/** 错误响应 DTO */
recordErrorResponse(
        ZonedDateTime timestamp,
        int status,
        String error,
        String message,
        List<FieldError> details) {}

recordFieldError(String path, String msg) {}

6.3 在业务层抛出统一异常

Set<ValidationMessage> errors = validator.validate(rawJson, userSchema);
if (!errors.isEmpty()) {
    throw new ValidationException(errors);
}

这样,所有校验错误都会统一走 GlobalExceptionHandler,前端只需要解析一次统一结构即可。


7. 在 Controller 中使用校验注解的完整示例

7.1 定义自定义注解

package com.example.demo.annotation;

import org.springframework.core.annotation.AliasFor;
import java.lang.annotation.*;

@Target({ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public@interface JsonValidated {
    /** 指定使用的 Schema Bean 名称 */
    @AliasFor("value")
    String schema()default"";

    @AliasFor("schema")
    String value()default"";
}

7.2 实现参数解析拦截器

package com.example.demo.resolver;

import com.example.demo.annotation.JsonValidated;
import com.example.demo.exception.ValidationException;
import com.example.demo.validator.JsonValidator;
import com.networknt.schema.JsonSchema;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.*;
import org.springframework.web.method.support.*;

import java.util.Set;

@Component
publicclassJsonValidatedArgumentResolverimplementsHandlerMethodArgumentResolver {

    @Autowired
    private JsonValidator validator;

    @Autowired
    private ApplicationContext ctx;   // 用于获取 Schema Bean

    @Override
    publicbooleansupportsParameter(MethodParameter parameter) {
        return parameter.hasParameterAnnotation(JsonValidated.class)
                && parameter.getParameterType().equals(String.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter,
                                  ModelAndViewContainer mavContainer,
                                  NativeWebRequest webRequest,
                                  WebDataBinderFactory binderFactory)throws Exception {

        JsonValidatedann= parameter.getParameterAnnotation(JsonValidated.class);
        StringschemaBeanName= ann.schema();
        JsonSchemaschema= (JsonSchema) ctx.getBean(schemaBeanName);

        Stringbody= webRequest.getNativeRequest(HttpServletRequest.class)
                .getReader()
                .lines()
                .reduce("", (acc, line) -> acc + line);

        Set<ValidationMessage> errors = validator.validate(body, schema);
        if (!errors.isEmpty()) {
            thrownewValidationException(errors);
        }
        return body;   // 校验通过后返回原始 JSON 字符串
    }
}

7.3 注册解析器

@Configuration
publicclassWebConfigimplementsWebMvcConfigurer {

    @Autowired
    private JsonValidatedArgumentResolver jsonResolver;

    @Override
    publicvoidaddArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(jsonResolver);
    }
}

7.4 使用示例

@PostMapping("/register")
public ResponseEntity<?> register(@JsonValidated("userSchema") String userJson) {
    // 此时 userJson 已经通过 userSchema 校验
    // 直接反序列化业务对象
    UserDto dto = objectMapper.readValue(userJson, UserDto.class);
    userService.register(dto);
    return ResponseEntity.ok("注册成功");
}

通过自定义注解 + 参数解析器,校验逻辑与业务代码彻底解耦,控制器只关心业务本身。


8. 单元测试与集成测试最佳实践

8.1 单元测试校验工具

@SpringBootTest
classJsonValidatorTest {

    @Autowired
    private JsonValidator validator;

    @Autowired
    @Qualifier("userSchema")
    private JsonSchema schema;

    @Test
    voidvalidJsonShouldPass()throws Exception {
        Stringjson="""
        {
          "id": 10,
          "name": "张三",
          "email": "zhangsan@example.com",
          "roles": ["ADMIN"]
        }
        """;
        Set<ValidationMessage> errors = validator.validate(json, schema);
        assertTrue(errors.isEmpty());
    }

    @Test
    voidinvalidJsonShouldReturnErrors()throws Exception {
        Stringjson="""
        {
          "id": -1,
          "name": "",
          "email": "not-an-email",
          "roles": []
        }
        """;
        Set<ValidationMessage> errors = validator.validate(json, schema);
        assertFalse(errors.isEmpty());
        // 断言具体错误路径
        assertTrue(errors.stream().anyMatch(v -> v.getPath().equals("$.id")));
        assertTrue(errors.stream().anyMatch(v -> v.getPath().equals("$.email")));
    }
}

8.2 集成测试 Controller

@AutoConfigureMockMvc
@SpringBootTest
classUserControllerTest {

    @Autowired
    private MockMvc mvc;

    @Test
    voidcreateUserWhenInvalidShouldReturn400()throws Exception {
        Stringpayload="""
        {
          "id": 0,
          "name": "",
          "email": "bad",
          "roles": ["UNKNOWN"]
        }
        """;

        mvc.perform(post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(payload))
           .andExpect(status().isBadRequest())
           .andExpect(jsonPath("$.details[?(@.path=='/email')].msg")
                     .value("must be a valid email address"));
    }
}
  • • 使用 MockMvc 可以完整走一遍 请求 → 校验 → 异常处理 → 响应 流程,确保错误信息的结构与内容符合约定。


9. 性能考量与缓存策略

9.1 Schema 加载成本

  • • 一次性加载:在启动阶段把所有 Schema 读取为 JsonSchema 对象并放入 Spring 容器(如前文 @Bean),后续直接复用,避免每次请求重新解析。

  • • 缓存 JsonSchemaJsonSchemaFactory 本身提供内部缓存(基于 $id),但显式缓存可以更好控制生命周期。

9.2 校验过程的 CPU 与内存占用

  • • CPU:校验本质是遍历 JSON 树并匹配关键字,复杂度与 JSON 大小呈线性关系。对大文件(>10 MB)建议使用 流式校验(Justify)或分块校验。

  • • 内存ObjectMapper.readTree 会把整个 JSON 读取为树结构,若业务只需要校验而不需要后续对象映射,可在校验后直接丢弃树对象,减轻 GC 压力。

9.3 并发场景下的优化

场景

优化手段

高并发短请求

采用 线程安全的 ObjectMapper(Spring Boot 默认已配置),避免每次创建实例。

大批量数据校验

使用 批量缓存:一次性校验一个数组,返回每条记录的错误集合,减少线程切换。

多租户环境

将租户相关的自定义关键字实现 无状态,通过 ThreadLocal 传递租户上下文,避免在校验器内部进行 I/O 查询。


10. 进阶特性:条件校验、动态模式、格式化校验

10.1 条件校验(if/then/else

{
  "type":"object",
"properties":{
    "type":{"enum":["PERSON","COMPANY"]},
    "personInfo":{"$ref":"#/definitions/person"},
    "companyInfo":{"$ref":"#/definitions/company"}
},
"required":["type"],
"if":{
    "properties":{"type":{"const":"PERSON"}}
},
"then":{
    "required":["personInfo"]
},
"else":{
    "required":["companyInfo"]
},
"definitions":{
    "person":{"type":"object","properties":{"name":{"type":"string"}}},
    "company":{"type":"object","properties":{"name":{"type":"string"}}}
}
}
  • • if/then/else 让同一对象在不同业务分支下拥有不同必填字段,极大提升 单一 Schema 的表达能力。

10.2 动态模式(patternProperties

{
  "type":"object",
"patternProperties":{
    "^prop_[0-9]+$":{"type":"string"}
},
"additionalProperties":false
}
  • • 适用于 键名可变、但键值类型统一的场景(如动态属性表、标签集合)。

10.3 自定义格式(format

JSON Schema 预定义的 format 包括 emaildate-timeuri 等。若业务需要 手机号身份证号 等自定义格式,可通过 自定义 FormatValidator 注入到 JsonSchemaFactory

JsonSchemaFactory factory = JsonSchemaFactory.builder()
        .formatValidator("phone", value -> 
            Pattern.matches("^1[3-9]\\d{9}$", value) ? Optional.empty()
                                                   : Optional.of("不是合法的手机号"))
        .build();

随后在 Schema 中使用:

{
  "type": "string",
  "format": "phone"
}

11. 常见问题排查与调优技巧

问题

可能原因

解决方案

校验报 null 指针

JSON 读取为 null(空请求体)或 Schema 中 type 与实际不匹配

在 Controller 首先判断请求体是否为空;在 Schema 明确 type 为 null 时使用 nullable:true(Draft‑07 通过 type 数组实现)。

错误路径不准确

使用了 additionalProperties:false 并在子对象中出现未声明字段

将 additionalProperties:true 或在子对象中添加 patternProperties 捕获动态字段。

自定义关键字不生效

未把自定义 KeywordFactory 注入到 JsonSchemaFactory

确认 JsonSchemaFactory 的构造器已调用 addKeyword(customFactory),并且 Bean 已被 Spring 管理。

性能瓶颈在 JSON 解析

大文件每次都完整读取为树结构

改用 流式校验(Justify)或 分块读取(Jackson JsonParser)配合 Validator 的 validate(JsonParser) 方法。

多租户唯一校验失效ValidationContext

 中租户信息未正确传递

使用 ThreadLocal 或 Spring RequestContextHolder 在拦截器层面注入租户 ID,确保自定义关键字能够获取到。

错误信息中文乱码ValidationMessage

 默认使用英文描述

在 JsonSchemaFactory 中通过 Locale 参数或自行翻译 ValidationMessage#getMessage()


12. 结语:让 JSON 校验成为项目的安全防线

  • • 声明式 的 JSON Schema 把“数据结构约束”从业务代码中抽离,使得 接口契约 与 实现 分离,降低了后期变更的风险。

  • • 统一的校验入口(如自定义注解 + 参数解析器)让所有入口点的校验行为保持一致,避免遗漏。

  • • 自定义关键字 与 格式校验 能够把业务规则直接嵌入 Schema,进一步提升系统的防御能力。

  • • 通过 缓存流式校验 与 错误信息统一化,我们可以在保持高吞吐的同时,提供友好的错误反馈。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值