Flink Table/Sql自定义Kudu Sink实战(其它Sink可参考)

news2024/9/29 17:28:16

目录

  • 1. 背景
  • 2. 原理
  • 3. 通过Trino创建Kudu表
  • 4. FlinkKuduTableSinkProject项目
    • 4.1 pom.xml
    • 4.2 FlinkKuduTableSinkFactory.scala
    • 4.3 META-INF/services
    • 4.4 FlinkKuduTableSinkTest.scala测试文件
  • 5. 查看Kudu表数据

1. 背景

使用第三方的org.apache.bahir » flink-connector-kudu,batch模式写入数据到Kudu会有FlushMode相关问题

具体可以参考我的这篇博客通过Flink SQL操作创建Kudu表,并读写Kudu表数据

2. 原理

Flink的Dynamic table能够统一处理batch和streaming

实现自定义Source或Sink有两种方式:

  1. 通过对已有的connector进行拓展。比如对connector = jdbc拓展Clickhouse的jdbc连接器
  2. 继承DynamicTableSourceFactory或DynamicTableSinkFactory,实现一个全新的connector。本节重点讲解这种

各个State的依赖关系
Metadata部分:Flink Catalog已有的Flink Table,或在Flink Catalog进行Flink Table的create sql声明。由CatalogTable实例进行表示

Planning部分:DynamicTableSourceFactory或DynamicTableSinkFactory将CatalogTable的metadata,转换成DynamicTableSource或DynamicTableSink的实例数据

DynamicTableFactory主要验证with子句的各个选项,并解析with子句的各个选项值。with子句的connector值必须和factoryIdentifier一致

DynamicTableFactory通过DynamicTableSource或DynamicTableSink进行runtime操作

runtime部分:Source主要需要实现ScanRuntimeProvider或LookupRuntimeProvider。Sink主要需要实现SinkRuntimeProvider。其中SinkRuntimeProvider有两个子类:

  1. OutputFormatProvider,可以接收org.apache.flink.api.common.io.OutputFormat
  2. SinkFunctionProvider,可以接收org.apache.flink.streaming.api.functions.sink.SinkFunction

3. 通过Trino创建Kudu表

trino:default> create table flink_table_test(
            -> id int with (primary_key = true), 
            -> name varchar
            -> ) with(
            -> partition_by_hash_columns = array['id'], 
            -> partition_by_hash_buckets = 15, 
            -> number_of_replicas =1
            -> );
CREATE TABLE
trino:default> 

4. FlinkKuduTableSinkProject项目

