Hive之自定义聚合函数UDAF

----本文笔记整理自 《Hive编程指南》13.9 用户自定义聚合函数
 

一、自定义聚合函数(GenericUDAFAverage实现)

1.聚合函数:指0行到多行的0个到多个列作为参数输入,返回单一值的函数,经常和group by子句一起用。

    如:sum(col),avg(col),max(col),std(col)等。

2.实现通用的自定义聚合函数 GenericUDAFAverage(column),如下Java代码:

    功能:实现对列数据求平均值

    源代码链接:http://svn.apache.org/repos/asf/hive/branches/branch-0.8/ql/src/java/org/apache/hadoop/hive/ql/udf/generic/GenericUDAFAverage.java 

    注:对于ObjectInspector的各子接口/子类的用法,在上篇博文中做过简单的总结-- Hive之ObjectInspector接口解析笔记。特别是其中的 7.利用ObjectInspector解析Object数据 对下面代码的理解有帮助。

package com.hive.udaf;

import java.util.ArrayList;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.parse.SemanticException;
import org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator;
import org.apache.hadoop.hive.serde2.io.DoubleWritable;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.StructField;
import org.apache.hadoop.hive.serde2.objectinspector.StructObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.DoubleObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.LongObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorUtils;
import org.apache.hadoop.hive.serde2.typeinfo.PrimitiveTypeInfo;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.util.StringUtils;

/**
 * GenericUDAFAverage.
 *
 */
@Description(name = "myavg", value = "_FUNC_(x) - Returns the mean of a set of numbers")
public class GenericUDAFAverage extends AbstractGenericUDAFResolver {

  static final Log LOG = LogFactory.getLog(GenericUDAFAverage.class.getName());

  //读入参数类型校验,满足条件时返回聚合函数数据处理对象
  @Override
  public GenericUDAFEvaluator getEvaluator(TypeInfo[] parameters)
      throws SemanticException {
    if (parameters.length != 1) {
      throw new UDFArgumentTypeException(parameters.length - 1,
          "Exactly one argument is expected.");
    }

    if (parameters[0].getCategory() != ObjectInspector.Category.PRIMITIVE) {
      throw new UDFArgumentTypeException(0,
          "Only primitive type arguments are accepted but "
          + parameters[0].getTypeName() + " is passed.");
    }
    switch (((PrimitiveTypeInfo) parameters[0]).getPrimitiveCategory()) {
    case BYTE:
    case SHORT:
    case INT:
    case LONG:
    case FLOAT:
    case DOUBLE:
    case STRING:
    case TIMESTAMP:
      return new GenericUDAFAverageEvaluator();
    case BOOLEAN:
    default:
      throw new UDFArgumentTypeException(0,
          "Only numeric or string type arguments are accepted but "
          + parameters[0].getTypeName() + " is passed.");
    }
  }

  /**
   * GenericUDAFAverageEvaluator.
     * 自定义静态内部类:数据处理类,继承GenericUDAFEvaluator抽象类
   */
  public static class GenericUDAFAverageEvaluator extends GenericUDAFEvaluator {

	//1.1.定义全局输入输出数据的类型OI实例,用于解析输入输出数据
    // input For PARTIAL1 and COMPLETE
    PrimitiveObjectInspector inputOI;

    // input For PARTIAL2 and FINAL
    // output For PARTIAL1 and PARTIAL2
    StructObjectInspector soi;
    StructField countField;
    StructField sumField;
    LongObjectInspector countFieldOI;
    DoubleObjectInspector sumFieldOI;

    //1.2.定义全局输出数据的类型,用于存储实际数据
    // output For PARTIAL1 and PARTIAL2
    Object[] partialResult;

    // output For FINAL and COMPLETE
    DoubleWritable result;

