为了账号安全,请及时绑定邮箱和手机立即绑定

使用Apache Spark过滤数据流,用Druid读取数据,Superset打造仪表盘

大家好。在这个项目中,我将演示如何使用Apache Kafka过滤流数据的过程。我们将原始数据存储在一个名为**raw_data**的主题中,该主题包含来自10,000个用户和1,000,000个进程的数据。原始数据将通过三个Kafka代理服务器来消费和处理。

在此过程中,我们将使用Scala Spark获取原始数据,然后将其筛选成三个不同的类别:

  • 余额少于500时,我们会把它发到 **low** 频道。
  • 余额在500到2000之间时,我们会把它发到 **mid** 频道。
  • 余额超过2000时,我们会把它发到 **high** 频道。

一旦流开始,我们将从**low****mid****high**主题读取数据,使用Apache Druid。这些数据将在Apache Druid中进行合并。我将演示如何在Druid中创建一个新表,该表可以通过SQL查询。这将对需要数据分析的团队非常有帮助。

此外,我们将创建一个 Superset 数据仪表板Apache Druid 在此过程中发挥着关键作用,它使我们能够高效地分析和可视化数据。

🛠️系统和软件版本:

节:

章节:
  1. Kafka KRaft 版本: 3 个 Broker.
  2. Provectus — Kafka-UI.
  3. Kafka-Schema-Registry.
  4. 用于原始数据集的数据生成器.
  5. Spark Master 和 Spark Worker.
  6. Scala Spark 脚本和实时流处理.
  7. Apache Druid.
  8. Superset.

附注:请检查我的 GitHub 项目中的 **docker-compose.yml**、Dockerfile、数据生成脚本、Scala Spark 脚本/程序以及其他相关配置文件。

链接 =https://github.com/mcagriaktas/End-to-End-Streaming-Filter-Project/tree/main

在访问GitHub仓库时,请运行以下命令进行克隆:

    git clone asdasdasdasd.git

这将克隆一个仓库到本地:

注意: 首先我会解释所有工具和过程。之后我会演示如何开始容器并运行项目。

  1. 如何启动容器
  2. 如何开始项目
1. Apache Kafka:

我们将使用Kafka KRaft 3.8.0,在3个节点的集群中。所有相关文件都在**container**文件夹里。

你也可以在以下文件夹里找到所有配置文件。

在这个场景中,我们的Kafka broker将使用PLAINTEXT相互连接。如果您需要使用SASL_PLAINTEXTKafka ACLs来实现安全功能,请访问我们的项目页面以获取更多详细信息。

ling: 参见《基于角色的访问控制在Apache Kafka上的实现——使用Provectus》https://medium.com/@mucagriaktas/rbac-on-apache-kafka-with-provectus-cf356c8a2ba7

我们将对Kafka进行极限测试,因此我们将创建一个具有3个分区的主题。因此,我按如下方式配置了**server.properties**文件:

    ############################# 基本日志设置 #############################  

    # 一个用逗号分隔的列表,列出要存储日志文件的目录  
    log.dirs=/data/kafka  

    # 每个主题的默认日志分区数量。更多的分区可以提供更大的消费并行性,但这也会导致更多文件分布在代理之间。  
    num.partitions=3  

    # 每个数据目录在启动时用于日志恢复和关闭时用于刷新的线程数。  
    # 对于数据目录位于RAID阵列中的安装,建议增加此值。  
    num.recovery.threads.per.data.dir=3  

    ############################# 内部主题设置  #############################  
    # 用于存储内部主题“__consumer_offsets”和“__transaction_state”的复制因子  
    # 除了开发测试之外,建议使用更大的值以确保高可用性,如3。  
    offsets.topic.replication.factor=3  
    transaction.state.log.replication.factor=3  
    transaction.state.log.min.isr=3  
    leader.imbalance.check.interval.seconds=300

此外,我们的Kafka代理需要同时连接到本地主机(localhost)和彼此。因此,**listener****advertised.listeners**配置必须同时包括内部和外部的listeneradvertised.listeners

    listeners=PLAINTEXT://0.0.0.0:9192,CONTROLLER://0.0.0.0:9093,LISTENER_DOCKER_EXTERNAL://0.0.0.0:19092  
    advertised.listeners=PLAINTEXT://kafka1:9192,LISTENER_DOCKER_EXTERNAL://localhost:19092

