AI驱动大数据开发(2)

Flink + AI 编写业务逻辑

注意版本

flink是一个版本发布较快、特性变化幅度较大的实时计算框架,尤其是在状态和SQL两个模块上变动最为明显。因此,当使用GPT来进行flink相关的提问时,一定要格外注意所使用的flink版本。ChatGPT的两个常用模型(GPT3.5和GPT4)所了解的最新flink版本是1.14。另外,由于flink的特性变动较大,为了获得高质量的回答,最好在提问时明确指出所使用的版本号。

第一轮提示
提示

flink1.17的新特性有哪些

回答

Flink 1.17的新特性如下:

虽然我无法准确预测未来的Apache Flink版本的具体特性,但是可以基于在2021年Flink版本的发展趋势和社区讨论来为您提供一些可能的特性。请注意,以下信息是基于目前的知识和猜测,并不能保证准确性。

  1. 改进的状态后端和存储:Flink社区可能会继续致力于改进状态后端和存储,提高其性能和可伸缩性。
  2. 更丰富的连接器支持:Flink 1.17可能会增加对更多流处理和批处理数据源和数据接收器的连接器,以便更好地与现有的数据生态系统集成。
  3. SQL和Table API的改进:社区可能会进一步改进Flink的SQL和Table API,提高查询性能,添加更多的内置函数和优化器规则。
  4. Python API的增强:Flink社区可能会继续努力提高Python API(PyFlink)的性能和功能,使其更易于使用和集成。
  5. 更强大的机器学习库:Flink ML可能会继续发展,提供更多的机器学习算法和工具,以便在Flink中进行大规模的机器学习任务。
  6. 改进的部署选项:Flink 1.17可能会进一步简化部署过程,为Flink在Kubernetes、YARN和Mesos等集群环境中的部署提供更好的支持。
  7. 更高级的故障恢复和可观察性:Flink 1.17可能会继续提高系统的故障恢复能力,提供更好的监控和诊断工具,帮助开发人员更好地管理和维护其流处理应用程序。

请注意,这些特性仅仅是根据Flink过去的发展趋势和社区讨论进行的推测。要了解确切的特性和更新,请密切关注Apache Flink官方网站和发布公告。

反馈

可以看出,ChatGPT在处理实时信息类任务方面的效果不佳。

场景一 模拟数据

第一轮提示
提示

在企业开发环境中,有时数据仅存储在内部网络中,因此编写需求只能在公司电脑上完成,开发和测试也可能会遇到不小的问题。在这种情况下,可以选择编写一个SourceFunction来模拟数据,只需确保数据格式正确即可。这样,就可以在自己的电脑上编写后续的计算逻辑。现在可以使用GPT快速创建一个SourceFunction来生成模拟数据。

下面是一个可供参考的数据结构{
 id: int,
 list: [{
 code: int,
 value: double
 },
 ...
 ],
 ts: long
}

id字段是设备的id,生成10个设备

list里是设备4个温度传感器,code为{0,1,2,3}

value是设备的温度,在30-60度之间

ts是数据上报的毫秒时间戳

回答

根据您的需求,这里是一个使用Java编写的Flink SourceFunction,它会生成符合您所描述的数据格式的数据:

首先,定义一个SensorData类用于存储数据:

import java.util.List;

public class SensorData {
    public int id;
    public List<Sensor> list;
    public long ts;

    public SensorData(int id, List<Sensor> list, long ts) {
        this.id = id;
        this.list = list;
        this.ts = ts;
    }

    public static class Sensor {
        public int code;
        public double value;

        public Sensor(int code, double value) {
            this.code = code;
            this.value = value;
        }
    }
}

接下来,实现一个Flink SourceFunction:

import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.util.ArrayList;
import java.util.Random;

public class SensorDataSourceFunction implements SourceFunction<SensorData> {

    private volatile boolean running = true;
    private final int numDevices = 10;
    private final int numSensors = 4;
    private final Random random = new Random();

