solr 用 Java客戶端 建索引+分頁查詢

在 solr 3.5 配置及應用(一) 講過一了 solr 3.5的詳細配置,本節我們講利用solr 的客戶端調用solr的應用了!

一、利用SolrJ操作solr API

     使用SolrJ操作Solr會比利用httpClient來操作Solr要簡單。SolrJ是封裝了httpClient方法,來操作solr的API的。SolrJ底層還是通過使用httpClient中的方法來完成Solr的操作。

     需要的包如下:

     1、 apache-solr-solrj-3.5.0.jar

     2、 commons-httpclient-3.1.jar

     3、slf4j-api-1.6.0.jar

     4、commons-logging-1.1.jar

     在solr 3.5的解壓包 apache-solr-3.5.0\apache-solr-3.5.0\dist\apache-solr-solrj-3.5.0.jar 就有這個包。

二、Solr理論

1、 solr基礎

因爲 Solr 包裝並擴展了 Lucene,所以它們使用很多相同的術語。更重要的是,Solr 創建的索引與 Lucene 搜索引擎庫完全兼容。通過對 Solr 進行適當的配置,某些情況下可能需要進行編碼,Solr 可以閱讀和使用構建到其他 Lucene 應用程序中的索引。

在 Solr 和 Lucene 中,使用一個或多個 Document 來構建索引。Document 包括一個或多個 Field。Field 包括名稱、內容以及告訴 Solr 如何處理內容的元數據。例如,Field 可以包含字符串、數字、布爾值或者日期,也可以包含你想添加的任何類型,只需用在solr的配置文件中進行相應的配置即可。Field 可以使用大量的選項來描述,這些選項告訴 Solr 在索引和搜索期間如何處理內容。現在,查看一下表 1 中列出的重要屬性的子集:

屬性名稱

描述

Indexed

Indexed Field 可以進行搜索和排序。你還可以在 indexed Field 上運行 Solr 分析過程,此過程可修改內容以改進或更改結果。

Stored

stored Field 內容保存在索引中。這對於檢索和醒目顯示內容很有用,但對於實際搜索則不是必需的。例如,很多應用程序存儲指向內容位置的指針而不是存儲實際的文件內容。

2、 solr索引操作

在 Solr 中,通過向部署在 servlet 容器中的 Solr Web 應用程序發送 HTTP 請求來啓動索引和搜索。Solr 接受請求,確定要使用的適當 SolrRequestHandler,然後處理請求。通過 HTTP 以同樣的方式返回響應。默認配置返回 Solr 的標準 XML 響應。你也可以配置 Solr 的備用響應格式,如json、csv格式的文本。

索引就是接受輸入元數據(數據格式在schema.xml中進行配置)並將它們傳遞給 Solr,從而在 HTTP Post XML 消息中進行索引的過程。你可以向 Solr 索引 servlet 傳遞四個不同的索引請求:

add/update 允許您向 Solr 添加文檔或更新文檔。直到提交後才能搜索到這些添加和更新。

commit 告訴 Solr,應該使上次提交以來所做的所有更改都可以搜索到。

optimize 重構 Lucene 的文件以改進搜索性能。索引完成後執行一下優化通常比較好。如果更新比較頻繁,則應該在使用率較低的時候安排優化。一個索引無需優化也可以正常地運行。優化是一個耗時較多的過程。

delete 可以通過 id 或查詢來指定。按 id 刪除將刪除具有指定 id 的文檔;按查詢刪除將刪除查詢返回的所有文檔。

Lucene中操作索引也有這幾個步驟,但是沒有更新。Lucene更新是先刪除,然後添加索引。因爲更新索引在一定情況下,效率沒有先刪除後添加的效率好。

3、 搜索

添加文檔後,就可以搜索這些文檔了。Solr 接受 HTTP GET 和 HTTP POST 查詢消息。收到的查詢由相應的 SolrRequestHandler 進行處理。

solr查詢參數描述:

參數

描述

示例

q

Solr 中用來搜索的查詢。有關該語法的完整描述,請參閱 參考資料。可以通過追加一個分號和已索引且未進行斷詞的字段(下面會進行解釋)的名稱來包含排序信息。默認的排序是 score desc,指按記分降序排序。