如果你想理解卡夫卡的逻辑,可以看看我写的另一篇文章:

ling:<https://medium.com/@mucagriaktas/单体架构与使用Apache Kafka的消息队列(消息总线)-55e8fd5030c8>

2. Provectus — Kafka-UI

实际上,Kafka-UI 是一个非常简单易用的工具。我们只需要两个文件即可:一个用于我们的 JAR 文件,另一个叫做名为config.yml的文件。你可以在这里查看这些文件,在containers > container-images > provectus > Dockerfile中找到这些文件。

这个 JAR 文件就是我们的 Kafka-UI 应用程序。我们需要创建或准备一个 **config.yml** 配置文件,然后使用 **starter-kafka-ui.sh** 脚本来运行 JAR 文件并确保 **config.yml** 文件参与其中。

RUN wget https://github.com/provectus/kafka-ui/releases/download/v0.7.2/kafka-ui-api-v0.7.2.jar # 下载 kafka-ui-api-v0.7.2.jar 文件

config => provectus 文件夹中,你会看到 config.yml 文件。在这里,我们设置了 Kafka 的 IP 地址(我使用了容器名)。但你也可以直接使用 IP 地址,因为我为每个容器在 docker-compose.yml 中设置了一个单独的子网。

    kafka:  
      clusters:  
        - name: cagri_cluster  
          bootstrapServers: kafka1:9192,kafka2:9192,kafka3:9192  

    server:  
      port: 18080  

    logging:  
      level:  
        root: INFO

**docker-compose.yml** 会通过 **.sh** 脚本启动 Provectus。

    #!/bin/bash  

    java -Dspring.config.additional-location=/mnt/config.yml -jar /mnt/kafka-ui-api-v0.7.2.jar

Kafka-UI 提供了多种功能:您可以创建主题、发送和接收消息,还可以查看您的 Kafka Schema Registry

3. Kafka 模式注册表

部署 Kafka Schema Registry 十分简单 — Confluent 已将它做得非常用户友好。你可以查看 **containers** 文件夹中的部署说明。

# 创建 Schema Registry 安装目录的文件夹  
RUN mkdir /opt/schema-registry && \  
    curl -SL "https://packages.confluent.io/archive/7.3/confluent-community-7.3.0.tar.gz" \  
    -o /opt/schema-registry/confluent.tar.gz && \  
    mkdir /schema-registry && cd /schema-registry && \  
    建立 /schema-registry 目录,切换到该目录并解压文件使用 tar -xvzf 命令解压文件并删除多余的目录层次 && \  
    然后删除下载的压缩文件

接着,**docker-compose.yml** 文件会通过调用 **start-schema-registry.sh** 脚本来启动我们运行的 Kafka Schema Registry 容器。同样,我们也需要使用一个 properties 文件来定义我们的 Kafka代理的连接配置。

kafkastore.bootstrap.servers=PLAINTEXT://kafka1:9192,PLAINTEXT://kafka2:9192,PLAINTEXT://kafka3:9192

配置行定义了Kafka集群中各个节点的地址,格式为PLAINTEXT://hostname:port,表示使用明文协议连接到指定的Kafka节点。

之后,我们的start-schema-registry.sh就会启动模式注册器。

    #!/bin/bash  

    # 启动注册表
    /schema-registry/bin/schema-registry-start /schema-registry/etc/schema-registry/schema-registry.properties

一旦我们部署了容器和配置文件,就可以访问它们了。当我们把架构推送到注册表时,我们就能在提供的链接里看到它了。

kafka-schema-registry 为什么重要。

由于Kafka Schema Registry管理着模式定义,我们将为我们的**raw topic**定义一个模式定义。当我们用Spark读取数据时,我们需要使用从Kafka Schema Registry中获取的模式来解析(也就是“展开”)数据。

另外,Kafka Schema Registry 还提供了:

该链接指向的是Confluent平台的模式注册表文档

  1. 模式版本控制:模式注册表管理不同版本的模式,允许你在保持兼容性的同时,随着时间的推移演进模式。你可以检索特定版本的模式,从而更轻松地处理数据结构的变更。
  2. 数据兼容性:模式注册表确保模式版本间的数据一致性。
  3. 集中式模式管理:通过为所有模式使用集中式注册表,你将拥有一个单一真实来源,简化跨不同微服务或数据处理管道的模式管理。
  4. 序列化和反序列化:模式注册表提供与Kafka客户端集成的序列化器和反序列化器,允许应用程序使用Avro、Protobuf或JSON模式格式。这简化了数据处理过程,并减少了生产者与消费者间序列化错误的发生。
4. 原始数据集生成器:

在这个场景中,我的核心逻辑是处理流式数据,过滤它,然后将其插入特定的主题中。因此,我不需要真实的数据集。相反,通过Python的Faker模块,我创建了一个自动数据集。我们将数据以JSON格式插入到我们的原始主题中。

**datagenerator** 文件夹中,你会看到 Avro 和 JSON 格式的子文件夹。我们将使用 JSON 格式的数据。数据集相当大,所以请在你的机器上运行脚本生成自己的数据集。这样,当你部署 Kafka 集群时,你就可以从原始数据开始。

    python create_dataset.py

运行创建数据集的Python脚本 (yùn háng chuàng jì shù jú de Python jiǎo běn)

如果您使用的是 16 GB 内存,也可以修改 create_dataset.py 脚本。

可以使用 **create_dataset.py** 脚本文件来生成自动数据集。

5. Spark Master 和 Worker

当我们的数据来自一个数据生成工具,例如模拟我们公司网页应用中10,000名用户的行为,我们需要有效地过滤这些数据。为此,我们正在使用相应的技术来处理大数据。为了处理负载,我设置了一个包含两个工作节点和一个主节点的架构。

要运行 **.jar** 文件,请切换到 **containers => config => spark => submitfiles** 目录。在这个文件夹里,你会找到两个重要的文件。

  1. scala-spark-submit.sh — 一个用于在 Spark 主容器中运行 **.jar** 文件的脚本文件。
  2. The**.jar**文件 — 我将在本文的第六部分解释如何在 Scala 中打包生成 **.jar** 文件。

此外,文件夹中还有一个名为 **download_jar.sh** 的脚本。有时候 Spark-submit 无法自动下载依赖。这种情况下,你可以在 Maven 仓库 上找到所需的库,复制下载链接并使用 **download_jar.sh** 脚本获取它。

更多详情请参考我另外两篇文章

  1. https://medium.com/@mucagriaktas/end-to-end-data-engineer-data-lake-project-scala-spark-3-5-1-150246b65d1f - 终到终的数据工程师数据湖项目 Scala Spark 3.5.1 介绍
  2. https://medium.com/@mucagriaktas/apache-spark-hadoop-apache-spark-and-parquet-orc-format-d352bf95833 - Apache Spark、Hadoop 以及 Parquet ORC 格式的介绍
6. Scala Spark 脚本和实时流过滤

当我们从我们的**原始主题**读取数据时,我们需要使用Kafka 架构注册表来处理我们Kafka主题中的数据,这就是我们包含Kafka 架构注册表容器的原因。你可以在**streaming => scala**文件夹中查看完整的脚本。如上所示,我们已经在**localhost:18081**设置了Kafka 架构注册表容器。

    val schemaRegistryUrl = "定义 schemaRegistryUrl 为 http://kafka-schema-registry:18081"  
    val structSchema = "将结构模式 structSchema 设置为 fetchSchemaFromRegistry 函数的返回值,参数包括 spark,schemaRegistryUrl 和 'raw-value'"  

    def fetchSchemaFromRegistry(spark: SparkSession, schemaRegistryUrl: String, subject: String): StructType = {  
      "定义一个名为 fetchSchemaFromRegistry 的函数,它接受 SparkSession,字符串类型的 schemaRegistryUrl 和 subject 作为参数,并返回一个 StructType 类型的结果"
      val url = "将 URL 设置为 schemaRegistryUrl 的连接加上 '/subjects/$subject/versions/latest'"
      val response = "将响应内容 response 设置为从 URL 获取的字符串"
      val json = Json.parse(response)  
      val schemaString = "将模式字符串 schemaString 设置为 json 中 'schema' 字段的字符串值"
      val schemaJson = "解析模式字符串 schemaString 并将其转换为 schemaJson"

      val fields = "将 schemaJson 中的 'fields' 字段转换为一个 JsObject 的序列,并映射到 fields 变量中"
      (schemaJson \ "fields").as[Seq[JsObject]].map { field =>  
        val fieldName = (field \ "name").as[String]  
        val fieldType = "根据字段类型 field\ "type" 的值,将 fieldType 设置为相应的类型"
        (field \ "type").as[String] match {  
          case "string" => "设置 fieldType 为 StringType"
          case "double" => "设置 fieldType 为 DoubleType"
          case _ => "设置 fieldType 为 StringType"
        }  
        StructField(fieldName, fieldType, nullable = true)  
      }  
      "返回结构类型 StructType,其字段为 fields"
      StructType(fields)  
    }

