Springboot整合Elasticsearch

一、安装es
* 下载:https://www.elastic.co/cn/downloads/elasticsearch
* 启动:cmd下进入安装目录中的bin中 输入 elasticsearch.bat 运行
* 浏览器输入:http://127.0.0.1:9200/ 查看是否安装成功,出现下图表示安装成功
在这里插入图片描述
*当在服务器安装es时需要在es的配置文件中修改相应内容,如下图:

将ip改为0.0.0.0,方便外网访问:
network.host: 0.0.0.0
将node-2节点删除:
cluster.initial_master_nodes: ["node-1"]

二、安装ik分词器

  • 安装:
    • 1.下载与elasticsearch同版本号的ik安装包,
    • 2.在elasticsearch安装包中的plugins目录新建文件夹ik
    • 3.将解压好的文件中的内容复制到elasticsearch安装包中的plugins目录
    • 注意:ik分词器的版本要同 elasticsearch 的版本号一致,否则会启动失败

三、安装kibana

  • 安装:下载相应安装包解压
    • 启动:打开解压文件下的bin目录,单击kibana.bat 运行
    • 浏览器输入:http://127.0.0.1:5601/ 查看是否安装成功
    • 当在服务器安装kibana时需要在kibana的配置文件中修改相应内容:
server.host: "0.0.0.0"

四、springboot整合es
1、pom文件引入相关jar包

 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-web</artifactId>
 </dependency>
 <dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-webflux</artifactId>
 </dependency>
 <dependency>
     <groupId>com.alibaba</groupId>
     <artifactId>fastjson</artifactId>
     <version>${fastjson.version}</version>
 </dependency>

2、yml配置

spring:
 elasticsearch:
    rest:
      uris: localhost:9200

3、代码编写
3.1 封装分页

public class Page<T> extends SimplePage implements java.io.Serializable,
		Paginable {

	public Page() {
	}
	
	public Page(int pageNo, int pageSize, int totalCount) {
		super(pageNo, pageSize, totalCount);
	}

	@SuppressWarnings({ "unchecked", "rawtypes" })
	public Page(int pageNo, int pageSize, int totalCount, List list) {
		super(pageNo, pageSize, totalCount);
		this.list = list;
	}

	public int getFirstResult() {
		return (pageNo - 1) * pageSize;
	}

	/**
	 * 当前页的数据
	 */
	private List<T> list;

	public List<T> getList() {
		return list;
	}

	public void setList(List<T> list) {
		this.list = list;
	}
}
/**
 * 分页实体
 */
public interface Paginable {
		/**
		 * 总记录数
		 * @return
		 */
		public int getTotalCount();

		/**
		 * 总页数
		 * @return
		 */
		public int getTotalPage();

		/**
		 * 每页记录数
		 * @return
		 */
		public int getPageSize();

		/**
		 * 当前页号
		 * @return
		 */
		public int getPageNo();

		/**
		 * 是否第一页
		 * @return
		 */
		public boolean isFirstPage();

		/**
		 * 是否最后一页
		 * @return
		 */
		public boolean isLastPage();

		/**
		 * 返回下页的页号
		 */
		public int getNextPage();

		/**
		 * 返回上页的页号
		 */
		public int getPrePage();
	}
public class SimplePage implements Paginable {
	private static final long serialVersionUID = 1L;
	public static final int DEF_COUNT = 20;
	public SimplePage() {
	}
	public SimplePage(int pageNo, int pageSize, int totalCount) {
		if (totalCount <= 0) {
			this.totalCount = 0;
		} else {
			this.totalCount = totalCount;
		}
		if (pageSize <= 0) {
			this.pageSize = DEF_COUNT;
		} else {
			this.pageSize = pageSize;
		}
		if (pageNo <= 0) {
			this.pageNo = 1;
		} else {
			this.pageNo = pageNo;
		}
		if ((this.pageNo - 1) * this.pageSize >= totalCount) {
			this.pageNo = totalCount / pageSize;
			if(this.pageNo==0){
				this.pageNo = 1 ;
			}
		}
	}
	/**
	 * 调整分页参数,使合理化
	 */
	public void adjustPage() {
		if (totalCount <= 0) {
			totalCount = 0;
		}
		if (pageSize <= 0) {
			pageSize = DEF_COUNT;
		}
		if (pageNo <= 0) {
			pageNo = 1;
		}
		if ((pageNo - 1) * pageSize >= totalCount) {
			pageNo = totalCount / pageSize;
		}
	}
	public int getPageNo() {
		return pageNo;
	}
	public int getPageSize() {
		return pageSize;
	}
	public int getTotalCount() {
		return totalCount;
	}
	public int getTotalPage() {
		int totalPage = totalCount / pageSize;
		if (totalCount % pageSize != 0 || totalPage == 0) {
			totalPage++;
		}
		return totalPage;
	}
	public boolean isFirstPage() {
		return pageNo <= 1;
	}
	public boolean isLastPage() {
		return pageNo >= getTotalPage();
	}
	public int getNextPage() {
		if (isLastPage()) {
			return pageNo;
		} else {
			return pageNo + 1;
		}
	}
	public int getPrePage() {
		if (isFirstPage()) {
			return pageNo;
		} else {
			return pageNo - 1;
		}
	}
	protected int totalCount = 0;
	protected int pageSize = 20;
	protected int pageNo = 1;

