Elasticsearch 超详细(包含简介、下载安装、使用示例等等)不收藏超级亏!!!

1. 引言

1.1 Elasticsearch 简介

Elasticsearch 是一个基于 Lucene 的分布式搜索和分析引擎,提供实时数据搜索、聚合分析、分布式存储等功能。其设计目标是实现高可用、可扩展、易维护,广泛应用于日志分析、电商搜索、实时监控等场景。Java 作为企业级开发的主流语言,与 Elasticsearch 的集成非常成熟,官方提供了完善的 Java 客户端库。

1.2 Java 客户端优势

  • 兼容性强:支持主流 Java 版本(Java 8+),适配 Spring、Spring Boot 等框架。
  • 功能完整:涵盖文档操作、复杂查询、聚合分析、集群管理等全功能。
  • 性能高效:通过连接池、批量操作等优化,满足高并发场景需求。

2. 环境准备

2.1 安装 Elasticsearch

2.1.1 下载与启动

  • 从 Elasticsearch 官网 下载对应版本(本文以 8.9.0 为例)。
  • 解压后执行启动命令:
    • Linux/macOS:./bin/elasticsearch
    • Windows:.\bin\elasticsearch.bat
  • 验证启动:访问 http://localhost:9200,返回包含集群信息的 JSON 数据。

2.1.2 配置文件

  • config/elasticsearch.yml 可配置集群名称、节点名称、网络绑定地址、端口等。
cluster:
    name: my-elasticsearch-cluster
node:
    name: node-1
network:
    host: 0.0.0.0
http:
    port: 9200

2.2 配置 Java 开发环境

2.2.1 引入依赖(Maven)

Elasticsearch 提供两种 Java 客户端:

  • Transport Client:基于 TCP 协议,已被弃用(6.x 版本后不再推荐,8.x 版本移除)。
  • REST Client:基于 HTTP 协议,推荐使用 高级 REST 客户端(RestHighLevelClient)

添加 Maven 依赖:

<dependencies>
    <!-- Elasticsearch 客户端 -->
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>8.9.0</version>
    <!-- Elasticsearch 核心库(需与服务端版本一致) -->
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>8.9.0</version>
    <!-- HTTP 客户端(OkHttp 或 Apache HttpClient) -->
    <groupId>org.apache.httpcomponents.client5</groupId>
    <artifactId>httpclient5</artifactId>
    <version>5.3</version>
</dependencies>

2.2.2 开发工具

  • IDE:IntelliJ IDEA / Eclipse
  • JDK:Java 8+(推荐 Java 11+)

3. 核心概念与术语

3.1 基本概念

概念描述
集群 (Cluster)由一个或多个节点组成,共享相同的集群名称,协同工作处理数据。
节点 (Node)集群中的单个服务器实例,可作为数据节点(存储数据)或协调节点(路由请求)。
索引 (Index)逻辑上的数据集,类似关系型数据库中的 “数据库”,由多个分片组成。
文档 (Document)索引中的基本数据单元,以 JSON 格式存储,类似关系型数据库中的 “行”。
类型 (Type)旧版本用于区分文档类别(6.x 版本后弃用,默认使用 _doc 类型)。
映射 (Mapping)定义索引中文档的字段类型、分词器、存储方式等元数据,类似表结构。
分片 (Shard)索引的物理拆分单元,分为主分片(Primary Shard)和副本分片(Replica Shard),提高吞吐量和可用性。

3.2 数据模型

  • 文档以 JSON 格式存储,每个文档有唯一 ID(自动生成或自定义)。
  • 映射支持丰富的数据类型:字符串(text、keyword)、数值(integer、double)、日期(date)、数组、对象等。

4. Java 客户端初始化

4.1 创建 RestHighLevelClient 实例

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;

public class ElasticsearchClientDemo {
    private static RestHighLevelClient client;

    static {
        // 配置集群节点(支持单个或多个节点)
        RestClient.builder(
                new HttpHost("localhost", 9200, "http"),
                new HttpHost("node2", 9200, "http")
        )
        // 配置连接池(可选,默认配置适用于大多数场景)
        .setRequestConfigCallback(requestConfigBuilder -> 
            requestConfigBuilder.setConnectTimeout(5000) // 连接超时时间(ms)
                                 .setSocketTimeout(30000) // 套接字超时时间(ms)
        )
        .setHttpClientConfigCallback(httpClientBuilder -> 
            httpClientBuilder.setMaxConnTotal(100) // 最大连接数
                             .setDefaultCookieStore(null)
        );

        client = new RestHighLevelClient(
            RestClient.builder(new HttpHost("localhost", 9200, "http"))
        );
    }