q=myField:Java AND otherField:developerWorks; date asc此查詢搜索指定的兩個字段,並根據一個日期字段對結果進行排序。

start

將初始偏移量指定到結果集中。可用於對結果進行分頁。默認值爲 0。

start=15 返回從第 15 個結果開始的結果。

rows

返回文檔的最大數目。默認值爲 10。

rows=25,返回25個結果集

fq

提供一個可選的篩選器查詢。查詢結果被限制爲僅搜索篩選器查詢返回的結果。篩選過的查詢由 Solr 進行緩存。它們對提高複雜查詢的速度非常有用。

任何可以用 q 參數傳遞的有效查詢,排序信息除外。

hl

當 hl=true 時,在查詢響應中醒目顯示片段。默認爲 false。參看醒目顯示參數(見 參考資料)。

hl=true

fl

作爲逗號分隔的列表指定文檔結果中應返回的 Field 集。默認爲 “*”,指所有的字段。“score” 指還應返回記分。

*,score

sort

排序,對查詢結果進行排序,參考

sort=date asc,price desc

4、 solr模式

上面有提到schema.xml這個配置,這個配置可以在你下載solr包的安裝解壓目錄的apache-solr-3.4.0\example\solr\conf中找到,它就是solr模式關聯的文件。打開這個配置文件,你會發現有詳細的註釋。

模式組織主要分爲三個重要配置

types 部分是一些常見的可重用定義,定義了 Solr(和 Lucene)如何處理 Field。也就是添加到索引中的xml文件屬性中的類型,如int、text、date等

fileds是你添加到索引文件中出現的屬性名稱,而聲明類型就需要用到上面的types

其他配置有

uniqueKey 唯一鍵,這裏配置的是上面出現的fileds,一般是id、url等不重複的。在更新、刪除的時候可以用到。

defaultSearchField默認搜索屬性,如q=solr就是默認的搜索那個字段

solrQueryParser查詢轉換模式,是並且還是或者(and/or)

5、 索引配置

Solr 性能因素,來了解與各種更改相關的性能權衡。

表 1 概括了可控制 Solr 索引處理的各種因素:

因素

描述

useCompoundFile

通過將很多 Lucene 內部文件整合到單一一個文件來減少使用中的文件的數量。這可有助於減少 Solr 使用的文件句柄數目,代價是降低了性能。除非是應用程序用完了文件句柄,否則 false 的默認值應該就已經足夠。

mergeFactor

決定低水平的 Lucene 段被合併的頻率。較小的值(最小爲 2)使用的內存較少但導致的索引時間也更慢。較大的值可使索引時間變快但會犧牲較多的內存。

maxBufferedDocs

在合併內存中文檔和創建新段之前,定義所需索引的最小文檔數。段 是用來存儲索引信息的 Lucene 文件。較大的值可使索引時間變快但會犧牲較多的內存。

maxMergeDocs

控制可由 Solr 合併的 Document 的最大數。較小的值 (< 10,000) 最適合於具有大量更新的應用程序。

maxFieldLength

對於給定的 Document,控制可添加到 Field 的最大條目數,進而截斷該文檔。如果文檔可能會很大,就需要增加這個數值。然而,若將這個值設置得過高會導致內存不足錯誤。

unlockOnStartup

unlockOnStartup 告知 Solr 忽略在多線程環境中用來保護索引的鎖定機制。在某些情況下,索引可能會由於不正確的關機或其他錯誤而一直處於鎖定,這就妨礙了添加和更新。將其設置爲 true 可以禁用啓動鎖定,進而允許進行添加和更新。

6、 查詢處理配置

<maxBooleanClauses> 標記定義了可組合在一起形成一個查詢的子句數量的上限。對於大多數應用程序而言,默認的 1024 就應該已經足夠;然而,如果應用程序大量使用了通配符或範圍查詢,增加這個限值將能避免當值超出時,拋出 TooManyClausesException。

若應用程序預期只會檢索 Document 上少數幾個 Field,那麼可以將 <enableLazyFieldLoading> 屬性設置爲 true。懶散加載的一個常見場景大都發生在應用程序返回和顯示一系列搜索結果的時候,用戶常常會單擊其中的一個來查看存儲在此索引中的原始文檔。初始的顯示常常只需要顯示很短的一段信息。若考慮到檢索大型 Document 的代價,除非必需,否則就應該避免加載整個文檔。

