CodeNavi 规则的基础节点和节点属性

1. 前期回顾

  • 《寻找适合编写静态分析规则的语言》
    根据代码检查中的一些痛点,提出了希望寻找一种适合编写静态分析规则的语言,这样可以满足用户对代码检查不断增加的各种需求;同时也使用户能够通过增加或减少对检查约束条件的控制,实现快速调整检查中出现的误报和漏报;同时这种检查语言能够有较低的使用门槛,使用户更专注于检查业务,而不需要关注工具是如何实现的。
    最后给出了两个应用场景,来说明用户对这种编写检查语言的期望。

  • 《CodeNavi 规则的语法结构》
    描述了检查语言在编写规则时的基本语法格式,通过对代码节点和节点属性的逻辑条件的组合来对应检查的约束条件,从而完成代码检查中需要满足的缺陷模式适配,找到满足要求的代码点。

2. 代码和检查规则

本篇将进一步介绍,代码中的基础节点和节点属性,主要包括:字面量、常量、枚举、成员变量(字段)、数组。

2.1. 代码的组成

程序是由空格分隔的字符串组成的序列。在程序分析中,这一个个的字符串被称为"token",是源代码中的最小语法单位,是构成编程语言语法的基本元素。

Token可以分为多种类型,常见的有关键字(如if、while)、标识符(变量名、函数名)、字面量(如数字、字符串)、运算符(如+、-、*、/)、分隔符(如逗号,、分号;)等。

程序在编译过程中,词法分析器(Lexer)读取源代码并将其分解成一系列的token。语法分析器(Parser)会使用这些 token 来构建一个抽象语法树(Abstract Syntax Tree, AST),这个树结构表示了代码的语法结构。这个时候每个 token 也可以称为抽象语法树的节点,树上某个节点的分支就是这个节点的子节点。每个节点都会有节点类型、属性、值。

这些是程序员非常容易理解的。

我们只需要给代码的不同节点给出一个定义,然后通过条件语句来描述对这些节点的要求,使之符合缺陷检查的模式,就可以完成检查规则的定义。

《CodeNavi 规则的语法结构》 中已经描述了规则的基础语法和对节点筛选的条件语句的语法,这篇里将进一步描述代码中节点的定义。

2.2. CodeNavi中的节点和节点属性语法

  • 结构图

  • 语法说明

    • 节点(node):是代码对应语法树的节点或子节点;
    • 属性(attribute):是代码对应语法树的节点或子节点的属性;
    • . : 节点、子节点、属性之间的连接符;

3. CodeNavi中的节点和节点属性

3.1. 规则节点和节点属性图例

3.1.1. 节点

  • 图例

  • 节点和子节点都使用这个图例

  • 规则语言中使用节点的 “英文名”,这样便于规则的编写。

3.1.2. 节点集

  • 图例

  • 节点的集合。

3.1.3. 属性

  • 图例

  • 节点的某个信息;

  • 属性根据存储信息类型,存在不同的类型。例如

  • 规则语言中使用属性的 “英文名”,这样便于规则的编写。如数字、字符串、布尔值等。


3.2. 节点类型

每个代码节点,按照作用不同拥有不同的类型。这里给出了,系统中使用的基础节点类型:

  • 类型节点(type)
  • 对象节点(objectType)
  • 数组节点(arrayType)

  • 图例

3.2.1. 类型节点(type)

类型节点与代码中节点的类型相对应,是AST节点的通用节点,表示一个变量、字段、方法返回值等的类型,变量声明的类型。

名称描述值类型示例DSL 规则
name类型的名称(包含包名的全类型)字符串String var;
Person person = new Person();
List values = new ArrayList<>();
variableDeclaration vd where
vd.type.name == “java.util.List”;
definition类型的定义recordDeclaration节点public class Person {}
public class MyClass {
public void fun() {
Person person = new Person();
}
}
variableDeclaration vd where
vd.type.definition.name == “com.huawei.secbrella.kirin.Persion”;
superTypes类型的父类型type节点List values = new ArrayList<>(list.size());variableDeclaration vd where
vd.type.superTypes contain parentType where
parentType.name == “java.util.Collection”;

3.2.2. 对象节点(objectType)

