flink cdc DataStream api 时区问题

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
实时计算 Flink 版,5000CU*H 3个月
简介: flink cdc DataStream api 时区问题

postgresql cdc时区问题

1:以postgrsql 作为数据源时,Date和timesatmp等类型cdc同步读出来时,会发现一下几个问题:

       时间,日期等类型的数据对应的会转化为Int,long等类型。

       源表同步后,时间相差8小时。这是因为时区不同的缘故。

源表:

60a6bcefe26f4b118e50f46e4d0afd1d.png

sink 表:

75f0e2306cfe4b549332ab598e15c984.png

解决方案:在自定义序列化时进行处理。

java code

package pg.cdc.ds;
import com.alibaba.fastjson.JSONObject;
import com.ververica.cdc.debezium.DebeziumDeserializationSchema;
import io.debezium.data.Envelope;
import org.apache.flink.api.common.typeinfo.BasicTypeInfo;
import org.apache.flink.api.common.typeinfo.TypeInformation;
import org.apache.flink.util.Collector;
import org.apache.kafka.connect.data.Field;
import org.apache.kafka.connect.data.Schema;
import org.apache.kafka.connect.data.Struct;
import org.apache.kafka.connect.source.SourceRecord;
import java.text.SimpleDateFormat;
import java.time.ZoneId;
import java.util.Date;
import java.util.List;
public class CustomerDeserialization implements DebeziumDeserializationSchema<String> {
    ZoneId serverTimeZone;
    @Override
    public void deserialize(SourceRecord sourceRecord, Collector<String> collector) throws Exception {
        //1.创建JSON对象用于存储最终数据
        JSONObject result = new JSONObject();
        Struct value = (Struct) sourceRecord.value();
        //2.获取库名&表名
        Struct sourceStruct = value.getStruct("source");
        String database = sourceStruct.getString("db");
        String schema = sourceStruct.getString("schema");
        String tableName = sourceStruct.getString("table");
        //3.获取"before"数据
        Struct before = value.getStruct("before");
        JSONObject beforeJson = new JSONObject();
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        SimpleDateFormat sdf1 = new SimpleDateFormat("yyyy-MM-dd");
        if (before != null) {
            Schema beforeSchema = before.schema();
            List<Field> beforeFields = beforeSchema.fields();
            for (Field field : beforeFields) {
                Object beforeValue = before.get(field);
                if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.MicroTimestamp".equals(field.schema().name())) {
                    if (beforeValue != null) {
                        long times = (long) beforeValue / 1000;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60 * 1000)));
                        beforeJson.put(field.name(), dateTime);
                    }
                }
                else if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.NanoTimestamp".equals(field.schema().name())) {
                    if (beforeValue != null) {
                        long times = (long) beforeValue;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60 * 1000)));
                        beforeJson.put(field.name(), dateTime);
                    }
                }  else if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.Timestamp".equals(field.schema().name())) {
                    if (beforeValue != null) {
                        long times = (long) beforeValue;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60 )));
                        beforeJson.put(field.name(), dateTime);
                    }
                } else if("int32".equals(field.schema().type().getName()) && "io.debezium.time.Date".equals(field.schema().name())){
                    if(beforeValue != null) {
                        int times = (int) beforeValue;
                        String dateTime = sdf1.format(new Date(times * 24 * 60 * 60L * 1000));
                        beforeJson.put(field.name(), dateTime);
                    }
                }
                else {
                    beforeJson.put(field.name(), beforeValue);
                }
            }
        }
        //4.获取"after"数据
        Struct after = value.getStruct("after");
        JSONObject afterJson = new JSONObject();
        if (after != null) {
            Schema afterSchema = after.schema();
            List<Field> afterFields = afterSchema.fields();
            for (Field field : afterFields) {
                Object afterValue = after.get(field);
                if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.MicroTimestamp".equals(field.schema().name())) {
                    if (afterValue != null) {
                        long times = (long) afterValue / 1000;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60 * 1000)));
                        afterJson.put(field.name(), dateTime);
                    }
                }
                else if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.NanoTimestamp".equals(field.schema().name())) {
                    if (afterValue != null) {
                        long times = (long) afterValue;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60 * 1000)));
                        afterJson.put(field.name(), dateTime);
                    }
                }  else if ("int64".equals(field.schema().type().getName()) && "io.debezium.time.Timestamp".equals(field.schema().name())) {
                    if (afterValue != null) {
                        long times = (long) afterValue;
                        String dateTime = sdf.format(new Date((times - 8 * 60 * 60)));
                        afterJson.put(field.name(), dateTime);
                    }
                }
                else if("int32".equals(field.schema().type().getName()) && "io.debezium.time.Date".equals(field.schema().name())){
                    if(afterValue != null) {
                        int times = (int) afterValue;
                        String dateTime = sdf1.format(new Date(times * 24 * 60 * 60L * 1000));
                        afterJson.put(field.name(), dateTime);
                    }
                }
                else {
                    afterJson.put(field.name(), afterValue);
                }
            }
        }
        //5.获取操作类型  CREATE UPDATE DELETE
        Envelope.Operation operation = Envelope.operationFor(sourceRecord);
        String type = operation.toString().toLowerCase();
        if ("create".equals(type) || "read".equals(type)) {
            type = "insert";
        }
        //6.将字段写入JSON对象
        result.put("database", database);
        result.put("schema", schema);
        result.put("tableName", tableName);
        result.put("before", beforeJson);
        result.put("after", afterJson);
        result.put("type", type);
        //7.输出数据
        collector.collect(result.toJSONString());
    }
    @Override
    public TypeInformation<String> getProducedType() {
        return BasicTypeInfo.STRING_TYPE_INFO;
    }
}

