2019-02-13良好的 API 設計指南-RESTful API

要說RESTful首先來說說REST – REpresentational State Transfer (表述性狀態(tài)傳遞)

表述性狀態(tài)轉移是一組架構約束條件和原則强岸。滿足這些約束條件和原則的應用程序或設計就是RESTful。需要注意的是卵洗,REST是設計風格而不是標準请唱。

以上的概念大概是許多關于RESTful中都會出現的定義概念弥咪。

那么什么是表述性狀態(tài)轉移呢?

首先十绑,之所以晦澀是因為前面主語被去掉了聚至,全稱是 Resource Representational State Transfer,通俗來講就是本橙,資源在網絡中以某種表現形式進行狀態(tài)轉移扳躬。

在查詢很多資料后看到一句很精簡的總結:

URL定位資源,用HTTP動詞(GET,POST,DELETE,PUT等)描述操作甚亭。

既然說到了是用HTTP動詞進行操作贷币。那么需要了解這里列出的4.5個非常重要的HTTP動作,這里的0.5個是指PATCH亏狰,因為它在功能上與PUT非常類似役纹,剩下4個通常被API開發(fā)人員兩兩結合使用

GET(SELECT):從服務器獲取一個指定資源或一個資源集合;

POST(CREATE):在服務器上創(chuàng)建一個資源暇唾;

PUT(UPDATE):更新服務器上的一個資源促脉,需要提供整個資源;

PATCH(UPDATE):更新服務器上的一個資源策州,只提供資源中改變的那部分屬性瘸味;

DELETE(DELETE):移除服務器上的一個資源;

還有兩個不常見的HTTP動作:

HEAD – 獲取一個資源的元數據够挂,例如一組hash數據或者資源的最近一次更新時間旁仿;

OPTIONS – 獲取當前用戶(Consumer)對資源的訪問權限;

關于RESTful的API設計風格孽糖,說完RESTful接下來該說說API了枯冈。

API是服務提供方和使用方之間的契約,打破該契約將會給服務端開發(fā)人員招來非常大的麻煩梭姓,這些麻煩來自于使用API的開發(fā)人員霜幼,因為對API的改動會導致他們的移動app無法工作。一個好的文檔對于解決這些事情能起到事半功倍的作用誉尖,但是絕對多數程序員都不喜歡寫文檔罪既。如果想讓服務端的價值更好的體現出來,就要好好設計API铡恕。通過這些API琢感,你的服務/核心程序將有可能成為其他項目所依賴的平臺;你提供的API越易用探熔,就會有越多人愿意使用它驹针。規(guī)劃API的展示形式可能比你想象的要簡單,首先要確定你的數據是如何設計以及核心程序是如何工作的诀艰。

image.png

也就是說Server提供的RESTful API中柬甥,URL中只使用名詞來指定資源饮六,原則上不使用動詞】疗眩“資源”是REST架構或者說整個網絡處理的核心卤橄。

那么下面來具體說說如何形成良好的RESTful風格的API設計

1. 使用名詞而不是動詞

Server提供的RESTful API中,URL中只使用名詞來指定資源臂外,原則上不使用動詞窟扑。“資源”是REST架構或者說整個網絡處理的核心漏健。比如:

http://api.qc.com/v1/newsfeed: 獲取某人的新鮮;

http://api.qc.com/v1/friends: 獲取某人關系列表;

http://api.qc.com/v1/profile: 獲取某人的詳細信息;

URL是對資源描述的抽象嚎货,資源的描述一定是名詞,如果引入了動詞蔫浆,那這個URL就表示了一個動作殖属,而非一個資源,這樣就偏離了REST的設計思想

2.Get方法和查詢參數不應該涉及狀態(tài)改變

使用PUT, POST 和DELETE 方法 而不是 GET 方法來改變狀態(tài)瓦盛,不要使用GET 進行狀態(tài)改變:

通常忱辅,GET請求能夠被瀏覽器緩存(而且通常都會這么做),例如谭溉,當用戶發(fā)起第二次POST請求時,緩存的GET請求(依賴于緩存首部)能夠加快用戶的訪問速度橡卤。一個HEAD請求基本上就是一個沒有返回體的GET請求扮念,因此也能被緩存。

3.使用復數名詞

不要混淆名詞單數和復數碧库,為了保持簡單柜与,只對所有資源使用復數。

4. 使用子資源表達關系

如果一個資源與另外一個資源有關系嵌灰,使用子資源:

5.使用Http頭聲明序列化格式

在客戶端和服務端弄匕,雙方都要知道通訊的格式,格式在HTTP-Header中指定

Content-Type 定義請求格式

Accept 定義系列可接受的響應格式

6.使用HATEOAS

Hypermedia as the Engine of Application State 超媒體作為應用狀態(tài)的引擎沽瞭,超文本鏈接可以建立更好的文本瀏覽:

7.為集合提供過濾 排序 選擇和分頁等功能

Filtering過濾:

使用唯一的查詢參數進行過濾:

GET /cars?color=red 返回紅色的cars

GET /cars?seats<=2 返回小于兩座位的cars集合

當用戶請求獲取一組對象列表時迁匠,你就需要對結果進行過濾并返回一組嚴格符合用戶要求的對象。有時返回結果的數量可能非常大驹溃,但是你也不能隨意對此進行約束城丧,因為這種服務端的隨意約束會造成第三方開發(fā)人員的困惑。如果用戶請求了一個集合豌鹤,并對返回結果進行遍歷亡哄,然后只要前100個對象,那么這里就需要由用戶來指明這個限制量布疙。這樣用戶就不會有這樣的疑惑:是他們程序的bug還是接口限制了100條蚊惯?還是網絡只允許傳這么大的包愿卸?

Sorting排序:

允許針對多個字段排序

GET /cars?sort=-manufactorer,+model

這是返回根據生產者降序和模型升序排列的car集合

Field selection

移動端能夠顯示其中一些字段,它們其實不需要一個資源的所有字段截型,給API消費者一個選擇字段的能力短绸,這會降低網絡流量,提高API可用性筋讨。

GET /cars?fields=manufacturer,model,id,color

Paging分頁

使用 limit 和offset.實現分頁淤刃,缺省limit=20 和offset=0;

GET /cars?offset=10&limit=5

為了將總數發(fā)給客戶端赶诊,使用訂制的HTTP頭: X-Total-Count.

鏈接到下一頁或上一頁可以在HTTP頭的link規(guī)定笼平,遵循Link規(guī)定:

Link:

https://blog.mwaysolutions.com/sample/api/v1/cars?offset=15&limit=5; rel=”next”,

https://blog.mwaysolutions.com/sample/api/v1/cars?offset=50&limit=3; rel=”last”,

https://blog.mwaysolutions.com/sample/api/v1/cars?offset=0&limit=5; rel=”first”,

https://blog.mwaysolutions.com/sample/api/v1/cars?offset=5&limit=5; rel=”prev”,

8.版本化你的API

也就是進行版本控制。無論你在設計什么系統(tǒng)舔痪,也不管你事先做了多么詳盡的計劃寓调,隨著時間的推移和業(yè)務的發(fā)展,你的程序總會發(fā)生變化锄码,數據關系也會發(fā)生變化夺英,資源可能會被添加或者刪除一些屬性。只要軟件還在生存期內并且還有人在用它滋捶,開發(fā)人員就得面對這些問題痛悯,對于API設計來說,尤其如此重窟。

在URL中加入版本號是一個優(yōu)秀的API設計载萌,當然還有另一個常用的解決辦法就是把版本號放在請求首部中

使得API版本變得強制性,不要發(fā)布無版本的API巡扇,使用簡單數字扭仁,避免小數點如2.5。一般在Url后面使用?v

/blog/api/v1

9. 使用Http狀態(tài)碼處理錯誤

