如何提高企業(yè)的研發(fā)效率--trunk-based development

image

企業(yè)的研發(fā)團隊在研發(fā)產(chǎn)品功能時通常會選擇2種分支管理策略,它們分別是Feature Branches Development和Trunk-based Development间护。2種分支管理策略都有它們適用的場景企蹭,比如在github上研發(fā)開源軟件時白筹,經(jīng)常會使用Feature Branches Development模式智末,而Google,F(xiàn)acebook徒河,LinkedIn系馆,微軟常常會使用Trunk-based Development模式。企業(yè)在實施CI(持續(xù)集成)時通常需要Trunk-based Development方面的實踐顽照,原因在于這種模式能夠快速輸出集成的結(jié)果由蘑。本文將圍繞Trunk-based Development展開,并提供一些可實施該模式的操作步驟代兵。

  1. 什么是Trunk-based Development尼酿?
  2. 團隊需要掌握哪些技巧來實踐Trunk-based Development?
  3. 為Trunk-Based Development配套CI服務
  4. Trunk-Based Development的實施細節(jié)
  5. 使用github來實施Trunk-Based Development的基本思路
  6. 結(jié)論和參考

什么是Trunk-based Development植影?

Trunk-based Development是指:所有研發(fā)人員圍繞主分支trunk(也就是github上的master分支)來共同研發(fā)裳擎,在研發(fā)過程中拒絕創(chuàng)建存活時間較長的分支,并使用Feature Toggles和Branch by Abstraction等技術(shù)在主分支上逐步發(fā)布需要長時間(通常是1周)才能研發(fā)完成的功能思币。官方對Trunk-based Development的概括如下所示:

A source-control branching model, where developers collaborate on code in a single branch called ‘trunk’ *, resist any pressure to create other long-lived development branches by employing documented techniques. They therefore avoid merge hell, do not break the build, and live happily ever after.

上面這段描述說明了Trunk-based Development的目的在于解決合并和持續(xù)構(gòu)建的問題鹿响。為了理解Trunk-based Development是如何解決以上2個問題的,需要從下面這張圖說起谷饿。下圖展示了:采用Trunk-based development來進行軟件研發(fā)時所涉及的一系列活動:

image

上圖規(guī)定了以下規(guī)則:

  1. 所有研發(fā)人員直接在trunk上提交代碼
  2. 對外發(fā)布產(chǎn)品的時候需要從trunk上拉取release分支(比如1.1.x和1.2.x)惶我,并基于release分支來發(fā)布(比如1.1.0和1.1.1)
  3. release分支中出現(xiàn)的bug或者需要性能優(yōu)化時,則需要在trunk上完成各墨,并通過cherry-pick的方式在trunk中挑選對應的commits合并到release分支指孤,此時的小版本號從1.1.0變成1.1.1
  4. 對外發(fā)布新功能時,需要基于trunk分支贬堵,重新拉取release分支,版本號從1.1.x變成1.2.x结洼,同時1.1.x的release分支將被廢棄
  5. trunk分支上黎做,每個commit之間的提交間隔很短,通常在一天之內(nèi)提交好幾次松忍,甚至更多次

在這些規(guī)則之下蒸殿,研發(fā)者可以持續(xù)地在trunk分支中提交代碼较木,而且期間沒有合并嘿棘。為了能夠使得每一次提交都能夠順利地在trunk分支中通過诬烹,則需要一些技巧和CI服務器裁赠。接下來讓我們看看都有哪些技巧能夠使得企業(yè)成功地實施Trunk-based Development糜芳。

團隊需要掌握哪些技巧來實踐Trunk-based Development仇祭?

為了使Trunk-based Development能夠在研發(fā)團隊中順利開展起來醋奠,需要團隊成員掌握以下技巧贱纠,并且達成共識莫换。

  1. 將任務劃分成許多可以在1天以內(nèi)完成的小模塊