	public void setTotalCount(int totalCount) {
		this.totalCount = totalCount;
	}
	public void setPageSize(int pageSize) {
		this.pageSize = pageSize;
	}
	public void setPageNo(int pageNo) {
		this.pageNo = pageNo;
	}
	protected int filterNo;

	public int getFilterNo() {
		return filterNo;
	}
	public void setFilterNo(int filterNo) {
		this.filterNo = filterNo;
	}
}

3.2 创建实体类

@Document(indexName = "book")
@Mapping(mappingPath = "productIndex.json") // 解决IK分词不能使用问题
@Data
public class Book implements Serializable {

    @Id
    private String id;

    @Field(analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")
    private String bookName;

    private String author;

    @Field(analyzer = "ik_max_word",searchAnalyzer = "ik_max_word")
    private String introduction;

    private String createTime;

    private String updateTime;
}

productIndex.json 该文件放在resources目录下

{
  "properties": {
    "createTime": {
      "type": "long"
    },
    "productDesc": {
      "type": "text",
      "analyzer": "ik_max_word",
      "search_analyzer": "ik_max_word"
    },
    "productName": {
      "type": "text",
      "analyzer": "ik_max_word",
      "search_analyzer": "ik_max_word"
    },
    "updateTime": {
      "type": "long"
    }
  }
}

3.3 创建 BookRespository

package com.cisdi.es.Repository;

import com.cisdi.es.domain.Book;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Component;

@Component
public interface BookRespository extends ElasticsearchRepository<Book,String> {
}

3.4 BookEsSearchService

package com.cisdi.es.service;

import com.cisdi.es.domain.Book;
import java.util.List;
public interface BookEsSearchService extends BookBaseSearchService<Book> {

    /**
     * 保存
     */
    void save(Book... books);

    /**
     * 删除
     * @param id
     */
    void delete(String id);

    /**
     * 清空索引
     */
    void deleteAll();

    /**
     * 根据ID查询
     * @param id
     * @return
     */
    Book getById(String id);

    /**
     * 查询全部
     * @return
     */
    List<Book> getAll();
}

3.5 BookBaseSearchService

package com.cisdi.es.service;

import com.cisdi.es.page.Page;
import java.io.IOException;
import java.util.List;
import java.util.Map;

public interface BookBaseSearchService<T> {

    /**
     * 搜 索
     * @param keyword 关键字
     * @param clazz 实体
     * @param indexName 索引库名称
     * @return
     */
    List<T> query(String keyword, String indexName, Class<T> clazz,String... fieldNames) throws IOException;

    /**
     * 搜索高亮显示
     * @param keyword       关键字
     * @param clazz     索引库
     * @param indexName 索引库名称
     * @param startTime     开始时间
     * @param endTime     结束时间
     * @param fieldNames    搜索的字段
     * @return
     */
    List<Map<String,Object>> queryHit(String keyword,String indexName, Class<T> clazz, String startTime,String endTime, String ... fieldNames)throws IOException;

    /**
     * 搜索高亮显示,返回分页
     * @param pageNo        当前页
     * @param pageSize      每页显示的总条数
     * @param keyword       关键字
     * @param clazz     索引库
     * @param startTime     开始时间
     * @param endTime     结束时间
     * @param fieldNames    搜索的字段
     * @return
     */
    Page<Map<String, Object>> queryHitByPage(int pageNo, int pageSize, String keyword,String indexName, Class<T> clazz,String startTime,String endTime, String ... fieldNames)throws IOException;