如果你的API沒有錯誤處理是很難的厅翔,只是返回500和出錯堆棧不一定有用

Http狀態(tài)碼提供70個出錯乖坠,我們只要使用10個左右:

200 – OK – 一切正常

201 – OK – 新的資源已經成功創(chuàng)建

204 – OK – 資源已經成功擅長

304 – Not Modified – 客戶端使用緩存數據

400 – Bad Request – 請求無效,需要附加細節(jié)解釋如 “JSON無效”

401 – Unauthorized – 請求需要用戶驗證

403 – Forbidden – 服務器已經理解了請求刀闷,但是拒絕服務或這種請求的訪問是不允許的熊泵。

404 – Not found – 沒有發(fā)現該資源

422 – Unprocessable Entity – 只有服務器不能處理實體時使用,比如圖像不能被格式化甸昏,或者重要字段丟失戈次。

500 – Internal Server Error – API開發(fā)者應該避免這種錯誤。

1XX的返回碼預留給HTTP的底層使用筒扒,在你的整個職業(yè)生涯中都不會主動發(fā)送這種返回碼怯邪;

2XX的返回碼表示請求按照預期執(zhí)行并成功返回了信息。服務端要盡可能給用戶返回這種結果花墩。

3XX的返回碼表示請求重定向悬秉,大多數API都不會經常使用這種請求()澄步,但是最新的超媒體API會充分使用這些功能。

4XX的返回碼主要表示由客戶端引起的錯誤和泌,例如請求參數錯誤或者訪問一個不存在的資源村缸,這些必須為冪等操作,并且不能改變服務器的狀態(tài)(其實服務器的狀態(tài)發(fā)生了改變就意味著操作不是冪等了)武氓。

5XX的返回碼主要表示由服務器引起的錯誤梯皿,通常情況下,這些錯誤都是開發(fā)人員

使用詳細的錯誤包裝錯誤:

{"errors": [? {"userMessage":"Sorry, the requested resource does not exist","internalMessage":"No car found in the database","code":34,"more info":"http://dev.mwaysolutions.com/blog/api/v1/errors/12345"}? ]}12345678910111213141516171819

10.允許覆蓋http方法

一些代理只支持POST 和 GET方法县恕, 為了使用這些有限方法支持RESTful API东羹,需要一種辦法覆蓋http原來的方法。

使用訂制的HTTP頭 X-HTTP-Method-Override 來覆蓋POST 方法.

使用場景

版本號

在 RESTful API 中忠烛,API 接口應該盡量兼容之前的版本属提。但是,在實際業(yè)務開發(fā)場景中美尸,可能隨著業(yè)務需求的不斷迭代冤议,現有的 API 接口無法支持舊版本的適配,此時如果強制升級服務端的 API 接口將導致客戶端舊有功能出現故障师坎。實際上恕酸,Web 端是部署在服務器,因此它可以很容易為了適配服務端的新的 API 接口進行版本升級胯陋,然而像 Android 端尸疆、IOS 端、PC 端等其他客戶端是運行在用戶的機器上惶岭,因此當前產品很難做到適配新的服務端的 API 接口,從而出現功能故障犯眠,這種情況下按灶,用戶必須升級產品到最新的版本才能正常使用。

為了解決這個版本不兼容問題筐咧,在設計 RESTful API 的一種實用的做法是使用版本號鸯旁。一般情況下,我們會在 url 中保留版本號量蕊,并同時兼容多個版本铺罢。

【GET】? /v1/users/{user_id}// 版本 v1 的查詢用戶列表的 API 接口【GET】? /v2/users/{user_id}// 版本 v2 的查詢用戶列表的 API 接口