对象类型包括:类、接口类、枚举类、泛型类,继承与 objectType 节点。

名称描述值类型示例DSL 规则
name类型的名称(包含包名的全类型)名称Person person;
Map<String, List> map;
variableDeclaration vd where
vd.type.name == “java.util.Map”;
isBounded泛型类型是否 extends/super 了另一个类, 仅对泛型类型生效布尔值Map<String, ? extends List> map;variableDeclaration vd where
vd.type isBounded;
boundedOperator泛型类型绑定符号是"extends" 或 “super”, 仅对泛型类型生效字符串Map<String, ? extends List> map;variableDeclaration vd where
vd.type.boundedOperator == “extends”;
genericBoundedType泛型类型绑定的类型type节点Map<String, ? extends List> map;variableDeclaration vd where
vd.type.boundedType.name endWith “List”;
generics类型的直接泛型类type节点集合Map<Integer, List<List<List>>> map;variableDeclaration vd where vd.type vt where
vt.generics.size() == 2;
allGenerics类型的所有直接+间接泛型类type节点集合Map<Integer, List<List<List>>> map;variableDeclaration vd where vd.type vt where
vt.allGenerics.size() == 5;
terminalGenerics类型的末端泛型类node listMap<Integer, List<List<List>>> map;variableDeclaration vd where vd.type vt where
vt.terminalGenerics contain ty where ty.name == “java.lang.String”;

3.2.3. 数组节点(arrayType)

数组类型,继承于type节点。

名称描述值类型示例DSL 规则
name数组类型的名称字符串int[] arr = new int[1];variableDeclaration vd where and(
vd.type is arrayType,
vd.type.name == “int[]”
);
dimension数组的维度大小数字int[][] arr = new int[1][1];variableDeclaration vd where and(
vd.type is arrayType,
vd.type.dimension == 2
);
baseType数组类型的基础类型objectType节点int[] arr = new int[1];variableDeclaration vd where and(
vd.type is arrayType,
vd.type.baseType.name == “int”
);

4. 字面量(Literal)

字面量是编程语言的基础组成部分,它们提供了一种简单、直接的方式来在代码中表示固定值,如数字、字符串、布尔值等。
正确和恰当地使用字面量可以提高代码的效率、可读性和可维护性。

字面量的一些主要作用:

  • 直接表示值
    字面量提供了一种直接在代码中表示具体值的方式,如数字、字符串、布尔值等。
  • 数组和集合初始化
    在初始化数组或集合时,字面量用于指定元素的初始值。
  • 数据结构
    在某些数据结构的实现中,字面量用于表示特定的状态或标记,如布尔值true或false。
  • 枚举和状态
    在枚举类型或状态机中,字面量可以表示特定的枚举值或状态。
  • 控制流程
    在控制结构(如循环和条件语句)中,字面量用于指定循环次数或条件值。
  • 配置和参数
    字面量常用于配置文件和函数参数中,表示固定的值或默认值。
  • 资源管理
    在资源管理中,字面量用于指定资源的大小或限制,如缓冲区大小或数组容量。
  • 减少错误
    在某些情况下,使用字面量可以减少因变量错误而导致的问题。例如,使用null字面量来表示空引用,避免了使用错误的变量。
  • 简化代码
    使用字面量可以避免创建不必要的变量,从而简化代码。例如,使用10作为数组的初始大小时,不需要额外声明一个常量。
  • 提高可读性
    在适当的情况下使用字面量可以使代码更直观易懂。例如,使用"Hello, World!"作为程序输出,读者可以立即理解程序的意图。

  • 图例

4.1. 字面量(literal)

名称描述值类型示例DSL 规则
value字符串、数值、布尔值等int a = 1;literal ll where ll.value == 1;

4.2. 数值(numLiteral)

名称描述值类型示例DSL 规则
value数值数值System.out.println(1);numLiteral l where l.value == 1;

4.3. 字符串(stringLiteral)

名称描述值类型示例DSL 规则
length字符串长度数值System.out.println(“aaa”);stringLiteral l where l.length == 3;
value字符串值字符串System.out.println(“aaa”);stringLiteral l where l.value == “aaa”;

4.4. 布尔值(boolLiteral)

