我们一起成长(二)elasticsearch + spring boot (下篇)

Spring boot + Elastic search实战


前言

上篇带大家一起了解了ES的一些基础知识点,下篇主要带大家结合着spring boot项目一起去了解一些ES的几个不错的Java API。


一、elastic search的一些Java API有哪些?

1.JestClient

在这里插入图片描述
一款不错的ElasticSearch Java REST client,基本上能满足各种需求,包括普通查询,带聚合的复杂查询,批量删除等,唯一遗憾的是很久不更新了,最后一次是2018年更新的,6.3.1的版本,如果你的spring boot版本是2.1.x,且连接的ES客户端是7.x及7.x以下,用这个应该也能满足你的基本需求

2.RestHighLevelClient

在这里插入图片描述

应该是网上呼声最高的ES客户端,也是spring一直在维护的客户端,该客户端应该能满足你的所有增删改查需求,唯一的遗憾是,该客户端默认对应ES 7.x的版本,如果你得spring boot版本不是2.2.x以上,最好还是老老实实用JestClient吧

3.ElasticsearchRepository,ElasticSearchTemplate

ElasticsearchRepository基于JPA框架,只需要定义接口,定义方法名,底层会根据方法名去自动生成语句,使开发者从繁琐的语句语法中解放出来。好处很明显,坏处也很明显,就是不能满足一些复杂查询,所以就有了ElasticSearchTemplate来支持你编写复杂查询语法

因为选择的多样性,怕大家有选择恐惧症,这里我会给大家把这三种主流的API,结合进代码里,我们一起分析
笔者这里用的ES版本为7.7.1,下面相同

二、实战之JestClient

因为我在实际项目中用的是JestClient,所以我可能会着重介绍一下

1.依赖(maven)

spring boot版本:2.1.5.RELEASE

代码如下(示例):

elasticsearch依赖,不指定版本,默认为boot对应的版本,这里对应的版本是6.4.3

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

jest依赖,不指定版本,默认为最新版本,这里对应的版本是6.3.1

		<dependency>
            <groupId>io.searchbox</groupId>
            <artifactId>jest</artifactId>
        </dependency>

2.配置文件

配置文件

从上到下分别填写ES的连接信息包括:url,username,password,timeout

3.查询代码

创建一个接口:ElasticSearchService

package cn.com.mgcc.kol.elasticSearch.service;

import cn.com.mgcc.kol.account.base.ElasticSearchCondition;
import io.searchbox.core.SearchResult;
import org.elasticsearch.search.builder.SearchSourceBuilder;

/**
 * @author WD
 * @date 2021/3/1 0001 上午 10:10
 * @description
 */
public interface ElasticSearchService {
    /**
     * 根据条件从ES中查询
     *
     * @param condition
     * @return
     */
    SearchResult getFromElasticSearchByCondition(ElasticSearchCondition condition);

    /**
     * 组装基础分页
     *
     * @param from
     * @param size
     * @param orderBy
     * @param asc
     * @return
     */
    SearchSourceBuilder getPageSearchSourceBuilder(int from, int size, String orderBy, boolean asc);

    /**
     * 设置返回全部数据条数
     *
     * @param searchSourceBuilder
     * @return
     */
    String putTrackTotalHits(SearchSourceBuilder searchSourceBuilder);

    /**
     * 从ES查询删除(主要是供品牌变更用)
     * @param indexName
     * @param keyword
     */
    void deleteByQueryBrandsKeyword(String indexName, String keyword);
}

生成该接口对应的实现类:ElasticSearchServiceImpl

package cn.com.mgcc.kol.elasticSearch.service.impl;

import cn.com.mgcc.kol.account.base.ElasticSearchCondition;
import cn.com.mgcc.kol.elasticSearch.service.ElasticSearchService;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import io.searchbox.client.JestClient;
import io.searchbox.client.JestResult;
import io.searchbox.core.DeleteByQuery;
import io.searchbox.core.Search;
import io.searchbox.core.SearchResult;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.Map;

/**
 * @author WD
 * @date 2021/3/1 0001 上午 10:11
 * @description
 */
@Slf4j
@Service
public class ElasticSearchServiceImpl implements ElasticSearchService {