    /*
         * 初始化:对各个模式处理过程,提取输入数据类型OI,返回输出数据类型OI  
     * .每个模式(Mode)都会执行初始化
     * 1.输入参数parameters:
     * .1.1.对于PARTIAL1 和COMPLETE模式来说,是原始数据(单值)
     *    .设定了iterate()方法的输入参数的类型OI为:
     *    .		 PrimitiveObjectInspector 的实现类 WritableDoubleObjectInspector 的实例
     *    .		 通过输入OI实例解析输入参数值
     * .1.2.对于PARTIAL2 和FINAL模式来说,是模式聚合数据(双值)
     *    .设定了merge()方法的输入参数的类型OI为:
     *    .		 StructObjectInspector 的实现类 StandardStructObjectInspector 的实例
     *    .		 通过输入OI实例解析输入参数值
     * 2.返回值OI:
     * .2.1.对于PARTIAL1 和PARTIAL2模式来说,是设定了方法terminatePartial()返回值的OI实例
     *    .输出OI为 StructObjectInspector 的实现类 StandardStructObjectInspector 的实例
     * .2.2.对于FINAL 和COMPLETE模式来说,是设定了方法terminate()返回值的OI实例
     *    .输出OI为 PrimitiveObjectInspector 的实现类 WritableDoubleObjectInspector 的实例
     */
    @Override
    public ObjectInspector init(Mode mode, ObjectInspector[] parameters)
        throws HiveException {
      assert (parameters.length == 1);
      super.init(mode, parameters);

      // init input
      if (mode == Mode.PARTIAL1 || mode == Mode.COMPLETE) {
        inputOI = (PrimitiveObjectInspector) parameters[0];
      } else {
    	//部分数据作为输入参数时,用到的struct的OI实例,指定输入数据类型,用于解析数据
        soi = (StructObjectInspector) parameters[0];
        countField = soi.getStructFieldRef("count");
        sumField = soi.getStructFieldRef("sum");
        //数组中的每个数据,需要其各自的基本类型OI实例解析
        countFieldOI = (LongObjectInspector) countField.getFieldObjectInspector();
        sumFieldOI = (DoubleObjectInspector) sumField.getFieldObjectInspector();
      }

      // init output
      if (mode == Mode.PARTIAL1 || mode == Mode.PARTIAL2) {
        // The output of a partial aggregation is a struct containing
        // a "long" count and a "double" sum.
    	//部分聚合结果是一个数组
    	partialResult = new Object[2];
        partialResult[0] = new LongWritable(0);
        partialResult[1] = new DoubleWritable(0);
        /*
         * .构造Struct的OI实例,用于设定聚合结果数组的类型
         * .需要字段名List和字段类型List作为参数来构造
         */
        ArrayList<String> fname = new ArrayList<String>();
        fname.add("count");
        fname.add("sum");
        ArrayList<ObjectInspector> foi = new ArrayList<ObjectInspector>();
        //注:此处的两个OI类型 描述的是 partialResult[] 的两个类型,故需一致
        foi.add(PrimitiveObjectInspectorFactory.writableLongObjectInspector);
        foi.add(PrimitiveObjectInspectorFactory.writableDoubleObjectInspector);
        return ObjectInspectorFactory.getStandardStructObjectInspector(fname, foi);
      } else {
    	//FINAL 最终聚合结果为一个数值,并用基本类型OI设定其类型
        result = new DoubleWritable(0);
        return PrimitiveObjectInspectorFactory.writableDoubleObjectInspector;
      }
    }

    /*
     * .聚合数据缓存存储结构
     */
    static class AverageAgg implements AggregationBuffer {
      long count;
      double sum;
    };

    @Override
    public AggregationBuffer getNewAggregationBuffer() throws HiveException {
      AverageAgg result = new AverageAgg();
      reset(result);
      return result;
    }

    @Override
    public void reset(AggregationBuffer agg) throws HiveException {
      AverageAgg myagg = (AverageAgg) agg;
      myagg.count = 0;
      myagg.sum = 0;
    }

