Flink项目1.1- 实时页面统计-热门页面topN

1、需求 

2、需求输出每个窗口访问量最大的5个地址,所以一定是无状态的输出,有两种办法

(1)、keyby后自己使用processfunction自己定义state,否则是有状态的输出

(2)、window后➕windowAll方法

3、正常情况下不考虑乱序的时候(没有窗口延迟关闭1min和延迟数据的侧输出流)

event时间窗口滑动窗口10min,滚动5s输出

程序一启动,就会创建所有的窗口,但是有数据才会输出  [10:15:45-10:25:50), [10:15:50-10:25:55), [10:15:55-10:25:60)

watermark:延时1s

定时器:windowEnd+1s(但是定时器的加1s输出需要依赖watermark的更新,所以在watermark+2s才会触发)

aggDataStream需要窗口触发才会输出

输入的原始数据

输入(在 [10:15:45-10:25:50) ):89.101 - - 17/05/2015:10:25:49 +0000 GET /presedent 

输出:dataStream> ApacheLogEvent(89.101,-,1421461549000,GET,/presedent)

输入:89.101 - - 17/05/2015:10:25:50 +0000 GET /presedent

输出:dataStream> ApacheLogEvent(89.101,-,1421461550000,GET,/presedent)

输入:89.101 - - 17/05/2015:10:25:51 +0000 GET /presedent

输出(watermark对应的[10:15:45-10:25:50)窗口触发)

dataStream> ApacheLogEvent(89.101,-,1421461551000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461550000,1)

输入:

89.101 - - 17/05/2015:10:25:52 +0000 GET /presedent

输出(定时器触发)

dataStream> ApacheLogEvent(89.101,-,1421461552000,GET,/presedent)
top N> ====================================
时间: 2015-01-17 10:25:50.0
No1:  页面url=/presedent  浏览量=1
====================================

输入:10:25:56秒的数据时 [10:15:50-10:25:55)窗口触发,会输出agg流结果

输入:89.101 - - 17/05/2015:10:25:57 +0000 GET /presedent

输出(定时器触发)

dataStream> ApacheLogEvent(89.101,-,1421461557000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461555000,4)
top N> ====================================
时间: 2015-01-17 10:25:55.0
No1:  页面url=/presedent  浏览量=4   包含了[10:15:50-10:25:55)的所有数据
====================================

4、考虑乱序的时候(包含窗口延迟关闭1min和延迟数据的侧输出流)

乱序的三大保证

(1)、watermark的使用

(2)、allowlateness方法使用,窗口延迟关闭,延迟数据依然会进行增量聚合

(3)、输出到侧输出流

event时间窗口滑动窗口10min,滚动5s输出

  [10:15:45-10:25:50), [10:15:50-10:25:55), [10:15:55-10:25:60)

watermark:延时1s

定时器:windowEnd+1s(但是定时器的加1s输出需要依赖watermark的更新,所以在watermark+2s才会触发)

alowLatenes 为1min

侧输出流为:late

aggDataStream需要窗口触发才会输出

输入:

输入:89.101 - - 17/05/2015:10:25:49 +0000 GET /presedent
输入:89.101 - - 17/05/2015:10:25:50 +0000 GET /presedent
输入:89.101 - - 17/05/2015:10:25:51 +0000 GET /presedent
输入:89.101 - - 17/05/2015:10:25:52 +0000 GET /presedent
前面这4条记录和上述基本一直,除了窗口延迟1min后才会关闭
输入:89.101 - - 17/05/2015:10:25:46 +0000 GET /presedent
输出:根据之前的输入watermark已经为10:25:51了,所以agg触发了,但是定时器需要新的watermark更新才会触发。
dataStream> ApacheLogEvent(89.101,-,1421461546000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461550000,2)
输入:89.101 - - 17/05/2015:10:25:53 +0000 GET /presedent

输出更新了watermark,所以定时器触发了:

dataStream> ApacheLogEvent(89.101,-,1421461553000,GET,/presedent)
top N> ====================================
时间: 2015-01-17 10:25:50.0
No1:  页面url=/presedent  浏览量=2
====================================

输入:

89.101 - - 17/05/2015:10:25:31 +0000 GET /presedent

输出:原因是允许1min迟到数据,并且有数据,所以输出了,原来没有输出是因为之前没有数据,所以没输出

dataStream> ApacheLogEvent(89.101,-,1421461531000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461550000,3)
aggStream> PageViewCount(/presedent,1421461545000,1)
aggStream> PageViewCount(/presedent,1421461540000,1)
aggStream> PageViewCount(/presedent,1421461535000,1)

