关于QueryParser类前后修改

在升级Lucene版本至4.3并实现实时索引的过程中,发现搜索结果数量发生变化。研究发现,QueryParser在新旧版本间产生了不同的解析行为。旧版返回精确的短语匹配,而新版则拆分成独立关键词,导致更多搜索结果。这涉及到Lucene的查询打分机制和用户输入错误的容错性。哪种效果更优,需要通过大量数据测试来决定。


转载请注明出处:http://blog.csdn.net/xiaojimanman/article/details/16972661

    这几天在公司做一个项目,就是去将以前的老项目的lucene版本切换成4.3版本,并将以前的索引架构修改为实时索引(公司以前的那个项目是好几年前的了),为了和以前的版本兼容,第一步就是只是把lucene版本修改下,同时支持实时索引,至于所有的接口,都要和以前一样。在这中间就遇到了一个很大的问题,就是在出搜索结果的时候,修改后的版本要比以前的版本多很多搜索结果(上面要求要和原来的接口出的数据完全一样),函数也是用的以前的,为什么就出现不同的结果呢?

     最后通过读源代码,发现是QueryParser类在作怪,下面先看一下QueryParser在前后3.1前后两个版本中的效果:


 /**  
 *@Description:     
 */ 
package cn.lulei.test;  

import org.apache.lucene.analysis.standard.StandardAnalyzer;
import org.apache.lucene.queryparser.classic.ParseException;
import org.apache.lucene.queryparser.classic.QueryParser;
import org.apache.lucene.search.Query;
import org.apache.lucene.util.Version;
  
public class TestQueryParser {

	public static Query getOneFieldQuery43(String key, String field) throws ParseException{
		QueryParser parse = new QueryParser(Version.LUCENE_43, field, new StandardAnalyzer(Version.LUCENE_43));
		Query query = null;
		query = parse.parse(key);
		return query;
	}
	
	@SuppressWarnings("deprecation")
	public static Query getOneFieldQuery30(String key, String field) throws ParseException{
		QueryParser parse = new QueryParser(Version.LUCENE_30, field, new StandardAnalyzer(Version.LUCENE_43));
		Query query = null;
		query = parse.parse(key);
		return query;
	}
	
	public static void main(String[] args) throws ParseException {
		String key = "斗破苍穹";
		System.out.println(key + "30版本结果:" + TestQueryParser.getOneFieldQuery30(key, "key").toString());
		System.out.println(key + "43版本结果:" + TestQueryParser.getOneFieldQuery43(key, "key").toString());
	}
}

    上面这个测试用例的结果为:

斗破苍穹30版本结果:key:"斗 破 苍 穹"
斗破苍穹43版本结果:key:斗 key:破 key:苍 key:穹

    得出两个完全不同的Query对象,这也就是为什么会多出这么多的搜索结果的原因,下面就从源代码中探究其原因,下面的源代码均是通过反编译软件实现的。

先看下QueryParse的构造方法:

  public QueryParser(Version matchVersion, String f, Analyzer a)
  {
    this(new FastCharStream(new StringReader("")));
    init(matchVersion, f, a);
  }
     在以前的版本中是没有Version这个参数的,具体从那个版本开始加的,我也不是很清楚,自己也是最近才刚开始接触lucene的。

     在进一步追一下,看起父类QueryParserBase是如何实现的init(Version matchVersion, String f, Analyzer a)方法

  public void init(Version matchVersion, String f, Analyzer a)
  {
    this.analyzer = a;
    this.field = f;
    if (matchVersion.onOrAfter(Version.LUCENE_31))
      setAutoGeneratePhraseQueries(false);
    else
      setAutoGeneratePhraseQueries(true);
  }
   其中setAutoGeneratePhraseQueries(boolean)方法修改的是类属性allowLeadingWildcard的值,这样allowLeadingWildcard的值对parse(String)的实现就有决定性的作用。

public Query parse(String query) throws ParseException
  {
    ReInit(new FastCharStream(new StringReader(query)));
    ParseException e;
    try {
      Query res = TopLevelQuery(this.field);
      return res != null ? res : newBooleanQuery(false);
    }
    catch (ParseException tme)
    {
      ParseException e = new ParseException(new StringBuilder().append("Cannot parse '").append(query).append("': ").append(tme.getMessage()).toString());
      e.initCause(tme);
      throw e;
    }
    catch (TokenMgrError tme) {
      ParseException e = new ParseException(new StringBuilder().append("Cannot parse '").append(query).append("': ").append(tme.getMessage()).toString());
      e.initCause(tme);
      throw e;
    }
    catch (BooleanQuery.TooManyClauses tmc) {
      e = new ParseException(new StringBuilder().append("Cannot parse '").append(query).append("': too many boolean clauses").toString());
      e.initCause(tmc);
    }throw e;
  }
