DDD診所——聚合過大綜合癥

“DDD診所”是Thoughtworks DDD社區(qū)的一項(xiàng)活動(dòng),通過對(duì)同事們?cè)趯?shí)施DDD過程中遇到的問題進(jìn)行分析和解答克蚂,共同提高開發(fā)水平你辣。我們將其中一些典型案例整理成文供大家參考旋廷。之后也會(huì)考慮在適當(dāng)?shù)臅r(shí)候?qū)⑦@一形式對(duì)外部開放急前。

就診日期:2022年6月8日

患者:某DevOps平臺(tái)持續(xù)集成模塊

診金:0元(免費(fèi)義診)

【患者主訴】

疑似問題設(shè)計(jì)

某DevOps平臺(tái)需提供持續(xù)交付流水線設(shè)計(jì)功能,使用戶可在界面上規(guī)劃完整流程宅广。因此葫掉,流水線設(shè)計(jì)頁(yè)面功能繁雜,涵蓋階段規(guī)劃跟狱、前置觸發(fā)條件設(shè)置俭厚、質(zhì)量門禁控制等。隨著功能擴(kuò)展驶臊,頁(yè)面復(fù)雜度逐漸提升挪挤。此類功能旨在協(xié)助用戶優(yōu)化持續(xù)交付流水線管理,提升交付效能與質(zhì)量关翎。

持續(xù)交付流水線界面原

功能介紹:

  1. 設(shè)計(jì)不同的階段:用戶可以根據(jù)需要設(shè)置不同的階段扛门,例如開發(fā)階段、測(cè)試階段等纵寝。對(duì)于每個(gè)階段论寨,用戶可以設(shè)置不同的步驟,例如Checkout、編譯政基、構(gòu)建鏡像和部署等贞铣。

  2. 設(shè)計(jì)前置觸發(fā)條件:用戶可以選擇兩種觸發(fā)方式闹啦,一種是某個(gè)代碼倉(cāng)庫(kù)提交觸發(fā)沮明,另一種是定時(shí)任務(wù)。用戶可以設(shè)置多個(gè)前置觸發(fā)條件窍奋。

  3. 設(shè)置質(zhì)量門禁:用戶可以選擇給階段設(shè)置質(zhì)量門禁荐健,例如單元測(cè)試覆蓋率大于80%等。這些門禁指標(biāo)可以在質(zhì)量門禁管理功能中設(shè)置琳袄。在流水線執(zhí)行時(shí)江场,如果不滿足門禁指標(biāo),會(huì)阻止流水線進(jìn)入下一個(gè)階段窖逗。

項(xiàng)目架構(gòu)師在接到需求后址否,根據(jù)需求設(shè)計(jì)了一個(gè)名為“持續(xù)交付流水線領(lǐng)域模型”。該模型的目的是方便用戶在界面上完成一系列操作碎紊,如代碼質(zhì)量檢查佑附、編譯代碼、構(gòu)建鏡像和部署等仗考。這些操作被看作是一個(gè)整體音同,并被組織在一個(gè)叫做“流水線”的聚合中。這個(gè)聚合包括質(zhì)量門禁秃嗜、不同的階段和觸發(fā)規(guī)則等权均。這種設(shè)計(jì)旨在保證流水線中各個(gè)組成部分的一致性。

請(qǐng)注意锅锨,圖中的“<>”是一種自定義的衍生關(guān)系叽赊,意味著該對(duì)象映射自其他上下文。

持續(xù)交付流水線領(lǐng)域模型

團(tuán)隊(duì)按照這個(gè)模型落地了代碼必搞,隨著交付的深入必指,這個(gè)模型的缺點(diǎn)也浮現(xiàn)出來(lái)。

引發(fā)問題

1. 認(rèn)知負(fù)載上升

這個(gè)聚合包含了7個(gè)實(shí)體(不包括抽象類)顾画,每個(gè)實(shí)體都有自己相關(guān)的業(yè)務(wù)取劫,因此這個(gè)聚合的認(rèn)知負(fù)載相對(duì)較大。此外研侣,該部分業(yè)務(wù)需要集成不同的外部依賴系統(tǒng)谱邪,如Sonar(用于實(shí)現(xiàn)質(zhì)量門禁)、定時(shí)任務(wù)組件(用于定時(shí)任務(wù)觸發(fā)器)庶诡、GitLab或GitHub(用于代碼提交觸發(fā)器)等惦银。盡管可以通過使用Repository模式和依賴倒置原則來(lái)分離功能和實(shí)現(xiàn),但開發(fā)人員或維護(hù)人員仍需要掌握相關(guān)知識(shí)。如果某個(gè)人接手了這個(gè)功能(例如修改質(zhì)量門禁相關(guān)功能)扯俱,他/她基本上需要了解整個(gè)系統(tǒng)的工作方式书蚪。