当前wartermark为10:25:52s,那么[,10:24:50]的窗口一定已经关闭了,那我们输入[,10:24:49]仍然不会输出到侧输出流里面,因为这条记录还属于[,10:24:55]等多个的窗口,该数据还是会被收进去的,输入侧输出流里面的数据需要该数据属于的所有窗口都已经关闭了,那我们输入一个10min之前的数据就会输出到侧输出流了,因为它的窗口都关闭了。

输入:

89.101 - - 17/05/2015:10:14:51 +0000 GET /presedent

 输出:侧输出流就会输出了

5、代码

package flinkProject

import java.sql.Timestamp
import java.text.SimpleDateFormat

import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor}
import org.apache.flink.configuration.ConfigOptions.ListConfigOptionBuilder
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment, _}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

import scala.collection.mutable.ListBuffer

//事件类
case class ApacheLogEvent(ip: String, userid: String, timestamp: Long, methord: String, url: String)

//窗口聚合结果
case class PageViewCount(url: String, windowEnd: Long, count: Long)

object HotNewworkLogFlow {
  def main(args: Array[String]): Unit = {
    val executionEnvironment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    executionEnvironment.setParallelism(1)
    executionEnvironment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //watermark周期性生成,默认是200ms
    //    executionEnvironment.getConfig.setAutoWatermarkInterval(500) //watermark周期性生成,默认是200ms,可以修改为500ms
    val stream2: DataStream[String] = executionEnvironment.socketTextStream("127.0.0.1", 1111)
    val transforStream: DataStream[ApacheLogEvent] = stream2.map(data => {
      val tmpList = data.split(" ")
      val simpleDateFormat = new SimpleDateFormat("dd/mm/yy:HH:mm:ss")
      val ts = simpleDateFormat.parse(tmpList(3)).getTime
      ApacheLogEvent(tmpList(0), tmpList(1), ts, tmpList(5), tmpList(6))
    })

    //增加watermark配置.punctated代表点状的,periodic代表周期性的(一般用这个)
    val transforEventStream: DataStream[ApacheLogEvent] = transforStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[ApacheLogEvent](Time.seconds(1)) {
      override def extractTimestamp(t: ApacheLogEvent) = t.timestamp
    })


    val aggResult: DataStream[PageViewCount] = transforEventStream
      .filter(_.methord == "GET")
      .keyBy(_.url)
      .timeWindow(Time.minutes(10), Time.seconds(5))
      .allowedLateness(Time.minutes(1))
      .sideOutputLateData(new OutputTag[ApacheLogEvent]("late"))
      .aggregate(new PageCountAgg, new PageCountWindow)

    transforStream.print("dataStream")
    aggResult.print("aggStream")
    aggResult.getSideOutput(new OutputTag[ApacheLogEvent]("late")).print()
    aggResult.keyBy(_.windowEnd).process(new HotProcessFunction(5)).print("top N")

    executionEnvironment.execute("transform")

  }
}

class PageCountAgg extends AggregateFunction[ApacheLogEvent, Long, Long] {
  override def createAccumulator(): Long = 0l

  override def add(value: ApacheLogEvent, accumulator: Long): Long = accumulator + 1

  override def getResult(accumulator: Long): Long = accumulator

  override def merge(a: Long, b: Long): Long = a + b
}


class PageCountWindow extends WindowFunction[Long, PageViewCount, String, TimeWindow] {
  override def apply(key: String, window: TimeWindow, input: Iterable[Long], out: Collector[PageViewCount]): Unit = {
    out.collect(PageViewCount(key, window.getEnd, input.iterator.next()))

  }
}


class HotProcessFunction(topSize: Int) extends KeyedProcessFunction[Long, PageViewCount, String] {

  var itemState: ListState[PageViewCount] = _

  override def open(parameters: Configuration): Unit = {
    super.open(parameters)
    // 命名状态变量的名字和状态变量的类型
    val itemsStateDesc = new ListStateDescriptor[PageViewCount]("itemState-state", classOf[PageViewCount])
    // 定义状态变量
    itemState = getRuntimeContext.getListState(itemsStateDesc)
  }

  override def processElement(i: PageViewCount, context: KeyedProcessFunction[Long, PageViewCount, String]#Context, collector: Collector[String]): Unit = {
    itemState.add(i)
    context.timerService().registerEventTimeTimer(i.windowEnd + 1)

  }

  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, PageViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
    val allPageViewCount: ListBuffer[PageViewCount] = ListBuffer.empty
    val iterable = itemState.get().iterator()
    while (iterable.hasNext) {
      allPageViewCount += iterable.next()
    }

    itemState.clear()