快速驗證想法的第一步就是將一個任務分解成多個可測試的子任務霞玄,并逐步實現(xiàn)骤铃。完成這些子任務所需的時間不應該超過一天,這么做的原因是每次完成子任務所需要的改動影響范圍較小坷剧,而且這些改動被提交之后惰爬,其他團隊成員能夠及時看到,從而使得團隊作為整體惫企,清楚產(chǎn)品的研發(fā)狀況撕瞧。

  1. 針對研發(fā)的功能編寫自動化測試用例,并在本地驗證

團隊中的每一名研發(fā)人員都應該針對自己的研發(fā)任務來編寫對應的單元測試狞尔,并在研發(fā)結(jié)束之后风范,在本地運行這些單元測試來驗證正在研發(fā)的功能。除此之外沪么,在開始研發(fā)時需要從trunk獲取最新代碼硼婿,并在本地運行已有的單元測試,確保拿到的代碼是正常的禽车。在設計單元測試時寇漫,須遵守的原則是每一個單元測試能夠獨立運行并且能夠在短時間內(nèi)運行結(jié)束(通常在本地執(zhí)行所有單元測試所需的時間不應該超過5分鐘)。在獲取或提交代碼時殉摔,在本地運行單元測試的原因是確保拿到的或者即將提交的代碼能夠正常工作州胳,從而降低了破壞trunk的風險(trunk分支不穩(wěn)定將會阻礙在這個分支上工作的所有人)。

  1. 執(zhí)行實時Code Review

當你研發(fā)的功能在本地驗證通過之后逸月,下一步就是尋找團隊中其他人幫忙Code Review栓撞。通常大家習慣發(fā)起一個Pull Request,然后等待團隊中的其他人進行Code Review碗硬。由于各種原因(比如沒有及時看到這個Pull Request)瓤湘,這個Code Review的過程將變得漫長起來。因此恩尾,在實踐Trunk-based Development時弛说,需要實時進行Code Review,以便縮短Code Review所需的時間翰意,進而能夠及時將改動推送到trunk分支木人。這種實時Code Review的做法有很多種,比如讓你身后的團隊成員到你的電腦前幫忙Code Review冀偶,同時你可以給他解釋為什么要這么做醒第。

  1. 使用branch by abstraction或者feature flags等技術(shù)來逐步提交還在研發(fā)中的功能

團隊在研發(fā)的日常中,總是會遇到一些復雜且耗時的任務进鸠,完成這些任務需要一周或是更長的時間稠曼。因此在處理這些任務時不僅需要將其分解成多個子任務,而且每次完成子任務的研發(fā)時都需要將其提交到trunk分支中并且通過branch by abstraction或者feature flags等技術(shù)禁用這些子功能堤如。直到所有子功能完成并且放在一起能夠工作時才將這個任務開放出來蒲列。應用branch by abstraction或者feature flags等技術(shù)時應該遵守簡單的原則窒朋,比如可以在代碼中為該任務編寫一個入口函數(shù),但是這個入口函數(shù)沒有被調(diào)用蝗岖。

  1. 每次向trunk提交代碼時侥猩,都應該自動地觸發(fā)編譯和測試

為了能夠自動化編譯和測試新的提交,需要借助CI服務器抵赢。通過CI服務器構(gòu)建編譯和測試2個階段(如下圖所示)欺劳。這么做的目的是確保每一次提交都能夠被機器自動化的驗證,從而確保每一次提交都沒有破壞trunk铅鲤。

image
  1. 如果某一次提交破壞了trunk分支划提,那么應該停下手中的任務,優(yōu)先恢復trunk分支

團隊在日常的研發(fā)事務中總是會犯錯邢享,如果一次疏忽導致trunk分支無法通過測試鹏往,那么需要第一時間解決這個問題。如果這個問題無法快速解決骇塘,那么需要將此次提交撤銷伊履,并回退到上一次提交。這么做就是要確保trunk分支隨時可用款违。