現在,我們可以不改變版本 v1 的查詢用戶列表的 API 接口的情況下残炮,新增版本 v2 的查詢用戶列表的 API 接口以滿足新的業(yè)務需求韭赘,此時,客戶端的產品的新功能將請求新的服務端的 API 接口地址势就。雖然服務端會同時兼容多個版本泉瞻,但是同時維護太多版本對于服務端而言是個不小的負擔脉漏,因為服務端要維護多套代碼。這種情況下袖牙,常見的做法不是維護所有的兼容版本侧巨,而是只維護最新的幾個兼容版本,例如維護最新的三個兼容版本鞭达。在一段時間后司忱,當絕大多數用戶升級到較新的版本后,廢棄一些使用量較少的服務端的老版本API 接口版本畴蹭,并要求使用產品的非常舊的版本的用戶強制升級坦仍。

注意的是,“不改變版本 v1 的查詢用戶列表的 API 接口”主要指的是對于客戶端的調用者而言它看起來是沒有改變撮胧。而實際上桨踪,如果業(yè)務變化太大,服務端的開發(fā)人員需要對舊版本的 API 接口使用適配器模式將請求適配到新的API 接口上芹啥。

資源路徑

RESTful API 的設計以資源為核心锻离,每一個 URI 代表一種資源。因此墓怀,URI 不能包含動詞汽纠,只能是名詞。注意的是傀履,形容詞也是可以使用的虱朵,但是盡量少用。一般來說钓账,不論資源是單個還是多個碴犬,API 的名詞要以復數進行命名。此外梆暮,命名名詞的時候服协,要使用小寫、數字及下劃線來區(qū)分多個單詞啦粹。這樣的設計是為了與 json 對象及屬性的命名方案保持一致偿荷。例如,一個查詢系統(tǒng)標簽的接口可以進行如下設計唠椭。

【GET】? /v1/tags/{tag_id}

同時跳纳,資源的路徑應該從根到子依次如下。

/{resources}/{resource_id}/{sub_resources}/{sub_resource_id}/{sub_resource_property}

我們來看一個“添加用戶的角色”的設計贪嫂,其中“用戶”是主資源寺庄,“角色”是子資源。

【POST】? /v1/users/{user_id}/roles/{role_id}// 添加用戶的角色

有的時候,當一個資源變化難以使用標準的 RESTful API 來命名铣揉,可以考慮使用一些特殊的 actions 命名饶深。

/{resources}/{resource_id}/actions/{action}

舉個例子,“密碼修改”這個接口的命名很難完全使用名詞來構建路徑逛拱,此時可以引入 action 命名敌厘。

【PUT】? /v1/users/{user_id}/password/actions/modify// 密碼修改

請求方式

可以通過 GET、 POST朽合、 PUT俱两、 PATCH、 DELETE 等方式對服務端的資源進行操作曹步。其中宪彩,GET 用于查詢資源,POST 用于創(chuàng)建資源讲婚,PUT 用于更新服務端的資源的全部信息尿孔,PATCH 用于更新服務端的資源的部分信息,DELETE 用于刪除服務端的資源筹麸。

這里活合,筆者使用“用戶”的案例進行回顧通過 GET、 POST物赶、 PUT白指、 PATCH、 DELETE 等方式對服務端的資源進行操作酵紫。

【GET】? ? ? ? ? /users# 查詢用戶信息列表【GET】? ? ? ? ? /users/1001# 查看某個用戶信息【POST】? ? ? ? /users# 新建用戶信息【PUT】? ? ? ? ? /users/1001# 更新用戶信息(全部字段)【PATCH】? ? ? ? /users/1001# 更新用戶信息(部分字段)【DELETE】? ? ? /users/1001# 刪除用戶信息

查詢參數

RESTful API 接口應該提供參數告嘲,過濾返回結果。其中奖地,offset 指定返回記錄的開始位置橄唬。一般情況下,它會結合 limit 來做分頁的查詢参歹,這里 limit 指定返回記錄的數量仰楚。

【GET】? /{version}/{resources}/{resource_id}?offset=0&limit=20

同時,orderby 可以用來排序泽示,但僅支持單個字符的排序,如果存在多個字段排序蜜氨,需要業(yè)務中擴展其他參數進行支持械筛。