2. 部分可用性難以實(shí)現(xiàn)

這個(gè)功能集成了多個(gè)第三方系統(tǒng),并被設(shè)計(jì)為一個(gè)聚合迅栅。由于這些功能中的任何一部分不可用時(shí)殊校,整個(gè)功能都將受到影響,因此難以實(shí)現(xiàn)部分可用性读存。例如为流,當(dāng)Sonar服務(wù)暫時(shí)不可用時(shí),代碼觸發(fā)和階段維護(hù)的部分也無(wú)法為用戶提供服務(wù)让簿。如果后續(xù)用戶提出了部分可用性需求敬察,要求Sonar不可用的情況下,要提示質(zhì)量門禁設(shè)置失敗尔当,但同時(shí)莲祸,其他部分仍需為用戶提供服務(wù),那么根據(jù)這個(gè)設(shè)計(jì)就很難實(shí)現(xiàn)了椭迎,只能推倒重建锐帜,成本非常高。

不幸的是侠碧,在設(shè)計(jì)過程中我們不知不覺地構(gòu)建了一個(gè)分布式單體抹估。分布式單體架構(gòu)是對(duì)一種設(shè)計(jì)糟糕的微服務(wù)架構(gòu)的戲稱。一般指那種由于架構(gòu)師沒有充分考慮和掌握分布式的優(yōu)勢(shì)和代價(jià)弄兜,憑感覺設(shè)計(jì)出的微服務(wù)架構(gòu)药蜻。這種架構(gòu)導(dǎo)致設(shè)計(jì)出的系統(tǒng)既不能享受分布式的彈性優(yōu)勢(shì),又丟失了單體服務(wù)易于實(shí)現(xiàn)ACID事務(wù)強(qiáng)一致性方面的便利替饿。通常出現(xiàn)在未經(jīng)良好設(shè)計(jì)的微服務(wù)風(fēng)格的分布式軟件中语泽。

3. 犧牲了性能和并發(fā)性

當(dāng)前的交互設(shè)計(jì)是用戶在設(shè)計(jì)界面對(duì)流水線的各部分進(jìn)行設(shè)計(jì),設(shè)計(jì)完成后按提交按鈕提交所有部分视卢。實(shí)際上踱卵,用戶在每次修改時(shí),不一定需要同時(shí)修改質(zhì)量門禁据过、階段惋砂、觸發(fā)規(guī)則等所有的部分。然而由于聚合模式的特點(diǎn)绳锅,我們每次都需要對(duì)整個(gè)聚合的所有實(shí)體進(jìn)行整存整取西饵。即使用戶只想修改其中一部分(如“觸發(fā)規(guī)則”),我們?nèi)孕枰旅麨椤傲魉€”的聚合的整個(gè)7個(gè)模型鳞芙,這將導(dǎo)致巨大的性能浪費(fèi)眷柔。此外期虾,如果兩個(gè)用戶同時(shí)操作業(yè)務(wù)上互不影響的兩部分如“觸發(fā)規(guī)則”和“質(zhì)量門禁”時(shí),會(huì)相互沖突驯嘱,有一個(gè)用戶要被提示“設(shè)計(jì)已變更镶苞,修改失敗”,降低了系統(tǒng)的吞吐量鞠评。

【診斷】

初步診斷茂蚓,患者的病情主要是聚合過大綜合癥,即聚合設(shè)計(jì)不合理谢澈,導(dǎo)致聚合過大煌贴。在DDD實(shí)踐中,合理劃分聚合是個(gè)比較有挑戰(zhàn)的問題锥忿。

聚合過大綜合癥的病理分析

在DDD的落地實(shí)踐過程中,聚合的大小經(jīng)常被描述為一個(gè)不可言說的知識(shí)怠肋。很多時(shí)候,憑經(jīng)驗(yàn)和感覺會(huì)導(dǎo)致比較差的設(shè)計(jì)。然而施逾,在實(shí)踐中邦鲫,識(shí)別大聚合仍有跡可循,一般來(lái)說杈抢,出現(xiàn)了這三種情況時(shí)数尿,就需要警惕聚合過大綜合癥:

1. 寬聚合