之后,我们配置了SparkKafka的JAR文件。您可以在https://mvnrepository.com/的Maven仓库页面中查看所需的依赖项。以下是我们的完整包需求。

    // 定义Scala版本号
    val scala2Version = "2.12.20"  
    // 定义Spark版本号
    val sparkVersion = "3.5.3"  

    // 引入Spark核心库
    "org.apache.spark" %% "spark-core" % sparkVersion,  
    // 引入Spark SQL库
    "org.apache.spark" %% "spark-sql" % sparkVersion,  
    // 引入Spark流处理库
    "org.apache.spark" %% "spark-streaming" % sparkVersion,  
    // 引入Spark Kafka 0.10流处理库
    "org.apache.spark" %% "spark-streaming-kafka-0-10" % sparkVersion,  
    // 引入Spark SQL Kafka 0.10库
    "org.apache.spark" %% "spark-sql-kafka-0-10" % sparkVersion,  
    // 引入Kafka客户端库
    "org.apache.kafka" % "kafka-clients" % "3.5.1",  
    // 引入Play JSON库
    "com.typesafe.play" %% "play-json" % "2.9.4"

我在脚本中设置了一个基本的过滤部分。你也可以修改过滤功能部分,利用Apache Spark进行自定义操作。

定义过滤数据函数 `filterData(data: DataFrame): (DataFrame, DataFrame, DataFrame)`,该函数接收一个 `DataFrame` 参数并返回三个 `DataFrame` 对象。具体逻辑如下:

- 将数据中 `balance` 小于等于 500 的记录筛选出来,存入 `lowTopic`。
- 将数据中 `balance` 大于 500 且小于等于 2000 的记录筛选出来,存入 `midTopic`。
- 将数据中 `balance` 大于 2000 的记录筛选出来,存入 `highTopic`。

最终返回这三个 `DataFrame` 对象 `(lowTopic, midTopic, highTopic)`。

我们可以详尽地讨论这些脚本,你也可以在网页上查看代码逻辑。在最后一步中,我们将通过流处理生成过滤后的数据。

      def 写入Kafka(data: 数据框, topic: String): Unit = {  
        data选择表达式("将id转换为字符串 as 键", "转换为JSON(结构(*)) AS 值")  
          .写入流  
          .格式("kafka")  
          .选项("kafka.bootstrap.servers", "kafka1:9192,kafka2:9192,kafka3:9192")  
          .选项("主题", topic)  
          .选项("检查点位置", s"/tmp/checkpoints/$topic")  
          .输出模式("追加")  
          .启动()  
      }

顺便提一下,建议将代码拆分成函数,这样在生产环境中每个人都能更容易看懂。

如果你修改了脚本内容,你需要重新编译一下。使用命令:sbt clean package 来完成。

cagri@Dahbest:~/projects/streaming/streaming/scala/target$ sbt clean package
7. Apache Druid,

Apache Druid 处理流数据并允许你对其运行 SQL 查询。它既可以本地部署,也可以云端部署,并且可以与 Apache Kafka 无缝对接。Apache Druid 低延迟,并可以直接从 Kafka 获取数据。

部署 Apache Druid 时,需要配置环境。在 **druid** 目录下的容器里,你可以找到我们部署的用于 PostgreSQL 的元数据存储 URL。此外,还需要在配置中指定 **druid_extensions_loadList**,包括 **druid-kafka-indexing-service** 在内。你还可以通过在环境配置中定义 **druid_indexer_runner_javaOptsArray** 来限制 Druid 的内存。

Apache Druid 组件在我的配置中。