也正事因为属性allowLeadingWildcard的值,决定了了第一个测试实例的结果,从源代码中也可以看出来,3.1(包括)之前的版本和之后的版本出来两种完全不同的结果,为什么会做这样的修改,我猜测可能是因为下面的原因:

        key:"斗 破 苍 穹"   这种结果是能够准确的定位到用户搜索到的结果,而且也十分准确,但是如果用户某一个词输入错误,或者是错别字,那得到的就不是用户想要的结果,甚至就搜索不出来任何的东西。

       key:斗 key:破 key:苍 key:穹    这种结果虽然出现的结果会有很多,但是根据lucene的打分机制,“斗破苍穹”这条记录也出出现在第一条,即使用户输出了其中的某一个字,一还是可以搜索到用户想要的结果的。

      至于这两种,那个效果比较好,自己还需大量的数据去测试,不能就根据自己的意愿去认为




package com.boe.cim.teacher.luence; import java.io.StringReader; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.highlight.Fragmenter; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import com.boe.cim.teacher.pojo.RequirementInfo; public class LuceneSearchRequirement { /** * @param indexreDir * 索引文件路径path * @param queryField * 被索引字段 Field * @param queryMsg * 索引值 * @return * @throws Exception */ public List<RequirementInfo> search(String indexreDir, String queryField, String queryMsg) throws Exception { // 得到读取索引文件的路径 Directory dir = FSDirectory.open(Paths.get(indexreDir)); // 通过dir得到的路径下的所有的文件 // 建立索引查询器 IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); // 中文分词器 SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer(); // 建立查询解析器 /** * 第一个参数是要查询的字段; 第二个参数是分析器Analyzer */ // QueryParser parser = new QueryParser(queryField, analyzer); // 根据传进来的par查找 // Query query = parser.parse(queryMsg); // Query query = new TermQuery(new Term("teacher",queryMsg)); // Query query = new WildcardQuery(new Term(queryField,"*"+queryMsg+"*")); Query query; //这四种型需要特别匹配,不需要分词器进行搜索 if(queryField.equals("requirement") || queryField.equals("department") || queryField.equals("liaisonman") || queryField.equals("requirementtype") ) { query = new WildcardQuery(new Term(queryField,"*"+queryMsg+"*")); }else { QueryParser parser = new QueryParser(queryField, analyzer); query = parser.parse(queryMsg); } // 计算索引开始时间 long start = System.currentTimeMillis(); // 开始查询 /** * 第一个参数是通过传过来的参数来查找得到的query; 第二个参数是要出查询的行数 */ TopDocs topDocs = searcher.search(query, 104); // 索引结束时间 long end = System.currentTimeMillis(); System.out.println("匹配:["+queryField+"]," + queryMsg + ",总共花费了" + (end - start) + "毫秒,共查到" + topDocs.totalHits + "条记录。"); // 高亮显示start // 算分 QueryScorer scorer = new QueryScorer(query); // 显示得分高的片段 Fragmenter fragmenter = new SimpleSpanFragmenter(scorer); // 设置标签内部关键字的颜色 // 第一个参数:标签的前半部分;第二个参数:标签的后半部分。 SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>"); // 第一个参数是对查到的结果进行实例化;第二个是片段得分(显示得分高的片段,即摘要) Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer); // 设置片段 highlighter.setTextFragmenter(fragmenter); // 高亮显示end // 遍历topDocs /** * ScoreDoc:是代表一个结果的相关度得分与文档编号等信息的对象。 scoreDocs:代表文件的数组 * * @throws Exception */ List<RequirementInfo> listinfo = new ArrayList<>(); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { RequirementInfo requirementinfo = new RequirementInfo(); // 获取文档 Document document = searcher.doc(scoreDoc.doc); // 输出全路径 String id = document.get("id"); String queryType = document.get(queryField); requirementinfo.setId(Integer.parseInt(id)); if (id != null) { // 把全部得分高的摘要给显示出来 // 第一个参数是对哪个参数进行设置;第二个是以流的方式读入 TokenStream tokenStream = analyzer.tokenStream(queryField, new StringReader(queryType)); // 获取最高的片段 String highlighterString; if(queryField.equals("requirement") || queryField.equals("department") || queryField.equals("liaisonman") || queryField.equals("requirementtype") ) { highlighterString = queryType.replaceAll(queryMsg, "<b><font color='red'>"+queryMsg+"</font></b>"); }else { highlighterString = highlighter.getBestFragment(tokenStream, queryType); } // String highlighterString = highlighter.getBestFragment(new SmartChineseAnalyzer(), queryField, queryType); //设置高亮字段 switch (queryField) { // 根据搜索条件进行赋值 case "requirement": //需求名称 requirementinfo.setRequirement(highlighterString); break; case "department": //需求组织 requirementinfo.setDepartment(highlighterString); break; case "liaisonman": //需求联系人 requirementinfo.setLiaisonman(highlighterString); break; case "requirementtype": //需求型 requirementinfo.setRequirementtype(Integer.parseInt(highlighterString)); break; case "requirementcontents": //需求内容 requirementinfo.setRequirementcontents(highlighterString); break; case "requirementbackground": //需求背景 requirementinfo.setRequirementbackground(highlighterString); break; } listinfo.add(requirementinfo); } } reader.close(); return listinfo; } } 报错匹配:[requirementtype],1,总共花费了18毫秒,共查到10条记录。 java.lang.NumberFormatException: For input string: "<b><font color='red'>1</font></b>" at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65) at java.lang.Integer.parseInt(Integer.java:580) at java.lang.Integer.parseInt(Integer.java:615) at com.boe.cim.teacher.luence.LuceneSearchRequirement.search(LuceneSearchRequirement.java:131) at com.boe.cim.teacher.controller.RequirementInfoSelectFunctionController.simpleselect(RequirementInfoSelectFunctionController.java:38) at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:190) at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:138)
最新发布
09-09
package com.boe.cim.teacher.luence; import java.nio.file.Paths; import java.util.List; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.document.Field; import org.apache.lucene.document.IntField; import org.apache.lucene.document.StringField; import org.apache.lucene.document.TextField; import org.apache.lucene.index.IndexWriter; import org.apache.lucene.index.IndexWriterConfig; import org.apache.lucene.index.Term; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import com.boe.cim.teacher.pojo.RequirementInfo; /** * @author hyh * @description 创建文档索引 */ public class LuceneIndexRequirement { private Directory dir; /** *实例化indexerWriter * @return * @throws Exception */ private IndexWriter getWriter()throws Exception{ //中文分词器 SmartChineseAnalyzer analyzer=new SmartChineseAnalyzer(); IndexWriterConfig iwc=new IndexWriterConfig(analyzer); IndexWriter writer=new IndexWriter(dir, iwc); return writer; } /** * 获取indexreDir * @param indexreDir * @throws Exception */ // public void index(String indexreDir, List<RequirementInfo> listRequirement) throws Exception { // dir = FSDirectory.open(Paths.get(indexreDir)); // IndexWriterConfig iwc = new IndexWriterConfig(new SmartChineseAnalyzer()); // iwc.setOpenMode(IndexWriterConfig.OpenMode.CREATE_OR_APPEND); // 明确指定打开模式 // try (IndexWriter writer = new IndexWriter(dir, iwc)) { // for (RequirementInfo requirement : listRequirement) { // Document doc = new Document(); // doc.add(new StringField("id", String.valueOf(requirement.getId()), Field.Store.YES)); // doc.add(new StringField("requirement", requirement.getRequirement(), Field.Store.YES)); // doc.add(new StringField("department", requirement.getDepartment(), Field.Store.YES)); // doc.add(new StringField("liaisonman", requirement.getLiaisonman(), Field.Store.YES)); // doc.add(new StringField("requirementtype", Integer.toString(requirement.getRequirementtype()), Field.Store.YES)); // doc.add(new TextField("requirementbackground", requirement.getRequirementbackground(), Field.Store.YES)); // doc.add(new TextField("requirementcontents", requirement.getRequirementcontents(), Field.Store.YES)); // writer.updateDocument(new Term("id", String.valueOf(requirement.getId())), doc); // } // } // } public void index(String indexreDir,List<RequirementInfo> listRequirement)throws Exception{ dir=FSDirectory.open(Paths.get(indexreDir)); IndexWriter writer=getWriter(); // writer.commit(); for(int i=0;i<listRequirement.size();i++){ Document doc=new Document(); RequirementInfo requirement = listRequirement.get(i); //StringField 只索引不分词 doc.add(new StringField("id",String.valueOf(requirement.getId()), Field.Store.YES)); doc.add(new StringField("requirement", requirement.getRequirement(), Field.Store.YES)); doc.add(new StringField("department",requirement.getDepartment(),Field.Store.YES)); doc.add(new StringField("liaisonman", requirement.getLiaisonman(), Field.Store.YES)); doc.add(new StringField("requirementtype", Integer.toString(requirement.getRequirementtype()), Field.Store.YES)); doc.add(new StringField("technicalfield", requirement.getTechnicalfield(), Field.Store.YES)); doc.add(new TextField("requirementbackground", requirement.getRequirementbackground(), Field.Store.YES)); doc.add(new TextField("requirementcontents", requirement.getRequirementcontents(), Field.Store.YES)); // doc.add(new StringField("requirementtype", Integer.toString(requirement.getRequirementtype()), Field.Store.YES)); // doc.add(new TextField("requirementbackground", requirement.getRequirementbackground(), Field.Store.YES)); // doc.add(new TextField("requirementcontents", requirement.getRequirementcontents(), Field.Store.YES)); // writer.deleteDocuments(new Term("id",String.valueOf(teacher.getId()))); writer.updateDocument(new Term("id",String.valueOf(requirement.getId())), doc); } writer.close(); } // public void indexSingleRequirement(String indexreDir, RequirementInfo requirement) throws Exception { // dir = FSDirectory.open(Paths.get(indexreDir)); // try (IndexWriter writer = getWriter()) { // Document doc = new Document(); // doc.add(new StringField("id",String.valueOf(requirement.getId()), Field.Store.YES)); // doc.add(new StringField("requirement", requirement.getRequirement(), Field.Store.YES)); // doc.add(new StringField("department",requirement.getDepartment(),Field.Store.YES)); // doc.add(new StringField("liaisonman", requirement.getLiaisonman(), Field.Store.YES)); // doc.add(new StringField("requirementtype", Integer.toString(requirement.getRequirementtype()), Field.Store.YES)); // doc.add(new StringField("technicalfield", requirement.getTechnicalfield(), Field.Store.YES)); // doc.add(new TextField("requirementbackground", requirement.getRequirementbackground(), Field.Store.YES)); // doc.add(new TextField("requirementcontents", requirement.getRequirementcontents(), Field.Store.YES)); // // 添加其他字段... // writer.updateDocument(new Term("id", String.valueOf(requirement.getId())), doc); // } // try-with-resources 自动关闭writer // } } 后端代码生成requirementtype索引 doc.add(new StringField("requirementtype", Integer.toString(requirement.getRequirementtype()), Field.Store.YES)); 检索代码为: package com.boe.cim.teacher.luence; import java.io.StringReader; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import org.apache.lucene.analysis.TokenStream; import org.apache.lucene.analysis.cn.smart.SmartChineseAnalyzer; import org.apache.lucene.document.Document; import org.apache.lucene.index.DirectoryReader; import org.apache.lucene.index.IndexReader; import org.apache.lucene.index.Term; import org.apache.lucene.queryparser.classic.QueryParser; import org.apache.lucene.search.IndexSearcher; import org.apache.lucene.search.Query; import org.apache.lucene.search.ScoreDoc; import org.apache.lucene.search.TopDocs; import org.apache.lucene.search.WildcardQuery; import org.apache.lucene.search.highlight.Fragmenter; import org.apache.lucene.search.highlight.Highlighter; import org.apache.lucene.search.highlight.QueryScorer; import org.apache.lucene.search.highlight.SimpleHTMLFormatter; import org.apache.lucene.search.highlight.SimpleSpanFragmenter; import org.apache.lucene.store.Directory; import org.apache.lucene.store.FSDirectory; import com.boe.cim.teacher.pojo.RequirementInfo; public class LuceneSearchRequirement { /** * @param indexreDir * 索引文件路径path * @param queryField * 被索引字段 Field * @param queryMsg * 索引值 * @return * @throws Exception */ public List<RequirementInfo> search(String indexreDir, String queryField, String queryMsg) throws Exception { // 得到读取索引文件的路径 Directory dir = FSDirectory.open(Paths.get(indexreDir)); // 通过dir得到的路径下的所有的文件 // 建立索引查询器 IndexReader reader = DirectoryReader.open(dir); IndexSearcher searcher = new IndexSearcher(reader); // 中文分词器 SmartChineseAnalyzer analyzer = new SmartChineseAnalyzer(); // 建立查询解析器 /** * 第一个参数是要查询的字段; 第二个参数是分析器Analyzer */ // QueryParser parser = new QueryParser(queryField, analyzer); // 根据传进来的par查找 // Query query = parser.parse(queryMsg); // Query query = new TermQuery(new Term("teacher",queryMsg)); // Query query = new WildcardQuery(new Term(queryField,"*"+queryMsg+"*")); Query query; //这四种型需要特别匹配,不需要分词器进行搜索 if(queryField.equals("requirement") || queryField.equals("department") || queryField.equals("liaisonman") || queryField.equals("requirementtype") ) { query = new WildcardQuery(new Term(queryField,"*"+queryMsg+"*")); }else { QueryParser parser = new QueryParser(queryField, analyzer); query = parser.parse(queryMsg); } // 计算索引开始时间 long start = System.currentTimeMillis(); // 开始查询 /** * 第一个参数是通过传过来的参数来查找得到的query; 第二个参数是要出查询的行数 */ TopDocs topDocs = searcher.search(query, 104); // 索引结束时间 long end = System.currentTimeMillis(); System.out.println("匹配:["+queryField+"]," + queryMsg + ",总共花费了" + (end - start) + "毫秒,共查到" + topDocs.totalHits + "条记录。"); // 高亮显示start // 算分 QueryScorer scorer = new QueryScorer(query); // 显示得分高的片段 Fragmenter fragmenter = new SimpleSpanFragmenter(scorer); // 设置标签内部关键字的颜色 // 第一个参数:标签的前半部分;第二个参数:标签的后半部分。 SimpleHTMLFormatter simpleHTMLFormatter = new SimpleHTMLFormatter("<b><font color='red'>", "</font></b>"); // 第一个参数是对查到的结果进行实例化;第二个是片段得分(显示得分高的片段,即摘要) Highlighter highlighter = new Highlighter(simpleHTMLFormatter, scorer); // 设置片段 highlighter.setTextFragmenter(fragmenter); // 高亮显示end // 遍历topDocs /** * ScoreDoc:是代表一个结果的相关度得分与文档编号等信息的对象。 scoreDocs:代表文件的数组 * * @throws Exception */ List<RequirementInfo> listinfo = new ArrayList<>(); for (ScoreDoc scoreDoc : topDocs.scoreDocs) { RequirementInfo requirementinfo = new RequirementInfo(); // 获取文档 Document document = searcher.doc(scoreDoc.doc); // 输出全路径 String id = document.get("id"); String queryType = document.get(queryField); requirementinfo.setId(Integer.parseInt(id)); if (id != null) { // 把全部得分高的摘要给显示出来 // 第一个参数是对哪个参数进行设置;第二个是以流的方式读入 TokenStream tokenStream = analyzer.tokenStream(queryField, new StringReader(queryType)); // 获取最高的片段 String highlighterString; if(queryField.equals("requirement") || queryField.equals("department") || queryField.equals("liaisonman") ) { highlighterString = queryType.replaceAll(queryMsg, "<b><font color='red'>"+queryMsg+"</font></b>"); }else { highlighterString = highlighter.getBestFragment(tokenStream, queryType); } // String highlighterString = highlighter.getBestFragment(new SmartChineseAnalyzer(), queryField, queryType); //设置高亮字段 switch (queryField) { // 根据搜索条件进行赋值 case "requirement": //需求名称 requirementinfo.setRequirement(highlighterString); break; case "department": //需求组织 requirementinfo.setDepartment(highlighterString); break; case "liaisonman": //需求联系人 requirementinfo.setLiaisonman(highlighterString); break; case "requirementtype": //需求型 requirementinfo.setRequirementtype(Integer.parseInt(highlighterString)); break; case "requirementcontents": //需求内容 requirementinfo.setRequirementcontents(highlighterString); break; case "requirementbackground": //需求背景 requirementinfo.setRequirementbackground(highlighterString); break; } listinfo.add(requirementinfo); } } reader.close(); return listinfo; } } 需要如何修改前端代码easySelect 才能通过requirementtype检索 easySelect () { if (this.selectvalue == '' || this.selectvalue == null) { return; } let params={}; if (this.selectFieldType == 'requirementtype' || this.selectvalue !== null) { params = { FieldType: this.selectFieldType, queryMsg: this.requirementTypes.indexOf(this.selectvalue).toString() } console.log('params 返回的信息1:', params); }else{ params = { FieldType: this.selectFieldType, queryMsg: this.selectvalue } } console.log('params 返回的信息2:', params); let config = { url: "/requirement/select/simpleselect", method: "get", params: params } request(config).then( res => { this.requirementinfoList = res.data.list; this.tableData = this.requirementinfoList[0]; this.count = res.data.data.count; this.page = res.data.data.count; this.title = "( " + this.selectDirecat + ":" + this.selectvalue + " )"; this.currentPage = 1; } ) },
09-05
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值