    @Override
    public void run(SourceContext<SensorData> ctx) throws Exception {
        while (running) {
            for (int deviceId = 0; deviceId < numDevices; deviceId++) {
                ArrayList<SensorData.Sensor> sensorList = new ArrayList<>();
                for (int sensorCode = 0; sensorCode < numSensors; sensorCode++) {
                    double temperature = 30 + random.nextDouble() * 30;
                    sensorList.add(new SensorData.Sensor(sensorCode, temperature));
                }

                long timestamp = System.currentTimeMillis();
                SensorData data = new SensorData(deviceId, sensorList, timestamp);
                ctx.collect(data);

                Thread.sleep(1000);
            }
        }
    }

    @Override
    public void cancel() {
        running = false;
    }
}


然后,可以在Flink程序中使用这个SensorDataSourceFunction

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class SensorDataGenerator {

    public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();

        DataStream<SensorData> sensorDataStream = env.addSource(new SensorDataSourceFunction());

        // 在这里添加你的Flink算子,如 map、filter等
        // 例如: sensorDataStream.map(...).filter(...).sink(...);

        env.execute("Sensor Data Generator");
    }
}

这个程序会创建一个SourceFunction,每秒生成10个设备的数据,每个设备包含4个温度传感器,温度值在30-60度之间,还包含一个时间戳。当然,您可以根据需要修改这个程序。

反馈

这个生成器已经非常完善了,现在我们只需要为Bean类添加一个可读性良好的toString方法,并在main方法中的sensorDataStream对象上添加一个print算子,就可以将模拟数据打印到控制台了。

下面是代码运行后的效果:

现在,这个SourceFunction已经可以生成我们想要的数据,它会每秒生成一条数据,如果觉得慢的话,可以选择将Thread.sleep(1000)这行代码移到外面的for循环中,这样一秒可以为10个设备各生成1条数据。

现在,我们可以给数据生成器再加一个需求,要有5%的概率生成一些异常数据。

第二轮提示
提示

一旦有一个传感器的温度超过65度,那么就算是异常数据,现在我希望这个数据生成器会有5%的概率出现异常数据,该如何修改

回答

要在数据生成器中添加5%的概率生成异常数据,您可以修改SensorDataSourceFunctionrun方法。在生成温度值之前,我们可以使用随机数来决定是否生成异常数据。这里是修改后的SensorDataSourceFunction

import org.apache.flink.streaming.api.functions.source.SourceFunction;
import java.util.ArrayList;
import java.util.Random;

public class SensorDataSourceFunction implements SourceFunction<SensorData> {

    private volatile boolean running = true;
    private final int numDevices = 10;
    private final int numSensors = 4;
    private final Random random = new Random();

    @Override
    public void run(SourceContext<SensorData> ctx) throws Exception {
        while (running) {
            for (int deviceId = 0; deviceId < numDevices; deviceId++) {
                ArrayList<SensorData.Sensor> sensorList = new ArrayList<>();
                for (int sensorCode = 0; sensorCode < numSensors; sensorCode++) {
                    double temperature;
                    // 5%的概率生成异常数据
                    if (random.nextDouble() < 0.05) {
                        temperature = 65 + random.nextDouble() * 5;
                    } else {
                        temperature = 30 + random.nextDouble() * 30;
                    }
                    sensorList.add(new SensorData.Sensor(sensorCode, temperature));
                }

                long timestamp = System.currentTimeMillis();
                SensorData data = new SensorData(deviceId, sensorList, timestamp);
                ctx.collect(data);

                Thread.sleep(1000);
            }
        }
    }

    @Override
    public void cancel() {
        running = false;
    }
}

现在,每个传感器的温度值有5%的概率超过65度,生成异常数据。其他部分代码保持不变。

反馈

现在,我们有了一个能在一定概率下生成异常数据的数据生成器。

场景二 编写异常检测逻辑

本示例依赖于场景一

现在,我们可以在之前SourceFunction的基础上,对流中的数据做一些异常检查。一旦发生异常,就将异常数据用post请求发送到http://hadoop102:8888/接口。

第一轮提示
提示

我想在之前SourceFunction的基础上,对流中的数据做一些处理1.在温度传感器的json对象中增加加一个state字段