1-) 动物园管理员

  • 负责 Druid 进程之间的服务发现和协调。
  • 担任集群管理的核心,并存储元数据,例如领导者选举的信息。

2. 协调人:

  • 处理数据管理任务,如分段放置任务、负载均衡任务和删除任务。
  • 确保数据在各个历史时间点间均匀分布。

3-) 中介

  • 响应用户的查询。
  • 作为网关,将查询转发到历史节点和实时节点之后,再合并结果。
  1. 历史 :
  • 存储不可变的预处理数据(分段)。
  • 响应代理请求的数据查询。

五) 中层经理

  • 执行摄取任务,将新数据引入系统。
  • 暂时存储数据,直到交给历史节点处理。

6) 路由器:

  • 为所有 Druid API 提供单一入口。
  • 将查询和 API 调用路由到适当的服务器(例如 broker 和 coordinator)。

然后我们需要将我们的 kafka brokers 与 apache druid 对接起来

你可以通过 **localhost:8081** 进入 Druid,然后依次点击 加载数据 > Apache Kafka > 连接数据

你需要输入你的完整 Kafka IP 地址。这个功能非常有用,因为它能让你把多个主题整合到一个表格里。

你也可以在过滤部分筛选你的数据。虽然在这个项目中我没有使用到所有的功能,Apache Druid 还是提供了很多强大的功能。确保将 参数设置 > 启用早期偏移 设置为 False — 这可以确保我们接收到完整的数据集。虽然我们不一定需要所有数据,这样可以让我们在流式仪表板上看到更清晰的视图。你可以将其他设置保持为默认值。

第八部分:超集

Superset 也是一个 Apache 工具,我们只是使用它来创建主题(topics)并设置一个 仪表盘。最初,我没有为创建 Superset 账户(account)编写一个 **.sh** 脚本,因为我希望手动展示账户创建的过程。

之后,你可以关闭程序并打开浏览器中的 Superset 面板,在 **localhost:8088**。然后你需要将我们的 Apache Druid 表(**low****mid****high**)导入 Superset

首先,我们得连接到我们的Apache Druid 代理,因为Druid通过代理来访问源数据,并使用PostgreSQL来存储元数据。因此,我们需要使用Apache Druid 代理

提示:别忘了,你需要首先在控制台创建用户,之后你就可以看如何创建仪表板的那部分了。你可以看文章最后的部分哦。

你需要挑出 Druid 这个选项。

连接数据库后,你可以在Superset中添加你的 **低等|中等|高等** 表。

德鲁伊(druid):低|中|高

如你所知,首先我们使用Apache Spark对数据进行分割并进行过滤,生成**low****mid****high**类别。然后,我们将这些数据在Apache Druid中进行合并,并通过Superset读取这些数据。在主菜单==>图表==>中

创建图表时,你非常灵活,因为Superset提供了丰富的数据可视化功能。我搭建了一个基本的仪表板,将我们的 **低段****中段****高段** 数据进行划分并计数。为此,我们需要使用自定义SQL查询。

    CASE   
      WHEN 「balance < 500」 THEN «低»  
      WHEN 「balance BETWEEN 500 AND 2000」 THEN «中»  
      WHEN 「balance > 2000」 THEN «高»  
    END

这里有图表示例:

保存完仪表板后,最后一步就是设置实时流数据:

最后,你需要设置实时流数据面板的时间间隔:

就这样,我们就成功地把仪表板部署好了。

怎么启动项目的方法

现在我们已经部署了容器,让我们回顾一下如何启动容器、配置容器并运行这个项目。

1-) 首先,确保你克隆了我的代码库。

    git clone asdasdasd.git

这行命令用于克隆名为asdasdasd的git仓库。

接下来,进入项目文件夹下的容器文件夹

     docker-compose up -d --build

启动并构建所有服务 (Start and build all services)

下载过程中,请耐心等待。所有容器启动完毕后,请额外等待2-3分钟让配置完成。

  1. 首先,我们需要创建我们的 Kafka 主题(topic)。虽然你可以使用 Provectus Kafka-UI 来完成这一步,但我将演示如何在控制台中创建它。
    docker exec -it kafka1 bash  

    ./kafka-topics.sh --create --bootstrap-server localhost:9192 --topic raw --partitions 3 --replication-factor 3  
    ./kafka-topics.sh --create --bootstrap-server localhost:9192 --topic low --partitions 3 --replication-factor 3  
    ./kafka-topics.sh --create --bootstrap-server localhost:9192 --topic mid --partitions 3 --replication-factor 3  
    ./kafka-topics.sh --create --bootstrap-server localhost:9192 --topic high --partitions 3 --replication-factor 3