一個(gè)聚合聚合了多個(gè)同級(jí)實(shí)體,一個(gè)“父親”多個(gè) “兒子”惶楼。如圖所示:一旦超過三就有大聚合的風(fēng)險(xiǎn)右蹦。

2. 深聚合

聚合的深度過深,例如聚合根有兒子實(shí)體歼捐,也有孫子實(shí)體何陆,也有重孫實(shí)體。當(dāng)層級(jí)達(dá)到三層時(shí)豹储,就存在大聚合的風(fēng)險(xiǎn)贷盲。

3. 胖聚合

盡管結(jié)構(gòu)簡(jiǎn)單,但實(shí)體對(duì)象實(shí)例多剥扣。例如訂單和訂單行作為了一個(gè)聚合巩剖,而實(shí)際業(yè)務(wù)經(jīng)常出現(xiàn)有幾千個(gè)訂單行的訂單。這種也存在大聚合的風(fēng)險(xiǎn)钠怯。

聚合過大的三個(gè)征兆

正如該案例所表現(xiàn)的那樣佳魔,這是一個(gè)具有寬聚合和深聚合的聚合過大綜合癥癥狀。聚合過大綜合癥會(huì)導(dǎo)致一系列問題呻疹,例如認(rèn)知負(fù)荷增加吃引、部分可用性下降以及性能問題筹陵。而在該案例中,這些問題正是由聚合過大綜合癥所導(dǎo)致的镊尺。

【治療建議】

出現(xiàn)聚合過大綜合癥朦佩,大多數(shù)情況是由于聚合劃分不合理所導(dǎo)致的。為了解決這個(gè)問題庐氮,我們可以回顧《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)》一書中有關(guān)聚合模式的定義语稠,以了解如何合理地劃分聚合。

識(shí)別聚合的方法

在《領(lǐng)域驅(qū)動(dòng)設(shè)計(jì):軟件核心復(fù)雜性應(yīng)對(duì)之道》一書中弄砍,聚合的定義如下:

在具有復(fù)雜關(guān)聯(lián)的模型中仙畦,要想保證對(duì)象更改的一致性是很困難的。需要維護(hù)適用于密切相關(guān)的對(duì)象組的Invariant音婶,而不僅僅是離散的對(duì)象慨畸。然而,過于謹(jǐn)慎的鎖定機(jī)制又會(huì)導(dǎo)致多個(gè)用戶之間毫無(wú)意義地互相干擾衣式,從而使系統(tǒng)不可用寸士。

我們應(yīng)該將 Entity和 Value Object分門別類地聚集到Aggregate中并定義每Aggregate的邊界。在每Aggregate中碴卧,選擇一個(gè)Entity作為根弱卡,并通過根來(lái)控制對(duì)邊界內(nèi)其他對(duì)象的所有訪問。只允許外部對(duì)象保持對(duì)根的引用住册。對(duì)內(nèi)部成員的臨時(shí)引用可以被傳遞出去婶博,但僅在一次操作中有效。由于根控制訪問荧飞,因此不能繞過它來(lái)修改內(nèi)部對(duì)象凡人。這種設(shè)計(jì)有利于確保Aggregate中的對(duì)象滿足所有固定規(guī)則,也可以確保在任何狀態(tài)變化時(shí)Aggregate作為一個(gè)整體滿足固定規(guī)則垢箕。

根據(jù)聚合模式的定義划栓,我們可以得出判斷兩個(gè)實(shí)體(Entity)是否屬于一個(gè)聚合的依據(jù),需要把握兩個(gè)條件:

1. 存在整體部分關(guān)系

當(dāng)某個(gè)Entity是另一個(gè)Entity的部分時(shí)条获,稱為整體部分關(guān)系忠荞。例如:

  • 汽車輪胎是汽車的一部分
  • 學(xué)生是班級(jí)的一部分
  • 訂單行是訂單的一部分

2. 實(shí)體之間存在變更時(shí)需要遵守的固定規(guī)則(Invariants)

固定規(guī)則或稱為不變量不變式,來(lái)自契約式設(shè)計(jì)帅掘。

在計(jì)算機(jī)科學(xué)中委煤,不變量是指在計(jì)算機(jī)程序執(zhí)行的某一階段始終為真的邏輯論斷。例如修档,循環(huán)不變量是一個(gè)條件碧绞,在一個(gè)循環(huán)的每個(gè)迭代開始和結(jié)束時(shí)都是真的。
--- wiki <https://en.wikipedia.org/wiki/Invariant_(mathematics)>