    public static void closeClient() throws IOException {
        if (client != null) {
            client.close();
        }
    }
}

4.2 SSL 加密连接(HTTPS)

若 Elasticsearch 启用 HTTPS 访问,需配置证书:

import org.apache.http.ssl.SSLContexts;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;

import java.io.File;
import java.security.KeyStore;

public class HttpsClientDemo {
    public static void main(String[] args) throws Exception {
        // 加载 CA 证书
        KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
        trustStore.load(new File("path/to/ca.crt"), "password".toCharArray());

        RestClient.builder(new HttpHost("localhost", 9243, "https"))
                .setHttpClientConfigCallback(httpClientBuilder -> {
                    httpClientBuilder.setSSLContext(SSLContexts.custom()
                            .loadTrustMaterial(trustStore, null)
                            .build());
                    return httpClientBuilder;
                });

        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(new HttpHost("localhost", 9243, "https"))
        );
    }
}

5. 数据操作:增删改查

5.1 索引文档(Create/Index)

5.1.1 自定义文档 ID

import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.common.xcontent.XContentType;

public class IndexDocument {
    public static void main(String[] args) throws IOException {
        // 创建文档数据(JSON 格式)
        String docJson = "{\n" +
                "  \"id\": 1001,\n" +
                "  \"name\": \"Elasticsearch 实战\",\n" +
                "  \"price\": 99.9,\n" +
                "  \"tags\": [\"搜索\", \"大数据\"]\n" +
                "}";

        // 构建 IndexRequest(指定索引名、文档类型(固定为 _doc)、文档 ID)
        IndexRequest request = new IndexRequest("products", "_doc", "1001")
                .source(docJson, XContentType.JSON);

        // 执行索引操作
        IndexResponse response = client.index(request, RequestOptions.DEFAULT);

        // 解析响应
        System.out.println("索引名: " + response.getIndex());
        System.out.println("文档 ID: " + response.getId());
        System.out.println("版本: " + response.getVersion());
        System.out.println("结果: " + response.getResult()); // CREATED 或 UPDATED
    }
}

5.1.2 自动生成文档 ID

IndexRequest request = new IndexRequest("products", "_doc")
        .source(docJson, XContentType.JSON);

5.2 查询文档(Get)

import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;

public class GetDocument {
    public static void main(String[] args) throws IOException {
        GetRequest request = new GetRequest("products", "_doc", "1001");
        
        // 可选:指定返回字段(避免返回全量数据)
        request.fetchSourceContext(new FetchSourceContext(false))
               .storedFields("_none_"); // 不返回任何字段,仅获取元数据
        
        GetResponse response = client.get(request, RequestOptions.DEFAULT);
        
        if (response.isExists()) {
            String sourceAsString = response.getSourceAsString(); // JSON 字符串
            Map<String, Object> sourceAsMap = response.getSourceAsMap(); // Map 结构
            System.out.println("文档存在,内容:" + sourceAsString);
        } else {
            System.out.println("文档不存在");
        }
    }
}

5.3 更新文档(Update)

5.3.1 全量更新(替换文档)

直接使用 IndexRequest,若文档 ID 已存在则覆盖。

5.3.2 部分更新(推荐)

import org.elasticsearch.action.update.UpdateRequest;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentFactory;

public class UpdateDocument {
    public static void main(String[] args) throws IOException {
        UpdateRequest request = new UpdateRequest("products", "_doc", "1001");
        
        // 构建更新脚本(使用 JSON 或脚本语言,如 Painless)
        XContentBuilder builder = XContentFactory.jsonBuilder();
        builder.startObject()
               .field("doc")
               .startObject()
               .field("price", 109.9)
               .endObject()
               .endObject();
        
        request.doc(builder);
        
        client.update(request, RequestOptions.DEFAULT);
    }
}

5.4 删除文档(Delete)

import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.delete.DeleteResponse;

public class DeleteDocument {
    public static void main(String[] args) throws IOException {
        DeleteRequest request = new DeleteRequest("products", "_doc", "1001");
        DeleteResponse response = client.delete(request, RequestOptions.DEFAULT);
        
        System.out.println("结果: " + response.getResult()); // DELETED 或 NOT_FOUND
    }
}

6. 复杂查询:从简单到高级

6.1 查询构建器(Query Builders)

Elasticsearch 提供丰富的查询类型,通过 QueryBuilders 类构建。

6.1.1 术语查询(Term Query)

精确匹配单个字段(不分词):

import org.elasticsearch.index.query.TermQueryBuilder;