<query> 部分負責定義與在 Solr 中發生的事件相關的幾個選項。Searcher 的 Java 類來處理 Query 實例。要改進這一設計和顯著提高性能,把這些新的 Searcher 聯機以便爲現場用戶提供查詢服務之前,先對它們進行 “熱身”。<query> 部分中的 <listener> 選項定義 newSearcher 和 firstSearcher 事件,您可以使用這些事件來指定實例化新搜索程序或第一個搜索程序時應該執行哪些查詢。如果應用程序期望請求某些特定的查詢,那麼在創建新搜索程序或第一個搜索程序時就應該反註釋這些部分並執行適當的查詢。

solrconfig.xml 文件的剩餘部分,除 <admin> 之外,涵蓋了與 緩存、複製 和 擴展或定製 Solr 有關的項目。admin 部分讓您可以定製管理界面。有關配置 admin 節的更多信息,請參看solrconfig.xml 文件中的註釋。

7、 監視、記錄和統計數據

用於監視、記錄和統計數據的 Solr 管理選項

菜單名

URL

描述

Statistics

http://localhost:8080/solr/admin/stats.jsp

Statistics 管理頁提供了與 Solr 性能相關的很多有用的統計數據。這些數據包括:

關於何時加載索引以及索引中有多少文檔的信息。

關於用來服務查詢的 SolrRequestHandler 的有用信息。

涵蓋索引過程的數據,包括添加、刪除、提交等的數量。

緩存實現和 hit/miss/eviction 信息

Info

http://localhost:8080/solr/admin/registry.jsp

有關正在運行的 Solr 的版本以及在當前實現中進行查詢、更新和緩存所使用的類的詳細信息。此外,還包括文件存於 Solr subversion 存儲庫的何處的信息以及對該文件功能的一個簡要描述。

Distribution

http://localhost:8080/solr/admin/distributiondump.jsp

顯示與索引發布和複製有關的信息。更多信息,請參見 “發佈和複製” 一節。

Ping

http://localhost:8080/solr/admin/ping

向服務器發出 ping 請求,包括在 solrconfig.xml 文件的 admin 部分定義的請求。

Logging

http://localhost:8080/solr/admin/logging.jsp

讓您可以動態更改當前應用程序的日誌記錄等級。更改日誌記錄等級對於調試在執行過程中可能出現的問題非常有用。

properties

http: //localhost:8080/solr/admin/get-properties.jsp

顯示當前系統正在使用的所有 Java 系統屬性。Solr 支持通過命令行的系統屬性替換。有關實現此特性的更多信息,請參見 solrconfig.xml 文件。

Thread dump

http://localhost:8080/solr/admin/threaddump.jsp

thread dump 選項顯示了在 JVM 中運行的所有線程的堆棧跟蹤信息。

8、 智能緩存

智能緩存是讓 Solr 得以成爲引人矚目的搜索服務器的一個關鍵性能特徵。Solr 提供了四種不同的緩存類型,所有四種類型都可在 solrconfig.xml 的 <query> 部分中配置。solrconfig.xml 文件中所用的標記名列出了這些緩存類型:

緩存標記名

描述

能否自熱

filterCache

通過存儲一個匹配給定查詢的文檔 id 的無序集,過濾器讓 Solr 能夠有效提高查詢的性能。緩存這些過濾器意味着對 Solr 的重複調用可以導致結果集的快速查找。更常見的場景是緩存一個過濾器,然後再發起後續的精煉查詢,這種查詢能使用過濾器來限制要搜索的文檔數。

可以

queryResultCache

爲查詢、排序條件和所請求文檔的數量緩存文檔 id 的有序 集合。

可以

documentCache

緩存 Lucene Document,使用內部 Lucene 文檔 id(以便不與 Solr 惟一 id 相混淆)。由於 Lucene 的內部 Document id 可以因索引操作而更改,這種緩存不能自熱。

不可以

Named caches

命名緩存是用戶定義的緩存,可被 Solr 定製插件 所使用。

可以,如果實現了 org.apache.solr.search.CacheRegenerator 的話。

每個緩存聲明都接受最多四個屬性:

class 是緩存實現的 Java 名。

size 是最大的條目數。

initialSize 是緩存的初始大小。