Trunk-Based Development自身并無沒有給團隊帶來任何好處唐瀑。為團隊帶來好處的是在Trunk-Based Development中應用了以上6點技巧。因此在企業(yè)研發(fā)部門實施Trunk-Based Development時插爹,需要團隊中的每一名成員都要掌握以上技巧哄辣,最終養(yǎng)成習慣。從這個角度來看赠尾,Trunk-Based Development更多的是依賴于人的行為力穗,在一致的行為下應用自動化工具能夠從整體上提高企業(yè)的研發(fā)效率!

當研發(fā)人員都掌握了以上技巧萍虽,同時睛廊,企業(yè)的研發(fā)部門已經(jīng)決定使用Trunk-Based Development來研發(fā)產(chǎn)品杉编,那么接下來就需要搭建一些自動化基礎(chǔ)設施來輔助整個研發(fā)團隊救军,其中CI服務就是構(gòu)建現(xiàn)代化高效軟件研發(fā)流程的初始環(huán)節(jié)财异。

為Trunk-Based Development配套CI服務

CI(Continues Integration)是指將各個研發(fā)團隊的研發(fā)成果正確且快速地集成在一起,并提供給其他團隊(測試團隊唱遭、DevOps團隊等)使用戳寸。為了將Trunk-Based Development向整個研發(fā)部門推廣,則需要一個好的CI服務拷泽。每一個提交到trunk上的改動疫鹊,都會自動地觸發(fā)CI服務,并由該服務獲取trunk上的源碼并順序執(zhí)行自定義的一些步驟司致。這些步驟有編譯該源碼和執(zhí)行單元測試拆吆,每一步執(zhí)行結(jié)束后都會輸出一些結(jié)果,這些結(jié)果有成功或者失敗脂矫,如果失敗則會出現(xiàn)失敗的信息枣耀。為了能夠讓團隊成員及時看到失敗的結(jié)果,一種做法是將在團隊周圍放置一臺大電視庭再,用于顯示CI服務的執(zhí)行結(jié)果捞奕。

除了要搭建CI服務,還需要應用一些發(fā)布策略佩微。比如上圖從trunk分支中拉出release分支缝彬,這么做是為了基于release分支對外發(fā)布產(chǎn)品,同時團隊的其他成員依然能夠在trunk上提交代碼哺眯。由于release分支主要是為了對外發(fā)布產(chǎn)品谷浅,因此它不僅需要CI的支持,還需要CD(Continuous Delivery)的支持奶卓,2者結(jié)合就是CI/CD一疯。與trunk不同,CI服務不僅需要監(jiān)測release分支的變化并自動地編譯源碼夺姑、執(zhí)行單元測試墩邀,而且還需要將編譯的結(jié)果歸檔到團隊內(nèi)部共享的存儲服務上,并自動地觸發(fā)CD服務盏浙,使得CD服務能夠?qū)⒕幾g結(jié)果從存儲服務中自動地部署到研發(fā)環(huán)境(dev)眉睹、測試環(huán)境(test)、預生產(chǎn)環(huán)境(stage)和生產(chǎn)環(huán)境(prod)废膘。環(huán)境越多竹海,實施自動化部署將任務也將增多,因此企業(yè)需要結(jié)合自身的現(xiàn)實狀況來決定哪些環(huán)境是需要的(比如大多數(shù)企業(yè)只需要stage和prod環(huán)境就足夠了)丐黄。

Trunk-Based Development的實施細節(jié)