TermQueryBuilder query = QueryBuilders.termQuery("tags", "搜索");

6.1.2 布尔查询(Bool Query)

组合多个查询条件(must、filter、should、mustNot):

import org.elasticsearch.index.query.BoolQueryBuilder;

BoolQueryBuilder boolQuery = QueryBuilders.boolQuery()
        .must(QueryBuilders.termQuery("category", "书籍")) // 必须匹配
        .filter(QueryBuilders.rangeQuery("price").lte(200)) // 过滤(不计算相关性)
        .should(QueryBuilders.termQuery("author", "张三")) // 可选匹配(提升分数)
        .mustNot(QueryBuilders.termQuery("status", "下架")); // 必须不匹配

6.1.3 全文搜索(Match Query)

对文本字段进行分词后匹配:

import org.elasticsearch.index.query.MatchQueryBuilder;

MatchQueryBuilder matchQuery = QueryBuilders.matchQuery("description", "分布式搜索");

6.1.4 模糊查询(Fuzzy Query)

允许拼写错误的近似匹配:

import org.elasticsearch.index.query.FuzzyQueryBuilder;

FuzzyQueryBuilder fuzzyQuery = QueryBuilders.fuzzyQuery("name", "Elastcsearch")
        .fuzziness(Fuzziness.AUTO); // 自动计算编辑距离

6.2 执行搜索请求

import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;

public class SearchDocuments {
    public static void main(String[] args) throws IOException {
        // 构建搜索请求
        SearchRequest searchRequest = new SearchRequest("products");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        
        // 设置查询条件
        sourceBuilder.query(QueryBuilders.termQuery("tags", "大数据"));
        
        // 设置返回字段(排除不需要的字段)
        sourceBuilder.fetchSource(new String[]{"name", "price"}, new String[]{"tags"});
        
        // 设置分页(默认从第 0 页开始,每页 10 条)
        sourceBuilder.from(0);
        sourceBuilder.size(20);
        
        // 设置排序
        sourceBuilder.sort("price", SortOrder.DESC);
        
        searchRequest.source(sourceBuilder);
        
        // 执行搜索
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        
        // 解析结果
        SearchHits hits = searchResponse.getHits();
        System.out.println("总命中数: " + hits.getTotalHits().value);
        
        for (SearchHit hit : hits) {
            String docId = hit.getId();
            Map<String, Object> sourceAsMap = hit.getSourceAsMap();
            System.out.println("文档 ID: " + docId + ", 内容: " + sourceAsMap);
        }
    }
}

6.3 聚合分析(Aggregation)

6.3.1 术语聚合(Terms Aggregation):分组统计

import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.aggregation(AggregationBuilders.terms("tag_agg")
        .field("tags")
        .size(10) // 最多返回 10 个桶
        .order(Terms.Order.count(false)) // 降序排列
);

SearchResponse response = client.search(new SearchRequest("products").source(sourceBuilder), RequestOptions.DEFAULT);

Terms tagAgg = response.getAggregations().get("tag_agg");
for (Terms.Bucket bucket : tagAgg.getBuckets()) {
    System.out.println("标签: " + bucket.getKeyAsString() + ", 数量: " + bucket.getDocCount());
}

6.3.2 数值聚合(Max/Avg/Min):统计价格最大值

sourceBuilder.aggregation(AggregationBuilders.max("max_price").field("price"));

Max maxPrice = response.getAggregations().get("max_price");
System.out.println("最高价格: " + maxPrice.getValue());

7. 高级功能与最佳实践

7.1 批量操作(Bulk API)

批量处理大量文档(显著提升性能):

import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;

public class BulkOperations {
    public static void main(String[] args) throws IOException {
        BulkRequest bulkRequest = new BulkRequest();
        
        // 添加多个索引/删除请求
        for (int i = 1002; i <= 1010; i++) {
            String docJson = "{\"id\":" + i + ", \"name\":\"产品" + i + "\"}";
            IndexRequest indexRequest = new IndexRequest("products", "_doc", String.valueOf(i))
                    .source(docJson, XContentType.JSON);
            bulkRequest.add(indexRequest);
        }
        
        BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
        
        // 处理错误(如有)
        if (bulkResponse.hasFailures()) {
            bulkResponse.getItems().forEach(item -> {
                if (item.getIndex() != null && item.getIndex().isFailed()) {
                    System.out.println("索引失败: " + item.getIndex().getFailureMessage());
                }
            });
        }
    }
}

7.2 滚动查询(Scroll Query)