名称描述值类型示例DSL 规则
value布尔值布尔值System.out.println(true);boolLiteral l where l.value == true;

5. 变量

变量是编程语言的基本构件,变量被用来存储数据,为程序提供了灵活性和动态性。
正确地使用变量对于编写高效、可读和可维护的代码至关重要。变量可以被定义为不同的类型,并且每个变量都必须在使用前声明。

变量的一些主要作用:

  • 存储数据
    变量的主要功能是存储数据。它们可以保存程序运行时需要的任何类型的数据,如数字、字符、字符串等。

  • 数据结构
    变量是实现数据结构的基础。数组、列表、树、图等数据结构都是通过变量来存储和管理数据的。

  • 封装
    变量可以封装数据和行为,这是面向对象编程的核心概念之一。通过将数据和操作这些数据的方法封装在类中,可以提高代码的可维护性和重用性。这种封装在类里的变量,被称为成员变量,通常也被称为字段(field)。

  • 计算和操作
    变量可以存储中间结果,用于复杂的计算和操作。例如,在科学计算或数据分析中,变量用于存储和处理大量的数值数据。

  • 数据交换
    在函数或方法中,变量用于传递数据。通过参数和返回值,变量可以在不同的函数或方法之间交换数据。

  • 资源标识
    在资源管理中,变量可以用于标识资源,如文件句柄、数据库连接等。

  • 状态表示和跟踪
    变量用于表示程序的状态。例如,在游戏程序中,变量可以表示玩家的得分、生命值或位置。
    在复杂的系统中,变量可以用于跟踪系统的状态,如网络连接状态、用户会话状态等。

  • 控制流程
    变量可以用于控制程序的流程。例如,循环计数器是控制循环次数的变量,条件语句中的条件变量可以决定程序的执行路径。

  • 错误处理
    变量可以用于存储错误信息或状态码,帮助程序处理异常情况。

  • 配置和参数
    变量可以用于存储程序的配置信息或参数。这些参数可以在程序启动时设置,也可以在运行时动态调整。

5.1. 常用变量

  • 代码样例
package com.dsl.base;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class CheckVar {
    private static final Logger LOG = LogManager.getLogger(CheckVar.class);

    public void varCheck() {
        // 变量声明
        int varInt1;
        int varInt2 = 10;
        String varStr1;
        String varStr2 = "str";
        boolean varBoolean1;
        boolean varBoolean2 = true;

        // 变量的使用
        varInt1 = varInt2;
        varStr1 = varStr2;
        varBoolean1 = varBoolean2;

        varInt2 = varInt1 + varInt2;
        LOG.info("varInt:{}", varInt2);

        varStr2 = varStr1 + varStr2;
        LOG.info("varStr:{}", varStr2);

        varBoolean2 = !varBoolean1;
        LOG.info("varBoolean:{}", varBoolean2);
    }
}


  • 图例

5.1.1. 变量的声明(variableDeclaration)

名称描述值类型示例DSL 规则
name变量名字符串int a = arr[3];variableDeclaration vd where vd.name == “a”;
initializer初始化值任意节点int a = arr[3];variableDeclaration vd where vd.initializer is arrayAccess;
valueUsages变量使用集任意节点int a = arr[3];
log.info(a);
variableDeclaration vd where
vd.valueUsages contain vu where
vu in functionCall;

5.1.2. 变量的使用(variableAccess)

名称描述值类型示例DSL 规则
name变量名字符串int a = arr[3];
log.info(a);
variableAccess va where va.name == “a”;
variable被访问的变量的定义variableDeclarationint a = arr[3];
log.info(a);
variableAccess va where va.variable.name == “a”;

5.2. 枚举

枚举提供了一种清晰、类型安全的方式来处理一组固定的值。正确地使用枚举可以提高代码的可读性、可维护性和健壮性。