    val sortedItems = allPageViewCount.sortBy(_.count)(Ordering.Long.reverse).take(topSize)

    // 将排名信息格式化成 String, 便于打印
    val result: StringBuilder = new StringBuilder
    result.append("====================================\n")
    result.append("时间: ").append(new Timestamp(timestamp - 1)).append("\n")

    for (i <- sortedItems.indices) {
      val currentItem: PageViewCount = sortedItems(i)
      // e.g.  No1:  商品ID=12224  浏览量=2413
      result.append("No").append(i + 1).append(":")
        .append("  页面url=").append(currentItem.url)
        .append("  浏览量=").append(currentItem.count).append("\n")
    }
    result.append("====================================\n\n")
    // 控制输出频率,模拟实时滚动结果
    Thread.sleep(1000)
    out.collect(result.toString)


  }
}





6、原始数据:

没有alowtness和侧输出的测试数据
89.101 - - 17/05/2015:10:25:49 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:50 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:51 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:52 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:56 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:57 +0000 GET /presedent
89.101 - - 17/05/2015:10:26:02 +0000 GET /presedent

有alowtness和侧输出的测试数据
89.101 - - 17/05/2015:10:25:49 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:50 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:51 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:52 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:46 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:53 +0000 GET /presedent
89.101 - - 17/05/2015:10:25:31 +0000 GET /presedent
89.101 - - 17/05/2015:10:14:51 +0000 GET /presedent

7、问题:输入迟到数据不能和历史数据重新排序输出

现在解决了乱序问题后,还有个问题,当有1min内的迟到数据到来时,会重新触发agg方法进行聚合,输出一个agg的结果,例如(url1,3)变成了(url1,4),这个时候我们要用(url1,4)替换掉(url1,3)并且进行重新排序,所以要保留这个窗口的所有数据在state中,在该窗口的endTime+1min后再清空状态

所以我们定义mapState和一个新的endTime+1min的定时器

输入迟到数据会和历史数据重新排序输出

输出结果如下:

dataStream> ApacheLogEvent(89.101,-,1421461549000,GET,/presedent)
dataStream> ApacheLogEvent(89.101,-,1421461546000,GET,/pre)
dataStream> ApacheLogEvent(89.101,-,1421461551000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461550000,1)
aggStream> PageViewCount(/pre,1421461550000,1)
dataStream> ApacheLogEvent(89.101,-,1421461552000,GET,/presedent)
top N> ====================================
时间: 2015-01-17 10:25:50.0
No1:  页面url=/pre  浏览量=1
No2:  页面url=/presedent  浏览量=1
====================================


dataStream> ApacheLogEvent(89.101,-,1421461546000,GET,/presedent)
aggStream> PageViewCount(/presedent,1421461550000,2)
dataStream> ApacheLogEvent(89.101,-,1421461553000,GET,/presedent)
top N> ====================================
时间: 2015-01-17 10:25:50.0
No1:  页面url=/presedent  浏览量=2
No2:  页面url=/pre  浏览量=1
====================================

优化代码如下: 

package flinkProject

import java.sql.Timestamp
import java.text.SimpleDateFormat
import java.util
import java.util.Map

import org.apache.flink.api.common.functions.AggregateFunction
import org.apache.flink.api.common.state.{ListState, ListStateDescriptor, MapState, MapStateDescriptor}
import org.apache.flink.configuration.ConfigOptions.ListConfigOptionBuilder
import org.apache.flink.configuration.Configuration
import org.apache.flink.streaming.api.TimeCharacteristic
import org.apache.flink.streaming.api.functions.KeyedProcessFunction
import org.apache.flink.streaming.api.functions.timestamps.BoundedOutOfOrdernessTimestampExtractor
import org.apache.flink.streaming.api.scala.function.WindowFunction
import org.apache.flink.streaming.api.scala.{DataStream, StreamExecutionEnvironment, _}
import org.apache.flink.streaming.api.windowing.time.Time
import org.apache.flink.streaming.api.windowing.windows.TimeWindow
import org.apache.flink.util.Collector

import scala.collection.mutable.ListBuffer

//事件类
case class ApacheLogEvent(ip: String, userid: String, timestamp: Long, methord: String, url: String)

//窗口聚合结果
case class PageViewCount(url: String, windowEnd: Long, count: Long)