处理超过分页限制的大量数据(单次查询最多返回 10000 条,滚动查询可突破限制):

import org.elasticsearch.search.Scroll;

public class ScrollSearch {
    public static void main(String[] args) throws IOException {
        Scroll scroll = new Scroll(TimeValue.timeValueMinutes(1)); // 滚动保持时间
        SearchRequest searchRequest = new SearchRequest("large_index");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder()
                .query(QueryBuilders.matchAllQuery())
                .size(1000); // 每页大小
        
        searchRequest.source(sourceBuilder).scroll(scroll);
        SearchResponse response = client.search(searchRequest, RequestOptions.DEFAULT);
        String scrollId = response.getScrollId();
        SearchHit[] hits = response.getHits().getHits();
        
        while (hits != null && hits.length > 0) {
            // 处理当前页数据
            for (SearchHit hit : hits) {
                // 业务逻辑
            }
            
            // 滚动获取下一页
            SearchScrollRequest scrollRequest = new SearchScrollRequest(scrollId);
            scrollRequest.scroll(scroll);
            response = client.scroll(scrollRequest, RequestOptions.DEFAULT);
            scrollId = response.getScrollId();
            hits = response.getHits().getHits();
        }
        
        // 清除滚动上下文(释放资源)
        ClearScrollRequest clearScrollRequest = new ClearScrollRequest();
        clearScrollRequest.addScrollId(scrollId);
        client.clearScroll(clearScrollRequest, RequestOptions.DEFAULT);
    }
}

7.3 映射管理

7.3.1 创建索引时定义映射

import org.elasticsearch.client.indices.CreateIndexRequest;
import org.elasticsearch.client.indices.CreateIndexResponse;
import org.elasticsearch.common.xcontent.XContentFactory;

public class CreateIndexWithMapping {
    public static void main(String[] args) throws IOException {
        CreateIndexRequest request = new CreateIndexRequest("blogs");
        
        // 定义映射
        String mapping = "{\n" +
                "  \"mappings\": {\n" +
                "    \"properties\": {\n" +
                "      \"title\": {\"type\": \"text\", \"analyzer\": \"ik_max_word\"},\n" + // 中文分词器
                "      \"author\": {\"type\": \"keyword\"},\n" + // 精确匹配
                "      \"publish_date\": {\"type\": \"date\", \"format\": \"yyyy-MM-dd HH:mm:ss\"}\n" +
                "    }\n" +
                "  }\n" +
                "}";
        
        request.mapping(mapping, XContentType.JSON);
        
        CreateIndexResponse response = client.indices().create(request, RequestOptions.DEFAULT);
        if (response.isAcknowledged()) {
            System.out.println("索引创建成功");
        }
    }
}

7.3.2 更新映射(仅支持新增字段,不能修改已有字段类型)

import org.elasticsearch.client.indices.PutMappingRequest;

PutMappingRequest request = new PutMappingRequest("blogs")
        .type("_doc")
        .source("{\"properties\": {\"new_field\": {\"type\": \"text\"}}}", XContentType.JSON);

client.indices().putMapping(request, RequestOptions.DEFAULT);

7.4 别名与模板

7.4.1 索引别名(Alias)

为索引创建别名,支持动态切换底层索引(如日志按天分索引时使用 logs-* 别名):

import org.elasticsearch.client.indices.AliasRequest;
import org.elasticsearch.client.indices.PutAliasRequest;

PutAliasRequest request = new PutAliasRequest("current_products")
        .addAlias(new AliasRequest.Alias("current_products").index("products_2025"));

client.indices().putAlias(request, RequestOptions.DEFAULT);

7.4.2 模板(Template)

预定义索引映射和设置,用于自动创建索引时应用规则(如日志索引模板):

import org.elasticsearch.client.indices.PutIndexTemplateRequest;

String template = "{\n" +
        "  \"index_patterns\": [\"logs-*\"],\n" +
        "  \"mappings\": {\n" +
        "    \"properties\": {\n" +
        "      \"message\": {\"type\": \"text\", \"analyzer\": \"logstash_analyzer\"}\n" +
        "    }\n" +
        "  }\n" +
        "}";

PutIndexTemplateRequest request = new PutIndexTemplateRequest("log_template")
        .source(template, XContentType.JSON);

client.indices().putTemplate(request, RequestOptions.DEFAULT);

8. 集成 Spring Boot

8.1 添加依赖

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
    </dependency>
    <!-- 排除旧版客户端,使用官方 REST 客户端 -->
    <exclusion>
        <groupId>org.elasticsearch.client</groupId>
        <artifactId>elasticsearch-rest-client</artifactId>
    </exclusion>
