在 IBM Lotus Notes 和 Domino 中編寫快速查找代碼

閱讀在 IBM Lotus Notes 和 Domino 中編寫快速查找代碼的 11 個技巧。作者考察了 Lotus Notes 和 Domino 中的 @DbLookup @Formula 並描述了一些新技巧,供開發人員在編寫新應用程序或對現有應用程序進行性能問題的故障檢修時使用。

本文將考察 @DbLookup,它可能是 IBM Lotus Notes 和 Domino 中最流行的 @Formula。現在的 Lotus Notes/Domino 應用程序開發人員可能無法想像在不使用此公式的情況下創建應用程序,而超過 15 年的性能測試和客戶故障檢修已經表明:在應用程序的一個表單中,常常會以多種形式使用此公式數十次。

但是同樣的經驗表明:性能問題往往也與這些 @DbLookup 公式有關。我們已經看到,複雜的企業應用程序由於使用了這些公式,從而導致了無法接受的低性能而陷入實際的停頓。

本文將描述 11 個技巧,使用這些技巧能保證加快幾乎任何應用程序的運行速度。這些技巧的範圍廣泛,包括從簡單的一行代碼到 @DbLookup 公式處理方法的根本性改變,但是所有這些技巧都已久經考驗。

本文假設您具有 Lotus Notes/Domino 開發的一些知識,因此諸如 @DbLookup 公式的一些基本參數等內容,將不再贅述。

本文將使用各種示例說明每個技巧的價值。爲簡單起見,我們使用一個 Help Desk 應用程序,它包含一個 Contact 表單和一個 Ticket 表單。此應用程序的基本工作流程是:客戶調用,創建 Ticket 文檔,接下來,獲得客戶的公司和名稱信息後從下拉列表中選擇適當的公司名稱和聯繫人名稱。還要選擇適當的調用類別(例如 Product Help、Sales 等等)。下面給出一些例子,在這幾種情況下使用 @DbLookup 公式會將非常有用:

  • 從一個包含所有公司的列表中選擇公司名稱
  • 從一個包含該公司所有聯繫人的列表中選擇客戶名稱
  • 從一個預先確定的或動態的問題類別列表中選擇問題類別

捕獲錯誤

我們都不希望進行錯誤捕獲,但是事與願違。在 @DbLookup 公式中(本文中 @DbLookup 既指 @DbLookup 又指 @DbColumn 公式),錯誤往往更加難以處理,因爲公式常常不僅取決於用戶輸入的鍵,還取決於其返回的數據。就是說公式可能在某一天能夠工作,而在另一天就不能用了 —— 對開發人員來說這一直是個難題。

例如,假設公司名稱在不同的 Contact 文檔中有三四種不同的寫法(比如 LSDevelopment Corporation 和 LS Development Corporation)。如果接下來要查找該公司的所有聯繫人名稱,但是鍵入了 “LSDevelopment Company”(比方說),則找不到任何匹配的名稱。

有多種方法可用於更精確的輸入鍵(比如下拉列表),但是爲了便於討論,假設即使使用嚴格的開發技術,@DbLookup 也返回了一個錯誤。

錯誤檢查的基本方法是編寫如清單 1 所示的代碼:


清單 1. 錯誤檢查
v := @DbLookup(“Notes”; “”; “(Lookup-ContactsByCompany)”; CompanyName; 
“ContactName”);
@If(@IsError(v); “The program cannot find any contacts for this 
company”; v)

這段代碼用於執行查找並檢查錯誤條件。如果存在錯誤,則返回一個文本字符串,告訴用戶存在一個問題;否則,則返回查找得到的值。

這雖然與性能無關,但卻是很多應用程序都存在的一個基本問題,因此不能忽略。可將其看作一個附加的技巧。





回頁首


使查找頻率最小化

考慮下面的查找公式代碼:

@If(@IsError(@DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1); “There are no
company names”; @DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1))

這是一個很常見的錯誤,因爲查找執行了兩次,從性能角度來說這就是個問題。您可能認爲第一個查找很好地緩存了查找結果,因此第二個查找就不會執行,但事實並非如此。第二個查找比第一個執行得快,因爲後者(確實)緩存了查找結果,而前者原樣執行。因爲沒有功能上的原因要求像那樣編寫公式代碼,所以您應編寫如下代碼作爲替代:

v := @DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1);
@If(@IsError(v); “There are no company names”; v)

另一種導致執行不必要的查找的錯誤是:比如,一個 computed 字段在以下情形時計算查找公式:

  • 第一次創建文檔
  • 按下各種按鈕以編輯模式刷新文檔
  • 保存文檔
  • 在稍後的日期以讀模式打開文檔
  • 在稍後的日期以編輯模式打開文檔
  • 在稍後的日期保存文檔

最常見的是:希望只在第一次創建查找時執行查找。在這種情況下,將字段設置爲 Computed When Composed 以防止再次計算。

在其他情況下,您可能希望每次以編輯模式打開文檔時都執行查找。此時,按如下內容重寫公式:

@If(@IsDocBeingEdited; “”; @Return(FieldName));
v := @DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1);
@If(@IsError(v); “There are no company names”; v)

在這種情況下,FieldName 是此公式所在字段的名稱。如果不是以編輯模式加載文檔,則公式將只是保持該值並停止執行(參見圖 1)。否則,將像以前一樣執行 @DbColumn。


圖 1. 關鍵字公式
關鍵字公式





回頁首


正確地使用緩存

另一個常見錯誤是誤解了 NoCache 參數的用法。通常,錯誤的推理是:認爲數據越重要就越應該使用 NoCache。事實上,正確的思路應該是:執行查找的頻率與數據變化的頻率有關。

例如,如果要查找的數據的變化頻率很低,比如,一個月變化一次,則很難想象查找公式中需要使用 NoCache。在示例中我們假設創建了一些供 Ticket 文檔引用的類別。該列表可能由應用程序的所有者維護,每隔數月(也許是發佈新產品或輸入新公司時)才執行一次更新。

另一方面,假設 Ticket 文檔查找發出調用的客戶名稱。如果此客戶一小時內發出兩次調用,則我們會因爲在第二個 ticket 上的查找中得不到客戶名稱而非常不方便,這是由於名稱列表被緩存在了前一個查找中。在此類應用程序中,我們常常會看見 Help Desk 職員整天開着數據庫,因此當天將緩存的查找長時間緩存起來比較方便。





回頁首


避開下拉列表陷阱

多年以來,表單中出現的所有嚴重的性能問題中,下拉列表顯得尤爲突出,有以下兩個方面的原因:下拉列表通常查找大列表,下拉列表在讀模式下也進行計算。

您可在自己的應用程序上執行一個簡單的測試。以讀模式打開一個其中的表單執行速度較慢的文檔。打開文檔時,仔細觀察屏幕,找出其停頓的地方並做上標記。這執行起來可能不太容易處理,因爲您只有一秒鐘的操作時間,然後屏幕就跳轉了,但是如果能找人幫忙的話,則可由其中一人調用字段標籤,同時由另一個人記下這些標籤。然後在 IBM Lotus Domino Designer 中打開表單並查看那些標記下的內容。最可能的情況是,您將發現標記所在之處是使用了 @DbLookup 公式的下拉列表。這是怎麼回事?

以讀模式打開文檔時,將檢驗所有的 @DbLookup 公式。因爲文檔處於讀模式下,所以並不會返回實際的檢驗值,但它們仍然會執行完該過程,而該過程很耗時。對於下拉列表,情況將更加糟糕,因爲關鍵字字段由用戶選擇的值填充,以防該用戶切換到編輯模式。您可能認爲可以等到用戶切換至編輯模式,然後讓用戶做出選擇,但 Lotus Notes 中並不是這樣運作的。有趣的是,Web 瀏覽器中下拉列表卻正是像那樣運作,即使在相同的表單上運行也是如此。

一個很好用的清理下拉列表的技巧是:將三種不同的特性結合使用以防止執行不必要的查找。這些特性操作起來都很容易,但您必須將三者結合使用。

首先,在下拉列表字段的公式中,使用以下公式:

@If(@IsDocBeingEdited; “”; @Return(FieldName));
v := @DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1);
@If(@IsError(v); “There are no company names”; v)