【GET】? /{version}/{resources}/{resource_id}?orderby={field} [asc|desc]

為了更好地選擇是否支持查詢總數,我們可以使用 count 字段飒炎,count 表示返回數據是否包含總條數埋哟,它的默認值為 false。

【GET】? /{version}/{resources}/{resource_id}?count=[true|false]

上面介紹的 offset、 limit赤赊、 orderby 是一些公共參數闯狱。此外,業(yè)務場景中還存在許多個性化的參數抛计。我們來看一個例子哄孤。

【GET】? /v1/categorys/{category_id}/apps/{app_id}?enable=[1|0]&os_type={field}&device_ids={field,field,…}

注意的是,不要過度設計吹截,只返回用戶需要的查詢參數瘦陈。此外,需要考慮是否對查詢參數創(chuàng)建數據庫索引以提高查詢性能波俄。

狀態(tài)碼

使用適合的狀態(tài)碼很重要晨逝,而不應該全部都返回狀態(tài)碼 200,或者隨便亂使用懦铺。這里捉貌,列舉筆者在實際開發(fā)過程中常用的一些狀態(tài)碼,以供參考冬念。

狀態(tài)碼描述

200請求成功

201創(chuàng)建成功

400錯誤的請求

401未驗證

403被拒絕

404無法找到

409資源沖突

500服務器內部錯誤

異常響應

當 RESTful API 接口出現非 2xx 的 HTTP 錯誤碼響應時趁窃,采用全局的異常結構響應信息。

HTTP/1.1 400 Bad RequestContent-Type: application/json{"code":"INVALID_ARGUMENT","message":"{error message}","cause":"{cause message}","request_id":"01234567-89ab-cdef-0123-456789abcdef","host_id":"{server identity}","server_time":"2014-01-01T12:00:00Z"}

請求參數

在設計服務端的 RESTful API 的時候刘急,我們還需要對請求參數進行限制說明棚菊。例如一個支持批量查詢的接口,我們要考慮最大支持查詢的數量叔汁。

【GET】? ? /v1/users/batch?user_ids=1001,1002// 批量查詢用戶信息參數說明- user_ids: 用戶ID串统求,最多允許20個。

此外据块,在設計新增或修改接口時码邻,我們還需要在文檔中明確告訴調用者哪些參數是必填項,哪些是選填項另假,以及它們的邊界值的限制像屋。

【POST】? ? /v1/users// 創(chuàng)建用戶信息請求內容{"username":"lgz",// 必填, 用戶名稱, max 10"realname":"梁桂釗",// 必填, 用戶名稱, max 10"password":"123456",// 必填, 用戶密碼, max 32"email":"lianggzone@163.com",// 選填, 電子郵箱, max 32"weixin":"LiangGzone",// 選填,微信賬號, max 32"sex":1// 必填, 用戶性別[1-男 2-女 99-未知]}

響應參數

針對不同操作边篮,服務端向用戶返回的結果應該符合以下規(guī)范己莺。

【GET】? ? /{version}/{resources}/{resource_id}// 返回單個資源對象【GET】? ? /{version}/{resources}// 返回資源對象的列表【POST】? ? /{version}/{resources}// 返回新生成的資源對象【PUT】? ? /{version}/{resources}/{resource_id}// 返回完整的資源對象【PATCH】? /{version}/{resources}/{resource_id}// 返回完整的資源對象【DELETE】? /{version}/{resources}/{resource_id}// 狀態(tài)碼 200,返回完整的資源對象戈轿。// 狀態(tài)碼 204凌受,返回一個空文檔

如果是單條數據,則返回一個對象的 JSON 字符串思杯。

HTTP/1.1 200 OK{"id":"01234567-89ab-cdef-0123-456789abcdef","name":"example","created_time": 1496676420000,"updated_time": 1496676420000,? ? ...}

如果是列表數據胜蛉,則返回一個封裝的結構體。

