ElasticSearch内容详讲

一、概述

Elasticsearch 是一个开源的搜索引擎,建立在一个全文搜索引擎库 Apache Lucene™ 基础之上。 Lucene 可以说是当下最先进、高性能、全功能的搜索引擎库—无论是开源还是私有。ES使用 Java 编写的,它的内部使用 Lucene 做索引与搜索。

特点

  • 一个分布式的实时文档存储,每个字段 可以被索引与搜索
  • 一个分布式实时分析搜索引擎
  • 能胜任上百个服务节点的扩展,并支持 PB 级别的结构化或者非结构化数据

非常好的教程推荐:https://www.tizi365.com/archives/590.html

1.存储结构与数据类型

类比MYSQL存储结构

Elasticsearch存储结构MYSQL存储结构
index(索引)
Document(文档)行,一行数据
Field(字段)表字段
mapping (映射)表结构定义

index(索引):相当于 mysql 的表

Document(文档):相当于表中的每一条记录,这里都是json数据

Field(文档字段):相当于表中的字段

  • 字符串:主要包括: text和keyword两种类型,keyword代表精确值不会参与分词,text类型的字符串会参与分词处理。
  • 数值:包括: long, integer, short, byte, double, float
  • 布尔值:boolean
  • 时间:date
  • 数组:数组类型不需要专门定义,只要插入的字段值是json数组就行。
  • GEO类型:主要涉及地理信息检索、多边形区域的表达,后面GEO相关的章节单独讲解

mapping (映射):Elasticsearch的mapping (映射)类似mysql中的表结构定义,每个索引都有一个映射规则,我们可以通过定义索引的映射规则,提前定义好文档的json结构和字段类型,如果没有定义索引的映射规则,Elasticsearch会在写入数据的时候,根据我们写入的数据字段推测出对应的字段类型,相当于自动定义索引的映射规则。

2.文档元数据

文档元数据,指的是插入JSON文档的时候,Elasticsearch为这条数据,自动生成的系统字段

元数据的字段名都是以下划线开头的。

常见的元数据如下:

  • _index - 代表当前JSON文档所属的文档名字
  • _type - 代表当前JSON文档所属的类型,虽然新版ES废弃了type的用法,但是元数据还是可以看到。
  • _id - 文档唯一Id, 如果我们没有为文档指定id,系统会自动生成
  • _source - 代表我们插入进去的JSON数据
  • _version - 文档的版本号,每修改一次文档数据,字段就会加1, 这个字段新版的ES已经不使用了
  • _seq_no - 文档的版本号, 替代老的_version字段
  • _primary_term - 文档所在主分区,这个可以跟_seq_no字段搭配实现乐观锁。

二、查询语法

1.查询基本语法结构

GET /{索引名}/_search
{
	"from" : 0,  // 返回搜索结果的开始位置
  "size" : 10, // 分页大小,一次返回多少数据
  "_source" :[ ...需要返回的字段数组... ],
	"query" : { ...query子句... },
	"aggs" : { ..aggs子句..  },
	"sort" : { ..sort子句..  }
}

query子句:主要用来编写类似SQL的Where语句,支持布尔查询(and/or)、IN、全文搜索、模糊匹配、范围查询(大于小于)。

aggs子句:主要用来编写统计分析语句,类似SQL的group by语句

sort子句:用来设置排序条件,类似SQL的order by语句

分页:主要通过from和size参数设置,类似MYSQL 的limit和offset语句

_source:用于设置查询结果返回什么字段,类似Select语句后面指定字段

2.Query查询

2.1匹配单个字段-match

通过match实现全文搜索,全文搜索的后面有单独的章节讲解,这里大家只要知道简单的用法就可以。

GET /{索引名}/_search
{
  "query": {
    "match": {
      "{FIELD}": "{TEXT}"
    }
  }
}
  • {FIELD} - 就是我们需要匹配的字段名
  • {TEXT} - 就是我们需要匹配的内容

2.2精确匹配单个字段-term

如果我们想要类似SQL语句中的等值匹配,不需要进行分词处理,例如:订单号、手机号、时间字段,不需要分值处理,只要精确匹配。

通过term实现精确匹配语法:

GET /{索引名}/_search
{
  "query": {
    "term": {
      "{FIELD}": "{VALUE}"
    }
  }
}
  • {FIELD} - 就是我们需要匹配的字段名
  • {VALUE} - 就是我们需要匹配的内容,除了TEXT类型字段以外的任意类型。

2.3实现SQL的in语句-terms

如果我们要实现SQL中的in语句,一个字段包含给定数组中的任意一个值就匹配。

GET /order_v2/_search
{
  "query": {
    "terms": {
      "{FIELD}": [
        "{VALUE1}",
        "{VALUE2}"
      ]
    }
  }
}
  • {FIELD} - 就是我们需要匹配的字段名
  • {VALUE1}, {VALUE2} … {VALUE N} - 就是我们需要匹配的内容,除了TEXT类型字段以外的任意类型。

2.4范围查询

通过range实现范围查询,类似SQL语句中的>, >=, <, <=表达式。

GET /{索引名}/_search
{
  "query": {
    "range": {
      "{FIELD}": {
        "gte": 10, 
        "lte": 20
      }
    }
  }
}
  • {FIELD} - 字段名
  • gte范围参数 - 等价于>=
  • lte范围参数 - 等价于 <=
  • 范围参数可以只写一个,例如:仅保留 “gte”: 10, 则代表 FIELD字段 >= 10

范围参数如下:

  • gt - 大于 ( > )
  • gte - 大于且等于 ( >= )
  • lt - 小于 ( < )
  • lte - 小于且等于 ( <= )

2.5bool组合查询

2.5.1bool查询基本语法结构

在ES中bool查询就是用来组合布尔查询条件,布尔查询条件,就是类似SQL中的and (且)、or (或)

前面的例子都是设置单个字段的查询条件,如果需要编写类似SQL的Where语句,组合多个字段的查询条件,可以使用bool语句。

GET /{索引名}/_search
{
  "query": {
    "bool": { // bool查询
      "must": [], // must条件,类似SQL中的and, 代表必须匹配条件
      "must_not": [], // must_not条件,跟must相反,必须不匹配条件
      "should": [] // should条件,类似SQL中or, 代表匹配其中一个条件
    }
  }
}

可以任意选择must、must_not和should条件的参数都是一个数组,意味着他们都支持设置多个条件

2.5.2must条件

类似SQL的and,代表必须匹配的条件。

GET /{索引名}/_search
{
  "query": {
    "bool": {
      "must": [
         {匹配条件1},
         {匹配条件2},
         ...可以有N个匹配条件...
        ]
    }
  }
}
2.5.3must_not条件

跟must的作用相反。

GET /{索引名}/_search
{
  "query": {
    "bool": {
      "must_not": [
         {匹配条件1},
         {匹配条件2},
         ...可以有N个匹配条件...
        ]
    }
  }
}
2.5.4should条件

类似SQL中的 or, 只要匹配其中一个条件即可

GET /{索引名}/_search
 {
   "query": {
     "bool": {
       "should": [
          {匹配条件1},
          {匹配条件2},
          …可以有N个匹配条件…
         ]
     }
   }
 }

3.全文搜索

3.1默认全文搜索

默认情况下,使用全文搜索很简单,只要将字段类型定义为text类型,然后用match语句匹配即可

ES对于text类型的字段,在插入数据的时候,会进行分词处理,然后根据分词的结果索引文档,当我们搜索text类型字段的时候,也会先对搜索关键词进行分词处理、然后根据分词的结果去搜索。

ES默认的分词器是standard,对英文比较友好,例如:hello world 会被分解成 hello和world两个关键词,如果是中文会分解成一个一个字,例如:上海大学 会分解成: 上、海、大、学。

3.2中文分词器

ES默认的analyzer(分词器),对英文单词比较友好,对中文分词效果不好。不过ES支持安装分词插件,增加新的分词器。

默认的分词器不满足需要,可以在定义索引映射的时候,指定text字段的分词器(analyzer)。

PUT /article
{
  "mappings": {
    "properties": {
      "title":   { 
          "type": "text",
          "analyzer": "smartcn"
      }
    }
  }
}

只要在定义text字段的时候,增加一个analyzer配置,指定分词器即可,这里指定的分词器是smartcn。