    @Autowired
    private JestClient jestClient;

    @Override
    public SearchResult getFromElasticSearchByCondition(ElasticSearchCondition condition) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.filter(QueryBuilders.termQuery("uuid.keyword", condition.getUuid()));
        searchSourceBuilder.query(boolQueryBuilder);
        Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex(condition.getIndexName()).build();
        System.out.println(searchSourceBuilder.toString());
        SearchResult result = null;
        try {
            result = jestClient.execute(search);
        } catch (IOException e) {
            log.error(e.getMessage());
        }
        return result;
    }

    @Override
    public SearchSourceBuilder getPageSearchSourceBuilder(int from, int size, String orderBy, boolean asc) {
        return new SearchSourceBuilder()
                .from(from)
                .size(size)
                .sort(orderBy, asc ? SortOrder.ASC : SortOrder.DESC)
                .trackTotalHits(true);
    }

    @Override
    public String putTrackTotalHits(SearchSourceBuilder searchSourceBuilder) {
        Map stringToMap = JSONObject.parseObject(searchSourceBuilder.toString());
        System.out.println("StringToMap=>" + stringToMap);
        stringToMap.put("track_total_hits", true);
        System.out.println("StringToMap=>" + JSON.toJSONString(stringToMap));
        return JSON.toJSONString(stringToMap);
    }

    @Override
    public void deleteByQueryBrandsKeyword(String indexName, String keyword) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        boolQueryBuilder.must(QueryBuilders.matchQuery("brands.keyword", keyword));
        searchSourceBuilder.query(boolQueryBuilder);
        searchSourceBuilder.size(100000);
        DeleteByQuery deleteByQuery = new DeleteByQuery.Builder(this.putTrackTotalHits(searchSourceBuilder)).addIndex(indexName).build();
        //删除ES
        JestResult jestResult;
        try {
            jestResult = jestClient.execute(deleteByQuery);
            log.info("删除成功:{}", jestResult.toString());
        } catch (IOException e) {
            log.error("删除有误:{}", e.getMessage());
        }
    }
}

我简单给大家总结一下查询步骤

① 自动注入JestClient 客户端
	@Autowired
    private JestClient jestClient;
② 初始化查询载体:SearchSourceBuilder
	SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
	//看你的需求需不需要分页 from:页数,size:每页最大数量,sort:根据哪个字段进行倒序还是正序
	searchSourceBuilder.from(1).size(10)
					// SortOrder.DESC:倒序,SortOrder.ASC:正序
	                .sort("要排序的字段名", SortOrder.ASC) 
	                //设置返回全部数量,对ES客户端为7.x以上的,不生效,需要转为JSON手动把该参数拼写进去
	                .trackTotalHits(true);
③ 初始化查询Buillder:BoolQueryBuilder,并组装查询条件
	SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
	//termQuery精准查询
	boolQueryBuilder.filter(QueryBuilders.termQuery("username.keyword", "美女"));
	//termsQuery一个字段精准匹配多个值
	String[] s = {"肤白","貌美","大长腿"};
	boolQueryBuilder.filter(QueryBuilders.termsQuery("tags", s));
	//范围查询,如粉丝数在1000-10000之间的
	boolQueryBuilder.filter(QueryBuilders.rangeQuery("fans").from(1000));
	boolQueryBuilder.filter(QueryBuilders.rangeQuery("fans").to(10000));
	//范围查询,如价格在0-10之间的,这里注意一下,ES中不支持bigDecimal小数类型,所以针对是小数的范围查询,一定要转为字符串
	BigDecimal lowPrice = BigDecimal.ZERO;
	BigDecimal highPrice = BigDecimal.TEN;
	boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").from(lowPrice + ""));
	boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").to(highPrice + ""));
	//模糊查询,要用**括住
	boolQueryBuilder.filter(QueryBuilders.wildcardQuery("area.keyword", "*濮阳*"));
