CQRS之旅——旅程8(後記:經驗教訓)

来源:https://www.cnblogs.com/angrymoto/archive/2019/07/05/cqrs-journey-day8.html
-Advertisement-
Play Games

這一章總結了我們旅程中的發現。它強調了我們在這個過程中所學到的最重要的經驗教訓,提出瞭如果我們用新知識開始這段旅程,我們將以不同的方式做的一些事情,並指出了Contoso會議管理系統的一些未來道路。 ...


旅程8:後記:經驗教訓

我們的地圖有多好?我們走了多遠?我們學到了什麼?我們迷路了嗎?

“這片土地可能對那些願意冒險的人有益。”亨利.哈德遜

這一章總結了我們旅程中的發現。它強調了我們在這個過程中所學到的最重要的經驗教訓,提出瞭如果我們用新知識開始這段旅程,我們將以不同的方式做的一些事情,並指出了Contoso會議管理系統的一些未來道路。

你應該記住,這個總結反映的是我們的具體旅程,並非所有這些發現都適用於你自己的CQRS旅行。例如,我們的目標之一是探索如何在部署到Microsoft Azure併在利用雲的可伸縮性和可靠性的應用程式中實現CQRS模式。對於我們的項目,這意味著使用消息傳遞來支持多個角色類型和實例之間的通信。您的項目可能不需要多個角色實例,或者沒有部署到雲中,因此可能不需要如此廣泛地(或者根本不需要)使用消息傳遞。

我們希望這些發現能夠被證明是有用的,特別是當您剛剛開始使用CQRS和事件源時。

我們學到了什麼

本節描述了我們學到的主要經驗教訓。它們沒有以任何特定的順序呈現。

性能問題

在我們的旅程開始時,我們對CQRS模式的一個概念是,通過分離應用程式的讀和寫方面,我們可以優化每個方面的性能。CQRS社區的許多人都認同這一觀點,例如:

“CQRS告訴我,我可以分別優化讀和寫,而且我不必總是手動的反規範化到平面表中。”

  • Kelly Sommers - CQRS顧問

這在我們的實踐過程中得到了證實,當我們確實需要解決性能問題時,這種分離使我們受益匪淺。

在旅程的最後階段,測試揭示了應用程式中的一組性能問題。當我們研究它們時,發現它們與我們實現CQRS模式的方式關係不大,而與我們使用基礎設施的方式關係更大。發現這些問題的根源是困難的,由於應用程式中有如此多的活動部件,獲得正確的跟蹤和用於分析的正確數據是一項挑戰。一旦我們確定了瓶頸,修複它們就相對容易了,這主要是因為CQRS模式使您能夠清楚地分離系統的不同元素,比如讀和寫。儘管實現CQRS模式所導致的關註點分離會使識別問題變得更加困難,但是一旦您識別出一個問題,不僅更容易修複它,而且更容易防止它的重現。解耦的體繫結構使得編寫重現問題的單元測試更加簡單。

我們在處理系統中的性能問題時遇到的挑戰更多地是由於我們的系統是一個分散式的、基於消息的系統,而不是因為它實現了CQRS模式。

第7章“添加彈性和優化性能”提供了關於我們處理系統中性能問題的方法的更多信息,並對我們想要進行但沒有時間實現的額外更改提出了一些建議。

實現消息驅動系統遠非易事

我們這個項目的基礎設施是在旅程中根據需要開發它。我們沒有預料(也沒有預先警告)需要多少時間和精力來創建應用程式所需的健壯基礎設施。我們在許多開發任務上花費的時間至少是最初計劃的兩倍,因為我們持續發現與基礎設施相關的額外需求。特別是,我們從一開始就瞭解到擁有健壯的事件存儲是至關重要的。我們從經驗中得到的另一個關鍵思想是,消息匯流排上的所有I/O都應該是非同步的。

Jana(軟體架構師)發言:

雖然我們的事件存儲還不是生產環境完備的,但是如果您決定實現自己的事件存儲,那麼當前的實現很好地指示了應該處理的問題類型。

儘管我們的應用程式並不大,但它向我們清楚地說明瞭end-to-end跟蹤的重要性,以及幫助我們理解系統中所有消息流的工具的價值。第4章“擴展和增強訂單和註冊限界上下文”描述了測試在幫助我們理解系統方面的價值,並討論了由我們的顧問之一Josh Elster創建的消息傳遞中間語言(messaging intermediate language, MIL)。

Gary(CQRS專家)發言:

如果我們有一個用於消息傳遞的標準符號,就可以幫助我們與領域專家和核心團隊之外的人員溝通一些問題,這也會有所幫助。

總之,我們一路上遇到的許多問題都與CQRS模式沒有特定的關係,而是與我們解決方案的分散式、消息驅動特性更相關。

Jana(軟體架構師)發言:

我們發現,使用不同的Topic來傳輸由不同聚合發佈的事件,通過這樣來劃分服務匯流排有助於實現可伸縮性。有關更多信息,請參見第7章“添加彈性和優化性能”。另外,請參閱這些博客文章:“Microsoft Azure Storage Abstractions and their Scalability Targets”和“Best Practices for Performance Improvements Using Service Bus Brokered Messaging”。

使用雲帶來的挑戰

雖然雲提供了很多好處,比如可靠的、可伸縮的、現成的服務,您只需單擊幾下滑鼠就可以使用這些服務,但是雲環境也帶來了一些挑戰:

  • 您可能無法在任何您想要的地方使用事務,因為雲的分散式特性使得ACID(原子性、一致性、隔離性、持久性)事務在許多場景中不切實際。因此,您需要瞭解如何使用最終的一致性。例如,請參見第5章“準備發佈V1版本”,以及第7章“添加彈性和優化性能”中減少UI延遲的部分章節。
  • 您可能需要重新檢查關於如何將應用程式組織到不同層的假設。例如,參見第7章“添加彈性和優化性能”中關於進程內同步命令的討論。
  • 您不僅必須考慮瀏覽器或內部環境與雲之間的延遲,還必須考慮在雲中運行的系統的不同部分之間的延遲。
  • 您必須考慮到瞬時錯誤,並瞭解不同的雲服務可能如何實現節流。如果您的應用程式使用幾個可能被節流的雲服務,那麼您必須協調應用程式如何處理不同服務在不同時間進行節流。

Markus(軟體開發人員)發言:

我們發現,代碼中只有一個匯流排抽象,這掩蓋了這樣一個事實,即有些消息是在本地進程內處理的,有些消息是在不同的角色實例中處理的。要查看這是如何實現的,請查看ICommandBus介面以及CommandBusSynchronousCommandBusDecorator類。第七章“增加彈性和優化性能”包括了對SynchronousCommandBusDecorator類的討論。

備註:我們的Visual Studio解決方案中的多個構建配置是為部分解決這個問題而設計的,也幫助人們下載和使用代碼來快速入門。

CQRS是不同的

在我們的旅程開始時,有人警告我們,儘管CQRS模式看起來很簡單,但實際上它要求您在考慮項目的許多方面時進行重大的轉變。我們在旅途中的經歷再次證明瞭這一點。您必須準備拋棄許多假設和預先設想的想法,在開始充分理解從模式中獲得的好處之前,您可能需要先在幾個限界上下文中實現CQRS模式。

這方面的一個例子是最終一致性的概念。如果您來自關係資料庫背景,並且已經習慣了事務的ACID屬性,那麼在系統的所有級別上接受最終的一致性並理解其含義是一個很大的步驟。第5章“準備發佈V1版本”和第7章“添加彈性和優化性能”都討論了系統不同領域的最終一致性。

除了與您可能熟悉的不同之外,還沒有一種正確的方法來實現CQRS模式。由於我們對模式和方法的不熟悉,我們在功能塊上做了更多錯誤的開始,並且對所需的時間估計很差。隨著我們對這種方法越來越熟悉,我們希望能夠更快地確定如何在特定情況下實現模式,並提高我們估算的準確性。

Markus(軟體開發人員)發言:

CQRS模式在概念上很簡單,而細節才決定成敗。