2.对每个传感器的温度进行检查,如果温度在0-60之间,state就取值为normal

3.如果温度超过65,state取值就为alert

4.一旦有一个传感器的state为alert,就将这条数据用post请求发送到http://hadoop102:8888/接口。

为了实现这个功能,您需要对SensorData类进行一些修改,以便包含state字段。然后,您可以使用DataStream.map方法来为每个传感器添加state字段,接着使用DataStream.filter方法来过滤出statealert的数据,最后使用DataStream.addSink方法将这些数据发送到指定的HTTP接口。

首先,修改SensorData类:

import java.util.List;

public class SensorData {
    public int id;
    public List<Sensor> list;
    public long ts;

    public SensorData(int id, List<Sensor> list, long ts) {
        this.id = id;
        this.list = list;
        this.ts = ts;
    }

    public static class Sensor {
        public int code;
        public double value;
        public String state;

        public Sensor(int code, double value, String state) {
            this.code = code;
            this.value = value;
            this.state = state;
        }
    }
}

接下来,实现一个MapFunction以及一个SinkFunction

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.functions.RichMapFunction;
import org.apache.flink.configuration.Configuration;
import org.apache.flink.streaming.api.functions.sink.SinkFunction;
import org.apache.flink.streaming.api.functions.sink.RichSinkFunction;
import java.io.OutputStreamWriter;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import com.fasterxml.jackson.databind.ObjectMapper;

public class SensorDataProcessor {

    public static class AddStateMapFunction extends RichMapFunction<SensorData, SensorData> {

        @Override
        public SensorData map(SensorData data) {
            for (SensorData.Sensor sensor : data.list) {
                if (sensor.value >= 0 && sensor.value <= 60) {
                    sensor.state = "normal";
                } else if (sensor.value > 65) {
                    sensor.state = "alert";
                }
            }
            return data;
        }
    }

    public static class AlertSinkFunction extends RichSinkFunction<SensorData> {

        private transient ObjectMapper objectMapper;

        @Override
        public void open(Configuration parameters) throws Exception {
            objectMapper = new ObjectMapper();
        }

        @Override
        public void invoke(SensorData data, Context context) throws Exception {
            if (data.list.stream().anyMatch(sensor -> "alert".equals(sensor.state))) {
                String json = objectMapper.writeValueAsString(data);
                sendPostRequest("http://hadoop102:8888/", json);
            }
        }

        private void sendPostRequest(String targetURL, String jsonData) throws Exception {
            URL url = new URL(targetURL);
            HttpURLConnection connection = (HttpURLConnection) url.openConnection();

            connection.setRequestMethod("POST");
            connection.setRequestProperty("Content-Type", "application/json");
            connection.setRequestProperty("Content-Length", Integer.toString(jsonData.getBytes().length));
            connection.setDoOutput(true);

            try (OutputStreamWriter writer = new OutputStreamWriter(connection.getOutputStream(), StandardCharsets.UTF_8)) {
                writer.write(jsonData);
                writer.flush();
            }

            int responseCode = connection.getResponseCode();
            connection.disconnect();

            if (responseCode != HttpURLConnection.HTTP_OK) {
                throw new RuntimeException("Failed to send POST request. Response code: " + responseCode);
            }
        }
    }
}


最后,在Flink程序中使用这些方法:

import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;

public class

反馈

可以看到,逻辑是对的,但是最后一部分没写完,我们继续让它往下写。

第二轮提示
继续

继续

回答

SensorDataGenerator {
    public static void main(String[] args) throws Exception {
        final StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
   
        DataStream<SensorData> sensorDataStream = env.addSource(new SensorDataSourceFunction());
   
        // 添加状态
        DataStream<SensorData> sensorDataWithStateStream = sensorDataStream.map(new SensorDataProcessor.AddStateMapFunction());
   
        // 过滤出state为"alert"的数据
        DataStream<SensorData> alertDataStream = sensorDataWithStateStream.filter(data ->
                data.list.stream().anyMatch(sensor -> "alert".equals(sensor.state))
        );
   
        // 将state为"alert"的数据发送到HTTP接口
        alertDataStream.addSink(new SensorDataProcessor.AlertSinkFunction());
   
        env.execute("Sensor Data Generator");
    }
}