    /**
     * 删除索引库
     * @param indexName
     * @return
     */
    void deleteIndex(String indexName)throws IOException;
}

3.6 实现类

package com.cisdi.es.service.impl;

import com.alibaba.fastjson.JSON;
import com.cisdi.es.Repository.BookRespository;
import com.cisdi.es.domain.Book;
import com.cisdi.es.service.BookEsSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

@Service
public class BookEsSearchServiceImpl extends BookBaseSearchServiceImpl<Book> implements BookEsSearchService {

    private Logger log = LoggerFactory.getLogger(getClass());

    @Resource
    private BookRespository bookRespository;

    @Override
    public void save(Book... books) {
        if(books.length>0){
            log.info("【保存索引】:{}", JSON.toJSONString(bookRespository.saveAll(Arrays.asList(books))));
        }
    }

    @Override
    public void delete(String id) {
        bookRespository.deleteById(id);
    }

    @Override
    public void deleteAll() {
        bookRespository.deleteAll();
    }

    @Override
    public Book getById(String id) {
        return bookRespository.findById(id).get();
    }

    @Override
    public List<Book> getAll() {
        List<Book> list = new ArrayList<>();
        bookRespository.findAll().forEach(list::add);
        return list;
    }
}

package com.cisdi.es.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.cisdi.es.page.Page;
import com.cisdi.es.service.BookBaseSearchService;
import org.elasticsearch.action.delete.DeleteRequest;
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.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.sort.SortBuilders;
import org.elasticsearch.search.sort.SortOrder;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.*;

public class BookBaseSearchServiceImpl <T> implements BookBaseSearchService<T> {

    private Logger log = LoggerFactory.getLogger(getClass());

    @Resource
    private RestHighLevelClient restHighLevelClient;

    @Override
    public List<T> query(String keyword,String indexName, Class<T> clazz,String... fieldNames)throws IOException {
        SearchRequest searchRequest = new SearchRequest(indexName);//discusspost是索引名,就是表名
        //构建搜索条件
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder()
                //在discusspost索引的title和content字段中都查询“互联网寒冬”
                .query(QueryBuilders.multiMatchQuery(keyword, fieldNames))
                // matchQuery是模糊查询,会对key进行分词:searchSourceBuilder.query(QueryBuilders.matchQuery(key,value));
                // termQuery是精准查询:searchSourceBuilder.query(QueryBuilders.termQuery(key,value));
                .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC));
                //一个可选项,用于控制允许搜索的时间:searchSourceBuilder.timeout(new TimeValue(60, TimeUnit.SECONDS));
                //.from(0)// 指定从哪条开始查询
                //.size(10);// 需要查出的总记录条数

        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        log.info("【检索数据】:{}",JSONObject.toJSON(searchResponse));