不同企業(yè)在研發(fā)團隊中實施Trunk-Based Development都會有一些細微的差別斋配,對于大多數(shù)需要研發(fā)軟件產(chǎn)品的企業(yè),可以參考以下步驟在研發(fā)團隊中實施Trunk-Based Development。

  • 將產(chǎn)品相關(guān)的代碼放到一個repository里艰争,并且嚴格要求這個repository的分支數(shù)量每天不能超過研發(fā)人數(shù)(比如該研發(fā)團隊有5個研發(fā)坏瞄,2個測試,1個PO甩卓,1個UX鸠匀,1個SM以及1個架構(gòu)師,一共11個人)
  • 該repository有一個長期存在的分支trunk或者master猛频。當需要對外發(fā)布的時候則需要拉出release分支狮崩,當有新的功能要發(fā)布的時候,將該分支刪除并拉取新的release分支鹿寻。研發(fā)人員可以直接在trunkmaster分支上提交代碼睦柴,當然也可以拉取feature分支,但是feature分支的生命周期應該在1天之內(nèi)
  • 搭建CI服務毡熏,比如可以考慮使用Jenkins或者使用github的Actions坦敌。前者需要自己搭建,工作量大痢法,服務器可以是自建狱窘,也可以使用云服務提供商的服務器(比如阿里云或AWS)。后者只需要編寫.yaml文件就可以構(gòu)建CI服務财搁,服務器是github提供的
  • CI服務器會檢測trunkrelease分支蘸炸,每個次提交,都會觸發(fā)CI服務器尖奔,構(gòu)建代碼和執(zhí)行自動化測試搭儒。trunk分支所對應的CI構(gòu)建流程,其運行一次所需要的時間需要控制在30分鐘之內(nèi)提茁,其目的是為了檢測每次提交都是正常的淹禾。release分支所對應的CI構(gòu)建流程,其運行一次所需要的時間也需要控制在30分鐘之內(nèi)并歸檔編譯出來的結(jié)果茴扁,除此之外還需為該分支搭建CD服務铃岔。CD服務能夠?qū)⒕幾g出來的結(jié)果自動部署到其它環(huán)境(比如stage和prod)
  • 為團隊的每一個研發(fā)人員預留時間學習和掌握之前提到的技巧,使得團隊成員達成共識
  • 每次發(fā)布只能通過release分支峭火,修復bug和性能優(yōu)化的改動應該提交到trunk分支毁习,最終通過cherry-pick的方式將這些提交merge到release分支。當有新功能對外發(fā)布時卖丸,需要刪除原來的release分支蜓洪,并從trunk分支拉取新的release分支

使用github來實施Trunk-Based Development的基本思路

github是一個非常有影響力的程序員社交平臺。這個平臺不僅向全世界的開發(fā)者提供了線上交流的平臺坯苹,而且提供了方便開發(fā)者研發(fā)軟件所需的功能。其中摇天,有2個功能非常流行粹湃,一個是代碼托管和Actions服務恐仑,而且這2個服務是免費的!這2個服務使得任何研發(fā)團隊都可以快速搭建現(xiàn)代化的CI/CD流程为鳄。

github提供了大多數(shù)功能給開發(fā)者使用裳仆,這些功能有:賬號管理、源碼托管孤钦、Action服務(也就是CI/CD)歧斟、文件存儲和協(xié)作溝通的線上通道。接下來我將介紹在github上實踐Trunk-Based Development的基本思路偏形。在掌握這些思路之后静袖,讀者需要進一步參考這篇文章<如何0成本在github上構(gòu)建CI>來實踐Trunk-Based Development。

  1. 在github上創(chuàng)建一個repository俊扭,這個repository用于放置產(chǎn)品的源碼
  2. masterrelease分支創(chuàng)建對應的CI服務队橙,也就是在.github/workflows目錄中創(chuàng)建2個文件:master.ymlrelease.yml。每個文件定義了一個workflow萨惑,每個workflow定義了觸發(fā)條件和一些執(zhí)行步驟捐康。master.yml針對master分支定義了build,test步驟庸蔼,每次提交到該分支都會執(zhí)行build和test步驟解总;release.yml針對release分支定義了build,test姐仅,archive花枫,每次在release分支上打tag(比如v0.0.1)都會在該分支上執(zhí)行build,test和archive步驟萍嬉。將創(chuàng)建的master.ymlrelease.yml文件推送到該repository上
  3. 將每一個參與研發(fā)的人員加入到這個repository中乌昔,并授予他們可讀寫的權(quán)限
  4. 研發(fā)人員可以直接在master分支上提交,也可以拉出feature分支進行修改壤追,最終合并到master分支磕道,但是這個feature分支的生命周期不能超過1天。當研發(fā)人員在master分支上提交代碼后行冰,會自動觸發(fā)master.yml所定義的workflow溺蕉。該workflow將執(zhí)行build和test步驟來確保master分支是正常的
  5. 如果使用feature分支,則會用到github的pull request功能悼做。這個功能可以幫助團隊成員進行Code Review疯特。當某一名成員發(fā)起pull request時,同樣也會觸發(fā)master.yml所定義的workflow肛走。團隊的其他成員則可以在pull request的操作面板上提交意見漓雅,查看此次發(fā)起提交的運行結(jié)果
  6. 當需要對外發(fā)布的時候,那么可以拉取另外一個分支release,然后對該分支打上tag(比如v0.0.1)邻吞,此時release.yml所定義的workflow會啟動组题,該workflow除了執(zhí)行步驟build和test,最終還要執(zhí)行archive步驟抱冷。執(zhí)行archive的時候會把生成的結(jié)果發(fā)布到github的release存儲崔列。release存儲是對外開放的,任何人都可以到release存儲獲取并使用你發(fā)布的產(chǎn)品
  7. 當發(fā)布的產(chǎn)品有bug的時(比如功能缺陷旺遮,性能差勁等)赵讯,可以通過cherry-pick從master分支選取對應的commit合并到release分支。當已知的bug都修復了耿眉,那么在release分支上打上一個tag(比如v0.0.2)边翼,此時會觸發(fā)release.yml所定義的workflow
  8. 當進入下一次迭代并準備好發(fā)布新功能的時候,那么需要把之前的release分支刪除跷敬,并且再從分支master拉出新的release分支讯私,此時產(chǎn)品的版本號應該變成v0.1.x