smartcn分词器:smartcn是目前ES官方推荐的中文分词插件,不过目前不支持自定义词库。

ik分词器:ik支持自定义扩展词库,有时候分词的结果不满足我们业务需要,需要根据业务设置专门的词库,词库的作用就是自定义一批关键词,分词的时候优先根据词库设置的关键词分割内容,例如:词库中包含 “上海大学” 关键词,如果对“上海大学在哪里?”进行分词,“上海大学” 会做为一个整体被切割出来。

4. 排序

ES的默认排序是根据相关性分数排序,如果我们想根据查询结果中的指定字段排序,需要使用sort Processors处理。

GET /{索引名}/_search
{
  "query": {
    ...查询条件....
  },
  "sort": [
    {
      "{Field1}": { // 排序字段1
        "order": "desc" // 排序方向,asc或者desc, 升序和降序
      }
    },
    {
      "{Field2}": { // 排序字段2
        "order": "desc" // 排序方向,asc或者desc, 升序和降序
      }
    }
    ....多个排序字段.....
  ]
}

sort子句支持多个字段排序,类似SQL的order by。

三、聚合分析

ES中的聚合查询,类似SQL的sum/avg/count/GROUP BY分组查询,主要用于统计分析场景。一般统计分析主要分为两个步骤:分组

、组内聚合

核心概念

  • :满足特定条件的文档的集合。桶的就是一组数据的集合,对数据分组后,得到一组组的数据,就是一个个的桶。

    提示:桶等同于组,分桶和分组是一个意思,ES使用桶代表一组相同特征的数据。

  • 指标:对文档进行统计计算方式,又叫指标聚合。桶内聚合,说的就是先对数据进行分组(分桶),然后对每一个桶内的数据进行指标聚合。说白了就是,前面将数据经过一轮桶聚合,把数据分成一个个的桶之后,我们根据上面计算指标对桶内的数据进行统计。

    常用的指标有:SUM、COUNT、MAX等统计函数。

1.指标聚合

ES指标聚合,就是类似SQL的统计函数,指标聚合可以单独使用,也可以跟桶聚合一起使用。

1.1 value_count 值聚合

主要用于统计文档总数,类似SQL的count函数。

GET /sales/_search?size=0
{
  "aggs": {
    "types_count": { // 聚合查询的名字,随便取个名字
      "value_count": { // 聚合类型为:value_count
        "field": "type" // 计算type这个字段值的总数
      }
    }
  }
}

等价SQL:

select count(type) from sales

返回结果:

{
    ...
    "aggregations": {
        "types_count": { // 聚合查询的名字
            "value": 7 // 统计结果
        }
    }
}

1.2 cardinality 基数聚合

基数聚合,也是用于统计文档的总数,跟Value Count的区别是,基数聚合会去重,不会统计重复的值,类似SQL的count(DISTINCT 字段)用法。

POST /sales/_search?size=0
{
    "aggs" : {
        "type_count" : { // 聚合查询的名字,随便取一个
            "cardinality" : { // 聚合查询类型为:cardinality
                "field" : "type" // 根据type这个字段统计文档总数
            }
        }
    }
}

等价SQL:

select count(DISTINCT type) from sales

返回结果:

{
    ...
    "aggregations" : {
        "type_count" : { // 聚合查询的名字
            "value" : 3 // 统计结果
        }
    }
}

1.3avg/sum/max/min

  • avg - 求平均值

  • sum - 求和

  • max - 求最大值

  • min - 求最小值

这些函数的使用形式和结果形式都和上面是类似的,仅需替换函数就好。

2.分组统计

数据分组在es中叫做桶聚合。桶聚合一般不单独使用,都是配合指标聚合一起使用,对数据分组之后肯定要统计桶内数据,在ES中如果没有明确指定指标聚合,默认使用value_count指标聚合,统计桶内文档总数。

2.1Terms聚合(唯一值分组)

terms聚合的作用跟SQL中group by作用一样,都是根据字段唯一值对数据进行分组(分桶),字段值相等的文档都分到同一个桶内。