在劃分聚合時(shí)吱窝,我們關(guān)注的是一些由業(yè)務(wù)原因所約束的固定規(guī)則讥邻。這些規(guī)則通常是通過與業(yè)務(wù)人員溝通迫靖,了解“A更新的時(shí)候,會(huì)不會(huì)引起B(yǎng)的某個(gè)屬性的更新”兴使,“如果兩個(gè)實(shí)體暫時(shí)不一致系宜,是否會(huì)導(dǎo)致難以承受的業(yè)務(wù)后果”等問題來(lái)得出的。

例如发魄,在訂單系統(tǒng)中盹牧,假設(shè)需求是整個(gè)訂單的總價(jià)等于訂單行的總價(jià)之和,且總價(jià)必須小于3000(左圖)励幼。在這種情況下汰寓,訂單與訂單行之間就存在固定規(guī)則。因此苹粟,可以把它們放在同一個(gè)聚合中有滑,并通過整存整取來(lái)維護(hù)這個(gè)固定規(guī)則。然而六水,如果業(yè)務(wù)場(chǎng)景是訂單僅作為訂單行的分組俺孙,用戶需要按照訂單行逐一結(jié)賬(右圖)那么訂單和訂單行就無(wú)需劃分成一個(gè)聚合。

固定規(guī)則是劃分聚合的重要條件

需要注意的是掷贾,固定規(guī)則中的一部分是由技術(shù)實(shí)現(xiàn)所約束的固定規(guī)則,例如數(shù)據(jù)庫(kù)ID不重復(fù)荣茫、訂單編號(hào)唯一等想帅。這些規(guī)則通常是全局性的固定規(guī)則,需要在整個(gè)系統(tǒng)范圍內(nèi)遵循啡莉。然而港准,由于這些全局性的固定規(guī)則通常與特定的業(yè)務(wù)邏輯無(wú)關(guān),因此在劃分聚合時(shí)咧欣,它們通常不是參考因素浅缸。

大聚合都必須拆小么?

盡管利用業(yè)務(wù)固定規(guī)則通常能夠確定較小的業(yè)務(wù)一致性邊界魄咕,從而得出比較合適的聚合規(guī)模衩椒,但并不是所有業(yè)務(wù)都適用這一原則。在有些業(yè)務(wù)場(chǎng)景中哮兰,大聚合可能是不可避免的毛萌。在這種情況下,如果想維護(hù)固定規(guī)則喝滞,是否采用聚合模式阁将,需要權(quán)衡使用大聚合帶來(lái)的成本是否可以接受。聚合模式是一種設(shè)計(jì)模式右遭,而非萬(wàn)能的解決方案做盅,在不適用的場(chǎng)景下強(qiáng)行應(yīng)用聚合可能會(huì)導(dǎo)致收益不成正比缤削。

聚合模式是為了解決復(fù)雜業(yè)務(wù)中的一致性問題,將具備固定規(guī)則的一組對(duì)象整存整取的一種方案吹榴。采用聚合模式的優(yōu)點(diǎn)在于比較簡(jiǎn)單地就能實(shí)現(xiàn)固定規(guī)則約束亭敢,代價(jià)就是整存整取帶來(lái)的性能損失等問題。

【治療方案】

在案例中腊尚,盡管流水線各部分之間存在整體部分關(guān)系吨拗,通過對(duì)業(yè)務(wù)進(jìn)行分析,我們確定了以下固定規(guī)則:

  1. 階段內(nèi)的步驟之間存在嚴(yán)格的順序依賴婿斥,因?yàn)橄乱粋€(gè)步驟通常依賴上一個(gè)步驟的產(chǎn)出物劝篷。因此,存在一個(gè)固定規(guī)則:某個(gè)步驟的執(zhí)行順序必須按照階段的步驟列表中的順序關(guān)系民宿。
  2. 階段質(zhì)量門禁和門禁項(xiàng)之間存在固定規(guī)則娇妓,即當(dāng)在門禁項(xiàng)發(fā)生變更時(shí),門禁版本也必須隨之更新門禁的版本有一定的業(yè)務(wù)含義(例如活鹰,門禁版本更新需要在界面上進(jìn)行明確提示)哈恰。

整改后的聚合

因此,根據(jù)上述整改方案志群,原本的一個(gè)大聚合被拆分成了四個(gè)小聚合着绷,每個(gè)聚合只包含少量實(shí)體。這種改動(dòng)可以有效地降低聚合的規(guī)模锌云,從而更好地平衡性能和業(yè)務(wù)一致性之間的關(guān)系荠医。