        List<T> list = new LinkedList<>();
        for (SearchHit hit : searchResponse.getHits().getHits()) {
            T discussPost = JSONObject.parseObject(hit.getSourceAsString(), clazz);
            list.add(discussPost);
        }
        return list;
    }

    @Override
    public List<Map<String,Object>> queryHit(String keyword,String indexName, Class<T> clazz, String startTime,String endTime,String... fieldNames)throws IOException {
        //discusspost是索引名,就是表名
        SearchRequest searchRequest = new SearchRequest(indexName);
        HighlightBuilder highlightBuilder =createHighlightBuilder(fieldNames);
        SearchSourceBuilder searchSourceBuilder=null;
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        //时间范围的设定
        RangeQueryBuilder rangequerybuilder = QueryBuilders
                .rangeQuery("createTime")
                .from(startTime).to(endTime);
        if(keyword.equals(null)||keyword.equals("")){
            //构建搜索条件
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
            boolQueryBuilder.must(rangequerybuilder);
            searchSourceBuilder = new SearchSourceBuilder()
                    .query(boolQueryBuilder)
                    .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                    .highlighter(highlightBuilder);
        }else{
            //构建搜索条件
            boolQueryBuilder.must(QueryBuilders.multiMatchQuery(keyword, fieldNames));
            boolQueryBuilder.must(rangequerybuilder);
            searchSourceBuilder = new SearchSourceBuilder()
                    .query(boolQueryBuilder)
                    .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                    .highlighter(highlightBuilder);
        }
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        log.info("【检索数据】:{}",JSONObject.toJSON(searchResponse));

        List<Map<String,Object>> lists = getHitList(searchResponse.getHits());
        return lists;
    }

    @Override
    public Page<Map<String, Object>> queryHitByPage(int pageNo, int pageSize, String keyword, String indexName,Class<T> clazz, String startTime,String endTime,
                                                    String... fieldNames)throws IOException {
        Page<Map<String, Object>> page = null;
        //discusspost是索引名,就是表名
        SearchRequest searchRequest = new SearchRequest(indexName);
        HighlightBuilder highlightBuilder =createHighlightBuilder(fieldNames);
        SearchSourceBuilder searchSourceBuilder=null;
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        //时间范围的设定
        RangeQueryBuilder rangequerybuilder = QueryBuilders
                .rangeQuery("createTime")
                .from(startTime).to(endTime);

        if(keyword.equals(null)||keyword.equals("")){
            //构建搜索条件
            boolQueryBuilder.must(QueryBuilders.matchAllQuery());
            boolQueryBuilder.must(rangequerybuilder);
            searchSourceBuilder = new SearchSourceBuilder()
                    .query(boolQueryBuilder)
                    .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                    .highlighter(highlightBuilder)
                    .from((pageNo-1)*pageSize)// 指定从哪条开始查询
                    .size(pageSize);// 需要查出的总记录条数
        }else{
            //构建搜索条件
            boolQueryBuilder.must(QueryBuilders.multiMatchQuery(keyword, fieldNames));
            boolQueryBuilder.must(rangequerybuilder);
            searchSourceBuilder = new SearchSourceBuilder()
                    .query(boolQueryBuilder)
                    .sort(SortBuilders.fieldSort("createTime").order(SortOrder.DESC))
                    .highlighter(highlightBuilder)
                    .from((pageNo-1)*pageSize)// 指定从哪条开始查询
                    .size(pageSize);// 需要查出的总记录条数
        }
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        log.info("【检索数据】:{}",JSONObject.toJSON(searchResponse));

        Long totalCount =searchResponse.getHits().getTotalHits().value;
        page = new Page<>(pageNo,pageSize,totalCount.intValue());
        page.setList(getHitList(searchResponse.getHits()));

        return page;
    }
    @Override
    public void deleteIndex(String indexName) throws IOException{
        DeleteRequest deleteRequest = new DeleteRequest(indexName);
        restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
        restHighLevelClient.close();
    }
    /**
     * 构造高亮器
     */
    private HighlightBuilder createHighlightBuilder(String... fieldNames){
        // 设置高亮,使用默认的highlighter高亮器
        HighlightBuilder highlightBuilder = new HighlightBuilder()
                // .field("productName")
                .preTags("<span style='color:red'>")
                .postTags("</span>");

        // 设置高亮字段
        for (String fieldName: fieldNames) highlightBuilder.field(fieldName);

        return highlightBuilder;
    }
    /**
     * 处理高亮结果
     */
    private List<Map<String,Object>> getHitList(SearchHits hits){
        List<Map<String,Object>> list = new ArrayList<>();
        Map<String,Object> map;
        for(SearchHit searchHit : hits){
            map = new HashMap<>();
            // 处理源数据
            map.put("source",searchHit.getSourceAsMap());
            // 处理高亮数据
            Map<String,Object> hitMap = new HashMap<>();
            searchHit.getHighlightFields().forEach((k,v) -> {
                String hight = "";
                for(Text text : v.getFragments()) hight += text.string();
                hitMap.put(v.getName(),hight);
            });
            map.put("highlight",hitMap);
            list.add(map);
        }
        return list;
    }


}

3.7 controller接口

package com.cisdi.es.controller;

import com.cisdi.es.domain.Book;
import com.cisdi.es.page.Page;
import com.cisdi.es.service.BookEsSearchService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.io.IOException;
import java.util.List;
import java.util.Map;

/**
 * elasticsearch 搜索
 */
@RestController
public class EsSearchController {
    private Logger log = LoggerFactory.getLogger(getClass());

    @Resource
    private BookEsSearchService bookEsSearchService;

    /**
     * 新增 / 修改索引
     * @return
     */
    @PostMapping("save")
    public String add(@RequestBody Book... book) {
        bookEsSearchService.save(book);
        return "success";
    }