    boolean warned = false;

    /*
     * .遍历原始数据
     */
    @Override
    public void iterate(AggregationBuffer agg, Object[] parameters)
        throws HiveException {
      assert (parameters.length == 1);
      Object p = parameters[0];
      if (p != null) {
        AverageAgg myagg = (AverageAgg) agg;
        try {
          //通过基本数据类型OI解析Object p的值
          double v = PrimitiveObjectInspectorUtils.getDouble(p, inputOI);
          myagg.count++;
          myagg.sum += v;
        } catch (NumberFormatException e) {
          if (!warned) {
            warned = true;
            LOG.warn(getClass().getSimpleName() + " "
                + StringUtils.stringifyException(e));
            LOG.warn(getClass().getSimpleName()
                + " ignoring similar exceptions.");
          }
        }
      }
    }

    /*
     * .得出部分聚合结果
     */
    @Override
    public Object terminatePartial(AggregationBuffer agg) throws HiveException {
      AverageAgg myagg = (AverageAgg) agg;
      ((LongWritable) partialResult[0]).set(myagg.count);
      ((DoubleWritable) partialResult[1]).set(myagg.sum);
      return partialResult;
    }

    /*
     * .合并部分聚合结果
     * .注:Object[] 是 Object 的子类,此处 partial 为 Object[]数组
     */
    @Override
    public void merge(AggregationBuffer agg, Object partial)
        throws HiveException {
      if (partial != null) {
        AverageAgg myagg = (AverageAgg) agg;
        //通过StandardStructObjectInspector实例,分解出 partial 数组元素值
        Object partialCount = soi.getStructFieldData(partial, countField);
        Object partialSum = soi.getStructFieldData(partial, sumField);
        //通过基本数据类型的OI实例解析Object的值
        myagg.count += countFieldOI.get(partialCount);
        myagg.sum += sumFieldOI.get(partialSum);
      }
    }

    /*
     * .得出最终聚合结果
     */
    @Override
    public Object terminate(AggregationBuffer agg) throws HiveException {
      AverageAgg myagg = (AverageAgg) agg;
      if (myagg.count == 0) {
        return null;
      } else {
        result.set(myagg.sum / myagg.count);
        return result;
      }
    }
  }

}

二、代码解析

1.聚合函数中的几个过程模式 Mode:(org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator.Mode)

public abstract class GenericUDAFEvaluator implements Closeable {

  public static enum Mode {
    /**
     * PARTIAL1: from original data to partial aggregation data: iterate() and
     * terminatePartial() will be called.
     * PARTIAL1: 从原始数据到部分聚合数据的过程,会调用iterate()和terminatePartial()
     * 可以理解为MapReduce过程中的map阶段
     */
    PARTIAL1,
        /**
     * PARTIAL2: from partial aggregation data to partial aggregation data:
     * merge() and terminatePartial() will be called.
     * PARTIAL2: 从部分聚合数据到部分聚合数据的过程(多次聚合),会调用merge()和terminatePartial()
     * 可以理解为MapReduce过程中的combine阶段
     */
    PARTIAL2,
        /**
     * FINAL: from partial aggregation to full aggregation: merge() and
     * terminate() will be called.
     * FINAL: 从部分聚合数据到全部聚合数据的过程,会调用merge()和terminate()
     * 可以理解为MapReduce过程中的reduce阶段
     */
    FINAL,
        /**
     * COMPLETE: from original data directly to full aggregation: iterate() and
     * terminate() will be called.
     * COMPLETE: 从原始数据直接到全部聚合数据的过程,会调用iterate()和terminate()
     * 可以理解为MapReduce过程中的直接map输出阶段,没有reduce阶段
     */
    COMPLETE
  };

}

2.代码结构:

1)需继承AbstractGenericUDAFResolver抽象类,重写方法getEvaluator(TypeInfo[] parameters);