通過以上思路便可以在github上創(chuàng)建一個高效的基于Trunk-Based Development的CI流程(如下圖所示),而且它是免費且適用于全世界的開發(fā)者的西傀。

image

當以上CI流程搭建完畢之后斤寇,研發(fā)人員只需要向master分支提交代碼,此時Actions服務就會自動執(zhí)行build和test步驟來驗證此次提交是正確的(通過master.yml所定義的workflow來保證)拥褂。

當研發(fā)人員在release分支打上tag的時候(比如v0.0.1或者v0.0.2)娘锁,此時Actions服務會自動執(zhí)行build、test和archive步驟(通過release.yml所定義的workflow來保證)饺鹃,其中archive步驟會把生成的結(jié)果發(fā)布到github的release存儲中莫秆。

為了能夠更好地理解以上過程,我將在這篇文章<如何0成本在github上構(gòu)建CI>中悔详,通過一個具體的例子來說明如何基于github搭建CI流程镊屎。

結(jié)論和參考

Trunk-based Development已經(jīng)被各大公司成功實踐了十幾年,這些公司有Google茄螃、Facebook缝驳、LinkedIn等。企業(yè)在研發(fā)產(chǎn)品時归苍,想要在研發(fā)部門中順利地實施Trunk-based Development用狱,還需要掌握一些技巧和搭建自動化基礎(chǔ)設施。這些技巧需要所有研發(fā)人員達成共識拼弃,并養(yǎng)成習慣夏伊。

當習慣形成之后,則需要借助一些自動化基礎(chǔ)設施來加速研發(fā)流程吻氧,這個研發(fā)流程就是CI/CD溺忧。目前有許多成熟的工具提供CI的支持咏连,比如我們可以免費使用github提供的服務來快速搭建CI流程(讀者可以參考這篇文章<如何0成本在github上構(gòu)建CI>)。

當研發(fā)流程搭建起來之后砸狞,則需要應用一些發(fā)布策略捻勉。一切就緒之后,整個研發(fā)團隊的研發(fā)效率將會達到一個質(zhì)的飛躍刀森。在研發(fā)團隊中實施Trunk-based Development以及為其搭建CI服務只是構(gòu)建企業(yè)級軟件研發(fā)流程的第一步。當新功能完成研發(fā)报账,并準備好發(fā)布的時候研底,也需要一套基礎(chǔ)設施將研發(fā)好的功能及時高效地發(fā)布到用戶現(xiàn)場,這就是Continuous Delivery(CD)透罢。企業(yè)要想搭建完整的CI/CD流程榜晦,除了實施本文提到CI部分,還需要參考這篇文章<如何提高企業(yè)的研發(fā)效率--CI/CD>來實施CD部分羽圃。