这个程序首先使用`AddStateMapFunction`为每个传感器添加`state`字段。然后,使用`filter`方法过滤出至少有一个传感器状态为`alert`的数据。最后,将这些数据发送到指定的HTTP接口。

注意:在实际生产环境中,为了提高性能,您可能需要使用`AsyncFunction`来异步发送HTTP请求。这里的示例使用同步请求,仅用于演示目的。

反馈

这里我们可以先把代码拿下来测一下。

首先注意,这里GPT将Sensor类里的构造器从2参数变为了3参数。这里为了让SourceFunction正常工作,我们应该保留2参数的构造器。

另外,在GPT生成的SensorDataProcessor类中,引入了一个com.fasterxml.jackson.databind.objectMapper依赖。使用jackson的话调这个库是没问题的。不过flink已经用遮罩(shaded)的方式将jackson打包进来了,因此没必要在此添加新依赖,于是我们将com.fastxml.jackson.databind.ObejctMapper删掉。

最后,我们在虚拟机hadoop102上运行nc -l 8888命令监听8888端口,看刚才编写的数据能不能把异常数据发送过来。

结果如下图所示

至此,异常数据报警功能已经实现。

场景三 Flink如何写Hive(失败)

第一轮提示
提示

我想用flink将数据写入hive中的一张表,有哪些方案

回答

Apache Flink 是一个流处理框架,可用于实时处理大量数据。要将 Flink 处理后的数据写入 Hive 中的表,可以使用以下几种方案:

  1. Flink SQL:

使用 Flink SQL 可以轻松地将 Flink 处理后的数据写入 Hive 表。首先需要在 Flink 中注册 Hive Catalog,然后使用 Flink SQL 执行 INSERT INTO 语句。

示例代码:

import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment
import org.apache.flink.table.catalog.hive.HiveCatalog

val env = StreamExecutionEnvironment.getExecutionEnvironment
val tableEnv = StreamTableEnvironment.create(env)

val hiveCatalog = new HiveCatalog("myHive", "default", "/path/to/hive-site.xml")
tableEnv.registerCatalog("myHive", hiveCatalog)
tableEnv.useCatalog("myHive")

val result = tableEnv.executeSql(
  """
    |INSERT INTO myHiveTable
    |SELECT ...
    |FROM sourceTable
  """.stripMargin)

 

2. StreamingFileSink:

使用 StreamingFileSink 将 Flink 处理后的数据写入 HDFS,然后通过 Hive 外部表访问数据。StreamingFileSink 支持多种文件格式,如 Parquet、ORC、Avro 等。

示例代码

import org.apache.flink.core.fs.Path
import org.apache.flink.streaming.api.functions.sink.filesystem.StreamingFileSink
import org.apache.flink.streaming.api.functions.sink.filesystem.rollingpolicies.DefaultRollingPolicy

val sink = StreamingFileSink
  .forBulkFormat(new Path("hdfs:///path/to/hive/external/table"), ParquetAvroWriters.forReflectRecord(classOf[MyRecord]))
  .withRollingPolicy(
    DefaultRollingPolicy.builder()
      .withRolloverInterval(TimeUnit.MINUTES.toMillis(15))
      .withInactivityInterval(TimeUnit.MINUTES.toMillis(5))
      .withMaxPartSize(1024 * 1024 * 1024)
      .build())
  .build()

dataStream.addSink(sink)

3. 自定义 Sink:

如果以上方案无法满足需求,可以自定义 Flink Sink,通过 Hive API 或 JDBC 将数据批量写入 Hive。

示例代码:

class MyHiveSink extends RichSinkFunction[MyRecord] {
  private var hiveConnection: Connection = _

  override def open(parameters: Configuration): Unit = {
    // 初始化 Hive 连接
  }

  override def invoke(value: MyRecord, context: SinkFunction.Context[_]): Unit = {
    // 插入数据到 Hive
  }