HTTP/1.1 200 OK{"count":100,"items":[? ? ? ? {"id":"01234567-89ab-cdef-0123-456789abcdef","name":"example","created_time": 1496676420000,"updated_time": 1496676420000,? ? ? ? ? ? ...? ? ? ? },? ? ? ? ...? ? ]}

一個完整的案例

最后,我們使用一個完整的案例將前面介紹的知識整合起來誊册。這里领突,使用“獲取用戶列表”的案例。

【GET】? ? /v1/users?[&keyword=xxx][&enable=1][&offset=0][&limit=20] 獲取用戶列表功能說明:獲取用戶列表請求方式:GET參數說明- keyword: 模糊查找的關鍵字案怯。[選填]-enable: 啟用狀態(tài)[1-啟用 2-禁用]君旦。[選填]- offset: 獲取位置偏移,從 0 開始殴泰。[選填]-limit: 每次獲取返回的條數于宙,缺省為 20 條,最大不超過 100悍汛。 [選填]響應內容HTTP/1.1 200 OK{"count":100,"items":[? ? ? ? {"id":"01234567-89ab-cdef-0123-456789abcdef","name":"example","created_time": 1496676420000,"updated_time": 1496676420000,? ? ? ? ? ? ...? ? ? ? },? ? ? ? ...? ? ]}失敗響應HTTP/1.1 403 UC/AUTH_DENIEDContent-Type: application/json{"code":"INVALID_ARGUMENT","message":"{error message}","cause":"{cause message}","request_id":"01234567-89ab-cdef-0123-456789abcdef","host_id":"{server identity}","server_time":"2014-01-01T12:00:00Z"}錯誤代碼- 403 UC/AUTH_DENIED? ? 授權受限

作者:趙客縵胡纓v吳鉤霜雪明

鏈接:http://www.reibang.com/p/a88d07ad1493

來源:簡書

簡書著作權歸作者所有捞魁,任何形式的轉載都請聯(lián)系作者獲得授權并注明出處。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末离咐,一起剝皮案震驚了整個濱河市谱俭,隨后出現的幾起案子,更是在濱河造成了極大的恐慌宵蛀,老刑警劉巖昆著,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現場離奇詭異术陶,居然都是意外死亡凑懂,警方通過查閱死者的電腦和手機,發(fā)現死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門梧宫,熙熙樓的掌柜王于貴愁眉苦臉地迎上來接谨,“玉大人,你說我怎么就攤上這事塘匣∨Ш溃” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵忌卤,是天一觀的道長扫夜。 經常有香客問我,道長驰徊,這世上最難降的妖魔是什么笤闯? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮棍厂,結果婚禮上颗味,老公的妹妹穿的比我還像新娘。我一直安慰自己勋桶,他們只是感情好脱衙,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著例驹,像睡著了一般捐韩。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上鹃锈,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天荤胁,我揣著相機與錄音,去河邊找鬼屎债。 笑死仅政,一個胖子當著我的面吹牛,可吹牛的內容都是我干的盆驹。 我是一名探鬼主播圆丹,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼躯喇!你這毒婦竟也來了辫封?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤廉丽,失蹤者是張志新(化名)和其女友劉穎倦微,沒想到半個月后,有當地人在樹林里發(fā)現了一具尸體正压,經...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡欣福,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了焦履。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片拓劝。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖裁良,靈堂內的尸體忽然破棺而出凿将,到底是詐尸還是另有隱情,我是刑警寧澤价脾,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布牧抵,位于F島的核電站,受9級特大地震影響侨把,放射性物質發(fā)生泄漏犀变。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一秋柄、第九天 我趴在偏房一處隱蔽的房頂上張望获枝。 院中可真熱鬧,春花似錦骇笔、人聲如沸省店。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽懦傍。三九已至雹舀,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間粗俱,已是汗流浹背说榆。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留寸认,地道東北人签财。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓,卻偏偏與公主長得像偏塞,于是被迫代替她去往敵國和親唱蒸。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內容