GET /order/_search?size=0
{
  "aggs": {
    "shop": { // 聚合查询的名字,随便取个名字
      "terms": { // 聚合类型为: terms
        "field": "shop_id" // 根据shop_id字段值,分桶
      }
    }
  }
}

等价SQL:

select shop_id, count(*) from order group by shop_id

返回结果:

{
    ...
    "aggregations" : {
        "shop" : { // 聚合查询名字
            "buckets" : [ // 桶聚合结果,下面返回各个桶的聚合结果
                {
                    "key" : "1", // key分桶的标识,在terms聚合中,代表的就是分桶的字段值
                    "doc_count" : 6 // 默认的指标聚合是统计桶内文档总数
                },
                {
                    "key" : "5",
                    "doc_count" : 3
                },
                {
                    "key" : "9",
                    "doc_count" : 2
                }
            ]
        }
    }
}

2.2Histogram聚合(数值间隔分组)

histogram(直方图)聚合,主要根据数值间隔分组,使用histogram聚合分桶统计结果,通常用在绘制条形图报表。

POST /sales/_search?size=0
{
    "aggs" : {
        "prices" : { // 聚合查询名字,随便取一个
            "histogram" : { // 聚合类型为:histogram
                "field" : "price", // 根据price字段分桶
                "interval" : 50 // 分桶的间隔为50,意思就是price字段值按50间隔分组
            }
        }
    }
}

返回结果:

{
    ...
    "aggregations": {
        "prices" : { // 聚合查询名字
            "buckets": [ // 分桶结果
                {
                    "key": 0.0, // 桶的标识,histogram分桶,这里通常是分组的间隔值
                    "doc_count": 1 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                { 
                    "key": 50.0,
                    "doc_count": 1
                },
                {
                    "key": 100.0,
                    "doc_count": 0
                },
                {
                    "key": 150.0,
                    "doc_count": 2
                }
            ]
        }
    }
}

2.3date_histogram聚合(时间间隔分组)

类似histogram聚合,区别是Date histogram可以很好的处理时间类型字段,主要用于根据时间、日期分桶的场景。

POST /sales/_search?size=0
{
    "aggs" : {
        "sales_over_time" : { // 聚合查询名字,随便取一个
            "date_histogram" : { // 聚合类型为: date_histogram
                "field" : "date", // 根据date字段分组
                "calendar_interval" : "month", // 分组间隔:month代表每月、支持minute(每分钟)、hour(每小时)、day(每天)、week(每周)、year(每年)
                "format" : "yyyy-MM-dd" // 设置返回结果中桶key的时间格式
            }
        }
    }
}

返回结果:

{
    ...
    "aggregations": {
        "sales_over_time": { // 聚合查询名字
            "buckets": [ // 桶聚合结果
                {
                    "key_as_string": "2015-01-01", // 每个桶key的字符串标识,格式由format指定
                    "key": 1420070400000, // key的具体字段值
                    "doc_count": 3 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                {
                    "key_as_string": "2015-02-01",
                    "key": 1422748800000,
                    "doc_count": 2
                },
                {
                    "key_as_string": "2015-03-01",
                    "key": 1425168000000,
                    "doc_count": 2
                }
            ]
        }
    }
}

2.4Range聚合(数值范围分组)

range聚合,按数值范围分桶。

GET /_search
{
    "aggs" : {
        "price_ranges" : { // 聚合查询名字,随便取一个
            "range" : { // 聚合类型为: range
                "field" : "price", // 根据price字段分桶
                "ranges" : [ // 范围配置
                    { "to" : 100.0 }, // 意思就是 price <= 100的文档归类到一个桶
                    { "from" : 100.0, "to" : 200.0 }, // price>100 and price<200的文档归类到一个桶
                    { "from" : 200.0 } // price>200的文档归类到一个桶
                ]
            }
        }
    }
}

返回结果:

{
    ...
    "aggregations": {
        "price_ranges" : { // 聚合查询名字
            "buckets": [ // 桶聚合结果
                {
                    "key": "*-100.0", // key可以表达分桶的范围
                    "to": 100.0, // 结束值
                    "doc_count": 2 // 默认按Value Count指标聚合,统计桶内文档总数
                },
                {
                    "key": "100.0-200.0",
                    "from": 100.0, // 起始值
                    "to": 200.0, // 结束值
                    "doc_count": 2
                },
                {
                    "key": "200.0-*",
                    "from": 200.0,
                    "doc_count": 3
                }
            ]
        }
    }
}

大家仔细观察的话,发现range分桶,默认key的值不太友好,尤其开发的时候,不知道key长什么样子,处理起来比较麻烦,我们可以为每一个分桶指定一个有意义的名字。

GET /_search
{
    "aggs" : {
        "price_ranges" : {
            "range" : {
                "field" : "price",
                "keyed" : true,
                "ranges" : [
                    // 通过key参数,配置每一个分桶的名字
                    { "key" : "cheap", "to" : 100 },
                    { "key" : "average", "from" : 100, "to" : 200 },
                    { "key" : "expensive", "from" : 200 }
                ]
            }
        }
    }
}

2.5综合例子

前面的例子,都是单独使用aggs聚合语句,代表直接统计所有的文档,实际应用中,经常需要配合query语句,先搜索目标文档,然后使用aggs聚合语句对搜索结果进行统计分析。聚合查询支持多层嵌套

GET /cars/_search
{
    "size": 0, // size=0代表不需要返回query查询结果,仅仅返回aggs统计结果
    "query" : { // 设置查询语句,先赛选文档
        "match" : {
            "make" : "ford"
        }
    },
    "aggs" : { // 然后对query搜索的结果,进行统计
        "colors" : { // 聚合查询名字
            "terms" : { // 聚合类型为:terms 先分桶
              "field" : "color"
            },
            "aggs": { // 通过嵌套聚合查询,设置桶内指标聚合条件
              "avg_price": { // 聚合查询名字
                "avg": { // 聚合类型为: avg指标聚合
                  "field": "price" // 根据price字段计算平均值
                }
              },
              "sum_price": { // 聚合查询名字
                "sum": { // 聚合类型为: sum指标聚合
                  "field": "price" // 根据price字段求和
                }
              }
            }
        }
    }
}

3.多桶排序

类似terms、histogram、date_histogram这类桶聚合都会动态生成多个桶,如果生成的桶特别多,我们如何确定这些桶的排序顺序,如何限制返回桶的数量。默认情况,ES会根据doc_count文档总数,降序排序。ES桶聚合支持两种方式排序:内置排序按度量指标排序

3.1多桶排序

3.1.1内置排序
  • _count - 按文档数排序。对 terms 、 histogram 、 date_histogram 有效
  • _term - 按词项的字符串值的字母顺序排序。只在 terms 内使用
  • _key - 按每个桶的键值数值排序, 仅对 histogram 和 date_histogram 有效
GET /cars/_search
{
    "size" : 0,
    "aggs" : {
        "colors" : { // 聚合查询名字,随便取一个
            "terms" : { // 聚合类型为: terms
              "field" : "color", 
              "order": { // 设置排序参数
                "_count" : "asc"  // 根据_count排序,asc升序,desc降序
              }
            }
        }
    }
}
3.1.2按度量排序

通常情况下,我们根据桶聚合分桶后,都会对桶内进行多个维度的指标聚合,所以我们也可以根据桶内指标聚合的结果进行排序。

GET /cars/_search
{
    "size" : 0,
    "aggs" : {
        "colors" : { // 聚合查询名字
            "terms" : { // 聚合类型: terms,先分桶
              "field" : "color", // 分桶字段为color
              "order": { // 设置排序参数
                "avg_price" : "asc"  // 根据avg_price指标聚合结果,升序排序。
              }
            },
            "aggs": { // 嵌套聚合查询,设置桶内聚合指标
                "avg_price": { // 聚合查询名字,前面排序引用的就是这个名字
                    "avg": {"field": "price"} // 计算price字段平均值
                }
            }
        }
    }
}

3.2限制返回桶的数量

如果分桶的数量太多,可以通过给桶聚合增加一个size参数限制返回桶的数量。

GET /_search
{
    "aggs" : {
        "products" : { // 聚合查询名字
            "terms" : { // 聚合类型为: terms
                "field" : "product", // 根据product字段分桶
                "size" : 5 // 限制最多返回5个桶
            }
        }
    }
}

四、Java快速接入ES

这里使用的es的版本为7.x

1.依赖与配置

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  <version>2.4.2</version>
</dependency>

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pwMiuPOF-1663066410029)(%E8%BD%AF%E4%BB%B6%E7%B3%BB%E7%BB%9F-7.ES.assets/image-20220902144006346.png)]