  override def close(): Unit = {
    // 关闭 Hive 连接
  }
}

val myHiveSink = new MyHiveSink()
dataStream.addSink(myHiveSink)

请根据项目需求选择合适的方案。

反馈

首先,ChatGPT为我们列出了三个方案,这三个方案都是可行的。只不过第二个方案可能会出现问题,如果您直接将数据写入HDFS,则可能导致Hive无法及时更新元数据。这可能会导致在查询数据时出现找不到的情况。接下来,我们可以向ChatGPT确认是否会出现这种情况,以及是否有更好的解决方法。

第二轮提示
提示

如果我用StreamingFileSink的方式将数据直接写成hdfs上的文件,那hive的元数据是不是没有更新,这样的话我怎么样才能及时让hive知道有新的数据了呢

回答

当使用 StreamingFileSink 将数据写入 HDFS 时,的确 Hive 的元数据不会自动更新。为了让 Hive 及时知道有新的数据,可以采取以下方法:

  1. 使用分区表:

Hive 表分区,将每个分区与一个子目录关联。当 Flink 将新数据写入新分区时,可以通过分区操作将新数据添加到 Hive 表中。例如,假设表的分区字段是 dt,当 Flink 将数据写入 HDFS 的 /path/to/hive/external/table/dt=2023-04-26 目录时,可以执行以下 Hive 分区操作:

ALTER TABLE my_table ADD PARTITION (dt='2023-04-26') LOCATION '/path/to/hive/external/table/dt=2023-04-26';

这样,Hive 就会知道新分区的存在,并且可以查询新分区中的数据。你可以将这个操作放在一个定时任务中,以便自动执行。

2. 使用 Apache Nifi 或其他数据管道工具:

可以使用 Apache Nifi 或其他类似的数据管道工具来监控 HDFS 中的新文件,并在检测到新文件时触发 Hive 的元数据更新。这可以通过 Apache Nifi 的 ListHDFS、FetchHDFS、PutHiveStreaming 等处理器来实现。这种方法的优势在于可以自动化整个流程。

3. 自定义 Flink Sink:

在自定义 Flink Sink 中,可以直接使用 Hive API 或 JDBC 批量写入数据,并在写入新数据后及时更新 Hive 的元数据。这种方法的优点是可以将写入数据和更新元数据的操作封装在一个 Flink Sink 中,但可能需要处理事务和故障恢复等问题。

无论采用哪种方法,都需要确保 Hive 元数据的及时更新,以便在新数据写入后能够正确查询到新数据。

反馈

现在,假设我们确定想使用flink sql的方式将数据写入hive,那么现在,就可以让ChatGPT来为我们编写Demo代码了。需要注意,之前GPT也为我们编写了一套Demo,但是我们当时没有指明编程语言,所以GPT自动选了Scala,所以这一次我们要把语言约束指明。

第三轮提示
提示

请用java帮我写一个使用flink sql将数据写到hive的demo。

表有2个字段

id: long

name: string

你可以自己写个sourceFunction来生成模拟数据

回答

以下是使用 Java 编写的 Flink SQL 将数据写入 Hive 的示例。在此示例中,我们创建一个自定义的 SourceFunction 来生成模拟数据。该数据包含两个字段:id(Long 类型)和 name(String 类型)。

import org.apache.flink.api.common.functions.MapFunction;
import org.apache.flink.api.common.typeinfo.Types;
import org.apache.flink.streaming.api.datastream.DataStream;
import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment;
import org.apache.flink.streaming.api.functions.source.SourceFunction;
import org.apache.flink.table.api.Table;
import org.apache.flink.table.api.bridge.java.StreamTableEnvironment;
import org.apache.flink.table.catalog.hive.HiveCatalog;

public class FlinkHiveDemo {