④ 执行查询
	searchSourceBuilder.query(boolQueryBuilder);
	Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex("索引名称").build();
	//打印查询体语句
	System.out.println(searchSourceBuilder.toString());
	SearchResult result = null;
	try {
	    result = jestClient.execute(search);
	} catch (IOException e) {
	    log.error(e.getMessage());
	}
	//BiliMapping为实体类,返回结果转为对应的实体类
	List<SearchResult.Hit<BiliMapping, Void>> hits = result.getHits(BiliMapping.class);
	List<BiliMapping> list = new ArrayList<>();
	//把结果输出到实体类list中
	hits.forEach(hit -> list.add(hit.source));

4.插入代码

代码如下(示例):

① 自动注入JestClient 客户端
	@Autowired
    private JestClient jestClient;
② 初始化Builder并设置索引名称
	Bulk.Builder bulk = new Bulk.Builder().defaultIndex("索引名");
③ 填充数据
	BiliPerson biliPerson = new BiliPerson();
	biliPerson.setId("123456789");
	biliPerson.setAccountNo("asdcdfada");
	biliPerson.setAccountName("叫我红领巾");
	Index index = new Index.Builder(biliPerson).id(biliPerson.getId()).build();
	bulk.addAction(index);
④ 同步
	//同步数据至ES
	BulkResult bulkResult = jestClient.execute(bulk.build());
	if (!bulkResult.isSucceeded()) {
	    log.error(bulkResult.getErrorMessage());
	    throw new BusinessException("同步数据至ES失败");
	}

同步数据时,注意要指定一个ID,指定ID的方式有两种
①Index index = new Index.Builder(biliPerson).id(“具体的ID”).build();
②在实体类的字段上加注解@JestId

5.删除代码(这里使用的是DeleteByQuery)

代码如下(示例):

① 自动注入JestClient 客户端
	@Autowired
    private JestClient jestClient;
② 初始化查询载体:SearchSourceBuilder,初始化查询Buillder:BoolQueryBuilder,并组装查询条件
	SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
    boolQueryBuilder.must(QueryBuilders.matchQuery("brands.keyword", keyword));
    searchSourceBuilder.query(boolQueryBuilder);
    searchSourceBuilder.size(100000);
③ 执行删除
	DeleteByQuery deleteByQuery = new DeleteByQuery.Builder(this.putTrackTotalHits(searchSourceBuilder)).addIndex("索引名称").build();
     //删除ES
     JestResult jestResult;
     try {
         jestResult = jestClient.execute(deleteByQuery);
         log.info("删除成功:{}", jestResult.toString());
     } catch (IOException e) {
         log.error("删除有误:{}", e.getMessage());
     }

三、实战之RestHighLevelClient

1.依赖(maven)

spring boot版本:2.4.4

elasticsearch依赖,不指定版本,默认为boot对应的版本,这里对应的版本是7.9.3

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

2.配置文件

基础配置

从上到下分别填写ES的连接信息包括:url,username,password

3.示例代码

创建一个接口 ClientService

package com.elasticsearch.demo.service;

import com.elasticsearch.demo.entity.ElasticsearchCondition;
import com.elasticsearch.demo.entity.WeiboPersoninfo;
import org.elasticsearch.search.builder.SearchSourceBuilder;

import java.io.IOException;

/**
 * @author WD
 * @date 2021/4/14 0014 下午 16:33
 * @description
 */
public interface ClientService {
    /**
     * 同步到ES
     * @param weiboPersoninfo
     */
    void sync(WeiboPersoninfo weiboPersoninfo);
    /**
     * 组装分页
     * @param elasticsearchCondition
     * @return
     */
    SearchSourceBuilder getPageSearchSourceBuilder(ElasticsearchCondition elasticsearchCondition);

    /**
     * 根据条件查询
     * @param elasticsearchCondition
     * @throws IOException
     */
    void search(ElasticsearchCondition elasticsearchCondition) throws IOException;

    /**
     * 根据条件查询删除
     * @param elasticsearchCondition
     * @throws IOException
     */
    void deleteByQuery(ElasticsearchCondition elasticsearchCondition) throws IOException;
}

创建一个接口实现类 ClientServiceImpl