es的版本差异不同,其结构发生很多变化,这里仅以其中一个版本介绍。

spring:
  elasticsearch:
    rest:
      uris: 127.0.0.1:9200

2.使用

Elasticsearch从6.x升级到7.x改动还真不是一般的大,ElasticsearchTemplate不建议使用了,改为使用ElasticsearchRestTemplate,ElasticsearchRepository实现复杂查询的方法也不建议使用了。从此我们简单的数据操作可以使用ElasticsearchRepository,而复杂的数据操作只能使用ElasticsearchRestTemplate了。

public interface EsRepository extends ElasticsearchRepository<UserDocument, Long> {
}

五、Java整合ES的注意

1.@Document

一般使用@Document(indexName = "technical_es_test", useServerConfiguration = true, createIndex = false)

@Persistent
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
public @interface Document {
    //索引库名称
    String indexName();
    //类型
    String type() default "";
    //
    boolean useServerConfiguration() default false;
    //默认分片数5
    short shards() default 5;
    //默认副本数1
    short replicas() default 1;
    //刷新间隔
    String refreshInterval() default "1s";
    //索引文件存储类型
    String indexStoreType() default "fs";
   //是否创建索引
    boolean createIndex() default true;
}

2.@Field 与 枚举类FieldType

作用于document文档的实体类

@Id:作用在成员变量,标记一个字段为id主键;一般id字段或是域不需要存储也不需要分词;

@Field作用在成员变量,标记为文档的字段,并制定映射属性;一般使用@Field(type = FieldType.Long, store = true)

@Field

(1)type:字段的类型,取值是枚举,FieldType;

(2)index:是否索引,布尔值类型,默认是true;

(3)store:是否存储,布尔值类型,默认值是false;

(4)analyzer:分词器名称

FieldType

public enum FieldType {
  Text,
  Integer,
  Long,
  Date,
  Float,
  Double,
  Boolean,
  Object,
  Auto,
  Nested,
  Ip,
  Attachment,
  Keyword;

  private FieldType() {
  }
}

【 @Field(type = FieldType.Keyword)和 @Field(type = FieldType.Text)区别】

  在早期elasticsearch5.x之前的版本存储字符串只有string字段;但是在elasticsearch5.x之后的版本存储了KeywordText,都是存储字符串的。FieldType.Keyword存储字符串数据时,不会建立索引;而FieldType.Text在存储字符串数据的时候,会自动建立索引,也会占用部分空间资源。

3.布尔查询 BoolQueryBuilder

BoolQueryBuilder queryBuilder = QueryBuilders.boolQuery();

  • must子句:文档必须匹配must查询条件;
  • should子句:文档应该匹配should子句查询的一个或多个;
  • must_not子句:文档不能匹配该查询条件;
  • filter子句:过滤器,文档必须匹配该过滤条件,跟must子句的唯一区别是,filter不影响查询的score;

filter子句中可以再传入一个BoolQueryBuilder,通过QueryBuilders的下列方式进行匹配。

termQuery("key", obj); 						 //完全匹配
termsQuery("key", obj1, obj2..);   //一次匹配多个值
matchQuery("key", Obj);		 				 //单个匹配, field不支持通配符, 前缀具高级特性
multiMatchQuery("text", "field1", "field2"..);  //匹配多个字段, field有通配符忒行
matchAllQuery();         					 //匹配所有文件

在布尔查询中:

各个子句之间的逻辑关系是与(and)。对于单个子句,只要一个文档满足该子句的查询条件,返回的逻辑结果就是true。

对于should子句,它一般包含多个子查询条件,参数 minimum_should_match 控制文档必须满足should子句中的子查询条件的数量,只有当文档满足指定数量的should查询条件时,should子句返回的逻辑结果才是true。

参考:https://www.cnblogs.com/ljhdo/p/5040252.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

我是lk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值