2)内部静态类需继承GenericUDAFEvaluator抽象类,重写方法init(),实现方法getNewAggregationBuffer(),reset(),iterate(),terminatePartial(),merge(),terminate()。

3.程序执行过程:

1)PARTIAL1(阶段1:map):init() --> iterate() --> terminatePartial()

2)PARTIAL2(阶段2:combine):init() --> merge() --> terminatePartial()

3)FINAL (最终阶段:reduce):init() --> merge() --> terminate()

4)COMPLETE(直接输出阶段:只有map):init() --> iterate() --> terminate()

注:每个阶段都会执行init()初始化操作。

 

三、打包Jar file,并运行测试

1.将com.hive.udaf包右键导出为 JAR file,命名为:"myUDAF.jar";
2.利用Windows的cmd或者PowerShell(推荐)将JAR文件上传到Linux服务器
  命令如下:(在JAR文件目录下执行)
  > scp myUDAF.jar root@remoteIP:~/myJars/hive/
 (其中remoteIP为远程服务器IP)
3.启动hadoop,启动hive('hive>'下输入,仅支持全路径名)
  > add jar /root/myJars/hive/myUDAF.jar
  <会提示成功加入 class path>
4.注册临时/永久函数
  > create temporary function myavg as 'com.hive.udaf.GenericUDAFAverage';(临时,作用本次会话)
  > create function myavg as 'com.hive.udaf.GenericUDAFAverage';(永久)
5.运行测试
  > select myavg(id) from data;
  4
  <此即聚合求平均值的结果>

四、另外实现了自定义聚合函数Concat(col)

功能:行转列,一列数据连接成一行。

如下代码:

package com.hive.udaf;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.apache.hadoop.hive.ql.exec.Description;
import org.apache.hadoop.hive.ql.exec.UDFArgumentTypeException;
import org.apache.hadoop.hive.ql.metadata.HiveException;
import org.apache.hadoop.hive.ql.parse.SemanticException;
import org.apache.hadoop.hive.ql.udf.generic.AbstractGenericUDAFResolver;
import org.apache.hadoop.hive.ql.udf.generic.GenericUDAFEvaluator;
import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.PrimitiveObjectInspector;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorFactory;
import org.apache.hadoop.hive.serde2.objectinspector.primitive.PrimitiveObjectInspectorUtils;
import org.apache.hadoop.hive.serde2.typeinfo.PrimitiveTypeInfo;
import org.apache.hadoop.hive.serde2.typeinfo.TypeInfo;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.util.StringUtils;

/*
 * .行转列
 */
@Description(name = "mycolconcat", value = "_FUNC_(x) - Returns the concat of a set of cols")
public class ConcatUDAF extends AbstractGenericUDAFResolver{
	
	static final Log LOG = LogFactory.getLog(ConcatUDAF.class.getName());
	
	@Override
	public GenericUDAFEvaluator getEvaluator(TypeInfo[] parameters) 
			throws SemanticException {
		if (parameters.length != 1) {
		      throw new UDFArgumentTypeException(parameters.length - 1,
		          "Exactly one argument is expected.");
	    }

	    if (parameters[0].getCategory() != ObjectInspector.Category.PRIMITIVE) {
	      throw new UDFArgumentTypeException(0,
	          "Only primitive type arguments are accepted but "
	          + parameters[0].getTypeName() + " is passed.");
	    }
	    switch (((PrimitiveTypeInfo) parameters[0]).getPrimitiveCategory()) {
	    case BYTE:
	    case SHORT:
	    case INT:
	    case LONG:
	    case FLOAT:
	    case DOUBLE:
	    case STRING:
	    case TIMESTAMP:
	    	return new ConcatUDAFEvaluator();
	    case BOOLEAN:
	    default:
	    	throw new UDFArgumentTypeException(0,
	    			"Only numeric or string type arguments are accepted but "
	    					+ parameters[0].getTypeName() + " is passed.");
	    }
	}
	