autoWarmCount 是取自舊緩存以預熱新緩存的條目數。如果條目很多,就意味着緩存的hit 會更多,只不過需要花更長的預熱時間。


三、實例

  1、獲取SolrServer 用單例的形式寫了個類;

package com.stu.commons; 
import java.net.MalformedURLException;
import org.apache.solr.client.solrj.impl.CommonsHttpSolrServer;

/**
 * Description:
 * @author  LiChunming
 * @version V1.0 
 * @createDateTime:2012-2-27 下午03:49:04 
 * @Company: MSD. 
 * @Copyright: Copyright (c) 2011
 **/
public class SolrServer {
    private static SolrServer solrServer = null;
    private static CommonsHttpSolrServer server=null;
    private static String url="http://localhost:8080/solr";
    
    public static synchronized SolrServer getInstance() {
        if (solrServer==null){
           solrServer=new SolrServer();
        }
        return solrServer;
    }
    public static CommonsHttpSolrServer getServer(){
         try {
            if(server==null){
              server = new CommonsHttpSolrServer(url);
              server.setSoTimeout(1000);  // socket read timeout
              server.setConnectionTimeout(1000);
              server.setDefaultMaxConnectionsPerHost(100);
              server.setMaxTotalConnections(100);
              server.setFollowRedirects(false);  // defaults to false
              //allowCompression defaults to false.
              //Server side must support gzip or deflate for this to have any effect.
              server.setAllowCompression(true);
              server.setMaxRetries(1); // defaults to 0.  > 1 not recommended.
            }
        } catch (MalformedURLException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        return server;
    }
}

2、打開目錄tomcat-7.0.14\solr\conf\schema.xml文件在<fields>下增加下字段。用於增加到搜索引擎的字段
<field name="blogId" type="string" indexed="true" stored="true"  required="true"/>
<field name="content" type="text" indexed="true" stored="true" omitNorms="true" />
<field name="bTypeId" type="string" indexed="true" stored="true" />
<field name="bTypeName" type="string" indexed="true" stored="true" />
<field name="nickName" type="string" indexed="true" stored="true" />
<field name="createTime" type="date" indexed="true" stored="true" omitNorms="true" />

3、增加信息到引擎文件中

public void writerBlog(BlogsDO blog) {
         // TODO Auto-generated method stub
          try {
              blog.setId(SerialNumberUtil.getRandomNum(4));
               //獲取連接服務
               CommonsHttpSolrServer solrServer= SolrServer.getInstance().getServer();
               SolrInputDocument doc1 = new SolrInputDocument();
               doc1.addField("id", SerialNumberUtil.getRandomNum(4) );
               doc1.addField("blogId", blog.getBlogsId());
               doc1.addField("title",blog.getTitle()  );
               doc1.addField("bTypeId", blog.getbTypeId());
               doc1.addField("bTypeName", blog.getbTypeName());
               doc1.addField("content",  blog.getContent());
               String createTime=DateUtils.formatDate(blog.getGmtCreate(),  "yyyyMMddHHmmss");
               doc1.addField("createTime",createTime);
               doc1.addField("nickName",blog.getNickName());
               solrServer.add(doc1);
               solrServer.commit();
         } catch (SolrServerException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         } catch (IOException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
     }

實體類 BlogsDO 

package com.stu.entity; 
 
 import java.io.Serializable;
 import java.util.Date;
 import javax.persistence.Column;
 import javax.persistence.Entity;
 import javax.persistence.GeneratedValue;
 import javax.persistence.GenerationType;
 import javax.persistence.Id;
 import javax.persistence.Table;
 import javax.persistence.Transient;
 import org.apache.solr.client.solrj.beans.Field;
 
 import com.stu.commons.util.DateUtils;
 
 /**
  * Description:
  * @author  LiChunming
  * @version V1.0 
  * @createDateTime:2011-5-17 下午04:38:11 
  * @Company: MSD. 
  * @Copyright: Copyright (c) 2011
  **/
 @Entity
 @Table(name="blogs")
 public class BlogsDO implements Serializable{
     /**
      * 
 */
     private static final long serialVersionUID = -4721368786493126226L;
     @Field
     private String id;
     @Field("blogId")
     private Integer blogsId;
     @Field
     private String title;
     @Field
     private String content="";
     @Field("createTime")
     private Date gmtCreate;
     @Field
     private String nickName;
     @Field
     private String bTypeId;
     @Field
     private String bTypeName;
     private Date gmtModified;
     private String revDate;
     private String sDate="";
     private String eDate="";
     
     @Transient
     public String getId() {
         return id;
     }
     public void setId(String id) {
         this.id = id;
     }
     @Id
     @GeneratedValue(strategy = GenerationType.IDENTITY)
     public Integer getBlogsId() {
         return blogsId;
     }
     public void setBlogsId(Integer blogsId) {
         this.blogsId = blogsId;
     }
     public String getTitle() {
         return title;
     }
     public void setTitle(String title) {
         this.title = title;
     }
     public String getContent() {
         return content;
     }
     public void setContent(String content) {
         this.content = content;
     }
     
     public String getNickName() {
         return nickName;
     }
     public void setNickName(String nickName) {
         this.nickName = nickName;
     }
     
     public String getbTypeId() {
         return bTypeId;
     }
     public void setbTypeId(String bTypeId) {
         this.bTypeId = bTypeId;
     }
     @Column(name="gmt_create")
     public Date getGmtCreate() {
         return gmtCreate;
     }
     public void setGmtCreate(Date gmtCreate) {
         this.gmtCreate = gmtCreate;
     }
     @Column(name="gmt_modified")
     public Date getGmtModified() {
         return gmtModified;
     }
     public void setGmtModified(Date gmtModified) {
         this.gmtModified = gmtModified;
     }
     @Transient
     public String getRevDate() {
          if (this.gmtCreate == null) {
                 return null;
             }
           return DateUtils.formatDate(gmtCreate, "yyyy-MM-dd HH:mm:ss");
     }
     public void setRevDate(String revDate) {
         this.revDate = revDate;
     }
     @Transient
     public String getbTypeName() {
         return bTypeName;
     }
     public void setbTypeName(String bTypeName) {
         this.bTypeName = bTypeName;
     }
     @Transient
     public String getsDate() {
         return sDate;
     }
     public void setsDate(String sDate) {
         this.sDate = sDate;
     }
     @Transient
     public String geteDate() {
         return eDate;
     }
     public void seteDate(String eDate) {
         this.eDate = eDate;
     }
     @Override
     public String toString() {
         return this.id + "#" + this.blogsId + "#" + this.title + "#" + this.content + "#" + this.bTypeId + "#" + this.bTypeName + "#" + this.nickName+"#" + this.gmtCreate;
     } 
 }
5、文檔查詢(注意查詢出來的文檔轉化爲List<object>比較麻煩,下次我們將使用

DocumentObjectBinder對象將SolrInputDocument 和 BlogsDO對象相互轉換)
public List<BlogsDO> searchBlogsList(String content, String bTypeId,
             String sDate, String eDate, Page page) throws IOException,
             ParseException {
          List<BlogsDO> blogList=new ArrayList<BlogsDO>();
          BlogsDO blogsDO=null;
          CommonsHttpSolrServer solrServer= SolrServer.getInstance().getServer();
          SolrQuery sQuery = new SolrQuery();
          String para="";
         //OR 或者  OR 一定要大寫
          if(StringUtils.isNotEmpty(content)){
              para=para+"(title:"+content+" OR content:"+content+")";
              //空格 等同於 OR
 // para=para+"(title:"+content+"  content:"+content+")";
          }
         //AND 並且  AND一定要大寫
          if(!bTypeId.equals("-1")){
              if(StringUtils.isNotEmpty(para)){
                   para=para+" AND bTypeId:"+bTypeId;
              }else{
                   para=para+"  bTypeId:"+bTypeId;
              }
          }
          if(StringUtils.isNotEmpty(sDate) && StringUtils.isNotEmpty(eDate)){
              if(StringUtils.isNotEmpty(para)){
                  para=para+" AND createTime:["+sDate+" TO "+eDate+"]";
              }else{
                  para=para+" createTime:["+sDate+" TO "+eDate+"]";
              }
         }
         //查詢name包含solr apple
 //sQuery.setQuery("name:solr,apple");
 //manu不包含inc
 //sQuery.setQuery("name:solr,apple NOT manu:inc");
 //50 <= price <= 200
 //sQuery.setQuery("price:[50 TO 200]");
 //sQuery.setQuery("popularity:[5 TO 6]");
 //params.setQuery("price:[50 TO 200] - popularity:[5 TO 6]");
 //params.setQuery("price:[50 TO 200] + popularity:[5 TO 6]");
 //50 <= price <= 200 AND 5 <= popularity <= 6
 //sQuery.setQuery("price:[50 TO 200] AND popularity:[5 TO 6]");
 //sQuery.setQuery("price:[50 TO 200] OR popularity:[5 TO 6]");
         
 // 查詢關鍵詞,*:*代表所有屬性、所有值,即所有index
         if(!StringUtils.isNotEmpty(para)){
              para="*:*"; 
          }
          logger.info("para:"+para);
          sQuery.setQuery(para);
          //設置分頁  start=0就是從0開始,,rows=5當前返回5條記錄,第二頁就是變化start這個值爲5就可以了。
          sQuery.setStart((page.getCurrentPage()-1)*page.getPerPageSize());
          sQuery.setRows(page.getPerPageSize());
          //排序 如果按照blogId 排序,,那麼將blogId desc(or asc) 改成 id desc(or asc)
          sQuery.addSortField("blogId", ORDER.asc);
         
          //設置高亮
         sQuery.setHighlight(true); // 開啓高亮組件
         sQuery.addHighlightField("content");// 高亮字段
         sQuery.addHighlightField("title");// 高亮字段
         sQuery.setHighlightSimplePre("<font color='red'>");//標記,高亮關鍵字前綴
         sQuery.setHighlightSimplePost("</font>");//後綴
         sQuery.setHighlightSnippets(2);//結果分片數,默認爲1
         sQuery.setHighlightFragsize(1000);//每個分片的最大長度,默認爲100
         
 //分片信息
         sQuery.setFacet(true)
             .setFacetMinCount(1)
             .setFacetLimit(5)//段
             .addFacetField("content");//分片字段
         
         try {
             QueryResponse response = solrServer.query(sQuery);
             SolrDocumentList list = response.getResults();
             Integer counts=(int) list.getNumFound();
             logger.info("counts:"+counts);
             page.setCounts(counts);
             //獲取所有高亮的字段
             Map<String,Map<String,List<String>>> highlightMap=response.getHighlighting();
             String blogId="";
             for (SolrDocument solrDocument : list) {
                 blogsDO=new BlogsDO();
                 blogId=solrDocument.getFieldValue("blogId").toString();
                 blogsDO.setBlogsId(Integer.valueOf(blogId));
                 blogsDO.setbTypeId(solrDocument.getFieldValue("bTypeId").toString());
                 blogsDO.setbTypeName(solrDocument.getFieldValue("bTypeName").toString());
                 blogsDO.setNickName(solrDocument.getFieldValue("nickName").toString());
                 List<String> titleList=highlightMap.get(blogId).get("title");
                 List<String> contentList=highlightMap.get(blogId).get("content");
                 if(titleList!=null && titleList.size()>0){
                     blogsDO.setTitle(titleList.get(0));
                 }else{
                     //獲取並設置高亮的字段title
                     blogsDO.setTitle(solrDocument.getFieldValue("title").toString());
                 }
                 if(contentList!=null && contentList.size()>0){
                     blogsDO.setContent(contentList.get(0));
                 }else{
                     //獲取並設置高亮的字段content
                     blogsDO.setContent(solrDocument.getFieldValue("content").toString());
                 }
                 blogsDO.setRevDate(solrDocument.getFieldValue("createTime").toString());
                 SimpleDateFormat sdf =  new  SimpleDateFormat("yyyyMMddHHmmss"); 
                 try {
                     blogsDO.setGmtCreate(sdf.parse(solrDocument.getFieldValue("createTime").toString()));
                 } catch (java.text.ParseException e) {
                     // TODO Auto-generated catch block
                     e.printStackTrace();
                 }
                 blogList.add(blogsDO);
             }
         } catch (SolrServerException e) {
             // TODO Auto-generated catch block
             e.printStackTrace();
         }
         return blogList;
     }

轉載於:http://blog.csdn.net/truong/article/details/48055881

發佈了64 篇原創文章 · 獲贊 61 · 訪問量 18萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章