scala code

import com.ververica.cdc.debezium.DebeziumDeserializationSchema
import com.ververica.cdc.debezium.utils.TemporalConversions
import io.debezium.time._
import org.apache.flink.api.common.typeinfo.TypeInformation
import org.apache.flink.types.Row
import org.apache.flink.util.Collector
import org.apache.kafka.connect.data.{SchemaBuilder, Struct}
import org.apache.kafka.connect.source.SourceRecord
import java.sql
import java.time.{Instant, LocalDateTime, ZoneId}
import scala.collection.JavaConverters._
import scala.util.parsing.json.JSONObject
class StructDebeziumDeserializationSchema(serverTimeZone: String) extends DebeziumDeserializationSchema[Row] {
  override def deserialize(sourceRecord: SourceRecord, collector: Collector[Row]): Unit = {
    // 解析主键
    val key = sourceRecord.key().asInstanceOf[Struct]
    val keyJs = parseStruct(key)
    // 解析值
    val value = sourceRecord.value().asInstanceOf[Struct]
    val source = value.getStruct("source")
    val before = parseStruct(value.getStruct("before"))
    val after = parseStruct(value.getStruct("after"))
    val row = Row.withNames()
    row.setField("table", s"${source.get("db")}.${source.get("table")}")
    row.setField("key", keyJs)
    row.setField("op", value.get("op"))
    row.setField("op_ts", LocalDateTime.ofInstant(Instant.ofEpochMilli(source.getInt64("ts_ms")), ZoneId.of(serverTimeZone)))
    row.setField("current_ts", LocalDateTime.ofInstant(Instant.ofEpochMilli(value.getInt64("ts_ms")), ZoneId.of(serverTimeZone)))
    row.setField("before", before)
    row.setField("after", after)
    collector.collect(row)
  }
  /** 解析[[Struct]]结构为json字符串 */
  private def parseStruct(struct: Struct): String = {
    if (struct == null) return null
    val map = struct.schema().fields().asScala.map(field => {
      val v = struct.get(field)
      val typ = field.schema().name()
      println(s"$v, $typ, ${field.name()}")
      val value = v match {
        case long if long.isInstanceOf[Long] => convertLongToTime(long.asInstanceOf[Long], typ)
        case iv if iv.isInstanceOf[Int] => convertIntToDate(iv.asInstanceOf[Int], typ)
        case iv if iv == null => null
        case _ => convertObjToTime(v, typ)
      }
      (field.name(), value)
    }).filter(_._2 != null).toMap
    JSONObject.apply(map).toString()
  }
  /** 类型转换 */
  private def convertObjToTime(obj: Any, typ: String): Any = {
    typ match {
      case Time.SCHEMA_NAME | MicroTime.SCHEMA_NAME | NanoTime.SCHEMA_NAME =>
        sql.Time.valueOf(TemporalConversions.toLocalTime(obj)).toString
      case Timestamp.SCHEMA_NAME | MicroTimestamp.SCHEMA_NAME | NanoTimestamp.SCHEMA_NAME | ZonedTimestamp.SCHEMA_NAME =>
        sql.Timestamp.valueOf(TemporalConversions.toLocalDateTime(obj, ZoneId.of(serverTimeZone))).toString
      case _ => obj
    }
  }
  /** long 转换为时间类型 */
  private def convertLongToTime(obj: Long, typ: String): Any = {
    val time_schema = SchemaBuilder.int64().name("org.apache.kafka.connect.data.Time")
    val date_schema = SchemaBuilder.int64().name("org.apache.kafka.connect.data.Date")
    val timestamp_schema = SchemaBuilder.int64().name("org.apache.kafka.connect.data.Timestamp")
    typ match {
      case Time.SCHEMA_NAME =>
        org.apache.kafka.connect.data.Time.toLogical(time_schema, obj.asInstanceOf[Int]).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalTime.toString
      case MicroTime.SCHEMA_NAME =>
        org.apache.kafka.connect.data.Time.toLogical(time_schema, (obj / 1000).asInstanceOf[Int]).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalTime.toString
      case NanoTime.SCHEMA_NAME =>
        org.apache.kafka.connect.data.Time.toLogical(time_schema, (obj / 1000 / 1000).asInstanceOf[Int]).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalTime.toString
      case Timestamp.SCHEMA_NAME =>
        val t = org.apache.kafka.connect.data.Timestamp.toLogical(timestamp_schema, obj).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalDateTime
        java.sql.Timestamp.valueOf(t).toString
      case MicroTimestamp.SCHEMA_NAME =>
        val t = org.apache.kafka.connect.data.Timestamp.toLogical(timestamp_schema, obj / 1000).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalDateTime
        java.sql.Timestamp.valueOf(t).toString
      case NanoTimestamp.SCHEMA_NAME =>
        val t = org.apache.kafka.connect.data.Timestamp.toLogical(timestamp_schema, obj / 1000 / 1000).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalDateTime
        java.sql.Timestamp.valueOf(t).toString
      case Date.SCHEMA_NAME =>
        org.apache.kafka.connect.data.Date.toLogical(date_schema, obj.asInstanceOf[Int]).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalDate.toString
      case _ => obj
    }
  }
  private def convertIntToDate(obj:Int, typ: String): Any ={
    val date_schema = SchemaBuilder.int64().name("org.apache.kafka.connect.data.Date")
    typ match {
      case Date.SCHEMA_NAME =>
        org.apache.kafka.connect.data.Date.toLogical(date_schema, obj).toInstant.atZone(ZoneId.of(serverTimeZone)).toLocalDate.toString
      case _ => obj
    }
  }
  override def getProducedType: TypeInformation[Row] = {
    TypeInformation.of(classOf[Row])
  }
}

    mysql cdc时区问题

    mysql cdc也会出现上述时区问题,Debezium默认将MySQL中datetime类型转成UTC的时间戳({@link io.debezium.time.Timestamp}),时区是写死的无法更改,导致数据库中设置的UTC+8,到kafka中变成了多八个小时的long型时间戳 Debezium默认将MySQL中的timestamp类型转成UTC的字符串。

    解决思路有两种:

    1:自定义序列化方式的时候做时区转换。
    2:自定义时间转换类,通过debezium配置文件指定转化格式。

    这里主要使用第二种方式。

    package com.zmn.schema;
    import io.debezium.spi.converter.CustomConverter;
    import io.debezium.spi.converter.RelationalColumn;
    import org.apache.kafka.connect.data.SchemaBuilder;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import java.time.*;
    import java.time.format.DateTimeFormatter;
    import java.util.Properties;
    import java.util.function.Consumer;
    /**
     * 处理Debezium时间转换的问题
     * Debezium默认将MySQL中datetime类型转成UTC的时间戳({@link io.debezium.time.Timestamp}),时区是写死的无法更改,
     * 导致数据库中设置的UTC+8,到kafka中变成了多八个小时的long型时间戳
     * Debezium默认将MySQL中的timestamp类型转成UTC的字符串。
     * | mysql                               | mysql-binlog-connector                   | debezium                          |
     * | ----------------------------------- | ---------------------------------------- | --------------------------------- |
     * | date<br>(2021-01-28)                | LocalDate<br/>(2021-01-28)               | Integer<br/>(18655)               |
     * | time<br/>(17:29:04)                 | Duration<br/>(PT17H29M4S)                | Long<br/>(62944000000)            |
     * | timestamp<br/>(2021-01-28 17:29:04) | ZonedDateTime<br/>(2021-01-28T09:29:04Z) | String<br/>(2021-01-28T09:29:04Z) |
     * | Datetime<br/>(2021-01-28 17:29:04)  | LocalDateTime<br/>(2021-01-28T17:29:04)  | Long<br/>(1611854944000)          |
     *
     * @see io.debezium.connector.mysql.converters.TinyIntOneToBooleanConverter
     */
    public class MySqlDateTimeConverter implements CustomConverter<SchemaBuilder, RelationalColumn> {
        private final static Logger logger = LoggerFactory.getLogger(MySqlDateTimeConverter.class);
        private DateTimeFormatter dateFormatter = DateTimeFormatter.ISO_DATE;
        private DateTimeFormatter timeFormatter = DateTimeFormatter.ISO_TIME;
        private DateTimeFormatter datetimeFormatter = DateTimeFormatter.ISO_DATE_TIME;
        private DateTimeFormatter timestampFormatter = DateTimeFormatter.ISO_DATE_TIME;
        private ZoneId timestampZoneId = ZoneId.systemDefault();
        @Override
        public void configure(Properties props) {
            readProps(props, "format.date", p -> dateFormatter = DateTimeFormatter.ofPattern(p));
            readProps(props, "format.time", p -> timeFormatter = DateTimeFormatter.ofPattern(p));
            readProps(props, "format.datetime", p -> datetimeFormatter = DateTimeFormatter.ofPattern(p));
            readProps(props, "format.timestamp", p -> timestampFormatter = DateTimeFormatter.ofPattern(p));
            readProps(props, "format.timestamp.zone", z -> timestampZoneId = ZoneId.of(z));
        }
        private void readProps(Properties properties, String settingKey, Consumer<String> callback) {
            String settingValue = (String) properties.get(settingKey);
            if (settingValue == null || settingValue.length() == 0) {
                return;
            }
            try {
                callback.accept(settingValue.trim());
            } catch (IllegalArgumentException | DateTimeException e) {
                logger.error("The {} setting is illegal: {}",settingKey,settingValue);
                throw e;
            }
        }
        @Override
        public void converterFor(RelationalColumn column, ConverterRegistration<SchemaBuilder> registration) {
            String sqlType = column.typeName().toUpperCase();
            SchemaBuilder schemaBuilder = null;
            Converter converter = null;
            if ("DATE".equals(sqlType)) {
                schemaBuilder = SchemaBuilder.string().optional().name("com.darcytech.debezium.date.string");
                converter = this::convertDate;
            }
            if ("TIME".equals(sqlType)) {
                schemaBuilder = SchemaBuilder.string().optional().name("com.darcytech.debezium.time.string");
                converter = this::convertTime;
            }
            if ("DATETIME".equals(sqlType)) {
                schemaBuilder = SchemaBuilder.string().optional().name("com.darcytech.debezium.datetime.string");
                converter = this::convertDateTime;
            }
            if ("TIMESTAMP".equals(sqlType)) {
                schemaBuilder = SchemaBuilder.string().optional().name("com.darcytech.debezium.timestamp.string");
                converter = this::convertTimestamp;
            }
            if (schemaBuilder != null) {
                registration.register(schemaBuilder, converter);
            }
        }
        private String convertDate(Object input) {
            if (input instanceof LocalDate) {
                return dateFormatter.format((LocalDate) input);
            }
            if (input instanceof Integer) {
                LocalDate date = LocalDate.ofEpochDay((Integer) input);
                return dateFormatter.format(date);
            }
            return null;
        }
        private String convertTime(Object input) {
            if (input instanceof Duration) {
                Duration duration = (Duration) input;
                long seconds = duration.getSeconds();
                int nano = duration.getNano();
                LocalTime time = LocalTime.ofSecondOfDay(seconds).withNano(nano);
                return timeFormatter.format(time);
            }
            return null;
        }
        private String convertDateTime(Object input) {
            if (input instanceof LocalDateTime) {
                return datetimeFormatter.format((LocalDateTime) input);
            }
            return null;
        }
        private String convertTimestamp(Object input) {
            if (input instanceof ZonedDateTime) {
                // mysql的timestamp会转成UTC存储,这里的zonedDatetime都是UTC时间
                ZonedDateTime zonedDateTime = (ZonedDateTime) input;
                LocalDateTime localDateTime = zonedDateTime.withZoneSameInstant(timestampZoneId).toLocalDateTime();
                return timestampFormatter.format(localDateTime);
            }
            return null;
        }
    }

    使用方式:

    StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
            Properties properties = new Properties();
            properties.setProperty("snapshot.mode", "schema_only"); // 增量读取
            //自定义时间转换配置
            properties.setProperty("converters", "dateConverters");
            properties.setProperty("dateConverters.type", "pg.cdc.ds.PgSQLDateTimeConverter");
            properties.setProperty("dateConverters.format.date", "yyyy-MM-dd");
            properties.setProperty("dateConverters.format.time", "HH:mm:ss");
            properties.setProperty("dateConverters.format.datetime", "yyyy-MM-dd HH:mm:ss");
            properties.setProperty("dateConverters.format.timestamp", "yyyy-MM-dd HH:mm:ss");
            properties.setProperty("dateConverters.format.timestamp.zone", "UTC+8");
            properties.setProperty("debezium.snapshot.locking.mode","none"); //全局读写锁,可能会影响在线业务,跳过锁设置        
            properties.setProperty("include.schema.changes", "true");
            // 使用flink mysql cdc 发现bigint unsigned类型的字段,capture以后转成了字符串类型,
           // 用的这个解析吧JsonDebeziumDeserializationSchema。
            properties.setProperty("bigint.unsigned.handling.mode","long");
            properties.setProperty("decimal.handling.mode","double");
            MySqlSource<String> mySqlSource = MySqlSource.<String>builder()
                    .hostname("192.168.10.102")
                    .port(3306)
                    .username("yusys")
                    .password("yusys")
                    .port(3306)
                    .databaseList("gmall")
                    .tableList("gmall.faker_user1")
                    .deserializer(new JsonDebeziumDeserializationSchema())
                    .debeziumProperties(properties)
                    .serverId(5409)
                    .build();
          SingleOutputStreamOperator<string> dataSource = env
                    .addSource(sourceFunction).setParallelism(10).name("binlog-source");
    


    相关实践学习
    基于Hologres轻松玩转一站式实时仓库
    本场景介绍如何利用阿里云MaxCompute、实时计算Flink和交互式分析服务Hologres开发离线、实时数据融合分析的数据大屏应用。
    Linux入门到精通
    本套课程是从入门开始的Linux学习课程,适合初学者阅读。由浅入深案例丰富,通俗易懂。主要涉及基础的系统操作以及工作中常用的各种服务软件的应用、部署和优化。即使是零基础的学员,只要能够坚持把所有章节都学完,也一定会受益匪浅。
    相关文章
    |
    6月前
    |
    SQL 分布式计算 测试技术
    概述Flink API中的4个层次
    【7月更文挑战第14天】Flink的API分为4个层次:核心底层API(如ProcessFunction)、DataStream/DataSet API、Table API和SQL。
    |
    7月前
    |
    SQL 关系型数据库 API
    实时计算 Flink版产品使用问题之如何使用stream api
    实时计算Flink版作为一种强大的流处理和批处理统一的计算框架,广泛应用于各种需要实时数据处理和分析的场景。实时计算Flink版通常结合SQL接口、DataStream API、以及与上下游数据源和存储系统的丰富连接器,提供了一套全面的解决方案,以应对各种实时计算需求。其低延迟、高吞吐、容错性强的特点,使其成为众多企业和组织实时数据处理首选的技术平台。以下是实时计算Flink版的一些典型使用合集。
    |
    7月前
    |
    Kubernetes Oracle 关系型数据库
    实时计算 Flink版操作报错合集之用dinky在k8s上提交作业,会报错:Caused by: org.apache.flink.table.api.ValidationException:,是什么原因
    在使用实时计算Flink版过程中,可能会遇到各种错误,了解这些错误的原因及解决方法对于高效排错至关重要。针对具体问题,查看Flink的日志是关键,它们通常会提供更详细的错误信息和堆栈跟踪,有助于定位问题。此外,Flink社区文档和官方论坛也是寻求帮助的好去处。以下是一些常见的操作报错及其可能的原因与解决策略。
    309 0
    |
    7月前
    |
    SQL 存储 API
    Flink(十五)【Flink SQL Connector、savepoint、CateLog、Table API】(5)
    Flink(十五)【Flink SQL Connector、savepoint、CateLog、Table API】
    |
    7天前
    |
    JSON 前端开发 搜索推荐
    关于商品详情 API 接口 JSON 格式返回数据解析的示例
    本文介绍商品详情API接口返回的JSON数据解析。最外层为`product`对象,包含商品基本信息(如id、name、price)、分类信息(category)、图片(images)、属性(attributes)、用户评价(reviews)、库存(stock)和卖家信息(seller)。每个字段详细描述了商品的不同方面,帮助开发者准确提取和展示数据。具体结构和字段含义需结合实际业务需求和API文档理解。
    |
    1天前
    |
    JSON 搜索推荐 API
    京东店铺所有商品接口系列(京东 API)
    本文介绍如何使用Python调用京东API获取店铺商品信息。前期需搭建Python环境,安装`requests`库并熟悉`json`库的使用。接口采用POST请求,参数包括`app_key`、`method`、`timestamp`、`v`、`sign`和业务参数`360buy_param_json`。通过示例代码展示如何生成签名并发送请求。应用场景涵盖店铺管理、竞品分析、数据统计及商品推荐系统,帮助商家优化运营和提升竞争力。
    33 23
    |
    13天前
    |
    JSON API 数据格式
    京东商品SKU价格接口(Jd.item_get)丨京东API接口指南
    京东商品SKU价格接口(Jd.item_get)是京东开放平台提供的API,用于获取商品详细信息及价格。开发者需先注册账号、申请权限并获取密钥,随后通过HTTP请求调用API,传入商品ID等参数,返回JSON格式的商品信息,包括价格、原价等。接口支持GET/POST方式,适用于Python等语言的开发环境。
    66 11
    |
    4天前
    |
    Web App开发 JSON 测试技术
    API测试工具集合:让接口测试更简单高效
    在当今软件开发领域,接口测试工具如Postman、Apifox、Swagger等成为确保API正确性、性能和可靠性的关键。Postman全球闻名但高级功能需付费,Apifox则集成了API文档、调试、Mock与自动化测试,简化工作流并提高团队协作效率,特别适合国内用户。Swagger自动生成文档,YApi开源但功能逐渐落后,Insomnia界面简洁却缺乏团队协作支持,Paw仅限Mac系统。综合来看,Apifox是国内用户的理想选择,提供中文界面和免费高效的功能。
    |
    1月前
    |
    人工智能 自然语言处理 API
    Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
    谷歌推出的Multimodal Live API是一个支持多模态交互、低延迟实时互动的AI接口,能够处理文本、音频和视频输入,提供自然流畅的对话体验,适用于多种应用场景。
    90 3
    Multimodal Live API:谷歌推出新的 AI 接口,支持多模态交互和低延迟实时互动
    |
    24天前
    |
    JSON 安全 API
    淘宝商品详情API接口(item get pro接口概述)
    淘宝商品详情API接口旨在帮助开发者获取淘宝商品的详细信息,包括商品标题、描述、价格、库存、销量、评价等。这些信息对于电商企业而言具有极高的价值,可用于商品信息展示、市场分析、价格比较等多种应用场景。

    热门文章

    最新文章