    /**
     *
     * 删除索引
     * @return
     */
    @DeleteMapping("delete/{id}")
    public String delete(@PathVariable String id) {
        bookEsSearchService.delete(id);
        return "success";
    }
    /**
     * 清空索引
     * @return
     */
    @DeleteMapping("delete_all")
    public String deleteAll() {
        bookEsSearchService.deleteAll();
        return "success";
    }

    /**
     * 根据ID获取
     * @return
     */
    @GetMapping("get/{id}")
    public Book getById(@PathVariable String id){
        return bookEsSearchService.getById(id);
    }

    /**
     * 根据获取全部
     * @return
     */
    @GetMapping("get_all")
    public List<Book> getAll(){
        return bookEsSearchService.getAll();
    }

    /**
     * 根据关键字不分页查询
     * @param keyword
     * @param fields    搜索字段名称,多个以“,”分割
     * @return
     */
    @GetMapping("query")
    public List<Book> query(@RequestParam String keyword, @RequestParam String fields) throws IOException {
        String indexName = "book";
        String[] fieldNames = {};
        if(fields.contains(",")) fieldNames = fields.split(",");
        else fieldNames[0] = fields;
        return bookEsSearchService.query(keyword,indexName,Book.class,fieldNames);
    }

    /**
     * 搜索,命中关键字高亮
     * @param keyword   关键字
     * @param fields    搜索字段名称,多个以“,”分割
     * @return
     */
    @GetMapping("query_hit")
    public List<Map<String,Object>> queryHit(@RequestParam String keyword, @RequestParam String fields ,@RequestParam String startTime,@RequestParam String endTime)throws IOException {
        String indexName = "book";
        String[] fieldNames = {};
        if(fields.contains(",")) fieldNames = fields.split(",");
        else fieldNames[0] = fields;
        return bookEsSearchService.queryHit(keyword,indexName,Book.class, startTime, endTime,fieldNames);
    }

    /**
     * 搜索,命中关键字高亮
     * @param pageNo    当前页
     * @param pageSize  每页显示的数据条数
     * @param keyword   关键字
     * @param fields    搜索字段名称,多个以“,”分割
     * @return
     */
    @GetMapping("query_hit_page")
    public Page<Map<String, Object>> queryHitByPage(@RequestParam int pageNo, @RequestParam int pageSize
                                                    , @RequestParam String keyword, @RequestParam String fields
                                                    ,@RequestParam String startTime,@RequestParam String endTime)throws IOException{
        String indexName = "book";
        String[] fieldNames = {};
        if(fields.contains(",")) fieldNames = fields.split(",");
        else fieldNames[0] = fields;
        return bookEsSearchService.queryHitByPage(pageNo,pageSize,keyword,indexName,Book.class,startTime,endTime,fieldNames);
    }

    /**
     * 删除索引库
     * @param indexName
     * @return
     */
    @DeleteMapping("delete_index/{indexName}")
    public String deleteIndex(@PathVariable String indexName)throws IOException{
        bookEsSearchService.deleteIndex(indexName);
        return "success";
    }
}

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
好的,下面是SpringBoot整合elasticsearch的步骤: 1. 引入elasticsearch和spring-boot-starter-data-elasticsearch的依赖: ``` <dependency> <groupId>org.elasticsearch</groupId> <artifactId>elasticsearch</artifactId> <version>7.12.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-elasticsearch</artifactId> <version>2.4.5</version> </dependency> ``` 2. 配置elasticsearch连接信息: ``` spring.data.elasticsearch.cluster-nodes=localhost:9200 ``` 3. 创建实体类: ``` @Document(indexName = "my_index") public class MyEntity { @Id private String id; private String name; // getter and setter } ``` 4. 创建es的Repository: ``` public interface MyRepository extends ElasticsearchRepository<MyEntity, String> { } ``` 5. 在service中使用Repository: ``` @Service public class MyService { @Autowired private MyRepository myRepository; public void save(MyEntity entity) { myRepository.save(entity); } public List<MyEntity> search(String name) { return myRepository.findByName(name); } } ``` 6. 在controller中调用service: ``` @RestController public class MyController { @Autowired private MyService myService; @PostMapping("/save") public void save(@RequestBody MyEntity entity) { myService.save(entity); } @GetMapping("/search") public List<MyEntity> search(@RequestParam String name) { return myService.search(name); } } ``` 这样就可以通过SpringBoot整合elasticsearch实现数据的增删改查了。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

@hhr

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

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

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

打赏作者

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

抵扣说明:

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

余额充值