	public static class ConcatUDAFEvaluator extends GenericUDAFEvaluator {
		
		//Mode的各部分的输入都是String类型,输出也是,所以对应的OI实例也都一样
		PrimitiveObjectInspector inputOI;
		
		Text partialResult;
		
		Text result;
		
		@Override
	    public ObjectInspector init(Mode mode, ObjectInspector[] parameters)
	        throws HiveException {
			assert (parameters.length == 1);
			super.init(mode, parameters);
			
			// init input
			inputOI = (PrimitiveObjectInspector) parameters[0];
			
			// init output
			result = new Text("");
			return PrimitiveObjectInspectorFactory.writableStringObjectInspector;
	    }
		
		static class ConcatAgg implements AggregationBuffer {
			StringBuilder line = new StringBuilder("");
	    };
		
		@Override
		public AggregationBuffer getNewAggregationBuffer() throws HiveException {
			ConcatAgg result = new ConcatAgg();
			reset(result);
			return result;
		}

		@Override
		public void reset(AggregationBuffer agg) throws HiveException {
			ConcatAgg myagg = (ConcatAgg) agg;
			myagg.line.delete(0, myagg.line.length());
		}
		
		boolean warned = false;

		@Override
		public void iterate(AggregationBuffer agg, Object[] parameters) throws HiveException {
			Object p = parameters[0];
			if (p != null) {
				ConcatAgg myagg = (ConcatAgg) agg;
				try {
					String v = PrimitiveObjectInspectorUtils.getString(p, inputOI);
					if (myagg.line.length() == 0)
						myagg.line.append(v);
					else
						myagg.line.append("," + v);
				} catch (RuntimeException e) {
					if (!warned) {
						warned = true;
						LOG.warn(getClass().getSimpleName() + " "
								+ StringUtils.stringifyException(e));
						LOG.warn(getClass().getSimpleName()
								+ " ignoring similar exceptions.");
					}
				}
			}
		}

		@Override
		public Object terminatePartial(AggregationBuffer agg) throws HiveException {
			ConcatAgg myagg = (ConcatAgg) agg;
			result.set(myagg.line.toString());
			return result;
		}

		@Override
		public void merge(AggregationBuffer agg, Object partial) throws HiveException {
			if (partial != null) {
				try {
					ConcatAgg myagg = (ConcatAgg) agg;
					String v = PrimitiveObjectInspectorUtils.getString(partial, inputOI);
					if (myagg.line.length() == 0)
						myagg.line.append(v);
					else
						myagg.line.append("," + v);
				} catch (RuntimeException e) {
					if (!warned) {
						warned = true;
						LOG.warn(getClass().getSimpleName() + " "
								+ StringUtils.stringifyException(e));
						LOG.warn(getClass().getSimpleName()
								+ " ignoring similar exceptions.");
					}
				}
			}
		}