package com.elasticsearch.demo.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.elasticsearch.demo.entity.ElasticsearchCondition;
import com.elasticsearch.demo.entity.WeiboPersoninfo;
import com.elasticsearch.demo.mapping.DyMapping;
import com.elasticsearch.demo.service.ClientService;
import org.apache.commons.lang3.StringUtils;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.index.query.BoolQueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.index.reindex.DeleteByQueryRequest;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.io.IOException;

/**
 * @author WD
 * @date 2021/4/14 0014 下午 16:34
 * @description
 */
@Service
public class ClientServiceImpl implements ClientService {

    @Autowired
    private RestHighLevelClient restHighLevelClient;
    @Value("${elasticsearch.index.kol-account-dy}")
    private String dyAccountIndexName;
    @Value("${elasticsearch.index.kol-account-xhs}")
    private String xhsAccountIndexName;
    @Value("${elasticsearch.index.kol-account-wb}")
    private String wbAccountIndexName;

    @Override
    public void sync(WeiboPersoninfo weiboPersoninfo) {
        String s = JSON.toJSONString(weiboPersoninfo);
        IndexRequest indexRequest = new IndexRequest(wbAccountIndexName);
        indexRequest.source(s, XContentType.JSON);
        IndexResponse response;
        try {
            response = restHighLevelClient.index(indexRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }

    }

    /**
     * 组装基础分页
     *
     * @param elasticsearchCondition es查询条件实体类
     * @return
     */
    @Override
    public SearchSourceBuilder getPageSearchSourceBuilder(ElasticsearchCondition elasticsearchCondition) {
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().trackTotalHits(true);
        if (null != elasticsearchCondition.getFrom()) {
            searchSourceBuilder.from(elasticsearchCondition.getFrom());
        }
        if (null != elasticsearchCondition.getSize()) {
            searchSourceBuilder.size(elasticsearchCondition.getSize());
        }
        if (null != elasticsearchCondition.getOrderBy()) {
            searchSourceBuilder.sort(elasticsearchCondition.getOrderBy());
        }
        if (null != elasticsearchCondition.getOrderBy()) {
            searchSourceBuilder.sort(elasticsearchCondition.getOrderBy(),elasticsearchCondition.getAsc() ? SortOrder.ASC : SortOrder.DESC);
        }
        return searchSourceBuilder;
    }

    @Override
    public void search(ElasticsearchCondition elasticsearchCondition) throws IOException {
        SearchRequest searchRequest = new SearchRequest();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //指定索引名称
        searchRequest.indices(dyAccountIndexName);
        //声明searchSourceBuilder,这里是通过一个自定义的分页方法,生成的,不需要分页的话,直接new初始化一个即可
        SearchSourceBuilder searchSourceBuilder = this.getPageSearchSourceBuilder(elasticsearchCondition);
        //组装查询条件
        if (StringUtils.isNotBlank(elasticsearchCondition.getKeyword())) {
            BoolQueryBuilder boolQueryBuilderWildcard = QueryBuilders.boolQuery();
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("accountName.keyword","*" + elasticsearchCondition.getKeyword() + "*"));
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("supplier.keyword","*" + elasticsearchCondition.getKeyword() + "*"));
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("accountNo.keyword","*" + elasticsearchCondition.getKeyword() + "*"));
            boolQueryBuilder.filter(boolQueryBuilderWildcard);
        }
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        this.analysisSearchResponse(searchResponse);
        System.out.println(searchRequest.source().toString());
        System.out.println(searchResponse.toString());
    }

    @Override
    public void deleteByQuery(ElasticsearchCondition elasticsearchCondition) throws IOException {
        DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest("kol_dy_videoinfo");
        deleteByQueryRequest.setQuery(new TermQueryBuilder("brands.keyword","智利食品"));
        restHighLevelClient.deleteByQuery(deleteByQueryRequest, RequestOptions.DEFAULT);
    }

    public void analysisSearchResponse(SearchResponse searchResponse){
        SearchHits  searchHits = searchResponse.getHits();
        long total = searchHits.getTotalHits().value;
        for (SearchHit hit : searchHits.getHits()) {
            String sourceAsString = hit.getSourceAsString();
            DyMapping dyMapping = JSON.toJavaObject(JSONObject.parseObject(sourceAsString),DyMapping.class);
            System.out.println(dyMapping.getAllTag());
        }
    }
}