</dependencies>

8.2 配置文件(application.yml)

spring:
  data:
    elasticsearch:
      cluster-name: my-elasticsearch-cluster
      cluster-nodes: localhost:9300 # Transport Client 端口(旧版,8.x 不推荐)
      rest:
        uris: http://localhost:9200 # 推荐使用 REST 客户端配置

8.3 定义实体类

import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Document(indexName = "products", type = "_doc", shards = 2, replicas = 1)
public class Product {
    @Id
    private String id;
    
    @Field(type = FieldType.Text, analyzer = "ik_max_word")
    private String name;
    
    @Field(type = FieldType.Keyword)
    private String category;
    
    // Getter/Setter
}

8.4 定义 Repository

import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;

public interface ProductRepository extends ElasticsearchRepository<Product, String> {
    // 自定义查询方法(基于方法名解析)
    List<Product> findByNameContaining(String keyword);
    
    // 复杂查询使用 @Query 注解
    @Query("{\"bool\": {\"must\": {\"term\": {\"category\": \"?0\"}}}}")
    List<Product> findByCategory(String category);
}

8.5 服务层使用

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Service;

@Service
public class ProductService {
    @Autowired
    private ProductRepository repository;
    
    public Product save(Product product) {
        return repository.save(product);
    }
    
    public Page<Product> search(String keyword, int page, int size) {
        return repository.findByNameContaining(keyword, PageRequest.of(page, size));
    }
}

9. 性能优化

9.1 客户端配置优化

  • 连接池调优:根据并发量设置 maxConnTotal(建议 100-200)和 maxConnPerRoute(建议 50-100)。
  • 超时控制:合理设置 connectTimeout(连接超时)和 socketTimeout(响应超时),避免长耗时阻塞。

9.2 索引设计优化

  • 映射优化
    • 对不需要搜索的字段设置 index: false
    • 对精确匹配字段使用 keyword 类型,全文搜索字段使用 text 并指定分词器。
  • 分片规划
    • 主分片数在创建索引时确定,建议单个分片大小控制在 10-50GB。
    • 副本分片数根据集群节点数和可用性需求设置(如 1 个副本提高容灾能力)。

9.3 查询优化

  • 避免深分页:使用 from + size 分页时,深度分页(如 from=10000)性能低下,建议改用滚动查询或搜索后处理。
  • 合理使用过滤(Filter):对不计算相关性的查询(如范围查询)使用 filter 而非 must,利用缓存提升性能。
  • 批量操作:使用 Bulk API 处理批量写入,减少网络开销。

9.4 集群调优

  • 节点角色分离:将节点分为数据节点、协调节点、机器学习节点,避免混合部署影响性能。
  • JVM 内存配置:设置 ES_JAVA_OPTS="-Xms8g -Xmx8g",内存大小不超过物理内存的 50%,且不超过 32GB(避免压缩指针失效)。

10. 错误处理与最佳实践

10.1 异常处理

Elasticsearch 客户端抛出 ElasticsearchException 及其子类,需捕获并处理:

try {
    // 客户端操作
} catch (ElasticsearchStatusException e) { // 处理 HTTP 状态码异常(如 404、400)
    int statusCode = e.statusCode();
    String errorMessage = e.getMessage();
    // 业务逻辑处理
} catch (IOException e) { // 处理网络连接异常
    // 重试或记录日志
}

10.2 日志管理

配置客户端日志输出(如使用 Logback),记录关键操作和异常:

<logger name="org.elasticsearch.client" level="WARN"/>
<logger name="org.apache.http" level="INFO"/>

10.3 版本兼容性

  • 确保客户端版本与 Elasticsearch 服务端版本完全一致(主版本号必须相同,次版本号建议兼容)。
  • 避免跨大版本升级(如 7.x 到 8.x),需参考官方升级指南。

10.4 安全配置

  • 认证授权:启用 Elasticsearch 的 X-Pack 安全模块,使用用户名密码或 API 密钥认证。
  • HTTPS 加密:客户端与服务端通信使用 HTTPS,防止数据泄露。

11. 总结

本文系统介绍了 Java 集成 Elasticsearch 的核心技术,包括环境搭建、客户端初始化、数据操作、复杂查询、高级功能、Spring Boot 集成、性能优化和最佳实践。Elasticsearch 的 Java 客户端提供了强大且灵活的接口,能够满足从简单搜索到复杂数据分析的各种需求。在实际开发中,需根据业务场景合理设计索引结构,优化查询性能,并结合集群管理确保高可用性。


附录:常用工具与资源

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值