枚举的一些主要作用:

  • 提供一组固定的常量
    枚举定义了一组命名的常量,这些常量是预定义的,并且在整个程序中保持不变。

  • 类型安全
    枚举提供了类型安全,确保变量只能赋予枚举中定义的值之一,从而避免了使用魔法数字或字符串常量可能导致的错误。

  • 状态表示
    枚举可以表示程序的状态,例如,表示用户的状态(如在线、离线、忙碌)。

  • 模式匹配
    在支持模式匹配的语言中,枚举可以用于简化复杂的条件语句。

  • 数据分组
    枚举可以用于将相关的数据分组,使得数据组织更加清晰。

  • 控制流程
    枚举可以用于控制程序的流程,例如,使用枚举值作为条件语句或循环的依据。

  • 封装行为
    枚举类型可以包含字段、方法和构造函数,这允许将与枚举值相关的行为封装在枚举类型中。

  • 资源管理
    枚举可以用于管理资源,例如,表示不同的资源状态或资源类型。

  • 配置和参数
    枚举可以用于表示程序的配置选项或参数,使得配置更加直观和易于管理。

  • 提高代码可读性
    使用枚举可以清晰地表达程序中的意图,使得代码更加易于理解和维护。


  • 代码样例
package com.dsl.base;

public enum CheckEnum {
    MONDAY("Monday", true),
    TUSDAY("Tusday", true),
    WEDNESDAY("Wednesday", true),
    THURSDAY("Thursday", true),
    FRIDAY("Friday", true),
    SATURDAY("Saturday", false),
    SUNRDAY("Sunday", false),
    OTHERS("Others", false);

    String day;
    boolean isWork;

    private CheckEnum(String day, boolean isWork) {
        this.day = day;
        this.isWork = isWork;
    }

    public static CheckEnum getCheckEnum(String day) {
        for (CheckEnum check : CheckEnum.values()) {
            if (check.getDay().equalsIgnoreCase(day) || check.name().equalsIgnoreCase(day)) {
                return check;
            }
        }
        return OTHERS;
    }

    public String getDay() {
        return day;
    }

    public boolean isWork() {
        return isWork;
    }
}
  • 图例

5.2.1. 枚举的声明

5.2.1.1. 枚举类的声明(enumDeclaration)
名称描述值类型示例DSL 规则
name枚举类名字符串enum TheEnum {PUT, HEAD, DELETE;}enumDeclaration ed where
ed.name == “com.dsl.TheEnum”;
enumConstants枚举常量集enumConstant节点集合见样例enumDeclaration ed where
ed.enumConstants.size() == 4;
definedFields字段集成员集合public enum DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY
}
enumDeclaration ed where
ed.definedFields.size() == 3;
definedMethods方法集functionDeclaration节点集合见样例enumDeclaration ed where
ed.definedMethods.size() == 1;

5.2.1.2. 枚举常量声明(enumConstantDeclaration)
名称描述值类型示例DSL 规则
name枚举常量名字符串public enum DayOfWeek {
MONDAY, TUESDAY, WEDNESDAY
}
enumConstantDeclaration ed where
ed.name == “Monday”;
definedEnumConstantFields枚举常量字段集fieldDeclaration节点的集合enum Week3 {
Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday {
private String tmp = “test”;
}
}
enumConstantDeclaration ed where
ed.definedEnumConstantFields.size() == 1;
definedEnumConstantMethods枚举常量方法集functionDeclaration节点的集合enum Week3 {
Monday, Tuesday, Wednesday, Thursday, Friday,
Saturday {
private String tmp = “test”;
},
Sunday {
public void work() {
System.out.println(“休息”);
}
};
}
enumConstantDeclaration ed where
ed.definedEnumConstantMethods contain fd where
fd.name == “work”;
enumConstantArguments枚举常量中的入参集literal节点、valueAccess类节点、functionCall节点等的集合见样例enumConstantDeclaration ed where
ed.enumConstantArguments.size() == 2;

5.2.2. 枚举的使用(enumConstantAccess)

名称描述值类型示例DSL 规则
base访问者任意节点Planet.EARTH;enumConstantAccess ea where
ea.base.name == “Planet”;
name枚举常量名字符串Planet.EARTH;enumConstantAccess ea where
ea.name == “EARTH”;
enumConstant访问的枚举常量声明enumConstant节点Planet.EARTH;enumConstantAccess ea where
ea.enumConstant.name == “EARTH”;

5.3. 成员变量(字段)

成员变量是面向对象编程中类的基本组成部分,它们为对象提供了状态和行为,是实现面向对象编程的基础。