這與 “使查找頻率最小化” 中描述的內容相似,並且在文檔處於讀模式時它可防止關鍵字下拉列表執行查找。如果用戶以編輯模式打開文檔,則下拉列表當然會正常地計算。現在我們必須確保用戶從讀模式轉換到編輯模式時將會計算查找。爲此,將繼續執行下面兩個步驟。

在下拉字段的屬性中,啓用 “Refresh choices on document refresh” 屬性(參見圖 2)。如果用戶從讀模式轉換到編輯模式,則此屬性可讓關鍵字下拉公式在強制刷新文檔時重新計算。


圖 2. 下拉字段的屬性
下拉字段的屬性

 

最後,在表單的 Postmodechange 事件中(參見圖 3),包含以下代碼,以便在用戶從讀模式轉換到編輯模式時強制執行刷新:

If source.EditMode Then Call source.Refresh

圖 3. Postmodechange 事件
Postmodechange 事件





回頁首


使用按鈕和選擇列表代替下拉列表

在某些情況下,有很多很大且使用非常頻繁的下拉列表,此時惟一合理的解決方案是:在主表單上停止使用這些下拉列表。正確的思路應該是思考以下問題:

  • 文檔在使用期限內會被讀取多少次?
  • 文檔會被編輯多少次?
  • 在文檔的編輯次數中,需執行多少次這樣的查找?

答案常常類似於:“文檔通常被讀取 10 次,但只被編輯一、兩次。在這些編輯次數中,這些下拉列表只被使用一次。”當然,答案總是隨所使用文檔的不同而變化,但這些卻是些值得思考的好問題。如果需要頻繁地編輯文檔,但很少需要執行查找,則前一部分將不能滿足,因爲每次用戶處於編輯模式時下拉列表都將重新計算,即使這些列表實際只需使用不超過一次時也是如此。在這種情況下,可考慮使用按鈕,可能需要與選擇列表結合使用。

使用按鈕的優點在於:可讓您從字段中刪除查找公式。字段將不再是一個下拉列表,而是成爲了一個常規的文本字段,可能是 Computed when Composed 字段,帶有 FieldName 公式(下面的例子中使用的是 ProblemCategory 公式)。爲避免混淆將在讀模式下隱藏按鈕,但是在編輯模式下單擊按鈕時,它將使用清單 2 所示的公式:


清單 2. 按鈕的公式
tlist := @DbColumn("Notes"; ""; "(Lookup-Categories)"; 1);
list := @Unique(@Explode(tlist; "~"));
@If(@IsError(list); @Return(@Prompt([Ok]; "Error"; "The program was unable to lookup the 
Problem Categories.")); "");

dv := @If(ProblemCategory = ""; @Subset(list; 1); ProblemCategory);
v := @Prompt([OkCancelListMult]; "Problem Category"; "Choose one or more problem 
categories for this ticket."; dv; list);

FIELD ProblemCategory := v;

通過使用 @Picklist 而不是 @Prompt 和 @DbLookup,可進一步使性能流線化。其缺點在於會減少對彈出窗口布局和底層數據的控制。例如,查看清單 3 所示的代碼。


清單 3. 使用 @Picklist
v := @PickList( [CUSTOM] ; “” ; “(Lookup-Categories)” ; “Problem Category” ; 
“Choose one or more categories from the list.” ; 1 )
result := @If(@IsError(v); “The program was unable to lookup the Problem Categories”; v);
FIELD ProblemCategory := result;





回頁首


使用佈局區域(或彈出窗口)代替下拉列表

由於前一部分對查找的大小和頻率進行了探討,因此似乎已經找到了應用程序的正確處理方法,但是如果得不到所需的控制又該如何,答案是考慮使用對話框。可使用 @Formula 語言或 LotusScript 彈出一個對話框,並且可在一個對話框中使用多個查找。當表單擁有多個相關的查找時,這常常是一個很好的解決方案。例如,當用戶選擇公司後,該用戶接下來應得到一張該公司聯繫人的列表,也許還有一個下拉列表,包含了該聯繫人的開放 tickets 的主題行。在此情況下,您可能希望擁有一個按鈕(如前一部分中那樣),但是要和清單 4 所示的代碼結合使用。


清單 4. 使用對話框
Dim w as NotesUIWorkspace
flag = w.DialogBox ( "(Dialog-CompanyLookup)", True, True, False, True, 
False, False,
"Company Name", doc, False, False, False )

這裏不會對每個參數做詳細介紹,但清單 5 顯示了開發者幫助參考。


清單 5. 幫助參考
flag = notesUIWorkspace.DialogBox( form$ , [autoHorzFit] , [autoVertFit] , 
[noCancel] , 
[noNewFields] , [noFieldUpdate] , [readOnly] , [title$] , [notesDocument] , 
[sizeToTable] ,
[noOkCancel] , [okCancelAtBottom] )

在 Dialog-CompanyLookup 表單中,現在可插入佈局區域並放入所有的下拉字段,而不用考慮 @IsDocBeingEdited。用戶單擊按鈕,彈出對話框時需要等待執行這些查找。

使用對話框的另外幾點想法:

  • 有時候創建一個臨時的 Notes 文檔會很有幫助,您不用保存該文檔,但它可作爲先前代碼示例中的 [notesdocument] 參數。這讓您能使用 LotusScript 執行更精細的查找,從而創建對話框中的列表。例如,也許您希望只引用最後三個月中聯繫過的公司。
  • 在對話框表單中執行驗證檢查,因此直到驗證完用戶數據,用戶才能回到主文檔,這樣處理可能比較方便。這可以減少用戶必須返回到對話框的煩惱。
  • 可添加圖形、幫助文本和更多其他東西,使對話框比一個簡單的下拉列表或 @Prompt 框更具吸引力且具有更多功能。
  • 如果將應用程序移植到 Web 上,則可以很輕鬆地使用 JavaScript 彈出窗口替換對話框。佈局區域本身不能在瀏覽器上顯示,但是您可以使用 <div> 標記或表來模擬佈局(<div> 標記是一種 HTML 代碼,用於在頁面上任何位置放置一段內容)。
  • 最後,還可以使用嵌套的表和 [sizeToTable] 參數使其在 Notes 客戶機中更具吸引力。此表單將更加易於移植到 Web 上。




回頁首


使用 @Eval 清理代碼

我們有時會看見大段的 @Formula 代碼,裏面充滿了使用 @DbLookup 公式的 @If 語句。這些代碼維護起來會很麻煩,而爲避免不必要的查找以及執行適當的錯誤檢查就更加困難(參見 “捕獲錯誤” 部分)。使用 @Eval 可讓您避免這類問題,如清單 6 所示。


清單 6. 使用 @Eval
companyLookup := {@DbColumn(“Notes”; “”; “(Lookup-Companies)”; 1);};
contactLookup := {@DbLookup(“Notes”; “”; “(Lookup-ContactsByCompany)”; 
CompanyName; “ContactName”);};

@If(someCondition = 1; @Eval(companyLookup); @Eval(contactLookup))

這段代碼可防止在 @If 語句之前執行查找。換句話說,您可以提前設置所有的查找,給它們提供適當的變量名稱,然後在需要時使用 @Eval 調用這些查找。





回頁首


使用一個查找獲取多個字段

如果您發現爲獲取不同的數據點要多次查找同一文檔,則可考慮將該數據與查找視圖中的一列連接起來。例如,假設我們需要從一個文檔中獲取下列數據點:

  • ContactName
  • ContactPhone
  • ContactAddress1
  • ContactAddress2
  • ContactCity
  • ContactState
  • ContactZip

爲此,我們可通過使用七個字段(每個字段都執行查找)來實現。ContactName 字段公式將類似於清單 7 所示的代碼。


清單 7. ContactName 字段公式
v := @DbLookup(“Notes”; “”; “(Lookup-ContactsByCompany)”; CompanyName; 
“ContactName”);
@If(@IsError(v); @Return(“The program could not locate that 
document.”); v);

而 ContactPhone 字段公式將類似於清單 8 所示的代碼。


清單 8. ContactPhone 字段公式
v := @DbLookup(“Notes”; “”; “(Lookup-ContactsByCompany)”; CompanyName; “
ContactPhone”);
@If(@IsError(v); @Return(“The program could not locate that 
document.”); v);

七個字段中的每一個都以此類推。

但是如果可以執行一個而不是七個查找的話,您就可獲得更好的性能,即使假定在同一視圖中對同一文檔執行七個查找時也是如此。有一種方法可達到此目的:設置查找視圖,使其包含具有清單 9 所示公式的另外一列。


清單 9. 設置查找視圖使其包含另外一列
@If(ContactName = “”; “NA”; ContactName) + “~” + 
@If(ContactPhone = “”; “NA”; ContactPhone) + “~” +  
@If(ContactAddress1 = “”; “NA”; ContactAddress1) + “~” + 
@If(ContactAddress2 = “”; “NA”; ContactAddress2) + “~” + 
@If(ContactCity = “”; “NA”; ContactCity) + “~” + 
@If(ContactState = “”; “NA”; ContactState) + “~” + 
@If(ContactZip = “”; “NA”; ContactZip); 

現在在表單中設置一個隱藏字段 —— 不妨稱之爲 BigLookup —— 並將清單 10 所示的公式放入 BigLookup 中:


清單 10. 查找 ContactsByCompany
v := @DbLookup(“Notes”; “”; “(Lookup-ContactsByCompany)”; 
CompanyName; 2);
@If(@IsError(v); @Return(“The program could not locate that 
document.”); v);

現在對於 ContactName 字段,其公式可能如清單 11 所示。


清單 11. 查找 ContactName
tv := @Explode(BigLookup; “~”);
v := tv[1];
@If(@IsError(v); @Return(“The program could not locate that document.”); v = “NA”; “”; v);

而對於 ContactPhone,查看清單 12 所示的代碼。


清單 12. 查找 ContactPhone
tv := @Explode(BigLookup; “~”);
v := tv[2];
@If(@IsError(v); @Return(“The program could not locate that document.”); v = “NA”; “”; v);

七個字段中的每一個都以此類推。

關於此技術的兩點注意:

  • 在視圖列公式中我們將 “NA” 替代爲 “”,如果不這樣做的話,@Explode 會破壞所有的空白值並生成一個少於七個值的列表,這將會出現問題。
  • 在每個字段公式中將 “” 替代爲 “NA”,但錯誤消息的處理完全取決於您的判斷。




回頁首


製作更快的查找視圖

任意種類查找視圖的性能,不論使用的是 @Formula 語言還是 LotusScript,都取決於該視圖的大小和運行速度。我們已經彙編了一些可用於優化查找視圖性能的技巧。

製作專用的查找視圖

在生產環境中索引(刷新)一個很典型的視圖大約會花費 100 ms。也就是說,每 15 分鐘服務器將花費不到十分之一秒的時間來更新每個查找視圖。當然,這些時間是近似值,而且每個應用程序和服務器之間差別很大,但是對此數據的多年研究讓我們相信:即使添加幾個查找視圖對性能的影響也不大。添加專用的查找視圖可讓您在用戶等待查找完成時使視圖性能流線化。

使視圖設計流線化

如果視圖只用於從某些文檔查找數據,則可將以下內容從視圖設計中消除:

  • 所有有趣的字體和顏色。
  • 所有的動作欄按鈕。
  • 視圖事件中的任何 LotusScript 代碼。
  • 對 Profile 文檔的任何引用。
  • 無關的列。如果要從這些列返回數據,則保留該列;反之則消除該列。
  • 視圖列的多個排序選項。例如,如果用戶從不訪問視圖,則無需對其按升序或降序排序;它所做的只是增加了視圖的大小。
  • 用於限制 @DbLookup 返回的選擇列表的讀者名稱字段。這些字段對性能有很大的影響。

還可將類別轉換成排序列,後者提供了相同的查找功能但可使視圖更快地進行索引。

使數據流線化

除使設計最小化外,還可以考慮如何使視圖中的數據最小化。下面有一些建議:

  • 如果可以的話,儘可能地將數據歸檔。用於企業的設計考慮通常要勝過本文介紹的這些,但常常可將數據歸檔到另一個 Notes 數據庫中,用戶可在只讀基礎上訪問該數據庫(比如)。這樣做可加快主數據庫中的很多函數的運行速度,當然也限制了視圖中的數據,使其運行得更快。
  • 仔細地優化視圖選擇公式以便只顯示實際需要的數據。
  • 確保沒有選擇某些不顯示但仍需包含到視圖索引中的響應文檔。選擇公式 SELECT Form = “Main” | @IsResponseDoc 可能不經意地做到了這一點。反過來,使用 @DocDescendants 確保只包含顯示的響應文檔。
  • 作爲刪除不必要數據的示例,我們看到了一些客戶應用程序,其中的查找視圖只需使用過去 30 天的數據,但數據庫卻必須將數據保存很長的時間,如兩年。在該視圖中使用時間敏感或日期敏感的公式顯然不合適(太慢),但是使用周代理在文檔中設置標記也許可行,使用該代理只標記那些已創建超過 30 天的文檔。現在選擇公式可以只引用該標記即可排除大量的文檔,而不排除這些文檔就會降低視圖的運行速度。
  • 消除任何沒有用於查找的數據顯示。

使用 “Generate unique keys in index” 消除重複條目

這是一個很好的技巧,可以極大地減小某些視圖的大小。如果查找得到了大量的重複值,而這些值後來又被使用 @Unique(或等效的 LotusScript)消除,則可使用此特性。在這種情況下,啓用 “Generate unique keys in index” 選項可使視圖索引只顯示找到的文本字符串的第一個實例(參見圖 4)。

但是如果排序列引用了多值字段,則使用此選項時要格外小心。在這種情況下,如果 Domino 服務器是 6.x 版本,則可能要使用 @Implode(MultiValueField; “~”),然後讓查找公式使用 @Unique(@Explode(@DbColumn(); “~”)) 以獲取真實的惟一值集。如果使用的是 Lotus Domino 7,則沒什麼關係,因爲服務器能智能地顯示所有的惟一值。請注意:視圖使用此特性時索引速度會稍慢一些,因爲刷新視圖時服務器可能要做比平時更多的工作。除非該特性能極大地減少視圖中顯示的文檔數量,否則不要使用它。在很多應用程序中,使用此特性可使視圖的大小減小到原來的 1/100,在這種情況下該特性可謂居功至偉。


圖 4. View Properties 對話框
View Properties 對話框





回頁首


使用 Profile 文檔

如果您擁有一些不是由多個用戶更新且更新不頻繁的列表,則檢索這些值的一個快速方法是將其存儲在 Profile 文檔中。不管您是在 Profile 文檔中手動更新列表,還是在常規文檔中手動更新列表然後將其移植到 Profile 文檔中,與緩存視圖相比,用戶都可更有效地緩存 Profile 文檔,因此可以更快速地執行重複查找。

但是,如果用戶正在更新列表,則使用 Profile 文檔可能就不適當了,因爲多個更新之間可能相互重寫,然後導致複製/保存衝突。





回頁首


使用查找數據庫

大型應用程序常常可使用單獨的查找數據庫,可能是依據以下理論:要查找的數據非常多,因此從主數據庫中獲取數據和所需的視圖比較方便。雖然這種說法具有一定的概念邏輯,但在現實中這樣做往往會得不償失。下面是一些技巧,可用於確定什麼時候將查找數據存放在單獨的數據庫中,以及什麼時候將數據存放在主數據庫中更合適:

  • 如果多個應用程序訪問相同的列表,則使用單獨的數據庫存放這些列表比較合適。
  • 如果要查找大量的數據並且這些數據需要使用多個表單,則從主用戶數據庫中獲取數據比較合適。
  • 另一方面,如果查找數據使用單個表單,並且只有幾百個文檔,則對主數據庫的影響幾乎可以忽略不計。基於維護和性能上的優點,因此在這種情況下我們鼓勵您將查找保存在主數據庫中。

所有這些情況中有一個例外就是,可能某些時候性能非常重要並且多個數據庫訪問幾個簡短的列表。在這種情況下,可能需要將這些列表保存在單獨的查找數據庫中,但是又通過 LotusScript 代理(或 Lotus Enterprise Integrator)將其移植到每個主數據庫中。這樣維護起來可能有些麻煩,但是可得到最好的性能。





回頁首


結束語

我們希望這些技巧能在您編寫下一個應用程序或對現有應用程序中的性能問題進行故障檢修時爲您提供一些可用的新工具。動態查找是大多數應用程序中的一個重要部分,因此,使用它們以使其性能影響最小化是確保擁有可順利運行多年的高質量程序的最佳方法。

 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章