本文的內(nèi)容參考了大量國外的資料乾胶,這些資料可以進一步加深讀者對Trunk-based development的理解,讀者可以根據(jù)自身的實際情況進一步學習國外最新的技術(shù)朽寞,并將其應用到自己的項目當中识窿。

  1. Trunk-Based Development
  2. Feature Toggles
  3. Branch By Abstraction
  4. Why I love Trunk Based Development
  5. Enabling Trunk Based Development with Deployment Pipelines
  6. Google DevOps tech
  7. The best branching model to work with Git
  8. What is Trunk-Based Development?
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市脑融,隨后出現(xiàn)的幾起案子喻频,更是在濱河造成了極大的恐慌,老刑警劉巖肘迎,帶你破解...
    沈念sama閱讀 218,386評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甥温,死亡現(xiàn)場離奇詭異,居然都是意外死亡妓布,警方通過查閱死者的電腦和手機姻蚓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,142評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來匣沼,“玉大人狰挡,你說我怎么就攤上這事「刂” “怎么了圆兵?”我有些...
    開封第一講書人閱讀 164,704評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長枢贿。 經(jīng)常有香客問我殉农,道長,這世上最難降的妖魔是什么局荚? 我笑而不...
    開封第一講書人閱讀 58,702評論 1 294
  • 正文 為了忘掉前任超凳,我火速辦了婚禮愈污,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘轮傍。我一直安慰自己暂雹,他們只是感情好,可當我...
    茶點故事閱讀 67,716評論 6 392
  • 文/花漫 我一把揭開白布创夜。 她就那樣靜靜地躺著杭跪,像睡著了一般。 火紅的嫁衣襯著肌膚如雪驰吓。 梳的紋絲不亂的頭發(fā)上涧尿,一...
    開封第一講書人閱讀 51,573評論 1 305
  • 那天,我揣著相機與錄音檬贰,去河邊找鬼姑廉。 笑死,一個胖子當著我的面吹牛翁涤,可吹牛的內(nèi)容都是我干的桥言。 我是一名探鬼主播,決...
    沈念sama閱讀 40,314評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼葵礼,長吁一口氣:“原來是場噩夢啊……” “哼号阿!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起章咧,我...
    開封第一講書人閱讀 39,230評論 0 276
  • 序言:老撾萬榮一對情侶失蹤倦西,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后赁严,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體扰柠,經(jīng)...
    沈念sama閱讀 45,680評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,873評論 3 336
  • 正文 我和宋清朗相戀三年疼约,在試婚紗的時候發(fā)現(xiàn)自己被綠了卤档。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,991評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡程剥,死狀恐怖劝枣,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情织鲸,我是刑警寧澤舔腾,帶...
    沈念sama閱讀 35,706評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站搂擦,受9級特大地震影響稳诚,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜瀑踢,卻給世界環(huán)境...
    茶點故事閱讀 41,329評論 3 330
  • 文/蒙蒙 一扳还、第九天 我趴在偏房一處隱蔽的房頂上張望才避。 院中可真熱鬧,春花似錦氨距、人聲如沸桑逝。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,910評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽楞遏。三九已至醉鳖,卻和暖如春韭邓,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背台妆。 一陣腳步聲響...
    開封第一講書人閱讀 33,038評論 1 270
  • 我被黑心中介騙來泰國打工沙廉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人臼节。 一個月前我還...
    沈念sama閱讀 48,158評論 3 370
  • 正文 我出身青樓撬陵,卻偏偏與公主長得像,于是被迫代替她去往敵國和親网缝。 傳聞我的和親對象是個殘疾皇子巨税,可洞房花燭夜當晚...
    茶點故事閱讀 44,941評論 2 355