object HotNewworkLogFlow {
  def main(args: Array[String]): Unit = {
    val executionEnvironment: StreamExecutionEnvironment = StreamExecutionEnvironment.getExecutionEnvironment
    executionEnvironment.setParallelism(1)
    executionEnvironment.setStreamTimeCharacteristic(TimeCharacteristic.EventTime) //watermark周期性生成,默认是200ms
    //    executionEnvironment.getConfig.setAutoWatermarkInterval(500) //watermark周期性生成,默认是200ms,可以修改为500ms
    val stream2: DataStream[String] = executionEnvironment.socketTextStream("127.0.0.1", 1111)
    val transforStream: DataStream[ApacheLogEvent] = stream2.map(data => {
      val tmpList = data.split(" ")
      val simpleDateFormat = new SimpleDateFormat("dd/mm/yy:HH:mm:ss")
      val ts = simpleDateFormat.parse(tmpList(3)).getTime
      ApacheLogEvent(tmpList(0), tmpList(1), ts, tmpList(5), tmpList(6))
    })

    //增加watermark配置.punctated代表点状的,periodic代表周期性的(一般用这个)
    val transforEventStream: DataStream[ApacheLogEvent] = transforStream.assignTimestampsAndWatermarks(new BoundedOutOfOrdernessTimestampExtractor[ApacheLogEvent](Time.seconds(1)) {
      override def extractTimestamp(t: ApacheLogEvent) = t.timestamp
    })


    val aggResult: DataStream[PageViewCount] = transforEventStream
      .filter(_.methord == "GET")
      .keyBy(_.url)
      .timeWindow(Time.minutes(10), Time.seconds(5))
      .allowedLateness(Time.minutes(1))
      .sideOutputLateData(new OutputTag[ApacheLogEvent]("late"))
      .aggregate(new PageCountAgg, new PageCountWindow)

    transforStream.print("dataStream")
    aggResult.print("aggStream")
    aggResult.getSideOutput(new OutputTag[ApacheLogEvent]("late")).print()
    aggResult.keyBy(_.windowEnd).process(new HotProcessFunction(5)).print("top N")

    executionEnvironment.execute("transform")

  }
}

class PageCountAgg extends AggregateFunction[ApacheLogEvent, Long, Long] {
  override def createAccumulator(): Long = 0l

  override def add(value: ApacheLogEvent, accumulator: Long): Long = accumulator + 1

  override def getResult(accumulator: Long): Long = accumulator

  override def merge(a: Long, b: Long): Long = a + b
}


class PageCountWindow extends WindowFunction[Long, PageViewCount, String, TimeWindow] {
  override def apply(key: String, window: TimeWindow, input: Iterable[Long], out: Collector[PageViewCount]): Unit = {
    out.collect(PageViewCount(key, window.getEnd, input.iterator.next()))

  }
}


class HotProcessFunction(topSize: Int) extends KeyedProcessFunction[Long, PageViewCount, String] {

  var itemState: ListState[PageViewCount] = _
  var hotUrlState:MapState[String,Long]=_

  override def open(parameters: Configuration): Unit = {
    super.open(parameters)
    // 命名状态变量的名字和状态变量的类型
//    val itemsStateDesc = new ListStateDescriptor[PageViewCount]("itemState-state", classOf[PageViewCount])
    val hotUrlStateDesc=new MapStateDescriptor[String,Long]("hot-url-state",classOf[String],classOf[Long])
    // 定义状态变量
//    itemState = getRuntimeContext.getListState(itemsStateDesc)
    hotUrlState = getRuntimeContext.getMapState(hotUrlStateDesc)
  }

  override def processElement(i: PageViewCount, context: KeyedProcessFunction[Long, PageViewCount, String]#Context, collector: Collector[String]): Unit = {
//    itemState.add(i)
    context.timerService().registerEventTimeTimer(i.windowEnd + 1)
    hotUrlState.put(i.url,i.count)
    //负责清空状态的定时器,这时窗口已经彻底关闭,不再有聚合结果输出,可以清空状态
    context.timerService().registerEventTimeTimer(i.windowEnd + 60000l)
  }