如我在提到的卡夫卡部分,由于我们有3个节点,我们在server.properties中设置了partitions=3。如果 我们没有3个节点,我们就不能设置partitions=3为3了。

当我们部署 kafka 集群时,我们需要创建 kafka 架构注册表。

首先,输入到kafka-schema-registry容器

运行bash shell进入kafka模式注册表容器
docker exec -it kafka-schema-registry bash

之后:

    curl -X POST -H "Content-Type: application/vnd.schemaregistry.v1+json" \  
    --data '{  
      "schema": "{\"type\": \"record\", \"name\": \"Transaction\", \"fields\": [ {\"name\": \"id\", \"type\": \"string\"}, {\"name\": \"first_name\", \"type\": \"string\"}, {\"name\": \"last_name\", \"type\": \"string\"}, {\"name\": \"balance\", \"type\": \"double\"} ] }"  
    }' \  
    http://kafka-schema-registry:18081/subjects/raw-value/versions

这命令会发送一个包含交易模式的POST请求到Kafka模式注册表。

你可以在浏览器中查看你的数据模型。

    http://localhost:18081/subjects/raw-value/versions/1

5-) 在那之后,启动 Scala Spark 的 JAR 文件,并确保你在 **containers/config/spark/submitfiles** 目录中。

你可能想知道为什么我在 **spark-submit** 这一行添加了这些包。与 Python 不同的是,Scala Spark 没有类似 **find-spark** 的工具,所以 **spark-submit** 有时可能找不到本地的 JAR 文件。这就是为什么我在命令中直接包含了这些包。

你需要把 jar 文件复制到文件夹里:

    chmod +x scala-spark-submit.sh

将scala-spark-submit.sh文件的权限改为可执行

然后:

    ./scala-spark-submit.sh

注:./scala-spark-submit.sh 是一个用于提交Scala Spark任务的脚本。

Spark 会自动下载 JAR 文件。

6-) 启动生成器。如第一节所述,确保你已创建了数据集。你可以在此路径找到Python脚本:root_project_folder ==> datagenerator ==> json ==> producer

备注: 我在说 **datagenerator** 部分时创建了它,请看第四个帖子。

在命令行中输入以下命令来运行名为producer.py的生产者脚本:
python producer.py

7-) 一旦你开始生成和消费数据,你就可以访问Apache Druid(专有名词)并通过访问Apache Druid在localhost:8081创建你的中等表。我们之前讨论过如何在Apache Druid中创建这些表。

8-) 之后,您可以创建您的流媒体监控面板**localhost:8088**。我们之前讲过如何设置您的流媒体监控面板

进入 Superset 容器环境:

在superset容器中打开bash终端:docker exec -it superset bash

更新 Superset 数据库

superset db, 更新数据库

创建一个管理账户

superset fab create-admin --username cagri --firstname Superset --lastname Admin --email admin@superset.com --password 35413541

启动 Superset:

    superset 启动

9-) 如果你需要重启项目,你可能需要清理日志文件。转到容器 > data_logs 并清理该文件夹,因为我已经将所有容器日志设置为使用 Docker 卷。在同一路径下,我提供了一个 **data_log_rm.sh** 脚本,可以用来清除日志文件。

运行数据日志删除脚本 ./data_log_rm.sh

**.sh**脚本不会影响您的Kafka架构,因为重启容器不会删除您的Kafka主题。但是,如果您需要清除Kafka代理日志或数据,可以在此路径下运行以下命令:

执行命令 sudo rm -rf ./data_logs/kafka_data/* 来删除 kafka_data 目录下的所有文件。

我知道这个项目很庞大,包含许多配置步骤,但相信我,一旦你部署并开始使用,你将学到很多。如果你需要任何帮助,有任何问题都可以在LinkedIn上找我——我的LinkedIn个人档案链接在我的Medium简介里。

大家平平安安 :)

点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消