【總結(jié)】

在領(lǐng)域驅(qū)動(dòng)設(shè)計(jì)實(shí)踐中,聚合的劃分確實(shí)是一個(gè)難以把握的問題桑涎。聚合本身是一種有代價(jià)的模式彬向,不合理的聚合劃分可能導(dǎo)致嚴(yán)重的問題,從而引發(fā)一系列疑問攻冷,例如“為什么使用DDD后仍然遇到各種問題娃胆?”聚合過大綜合癥是聚合劃分中的一種常見問題,當(dāng)模型中出現(xiàn)寬聚合等曼、深聚合或胖聚合時(shí)里烦,我們需要警惕聚合過大綜合癥及其帶來(lái)的難以維護(hù)、犧牲可用性和性能損失等問題涉兽。

要解決這個(gè)問題招驴,我們需要回顧聚合解決的核心問題:如何維護(hù)對(duì)象之間的固定規(guī)則。再次考慮聚合的劃分時(shí)枷畏,需要滿足兩個(gè)條件:整體-部分關(guān)系和實(shí)體間需要遵循的固定規(guī)則别厘。當(dāng)使用聚合模式的代價(jià)過大時(shí),可以考慮其他方法來(lái)實(shí)現(xiàn)拥诡,例如通過鎖機(jī)制鎖定需要維護(hù)一致性的對(duì)象方法触趴。

總之氮发,在實(shí)踐DDD時(shí),我們應(yīng)該關(guān)注聚合的劃分和優(yōu)化冗懦,以確保在保持業(yè)務(wù)一致性和完整性的同時(shí)爽冕,避免因聚合過大導(dǎo)致的性能和可用性問題。在面臨聚合模式代價(jià)過大的情況時(shí)披蕉,可以靈活選擇其他方法來(lái)實(shí)現(xiàn)業(yè)務(wù)一致性和完整性颈畸。


文/Thoughtworks 付施威
原文鏈接:DDD之聚合過大-Thoughtworks洞見

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市没讲,隨后出現(xiàn)的幾起案子眯娱,更是在濱河造成了極大的恐慌,老刑警劉巖爬凑,帶你破解...
    沈念sama閱讀 206,013評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件徙缴,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡嘁信,警方通過查閱死者的電腦和手機(jī)于样,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,205評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)潘靖,“玉大人穿剖,你說我怎么就攤上這事∝砸纾” “怎么了携御?”我有些...
    開封第一講書人閱讀 152,370評(píng)論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)既绕。 經(jīng)常有香客問我,道長(zhǎng)涮坐,這世上最難降的妖魔是什么凄贩? 我笑而不...
    開封第一講書人閱讀 55,168評(píng)論 1 278
  • 正文 為了忘掉前任,我火速辦了婚禮袱讹,結(jié)果婚禮上疲扎,老公的妹妹穿的比我還像新娘。我一直安慰自己捷雕,他們只是感情好椒丧,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,153評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著救巷,像睡著了一般壶熏。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上浦译,一...
    開封第一講書人閱讀 48,954評(píng)論 1 283
  • 那天棒假,我揣著相機(jī)與錄音溯职,去河邊找鬼。 笑死帽哑,一個(gè)胖子當(dāng)著我的面吹牛谜酒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播妻枕,決...
    沈念sama閱讀 38,271評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼僻族,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了屡谐?” 一聲冷哼從身側(cè)響起述么,我...
    開封第一講書人閱讀 36,916評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎康嘉,沒想到半個(gè)月后碉输,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,382評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亭珍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,877評(píng)論 2 323
  • 正文 我和宋清朗相戀三年敷钾,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肄梨。...
    茶點(diǎn)故事閱讀 37,989評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阻荒,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出众羡,到底是詐尸還是另有隱情侨赡,我是刑警寧澤,帶...
    沈念sama閱讀 33,624評(píng)論 4 322
  • 正文 年R本政府宣布粱侣,位于F島的核電站羊壹,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏齐婴。R本人自食惡果不足惜油猫,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,209評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望柠偶。 院中可真熱鬧情妖,春花似錦、人聲如沸诱担。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,199評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)蔫仙。三九已至料睛,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背秦效。 一陣腳步聲響...
    開封第一講書人閱讀 31,418評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工雏蛮, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人阱州。 一個(gè)月前我還...
    沈念sama閱讀 45,401評(píng)論 2 352
  • 正文 我出身青樓挑秉,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親苔货。 傳聞我的和親對(duì)象是個(gè)殘疾皇子犀概,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,700評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容