类中的成员变量通常也被称为字段(Field)。字段是类的一部分,它可以在类的任何方法之外声明,并且可以在整个类的方法中被访问。字段用于存储对象的状态信息,每个对象实例都有自己的字段副本。

字段可以是基本数据类型,也可以是引用类型,它们可以是静态的(属于类本身)或非静态的(属于类的实例)。字段的声明通常包括类型、名称和可选的初始值。

正确地使用成员变量对于设计健壮、灵活和可维护的类至关重要。

成员变量的一些主要作用:

  • 存储状态信息
    成员变量用于存储对象的状态信息。它们代表了对象的属性或特征,如一个人的名字、年龄等。

  • 定义对象特征
    每个对象都有自己的成员变量副本,这些副本定义了对象的特定特征。

  • 数据封装
    成员变量是封装数据的一种方式,它们可以与方法一起,将数据和操作数据的行为组合在一起,形成类。

  • 实现功能
    成员变量可以与类中的方法一起工作,实现类的功能。例如,一个成员变量可以存储一个值,而类的方法可以修改或使用这个值。

  • 提供接口
    成员变量可以通过公共(public)、受保护(protected)、私有(private)等访问修饰符来控制其在类外部的可见性,从而提供不同的接口级别。

  • 数据共享
    静态成员变量是类的所有实例共享的,它们可以用于在所有对象之间共享数据。

  • 控制访问
    通过使用访问修饰符,可以控制对成员变量的访问,确保数据的安全性和完整性。

  • 实现继承
    成员变量可以被子类继承,子类可以访问或重写父类的成员变量。

  • 多态性
    成员变量可以用于展示多态性,即同一个接口可以有多个不同的数据类型或形态。

  • 初始化
    成员变量可以在构造函数中被初始化,确保对象在使用前具有正确的初始状态。

  • 数据持久性
    成员变量可以用于在对象的整个生命周期内保持数据的持久性。

  • 资源管理
    成员变量可以用于管理资源,如文件句柄、数据库连接等。

  • 配置管理
    成员变量可以用于存储配置信息,这些信息可以在程序运行时被读取和修改。


  • 代码样例
package com.dsl.base;

import java.util.List;

public class CheckField {
    private int field1;
    private String field2;
    private boolean field3;
    private List<String> fieldList;

    public int getField1() {
        return field1;
    }

    public void setField1(int field1) {
        this.field1 = field1;
    }

    public String getField2() {
        return field2;
    }

    public void setField2(String field2) {
        this.field2 = field2;
    }

    public boolean isField3() {
        return field3;
    }

    public void setField3(boolean field3) {
        this.field3 = field3;
    }

    public List<String> getFieldList() {
        return fieldList;
    }

    public void setFieldList(List<String> fieldList) {
        this.fieldList = fieldList;
    }
}
  • 图例

5.3.1. 字段的声明(fieldDeclaration)

名称描述值类型示例DSL 规则
name字段名字符串private int aes_key = 3;fieldDeclaration fd where
fd.name == “aes_key”;
initializer初始化值任意节点private int aes_key = 3;fieldDeclaration fd where
fd.initializer.value == 3;
valueUsages字段使用集valueAccess类节点、functionCall节点等private int aes_key = 3;

public void test() {
this.aes_key = 10;
}
fieldDeclaration fed where
fed.valueUsages contain va where
va in functionDeclaration fd where
fd.name == “test”;
comments注释集注释集合// 注释
private int aes_key = 3;
fieldDeclaration fd where
fd.comments contain lineComment;

5.3.2. 字段的使用(fieldAccess)

名称描述值类型示例DSL 规则
name字段名字符串Person.namefieldAccess fa where fa.name == “name”;
field字段的声明fieldDeclaration节点private int aes_key = 3;

public void test() {
this.aes_key = 10;
}
fieldAccess fa where fa.field.initializer.value == 3;
base访问者任意节点Person.namefieldAccess fa where fa.base.name == “Person”;

6. 数组

数组是编程中最基本的数据结构之一,它们为程序员提供了一种高效的方式来处理和操作大量数据。正确地使用数组对于编写高效、可读和可维护的代码至关重要。