    public static void main(String[] args) throws Exception {
        StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
        StreamTableEnvironment tableEnv = StreamTableEnvironment.create(env);

        // Register Hive catalog
        HiveCatalog hiveCatalog = new HiveCatalog("myHive", "default", "/path/to/hive-site.xml");
        tableEnv.registerCatalog("myHive", hiveCatalog);
        tableEnv.useCatalog("myHive");

        // Define a custom SourceFunction to generate data
        DataStream<SampleRecord> dataStream = env.addSource(new SampleSourceFunction());

        // Map the generated data to a Row
        DataStream<org.apache.flink.types.Row> rowDataStream = dataStream.map(
            (MapFunction<SampleRecord, org.apache.flink.types.Row>) record -> {
                org.apache.flink.types.Row row = new org.apache.flink.types.Row(2);
                row.setField(0, record.getId());
                row.setField(1, record.getName());
                return row;
            }).returns(Types.ROW(Types.LONG, Types.STRING));

        // Register the DataStream as a table
        tableEnv.createTemporaryView("sourceTable", rowDataStream, "id, name");

        // Execute the SQL query to insert the data into the Hive table
        String sql = "INSERT INTO myHiveTable SELECT id, name FROM sourceTable";
        tableEnv.executeSql(sql);
    }

    // Define a custom SourceFunction to generate sample data
    public static class SampleSourceFunction implements SourceFunction<SampleRecord> {
        private volatile boolean isRunning = true;

        @Override
        public void run(SourceContext<SampleRecord> ctx) {
            long id = 0;
            while (isRunning) {
                ctx.collect(new SampleRecord(id, "Name_" + id));
                id++;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        @Override
        public void cancel() {
            isRunning = false;
        }
    }

    // Define a POJO class for the sample data
    public static class SampleRecord {
        private long id;
        private String name;

        public SampleRecord(long id, String name) {
            this.id = id;
            this.name = name;
        }

        public long getId() {
            return id;
        }

        public void setId(long id) {
            this.id = id;
        }

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }
    }
}

这个示例程序首先创建一个 Flink 流处理环境和一个 Flink 表环境。然后,它注册了一个 Hive Catalog,并将其设置为当前使用的 Catalog。接下来,它定义了一个自定义的 SourceFunction

反馈

没写完,继续。

第四轮提示
提示

继续

回答

生成模拟数据,并将生成的 DataStream 转换为 Row 类型的 DataStream。接着,我们将这个 DataStream 注册为一个临时表 "sourceTable"。

最后,我们执行一个 Flink SQL 语句,将 "sourceTable" 中的数据插入到 Hive 表 "myHiveTable" 中。请注意,你需要根据实际情况替换 /path/to/hive-site.xml 和 Hive 表名称 "myHiveTable"。

这个示例程序将会持续生成数据并插入到 Hive 表中。你可以通过修改 SampleSourceFunction 中的逻辑来更改数据生成的方式。

为了运行这个示例程序,你需要在项目中添加 Flink 和 Hive 相关的依赖。以下是 Maven 依赖的例子:

<dependencies>
    <!-- Flink dependencies -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-streaming-java_2.12</artifactId>
        <version>1.14.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-table-api-java-bridge_2.12</artifactId>
        <version>1.14.0</version>
    </dependency>
    <!-- Flink Connector for Hive -->
    <dependency>
        <groupId>org.apache.flink</groupId>
        <artifactId>flink-connector-hive_2.12</artifactId>
        <version>1.14.0</version>
    </dependency>
    <!-- Hive dependencies -->
    <dependency>
        <groupId>org.apache.hive</groupId>
        <artifactId>hive-exec</artifactId>
        <version>3.1.2</version>
    </dependency>
    <dependency>
        <groupId>org.apache.hive</groupId>
        <artifactId>hive-metastore</artifactId>
        <version>3.1.2</version>
    </dependency>
</dependencies>

请根据你使用的 Flink 和 Hive 版本进行相应的调整。运行该程序时,你应该能够看到数据被成功插入到 Hive 表中。

反馈

并不能,我后面尝试了多次提示,来回问答已达8次,GPT还是未能给出准确的代码。这里需要注意,flink写数据到hive其实是一个小众场景,网上能找到的有效文章也是个位数的。这个时候向GPT索要高质量答案就比较困困难。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值