我們花了一些時間來理解CQRS方法及其含義的另一種情況是在限界上下文之間的集成期間。第5章“準備發佈V1版本(https://www.cnblogs.com/angrymoto/p/cqrs-journey-day5.html)”詳細討論了團隊如何處理會議管理與訂單和註冊上下文之間的集成問題。這部分旅程揭示了一些額外的複雜性,當您使用事件作為集成機制時,這些複雜性與限界上下文之間的耦合級別有關。我們的假設是,事件應該只包含關於聚合或限界上下文中變化的信息,但事實證明這種假設是沒有幫助的,事件可以包含對一個或多個訂閱者有用的附加信息,並有助於減少訂閱者必須執行的工作量。

CQRS模式為如何劃分系統引入了額外的思考。您不僅需要考慮如何將系統劃分為層,還需要考慮如何將系統劃分為限界上下文,其中一些上下文將包含CQRS模式的實現。在旅程的最後階段,我們修改了關於層的一些假設,將一些處理從最初完成處理的工作者角色引入到web角色中。在第7章“增加彈性和優化性能”中討論瞭如何在進程中發送和處理命令。應該根據領域模型將系統劃分為限界上下文,每個限界上下文都有自己的領域模型和通用語言。一旦確定了限界上下文,就可以確定在哪些限界上下文中實現CQRS模式。這將影響如何以及在何處需要實現這些隔離限界上下文之間的集成。第二章“[分解領域]”介紹了我們對Contoso會議管理系統的所作的決策。

Gary(CQRS專家)發言:

單個進程(部署中的角色實例)可以承載多個限界上下文。在此場景中,您不需要為限界上下文使用服務匯流排來彼此通信。

實現CQRS模式比實現傳統的(創建、讀取、更新、刪除)CRUD風格的系統更複雜。對於這個項目,第一次學習CQRS和創建分散式、非同步消息傳遞基礎設施的開銷也很大。我們在此過程中的經驗清楚地向我們證實了為什麼CQRS模式不是頂級體繫結構。您必須確保實現基於CQRS的限界上下文相關的成本是值得的,通常,您將在高競爭、高協作的領域中看到CQRS模式的好處。

Gary(CQRS專家)發言:

分析業務需求、構建有用的模型、維護模型、用代碼表示它以及使用CQRS模式實現它都需要時間和金錢。如果這是您第一次實現CQRS模式,那麼您還需要對基礎設施元素(如消息匯流排和事件存儲)進行開銷投資。

事件源和事務日誌

對於事件源和事務日誌是否等同於同一件事,我們進行了一些討論:它們都創建了所發生事情的記錄,並且都允許您通過重播歷史數據來重新創建系統的狀態。結論是,事件的顯著特征是除了記錄所發生的事實之外,還能捕獲意圖。有關我們所說的意圖的更多細節,請參閱參考指南中的第4章“深入CQRS和ES”。

涉及到領域專家的

實現CQRS模式鼓勵領域專家的參與。該模式使您能夠將寫端上的領域和讀端上的報告需求分離出來,並將它們與基礎設施關註點分離開來。這種分離使領域專家更容易參與系統中他的專業知識最有價值的方面。使用領域驅動的設計概念,如限界上下文和通用語言,也有助於集中團隊的註意力,並促進與領域專家的清晰溝通。

我們的驗收測試證明是一種有效的方法,可以讓領域專家參與進來並獲取他的知識。第4章“擴展和增強訂單和註冊有界上下文”詳細描述了這種測試方法。

Jana(軟體架構師)發言:

作為一個副作用,這些驗收測試還有助於我們處理偽生產版本的快速發佈,因為它們使我們能夠在UI級別運行一組完整的測試,以驗證除單元測試和集成測試之外的系統行為。

除了幫助團隊定義系統的功能需求之外,領域專家還應該參與評估一致性、可用性、持久性和成本之間的權衡。例如,領域專家應該幫助確定什麼時候手動流程是可接受的,以及在系統的不同區域中需要什麼級別的一致性。

Gary(CQRS專家)發言:

開發人員傾向於將所有內容都鎖定到事務中,以確保完全的一致性,但有時並不值得這樣做。

何時使用CQRS

現在我們已經完成了我們的旅程,我們現在可以建議您應該評估的一些標準,以確定是否應該考慮在應用程式中的一個或多個限界上下文中實現CQRS模式。您能正面回答的問題越多,就越有可能將CQRS模式應用到給定的限界上下文中,從而使您的解決方案受益:

  • 限界上下文是否實現了業務功能的一個領域,這個領域是您的市場中的一個關鍵區別點?
  • 限界上下文本質上是否與可能在運行時具有高爭用級別的元素協作?換句話說,多個用戶是否會為了訪問相同的資源而競爭?
  • 限界上下文是否可能經歷不斷變化的業務規則?
  • 您是否已經具備了健壯的、可伸縮的消息傳遞和持久性基礎設施?
  • 可伸縮性是這個限界上下文面臨的挑戰之一嗎?
  • 限界上下文中的業務邏輯複雜嗎?
  • 您清楚CQRS模式將給這個限界上下文帶來的好處嗎?

Gary(CQRS專家)發言:

這些都是經驗法則,不是硬性規定。

如果我們重新開始,會有什麼不同?

本節是我們反思我們的旅程的結果,以及確定了一些我們想以不同方式去做的事情和一些我們希望追求的其他機會。如果在我們掌握了現在我們所瞭解的CQRS和ES知識之後重來一次的話。

從消息傳遞和持久性的堅實基礎設施開始

我們將從一個可靠的消息傳遞和持久性基礎設施開始。我們採取的方法是從簡單的先開始,並根據需要建立基礎設施,這意味著我們在旅程中積累了技術債務。我們還發現,採用這種方法意味著在某些情況下,我們對基礎設施的選擇影響了我們實現領域的方式。

Jana(軟體架構師)發言:

從旅行的角度來看,如果我們從一個堅實的基礎設施開始,我們將有時間處理領域中一些更複雜的部分,比如等待列表(Wating-list)。

從一個可靠的基礎設施開始也能使我們更早地開始性能測試。我們還將進一步研究其他人如何在基於CQRS的系統上進行性能測試,併在其他系統上尋找性能基準,比如Jonathan Oliver的EventStore

我們採取這種方法的原因之一是我們從顧問那裡得到的建議:“不要擔心基礎設施。”

更多地利用基礎設施的能力

從一個堅實的基礎設施開始也將允許我們更多地利用基礎設施的能力。例如,當我們發佈一個事件時,我們使用消息發起者的ID作為會話ID在Azure服務匯流排傳遞,但從系統處理事件的部分來看,這並不總是最好的使用會話ID的方式。

作為其中的一部分,我們還將研究基礎設施如何支持其他最終一致性的特殊情況,如時間一致性、單調一致性、“read my writes”和自我一致性。

我們想探討的另一個想法是使用基礎設施來支持版本之間的遷移。我們可以考慮使用基於消息的流程或實時通信流程來協調把新版本上線,而不是針對每個版本以特定的方式處理遷移。

採用更系統的方法來實現過程管理器

我們在旅程的早期就開始實現我們的過程管理器,並且仍然在強化它,並確保它的行為在旅程的最後階段是冪等的。同樣,從為流程管理人員提供一些堅實的基礎設施支持開始,使他們更有彈性,這將對我們有所幫助。但是,如果我們要重新開始,我們也會等到過程的後期再實現流程管理器,而不是直接開始。

在旅程的第一階段,我們開始實現RegistrationProcessManager類。第3章“訂單和註冊限界上下文”描述了初始實現。在旅程的每個後續階段,我們都對流程管理器進行了更改。

以不同的方式劃分應用程式

在項目開始時,我們會更仔細地考慮系統的分層。我們發現我們的劃分的方式是把應用程式分到web角色和工作者角色中,這在第4章“擴展和增強訂單和註冊限界上下文“中進行了描述。但這不是最優的,在旅程的最後階段,在第7章“增加彈性和優化性能”中,作為性能優化的一部分,我們對架構做了一些重大改變。

例如,在旅程的最後階段,作為重新組織的一部分,我們在web應用程式中引入了同步命令處理,同時引入了已存在的非同步命令處理。

以不同的方式組織開發團隊

我們學習CQRS模式的方法是迭代開發、回顧、討論,然後重構。但是,我們可以通過讓幾個開發人員在相同的特性上獨立工作,然後比較結果,從而學到更多。這可能揭示了更廣泛的解決方案和方法。

評估領域域和限界上下文是否適合使用CQRS模式

我們希望從一組更清晰的啟發開始(如本章前面概述的啟發),以確定特定的限界上下文是否會受益於CQRS模式。如果我們關註領域中更複雜的地方,比如等待列表(Wating-list),而不是訂單、註冊和支付的限界上下文,我們可能會學到更多。

性能計劃

我們將在旅程的早期處理性能問題。我們尤其要:

  • 提前設定明確的性能目標。
  • 在過程中更早地運行性能測試。
  • 使用更大更實際的負載。

我們沒有做任何性能測試,直到旅程的最後階段。有關我們發現的問題以及如何解決這些問題的詳細討論,請參見第7章“添加彈性和優化性能”。

在旅程的最後階段,我們在服務匯流排上引入了一些分區,以提高事件的吞吐量。此分區是基於事件的發佈者完成的,因此由同一個聚合類型發佈的事件將發佈到同一個Topic。我們希望把當前使用一個Topic的擴展到使用多個Topic,可能會基於消息中OrderID的hash進行分區(這種方法通常稱為分片)。這將為應用程式提供更大的擴展。

以不同的方式思考UI

我們認為UI與讀寫模型交互的方式,以及它處理最終一致性的方式都很好,並且滿足了業務需求。特別是,UI檢查預訂是否可能成功並相應地修改其行為的方式,以及UI允許用戶在等待更新讀模型時繼續輸入數據的方式。有關當前解決方案如何工作的更多細節,請參見第7章“添加彈性和優化性能”中的“優化UI”一節。

我們想研究除非絕對需要,其他避免在UI中等待的方法,比如使用瀏覽器推送技術。在某些地方,當前系統中的UI仍然需要等待針對讀模型的非同步更新。

探索事件源的一些額外好處

我們發現在旅程的第三階段,第5章“準備發佈V1版本”中,修改訂單和註冊限界上下文來使用事件有助於簡化這個限界上下文的實現,一部分是因為它已經使用了大量的事件。

在當前的旅程中,我們沒有機會進一步探索靈活性的承諾,以及從事件源中挖掘過去事件以獲得新的業務見解的能力。但是,我們確實確保系統保存了所有事件的副本(不僅僅是那些重建聚合狀態所需的副本)和命令,以便在將來啟用這些類型的場景。

Gary(CQRS專家)發言:

同樣有趣的是,通過事件源或其他技術(如資料庫事務日誌或SQL Server的StreamInsight特性)來挖掘過去的事件流以獲取新的業務洞察是否更容易實現?

探索關於限界上下文集成的相關問題

在我們的V3版本中,所有限界上下文都由同一個核心開發團隊實現。我們希望研究在實踐中,由不同開發團隊實現的限界上下文與現有系統集成起來有多容易。

這是您為學習經驗做出貢獻的一個很好的機會:繼續實現另一個限界上下文(請參閱產品backlog中的優秀用戶故事),將它集成到Contoso會議管理系統中,併在旅程的另一章中描述您的經驗。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 1. vue項目打包採坑 1.1. vue運行報錯error:Cannot assign to read only property 'exports' of object ' ' 這個錯誤我是在打包完部署到nginx上才會報的,在本地環境可以正常運行,真坑; 網上的資料說的報錯原因是export和 ...
  • 官網地址:http://www.bacubacu.com/colresizable/ 這裡值得註意的是,如果是動態加入的列,則需要先清理調用插件生成的class,id和div之後再重新調用才會有作用。 至於為何動態載入的列沒有效果呢。首先,我想到了可能是方法載入在了動態生成列之前,所以我便手動在生成 ...
  • 雙大括弧會將數據解釋為普通文本,而非 HTML 代碼。為了輸出真正的 HTML,你需要使用 v-html 指令,例如: 渲染結果為: <p>{{message}}</p>里的message被解釋為了普通文本,而不是輸出真正的 HTML,而<p v-html="message"></p>輸出了真正的h ...
  • 這12個問題,基本上就是HTML和CSS基礎中的重點難點了,也是必須要弄清楚的基本問題,其中定位的絕對定位和相對定位到底相對什麼定位?這個還是容易被忽視的,浮動也是一個大坑,有很多細節。 這12個知識點是我個人認為的,下麵我們就來看看這12個知識點。 1.怎麼讓一個不定寬高的 DIV,垂直水平居中? ...
  • SpringCloud系列教程 | 第六篇:Spring Cloud Config Github配置中心 Springboot: 2.1.6.RELEASE SpringCloud: Greenwich.SR1 如無特殊說明,本系列教程全採用以上版本 隨著分散式項目越來越大,勤勞的程式猿們會開始面臨 ...
  • extern可置於變數或者函數前,以表示變數或者函數的定義在別的文件中,提示編譯器遇到此變數和函數時在其他模塊中尋找其定義。 另外,extern也可用來進行鏈接指定。用法分析: 在一個變數前加extern 比如:extern unsigned int Test;編譯器編譯的時候,會把Test當成是外 ...
  • 工廠模式的使用場景、讓自己的代碼解耦更優雅。包含簡單工廠、工廠方法、抽象工廠。一文就夠了 ...
  • 為了更好地支持交易業務的快速發展,馬蜂窩支付中心從最初只支持基礎支付和退款的「刀耕火種」階段,經歷了架構調整的「刮骨療傷」階段,完成了到實現綜合產品平臺形態的「沉澱蓄力」階段的演進。 目前,馬蜂窩支付中心集成了包括基礎訂單、收銀台、路由管理、支付通道、清算核對、報表統計等多種能力,為馬蜂窩度假(平臺 ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...