  //参数timestamp是窗口触发的时间戳,之前注册的每个定时器都会走onTimer方法
  override def onTimer(timestamp: Long, ctx: KeyedProcessFunction[Long, PageViewCount, String]#OnTimerContext, out: Collector[String]): Unit = {
//    val allPageViewCount: ListBuffer[PageViewCount] = ListBuffer.empty
//    val iterable = itemState.get().iterator()
//    while (iterable.hasNext) {
//      allPageViewCount += iterable.next()
//    }
//
//    itemState.clear()
    if(timestamp==ctx.getCurrentKey+60000l){
      hotUrlState.clear()
      return
    }
    val allPageViewCount:ListBuffer[(String,Long)]=ListBuffer()
    val iterable: util.Iterator[Map.Entry[String, Long]] = hotUrlState.entries().iterator()
    while (iterable.hasNext){
      val map: Map.Entry[String, Long] = iterable.next()
      allPageViewCount+=((map.getKey,map.getValue))
    }

    val sortedItems = allPageViewCount.sortBy(_._2)(Ordering.Long.reverse).take(topSize)

    // 将排名信息格式化成 String, 便于打印
    val result: StringBuilder = new StringBuilder
    result.append("====================================\n")
    result.append("时间: ").append(new Timestamp(timestamp - 1)).append("\n")

    for (i <- sortedItems.indices) {
      val currentItem: (String,Long) = sortedItems(i)
      // e.g.  No1:  商品ID=12224  浏览量=2413
      result.append("No").append(i + 1).append(":")
        .append("  页面url=").append(currentItem._1)
        .append("  浏览量=").append(currentItem._2).append("\n")
    }
    result.append("====================================\n\n")
    // 控制输出频率,模拟实时滚动结果
    Thread.sleep(1000)
    out.collect(result.toString)


  }
}





  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
### 回答1: flink 实时计算是一种流式计算框架,可以用于实时处理大规模数据。词频统计flink 实时计算的一个常见应用场景,可以用于统计文本中每个单词出现的频率。初体验可以通过编写简单的代码实现,对 flink 实时计算有一个初步的了解。 ### 回答2: Flink 是一种流数据处理框架,可以进行实时计算和批处理,适用于对实时数据进行分析和处理的场景。其中,词频统计是一个常用的实时应用场景,可以用于分析用户搜索的热门关键词、监控社交媒体的流行话题等等。 使用 Flink 进行词频统计的过程如下: 1. 从数据源中读取数据:可以使用 Flink 提供的多种数据源,例如 Kafka、HDFS、本地文件等等; 2. 进行数据清洗:对数据进行过滤、去重等操作,保证数据的准确性和完整性; 3. 进行分词:将读取到的文本数据进行分词,可以使用开源的中文分词工具,例如 HanLP、jieba 等等; 4. 进行词频统计:通过对每个词语进行计数,得到每个词语的出现次数; 5. 输出结果:将词频统计的结果输出到指定的数据源上,例如 Kafka、HDFS等等。 在实际应用中,基于 Flink 的词频统计可以应用于多种场景。例如在电商网站中,词频统计可以用于分析用户搜索热度,从而为商家提供商品推荐,优化营销策略。又例如在新闻媒体中,词频统计可以用于监控事件热点,分析社交媒体上的流行话题,帮助新闻工作者快速捕捉社会热点。 总之,通过 Flink 实现实时的词频统计,可以帮助企业和个人快速获取实时数据,优化决策和服务。对于初学者来说,可以从简单的单词计数入手,逐步深入理解流计算和分布式计算的基本概念,提高数据处理的效率和准确性。 ### 回答3: Flink 是现代流式处理引擎,广泛用于实时计算场景。它通过高速数据流的处理能力,使得实时计算成为了可能。在 Flink 中,词频统计是一个非常重要的实时计算应用场景,其主要用途是统计某个文本中每个单词出现的频次,从而揭示文本的特点和蕴含的信息。 词频统计初体验中,我们需要先确定数据的输入源,这可以是数据流(Stream)或数据集(DataSet)。对于流式输入源,我们需要使用 Flink 的 DataStream API,而对于有界的离线输入源,我们需要使用 Flink 的 Batch API。 在进一步设计统计模型之前,我们需要对数据进行预处理,以清理噪声和冗余信息,并将其转换为可用于分析的形式。Flink 提供了许多数据预处理操作,我们可以将其组合使用,例如 map()、filter()、flatmap() 和 reduce() 等等。这些操作可以将数据流转换为指定格式的数据集,以方便数据分析。对于文本数据,我们通常需要将其转换为单词流,以便进行词频统计。 在 Flink 中,我们可以使用 Window 操作将数据流分成可管理的时间窗口,以便对其进行分析。常见的窗口类型有滚动窗口、滑动窗口、会话窗口等等。在词频统计场景中,我们可以使用 Tumbling Window 将数据流划分为固定大小的窗口,然后对每个窗口中的所有单词进行计数。 在得到了每个窗口内所有单词的计数值后,我们可以再进一步使用 reduce() 操作进行累计计算,得到每个单词的总出现次数。这些数据可以存储到外部系统中,如数据库或文件系统中,以方便后续分析或展示。 总之,通过实践词频统计场景,我们可以深入理解 Flink 流式处理引擎的设计理念和使用方法。同时,我们也能够更好地掌握实时数据流处理的实践中常见的数据处理和分析方法。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值