我简单给大家总结一下查询步骤

① 自动注入RestHighLevelClient客户端
	@Autowired
    private RestHighLevelClient restHighLevelClient;
② 初始化查询载体:SearchRequest,及拼装查询语句,再执行
		SearchRequest searchRequest = new SearchRequest();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
		//指定索引名称
        searchRequest.indices("索引名");
        //声明searchSourceBuilder
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder().trackTotalHits(true);
        //组装查询条件
        if (StringUtils.isNotBlank(elasticsearchCondition.getKeyword())) {
            BoolQueryBuilder boolQueryBuilderWildcard = QueryBuilders.boolQuery();
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("accountName.keyword","*" + "肤白" + "*"));
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("supplier.keyword","*" + "貌美"+ "*"));
            boolQueryBuilderWildcard.should(QueryBuilders.wildcardQuery("accountNo.keyword","*" + "大长腿" + "*"));
            boolQueryBuilder.filter(boolQueryBuilderWildcard);
        }
        searchSourceBuilder.query(boolQueryBuilder);
        searchRequest.source(searchSourceBuilder);
        //执行
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        //解析查询的返回结果
        SearchHits  searchHits = searchResponse.getHits();
        //返回数据总数
        long total = searchHits.getTotalHits().value;
        //循环解析返回结果,这里用fastjson转换即可
        for (SearchHit hit : searchHits.getHits()) {
            String sourceAsString = hit.getSourceAsString();
            DyMapping dyMapping = JSON.toJavaObject(JSONObject.parseObject(sourceAsString),DyMapping.class);
            System.out.println(dyMapping.getAllTag());
        }

4.写入数据代码

代码如下(示例):

① 自动注入RestHighLevelClient 客户端
	@Autowired
    private RestHighLevelClient restHighLevelClient;
② 初始化并同步
		//填充数据
		WeiboPersoninfo weiboPersoninfo = new WeiboPersoninfo();
        weiboPersoninfo.setUsername("叫我红领巾");
        weiboPersoninfo.setArea("河南·濮阳");
        //转为json字符串
		String s = JSON.toJSONString(weiboPersoninfo);
        IndexRequest indexRequest = new IndexRequest("索引名称");
        indexRequest.source(s, XContentType.JSON);
        IndexResponse response;
        try {
            response = restHighLevelClient.index(indexRequest,RequestOptions.DEFAULT);
        } catch (IOException e) {
            e.printStackTrace();
        }

5.删除数据代码(DeleteByQuery)

代码如下(示例):

① 自动注入RestHighLevelClient客户端
	@Autowired
    private RestHighLevelClient restHighLevelClient;
② 初始化查询载体:DeleteByQueryRequest,并组装查询条件
	DeleteByQueryRequest deleteByQueryRequest = new DeleteByQueryRequest("kol_dy_videoinfo");
   	deleteByQueryRequest.setQuery(new TermQueryBuilder("brands.keyword","智利食品"));
③ 执行删除
	restHighLevelClient.deleteByQuery(deleteByQueryRequest, RequestOptions.DEFAULT);

四、实战之ElasticsearchRepository

1.spring boot版本及ES的依赖同上(和二、实战之RestHighLevelClient中的配置一样)

2.配置文件同上(和二、实战之RestHighLevelClient中的配置一样)

3.示例代码

创建一个接口 XhsElasticsearchService ,继承ElasticsearchRepository

package com.elasticsearch.demo.service;

import com.elasticsearch.demo.entity.XhsNoteinfo;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;
import java.util.List;

/**
 * @author WD
 * @date 2021/4/12 0012 上午 11:16
 * @description
 */
@Repository
public interface XhsElasticsearchService extends ElasticsearchRepository<XhsNoteinfo,String> {

    List<XhsNoteinfo> findByKeyword(String keyword);

    void deleteByBrandsContains(String[] brands);
}

ElasticsearchRepository比较简单,遵循JPA框架,我在这里不再赘述,感兴趣的同学可以去网上查询资料


总结

这一大章为spring boot集成ES的教程,因为时间紧可能有部分写的不太严谨,有时间的话我会完善好,希望大家能找我多多交流,我们一起进步

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值