4.1 pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.mq</groupId>
    <artifactId>flinkKuduTableSinkProject</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <scala.binary.version>2.12</scala.binary.version>
        <scala.version>2.12.15</scala.version>
        <flink.version>1.14.4</flink.version>
        <kudu.version>1.15.0</kudu.version>

    </properties>

    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-scala_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-clients_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-streaming-scala_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-api-scala-bridge_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-planner_${scala.binary.version}</artifactId>
            <version>${flink.version}</version>
        </dependency>


        <dependency>
            <groupId>org.apache.flink</groupId>
            <artifactId>flink-table-common</artifactId>
            <version>${flink.version}</version>
        </dependency>

        <dependency>
            <groupId>org.apache.kudu</groupId>
            <artifactId>kudu-client</artifactId>
            <version>${kudu.version}</version>
        </dependency>

    </dependencies>


    <build>
        <plugins>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>3.3.0</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <exclude></exclude>
                                </excludes>
                            </artifactSet>
                            <filters>
                                <filter>
                                    <artifact>*:*</artifact>
                                    <excludes>
                                        <exclude>META-INF/*.SF</exclude>
                                        <exclude>META-INF/*.RSA</exclude>
                                        <exclude>META-INF/*.DSA</exclude>
                                    </excludes>
                                </filter>
                            </filters>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <mainClass></mainClass>
                                </transformer>
                            </transformers>
                        </configuration>
                    </execution>
                </executions>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>

            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>4.6.1</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <args>
                        <arg>-nobootcp</arg>
                        <arg>-target:jvm-1.8</arg>
                    </args>
                </configuration>
            </plugin>

        </plugins>
    </build>

</project>

4.2 FlinkKuduTableSinkFactory.scala

定义FlinkKuduTableSinkFactory类,主要包含四个部分

  1. FlinkKuduTableSinkFactory
  2. FlinkKuduTableSinkFactory的伴生对象Object
  3. FlinkKuduTableSink
  4. FlinkKuduRowDataRichSinkFunction
package org.mq

import org.apache.flink.configuration.ConfigOptions.key
import org.apache.flink.configuration.{ConfigOption, Configuration, ReadableConfig}
import org.apache.flink.streaming.api.functions.sink.{RichSinkFunction, SinkFunction}
import org.apache.flink.table.connector.ChangelogMode
import org.apache.flink.table.connector.sink.DynamicTableSink.DataStructureConverter
import org.apache.flink.table.connector.sink.{DynamicTableSink, SinkFunctionProvider}
import org.apache.flink.table.data.RowData
import org.apache.flink.table.factories.{DynamicTableFactory, DynamicTableSinkFactory, FactoryUtil}
import org.apache.flink.table.types.DataType
import org.apache.flink.table.types.logical.RowType
import org.apache.flink.types.{Row, RowKind}
import org.apache.kudu.client.SessionConfiguration.FlushMode
import org.apache.kudu.client._

import java.util
import scala.collection.JavaConverters.asScalaBufferConverter
import scala.collection.mutable.ArrayBuffer

// 由于KuduSqlSinkFactory是Serializable,其属性也应该是Serializable。将属性定义在Object可以实现该功能
object FlinkKuduTableSinkFactory {
  val kudu_masters: ConfigOption[String] = key("kudu.masters")
    .stringType()
    .noDefaultValue()
    .withDescription("kudu masters")

  val kudu_table: ConfigOption[String] = key("kudu.table")
    .stringType()
    .noDefaultValue()
    .withDescription("kudu table")

}


class FlinkKuduTableSinkFactory extends DynamicTableSinkFactory with Serializable {

  import FlinkKuduTableSinkFactory._

  // 定义connector的name
  override def factoryIdentifier(): String = "kudu"

  // 定义with子句中必填的选项
  override def requiredOptions(): util.Set[ConfigOption[_]] = {
    val requiredSet: util.HashSet[ConfigOption[_]] = new util.HashSet[ConfigOption[_]]()
    requiredSet.add(kudu_masters)
    requiredSet.add(kudu_table)

    requiredSet

  }

  // // 定义with子句中可填的选项
  override def optionalOptions(): util.Set[ConfigOption[_]] = {
    new util.HashSet[ConfigOption[_]]()
  }


  override def createDynamicTableSink(context: DynamicTableFactory.Context): DynamicTableSink = {

    // 验证with子句选项,并获取各选项的值
    val FactoryHelper: FactoryUtil.TableFactoryHelper = FactoryUtil.createTableFactoryHelper(this, context)
    FactoryHelper.validate()
    val withOptions: ReadableConfig = FactoryHelper.getOptions()

    // 获取各字段的数据类型
    val fieldDataTypes: DataType = context.getCatalogTable().getResolvedSchema.toPhysicalRowDataType
    // Buffer(id)
    val primaryKeys: Seq[String] = context.getCatalogTable().getResolvedSchema.getPrimaryKey
      .get().getColumns.asScala.toSeq

    new FlinkKuduTableSink(fieldDataTypes, withOptions)
  }


}


class FlinkKuduTableSink(fieldDataTypes: DataType,
                         withOptions: ReadableConfig) extends DynamicTableSink {

  // 定义Sink支持的ChangelogMode。insertOnly或upsert
  override def getChangelogMode(requestedMode: ChangelogMode): ChangelogMode = {
    requestedMode
  }

  // 调用用户自己定义的streaming sink ,建立sql与streaming的联系
  override def getSinkRuntimeProvider(context: DynamicTableSink.Context): DynamicTableSink.SinkRuntimeProvider = {
    val dataStructureConverter: DataStructureConverter = context.createDataStructureConverter(fieldDataTypes)

    SinkFunctionProvider.of(new FlinkKuduRowDataRichSinkFunction(dataStructureConverter, withOptions, fieldDataTypes))

  }

  // sink可以不用实现,主要用来source的谓词下推
  override def copy(): DynamicTableSink = {
    new FlinkKuduTableSink(fieldDataTypes, withOptions)
  }

  // 定义sink的汇总信息,用于打印到控制台和log
  override def asSummaryString(): String = "kudu"

}


// 同flink streaming的自定义sink ,只不过处理的是RowData
class FlinkKuduRowDataRichSinkFunction(dataStructureConverter: DataStructureConverter,
                                       withOptions: ReadableConfig,
                                       fieldDataTypes: DataType) extends RichSinkFunction[RowData] {

  import FlinkKuduTableSinkFactory.{kudu_masters, kudu_table}

  private val serialVersionUID: Long = 1L

  private val fieldNameDatatypes: ArrayBuffer[(String, String)] = ArrayBuffer()
  private var kuduClient: KuduClient = _
  private var kuduSession: KuduSession = _
  private var kuduTable: KuduTable = _

  // 进行各种参数的初始化
  override def open(parameters: Configuration): Unit = {
    super.open(parameters)

    val rowFields: util.List[RowType.RowField]
    = fieldDataTypes.getLogicalType.asInstanceOf[RowType].getFields

    rowFields.asScala.foreach(rowField => {
      val rowFieldDatatype: String = rowField.getType.asSummaryString()
        .split(" ").apply(0).split("\\(").apply(0)
      fieldNameDatatypes += ((rowField.getName, rowFieldDatatype))
    })

    kuduClient = new KuduClient.KuduClientBuilder(withOptions.get(kudu_masters)).build()
    kuduSession = kuduClient.newSession()
    kuduSession.setFlushMode(FlushMode.AUTO_FLUSH_BACKGROUND)
    kuduTable = kuduClient.openTable(withOptions.get(kudu_table))
  }

  // 对每个rowData进行具体的处理
  override def invoke(rowData: RowData, context: SinkFunction.Context): Unit = {


    val rowKind: RowKind = rowData.getRowKind()
    val row: Row = dataStructureConverter.toExternal(rowData).asInstanceOf[Row]
    // 处理insert和upsert
    if (rowKind.equals(RowKind.INSERT) || rowKind.equals(RowKind.UPDATE_AFTER)) {

      // 插入一条数据
      val upsert: Upsert = kuduTable.newUpsert()
      val partialRow: PartialRow = upsert.getRow()

      fieldNameDatatypes.foreach(fieldNameDatatype => {
        val fieldName: String = fieldNameDatatype._1
        val fieldDatatype: String = fieldNameDatatype._2

        fieldDatatype match {
          case "INT" => {
            var partialRowValue: Int = row.getFieldAs[Int](fieldName)
            if (partialRowValue == null) partialRowValue = 0
            partialRow.addInt(fieldName, partialRowValue)
          }
          case "BIGINT" => {
            var partialRowValue: Long = row.getFieldAs[Long](fieldName)
            if (partialRowValue == null) partialRowValue = 0L
            partialRow.addLong(fieldName, partialRowValue)
          }
          case "FLOAT" => {
            var partialRowValue: Float = row.getFieldAs[Float](fieldName)
            if (partialRowValue == null) partialRowValue = 0.0F
            partialRow.addFloat(fieldName, partialRowValue)
          }
          case "DOUBLE" => {
            var partialRowValue: Double = row.getFieldAs[Double](fieldName)
            if (partialRowValue == null) partialRowValue = 0.0
            partialRow.addDouble(fieldName, partialRowValue)
          }
          case "DECIMAL" => {
            val partialRowValue: java.math.BigDecimal =
              row.getFieldAs[java.math.BigDecimal](fieldName)

            partialRow.addDouble(fieldName,
              if (partialRowValue == null) {
                0.0
              } else {
                partialRowValue.doubleValue()
              })

          }
          case "STRING" => {
            var partialRowValue: String = row.getFieldAs[String](fieldName)
            if (partialRowValue == null) partialRowValue = ""
            partialRow.addString(fieldName, partialRowValue)
          }
          case "TIME" => {
            val partialRowValue: java.time.LocalTime =
              row.getFieldAs[java.time.LocalTime](fieldName)

            partialRow.addString(fieldName,
              if (partialRowValue == null) {
                ""
              } else {
                partialRowValue.toString
              })
          }
          case "DATE" => {
            val partialRowValue: java.time.LocalDate =
              row.getFieldAs[java.time.LocalDate](fieldName)

            partialRow.addDate(fieldName,
              if (partialRowValue == null) {
                new java.sql.Date(0L)
              } else {
                java.sql.Date.valueOf(partialRowValue)
              })

          }
          case "TIMESTAMP" => {
            val partialRowValue: java.time.LocalDateTime =
              row.getFieldAs[java.time.LocalDateTime](fieldName)

            partialRow.addTimestamp(fieldName,
              if (partialRowValue == null) {
                new java.sql.Timestamp(0L)
              } else {
                // 注意是否有时区的8小时偏差
                java.sql.Timestamp.valueOf(partialRowValue.plusHours(8L))
              })

          }
          case "BYTES" => {
            val partialRowValue: Array[Byte] =
              row.getFieldAs[Array[Byte]](fieldName)

            partialRow.addBinary(fieldName, partialRowValue)
          }

        }

      })
      kuduSession.apply(upsert)

      // 也可以手动调用flush
      // kuduSession.flush()

    }

  }

  // 进行各种资源的关闭
  override def close(): Unit = {
    super.close()

    kuduSession.close()
  }

}

4.3 META-INF/services

  1. 在项目的resource目录下,新建META-INF/services目录
  2. 在services目录下新建文件:org.apache.flink.table.factories.Factory
  3. Factory文件添加DynamicTableSinkFactory的全路径:org.mq.FlinkKuduTableSinkFactory

4.4 FlinkKuduTableSinkTest.scala测试文件

package org.mq

import org.apache.flink.streaming.api.scala.StreamExecutionEnvironment
import org.apache.flink.table.api.bridge.scala.StreamTableEnvironment

object FlinkKuduTableSinkTest {

  def main(args: Array[String]): Unit = {

    val senv = StreamExecutionEnvironment.getExecutionEnvironment
    val tEnv = StreamTableEnvironment.create(senv)

    tEnv.executeSql(
      """
        |create table flink_table_test(
        |id int,
        |name string,
        |primary key (id) not enforced
        |) with (
        |'connector' = 'kudu',
        |'kudu.masters' = '192.168.8.112:7051,192.168.8.113:7051',
        |'kudu.table' = 'flink_table_test'
        |)
        |""".stripMargin
    )

    tEnv.executeSql("insert into flink_table_test(id, name) values(2, 'li_si2')")


  }

}

执行程序,然后查看Kudu表数据

5. 查看Kudu表数据

trino:default> select * from flink_table_test;
 id |   name    
----+-----------
  1 | zhang_san 
(1 row)

Query 20220517_095005_00109_i893r, FINISHED, 2 nodes
Splits: 19 total, 19 done (100.00%)
0.22 [1 rows, 20B] [4 rows/s, 90B/s]

trino:default> 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/636612.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

​2023年湖北企业人力资源管理师报考条件是什么?启程别告诉你

2023年湖北企业人力资源管理师报考条件是什么&#xff1f;启程别告诉你 2019年国家就取消了企业人力资源管理师国家职业资格考试&#xff0c;现在是改革为职业技能等级认证&#xff0c;由人社部监管的第三方组织机构组织考试和颁发证书&#xff0c;那改革后的企业人力资源管理师…

创建镜像-dockerfile

Docker 镜像的创建 创建镜像有三种方法&#xff1a; 1.基于已有镜像创建、 2.基于本地模板创建 3.基于Dockerfile创建。 基于现有镜像创建 首先启动一个镜像&#xff0c;在容器里做修改 docker create -it centos:7 /bin/bash然后将修改后的容器提交为新的镜像&#xff…

在JavaScript中的数据结构(队列)

文章目录 什么是队列&#xff1f;创建队列新建队列队列可用的方法队列添加元素队列移除元素队列查看元素查看队列头元素检查队列是否为空检查队列的长度打印队列元素 完整队列代码 循环队列优先队列是什么&#xff1f;总结 什么是队列&#xff1f; 当我们在浏览器中打开新标签…

【1483. 树节点的第 K 个祖先】

来源&#xff1a;力扣&#xff08;LeetCode&#xff09; 描述&#xff1a; 给你一棵树&#xff0c;树上有 n 个节点&#xff0c;按从 0 到 n-1 编号。树以父节点数组的形式给出&#xff0c;其中 parent[i] 是节点 i 的父节点。树的根节点是编号为 0 的节点。 树节点的第 k 个…

pyecharts使用案例二——全国疫情可视化地图开发

代码 import json from pyecharts.charts import Map from pyecharts.options import *f open("./疫情.txt", "r", encoding"UTF-8") data f.read()f.close()# 取到各省份数据 # 将json字符串转为python字典,反序列化 data_dict json.loads(…

vue3-实战-07-管理后台-属性管理模块开发

目录 1-需求原型分析 2-三级分类全局组件封装 2.1-三级分类组件请求接口和数据类型封装 2.2-组件获取数据渲染数据 3-属性管理列表开发 3.1-请求接口和数据类型封装 3.2-获取数据渲染数据 4-新增编辑属性 4.1-需求原型分析 4.2-新增编辑接口封装和数据类型定义 4.3-…

IDEA在Maven settings.xml失效的情况下反编译代码

在我们日常的工作中有时候会遇到需要调试别人的代码的问题&#xff0c;这个时候别人往往会给你一个jar包&#xff0c;这个包里面的代码都是经过编译的&#xff0c;点击打开函数以后都是后缀是.class的文件&#xff0c;我们调试起来非常不方便&#xff0c;这个时候如果我们想要下…

Vue中如何进行剪贴板操作?

Vue中如何进行剪贴板操作&#xff1f; 在Web应用程序中&#xff0c;剪贴板&#xff08;Clipboard&#xff09;操作是非常常见的操作之一。Vue.js是一款流行的JavaScript框架&#xff0c;它提供了一些有用的工具来处理DOM元素和用户界面。本文将介绍如何在Vue.js中使用剪贴板操…

行业拐点已至?解码装备制造企业四大转型方向!

当前&#xff0c;国内外经济形势复杂严峻&#xff0c;不稳定、不确定性因素增多。装备制造业企业面对行业增速放缓、内外部环境变化的挑战&#xff0c;叠加国家政策的鼓励与引导&#xff0c;数字化转型已经成为装备制造企业的迫切需求。 01 装备制造业发展现状&#xff08;SWOT…

抛售股票提现15亿美元救急,英特尔「下手」Mobileye这颗「摇钱树」

本周&#xff0c;全球半导体巨头英特尔宣布&#xff0c;将出售旗下上市公司Mobileye的部分股权&#xff0c;预计价值约15亿美元&#xff0c;股份将从目前的99.3%降至约98.7%。截止昨天收盘&#xff0c;Mobileye的最新市值约为339.4亿美元&#xff08;同期&#xff0c;英特尔市值…

VSCode连接远程服务器上docker容器进行代码调试

VSCode连接远程服务器上docker容器进行代码调试 1、本教程默认已经在本地安装完毕VSCode&#xff0c;并且安装ssh2、本教程默认已经在服务器端安装完毕docker与nvidia-docker、ssh服务并自动启动、且容器内安装anaconda3、服务器端创建docker容器&#xff0c;并增加端口映射&am…

【JS】时间格式转化相差8小时的原因

文章目录 时间类型UTCGMTCST 出现时间转化中相差8小时的原因 时间类型 UTC 协调世界时&#xff0c;又称世界统一时间、世界标准时间、国际协调时间&#xff0c;简称UTC&#xff08;Coordinated Universal Time&#xff09; UTC8即为北京时间&#xff0c;目前一般认为UTC与GMT…

AIGC|我让AI来写今年高考作文

作者&#xff1a;谢凯 | 神州数码云基地-需求分析师 目录 一、人工智能究竟强在哪 //以ChatGPT为例&#xff0c;人工智能其优势何在&#xff1f; 二、BingAI如何处理高考作文 三、总结 一、人工智能究竟强在哪 随着ChatGPT&#xff08;Chat Generative Pre-trained Transfo…

Vue CLI在 CommonJS 模块中不支持顶级 await 语法的解决办法

这是因为默认情况下&#xff0c;Vue CLI 会将你的代码转换为 CommonJS 模块&#xff0c;而在 CommonJS 模块中不支持顶级 await 语法。 在vue3项目的utils里面的js文件中写export全局调用方法时&#xff0c;打包依赖报错&#xff1b; 解决办法: 新增依赖vue/cli-plugin-top-…

iToolab UnlockGo for Mac,iOS设备解锁工具

iToolab UnlockGo是一款专业的 iPhone/iPad 解锁工具&#xff0c;可以帮助用户解除 iOS 设备的各种锁定&#xff0c;包括屏幕密码、指纹密码、Face ID 等。以下是 iToolab UnlockGo 的一些主要特点&#xff1a; 针对多种锁定类型&#xff1a;iToolab UnlockGo 可以解除多种 iO…

对数组的“reg [7:0] a_tmp[32:0]”理解

数组 在verilog中&#xff0c;对数组reg [7:0] a_tmp[32:0]进行操作时&#xff0c;分不清楚先对[32:0]进行操作还是先对 [7:0]进行操作&#xff0c;为此进行下面的实验。进一步加深对数组的理解。 实验1&#xff1a; reg [7:0] a_tmp[32:0];遍历的方式&#xff1a; integer …

架构师日记-从技术角度揭露电商大促备战的奥秘 | 京东云技术团队

一 背景 今年的618大促已经如期而至&#xff0c;接下来我会从技术的角度&#xff0c;跟大家聊聊大促备战的底层逻辑和实战方案&#xff0c;希望能够解答大家心中的一些疑惑。 首先&#xff0c;618大促为什么如此重要呢&#xff1f;先从数据的角度简单做一下分析&#xff0c;以…

Arcgis根据经纬度获得点的地理信息/行政区划信息

步骤可以总结为&#xff1a; 导入shp文件&#xff08;面数据&#xff0c;也就是行政区划的依据&#xff09; 导入栅格数据 将栅格数据落入到坐标系中 将导入的栅格点导出为shp图层 栅格点与面数据连接对齐 导出结果 1、导入shp文件&#xff08;面数据&#xff0c;也就是行…

去中心化的信任,Web3如何塑造更加牢固的客户忠诚度

在当今数字化的时代&#xff0c;客户忠诚度对于企业的成功至关重要。而随着Web3技术的崛起&#xff0c;我们正面临着一场信任的革命。本文将探讨Web3如何塑造更加牢固的客户忠诚度&#xff0c;并为企业带来长期的商业价值。 1.去中心化的信任机制 在传统的Web2模式下&#xff…

支付宝小程序开发

支付宝小程序商城是一种基于支付宝平台开发的小程序应用&#xff0c;它可以帮助商家在小程序中完成商品展示、下单购买、在线支付等操作。下面我们来介绍支付宝小程序商城的好处。 一、便捷快速 支付宝小程序商城可以直接在支付宝App中使用&#xff0c;无需下载和安装&#x…