数组的一些主要作用:

  • 存储集合数据
    数组用于存储相同类型的多个数据项,形成一个有序集合。

  • 数据结构基础
    数组是许多复杂数据结构(如链表、树、图等)的基础组件。

  • 多维数据处理
    多维数组可以用于表示和操作表格数据或矩阵。

  • 函数参数传递
    数组经常作为函数的参数传递,允许函数对数据集合进行操作。

  • 索引访问
    数组提供了通过索引快速访问元素的能力,这使得数据检索非常高效。

  • 简化数据操作
    使用数组可以简化对数据集合的操作,如排序、搜索、统计等。

  • 代码样例

package com.dsl.base;

import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;

public class CheckArray {
    private static final Logger LOG = LogManager.getLogger(CheckArray.class);

    public void arrayCheck() {
        // 数组创建和初始化
        String[] arr1 = new String[5];
        String[] arr2 = { "0", "1", "2", "3", "4" };
        String[] arr3 = new String[] { "0", "1", "2", "3", "4" };
        String[][] arr4 = new String[2][2];
        String[][] arr5 = new String[][] { { "0", "0" }, { "0", "1" }, { "0", "2" }, { "1", "0" }, { "1", "1" },
                { "1", "2" } };

        int[] arr6 = { 0, 1, 2, 3 };

        // 数组的使用
        for (int ind = 0; ind < arr2.length; ind++) {
            arr1[ind] = arr2[ind];
            LOG.info("arr1 + arr2 = {}", arr1[ind] + arr2[ind]);
        }

        for (int id = 0; id < arr3.length; id++) {
            LOG.info("arr:{}", arr3[id]);
        }

        for (int i = 0; i < 2; i++) {
            for (int j = 0; j < 2; j++) {
                arr4[i][j] = arr5[i][j];
                LOG.info("arr[{}][{}]:{}", i, j, arr4[i][j]);
            }
        }

        LOG.info("arr:{}", arr6[3]);
    }
}
  • 图例

6.1. 创建数组(arrayCreationExpression)

名称描述值类型示例DSL 规则
dimensions数组的维度集合节点集合String[] array = new String[10];arrayCreationExpression ac where
ac.dimensions.size() == 1;
dimensions[]数组的维度numLiteral、access类节点、functionCall节点等String[] array = new String[10][a];arrayCreationExpression ac where and(
ac.dimensions.size() == 2,
ac.dimensions[0] dm where and(
dm is numLiteral,
dm.value == 10
)
);
initArray数组创建的初始化值initArrayExpressionString[] array2 = new String[]{“abc”, “dec”};arrayCreationExpression ac where
ac.initArray.elements contain ele where
ele.value == “abc”;
type数组创建的类型objectType节点String[] array = new String[10];arrayCreationExpression ac where
ac.type.name == “java.lang.String”;

6.2. 初始化数组(initArrayExpression)

名称描述值类型示例DSL 规则
elements数组初始化集合节点集合names = {“ok”, “hello”, “yes”}initArrayExpression ia where
ia.elements.size() == 3;
elements[]数组初始化的元素literal类节点、变量类new String[] {“ok”, “yes”};initArrayExpression ia where
ia.elements[0].value == “ok”;

6.3. 数组的使用(arrayAccess)

名称描述值类型示例DSL 规则
arrayIndex数组索引literal、任意access类节点、functionCall节点等Integer[] arr = {0,1,2,3,4};
Integer i = arr[3];
arrayAccess aa
where and(
aa.arrayIndex is numLiteral,
aa.arrayIndex.value == 3
);
base访问者variableAccess等任意access类节点、functionCall节点等int a = arr[3];
log.info(arr2[1][3]);
arrayAccess arr where
or(
arr.base is variableAccess,
arr.base is arrayAccess
);
rootBase访问者的根节点variableAccess等任意access类节点、functionCall节点等String[] a1 = arr[3];
String a2 = arr[1][3];
arrayAccess arr where arr.rootBase rb
where and(
rb is variableAccess,
rb.name == “arr”
);
type数组类型objectType节点System.out.println(hello[1]);arrayAccess arr where
arr.type.name == “int”;

7. CodeNavi插件

  • 在Vscode 的插件中,查询:codenavi,并安装。

  • 使用插件的链接安装: https://marketplace.visualstudio.com/items?itemName=HuaweiCloud.codenavi

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值