		@Override
		public Object terminate(AggregationBuffer agg) throws HiveException {
			ConcatAgg myagg = (ConcatAgg) agg;
			result.set(myagg.line.toString());
			return result;
		}
	}
	
}

 

  • 4
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 可以使用Java语言编写Hive自定义UDAF函数,但需要使用Hadoop Streaming API和Hadoop MapReduce API,并且需要确保实现的UDAF函数符合Hive的语义规范。 ### 回答2: Hive是一个开源的大数据仓库系统,用于处理和分析大规模结构化数据。Hive提供了丰富的函数库,以支持各种用例。除了内置函数外,Hive还支持自定义函数,其中包括自定义UDAF(用户定义的聚合函数)。 使用Java编写Hive自定义UDAF函数可以按照以下步骤进行: 1. 创建一个Java类,用于实现自定义UDAF函数。这个类需要继承Hive的GenericUDAFResolver2接口,并实现其中的方法。 2. 在Java类中,需要定义输入参数类型、中间状态类型和输出类型。根据自定义UDAF函数的需求,可以使用Hive提供的数据类型,如IntWritable、DoubleWritable等。 3. 在Java类中,需要实现initialize、iterate、merge和terminatePartial等方法,用于初始化和处理计算逻辑。 - initialize方法用于初始化中间状态; - iterate方法用于迭代处理每一行输入数据; - merge方法用于合并不同mapper或reducer的中间状态; - terminatePartial方法用于返回部分聚合结果。 4. 在Java类中,需要实现terminate方法,用于返回最终的聚合结果。 5. 编译Java类,并将生成的jar文件添加到Hive的classpath中。 6. 在Hive中,使用CREATE FUNCTION语句创建自定义UDAF函数,并指定使用的jar文件和Java类名。 7. 在Hive中,可以使用自定义UDAF函数进行聚合操作,例如使用SELECT语句。 编写Java类时,需要根据自定义UDAF函数的需求进行逻辑的实现。在编写完成后,应当进行测试和调试,确保函数的正确性和性能。 通过以上步骤,就可以使用Java编写Hive自定义UDAF函数,以满足特定的需求,对大规模结构化数据进行聚合和分析。 ### 回答3: 使用Java编写Hive自定义UDAF函数需要以下步骤: 1. 创建一个Java类,实现Hive中的GenericUDAFEvaluator接口。该接口定义了自定义UDAF函数的行为。 2. 在类中实现五个方法:init()、iterate()、terminatePartial()、merge()和terminate()。 - init()方法用于初始化函数的内部状态。 - iterate()方法用于每次处理输入值。 - terminatePartial()方法在部分聚合完成后返回部分结果。 - merge()方法用于合并部分结果。 - terminate()方法在整个聚合完成后返回最终结果。 3. 在类中定义一个静态内部类,实现AggregationBuffer接口,用于存储聚合结果的中间状态。 4. 在类中重写toString()方法,用于返回自定义聚合函数的名称。 5. 在Hive中使用CREATE FUNCTION语句注册自定义UDAF函数,指定Java类的路径和函数名称。 下面是一个示例: ```java import org.apache.hadoop.hive.ql.exec.UDAF; import org.apache.hadoop.hive.ql.exec.UDAFEvaluator; import org.apache.hadoop.hive.ql.metadata.HiveException; import org.apache.hadoop.hive.serde2.objectinspector.ObjectInspector; import org.apache.hadoop.hive.serde2.objectinspector.StandardListObjectInspector; import org.apache.hadoop.hive.serde2.objectinspector.StandardPrimitiveObjectInspector; import java.util.ArrayList; import java.util.List; public class CustomUDAF extends UDAF { public static class Evaluator implements UDAFEvaluator { private List<Double> values; @Override public void init() throws HiveException { values = new ArrayList<Double>(); } // 输入值处理 public boolean iterate(Double value) throws HiveException { if (value != null) { values.add(value); } return true; } // 返回部分结果 public List<Double> terminatePartial() { return values; } // 合并部分结果 public boolean merge(List<Double> other) { if (other != null) { values.addAll(other); } return true; } // 返回最终结果 public Double terminate() { Double sum = 0.0; for (Double value : values) { sum += value; } return sum; } // 定义输入和输出值的类型 public ObjectInspector init(Mode m, ObjectInspector[] parameters) throws HiveException { return StandardListObjectInspector .getListObjectInspector(StandardPrimitiveObjectInspector.PrimitiveCategory.DOUBLE); } } @Override public String toString() { return "custom_udaf"; } } ``` 在Hive中使用以下命令注册UDAF函数: ```sql CREATE FUNCTION custom_udaf AS 'com.example.CustomUDAF' USING JAR 'path/to/custom_udaf.jar'; ``` 然后可以在Hive中使用自定义UDAF函数进行聚合操作,例如: ```sql SELECT column, custom_udaf(column) AS sum FROM table GROUP BY column; ```

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值