《一個(gè)Android工程的從零開始》階段總結(jié)與修改2-Retrofit 上傳JSON及尾址特殊字符轉(zhuǎn)譯問題

先扯兩句

做個(gè)項(xiàng)目是真占時(shí)間啊图甜,不知不覺已經(jīng)三周沒有發(fā)新的博客了(這個(gè)接口不錯吧)碍粥,總算是有個(gè)周末,趁休息黑毅,將最近遇到的一些問題再次貼出來嚼摩,算是完善一下工程。
閑言少敘矿瘦,老規(guī)矩還是先上我的Git庫枕面,然后開始正文。
MyBaseApplication (https://github.com/BanShouWeng/MyBaseApplication)

正文

“實(shí)踐是檢驗(yàn)真理的唯一標(biāo)準(zhǔn)”缚去,這句話的唯一兩字是否正確我們先放下不表潮秘,不過至少在這段做項(xiàng)目的時(shí)間中,對于之前自己所寫的內(nèi)容還是有了全新的認(rèn)識的易结,上一次發(fā)的內(nèi)容枕荞,主要是總結(jié)了一下漏洞柜候,希望看到的大家看到后能夠避免走上相同的彎路,而今天這篇呢躏精,看到標(biāo)題大家應(yīng)該也知道了渣刷,還是一些階段總結(jié)的內(nèi)容,不過這次下手的只是Retrofit矗烛,但想必誤入這些彎路的絕不止我一個(gè)辅柴,那么就進(jìn)入今天的內(nèi)容吧。

Retrofit 上傳JSON

具體還是從需求說起瞭吃,當(dāng)然碌嘀,還是網(wǎng)絡(luò)訪問的部分,那就是我參與的項(xiàng)目需要給后臺傳輸JSON數(shù)據(jù)虱而,之前也查到了一些方法筏餐,那就是在Header中添加設(shè)置Content-Type為application/json开泽,看了我前面的博客《一個(gè)Android工程的從零開始》階段總結(jié)與修改1-base的應(yīng)該會知道牡拇,其中最后一部分就是闡述的如何進(jìn)行Retrofit header的動態(tài)添加封裝,這里自然也就方便了許多穆律,只需要在初始化中加下屬這部分即可惠呼。

headerParams.put("Content-Type", "application/json");

這么簡單的操作還不是分分鐘結(jié)束戰(zhàn)斗啊,于是試了第一個(gè)接口峦耘,get請求剔蹋,果然沒有問題「ㄋ瑁可當(dāng)嘗試POST請求的時(shí)候泣崩,卻是給我返回來了服務(wù)器端的異常信息,我還憤憤的找服務(wù)器端算賬洛口,可當(dāng)看到我的請求數(shù)據(jù)矫付,真是恨不得找個(gè)地縫鉆進(jìn)去。
老子已經(jīng)進(jìn)行了設(shè)置第焰,為毛上傳的信息還是key-value形式的B蛴拧!挺举!
別問我為什么get可以杀赢,本來get就不管上傳什么形式的,統(tǒng)統(tǒng)url湘纵,自然不可能出問題脂崔。
為了告訴后臺我確實(shí)上傳的是JSON形式的參數(shù),還將傳遞的參數(shù)攔截了下來梧喷,給他們看:
攔截方法如下:

private void initBaseData(boolean isFromLogin) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(5, TimeUnit.SECONDS);
        builder.addInterceptor(new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                LogUtil.info("responseString", "request====" + action);
                LogUtil.info("responseString", "request====" + request.headers().toString());
                LogUtil.info("responseString", "request====" + request.toString());
                okhttp3.Response proceed = chain.proceed(request);
                LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
                return proceed;
            }
        });

        Retrofit.Builder builder1 = new Retrofit.Builder()
                .client(builder.build())
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create());
            builder1.baseUrl(baseUrl);

        retrofit = builder1.build();
    }

從中也可以看出我們的Retrofit只是對于OkHttp的一種封裝脱篙,具體到一些功能性的內(nèi)容娇钱,還是得從OkHttp下手,這點(diǎn)我也很無奈啊绊困。
不過需要說明的一點(diǎn)是文搂,大家可以看到,這里我們只輸出的Header對應(yīng)的信息秤朗,卻沒有對Body下手煤蹭,這個(gè)真不是我不想,而是真心無能為例取视,body下只有一個(gè)toString方法硝皂,輸出的還是對應(yīng)body的Id,所以大家如果想查看自己的傳輸?shù)腷ody的信息作谭,暫時(shí)我能提供的建議稽物,就只有通過斷點(diǎn)調(diào)試了。
從proceed.headers().toString()對應(yīng)的輸出語句中確實(shí)可以看到如下日志:

proceed====Date: Sat, 26 Aug 2017 08:02:59 GMT
                                                                      Content-Type: application/json; charset=utf-8

可是日志顯示出來花來折欠,服務(wù)器端Spring的JSON接受都沒過就報(bào)空指針的事實(shí)還是擺在那里贝或。
苦逼的查了一番資料后,才發(fā)現(xiàn)一種解決方法锐秦,竟然需要我自己把JSON串寫好咪奖,通過RequestBody傳才可以,那真是一萬只草泥馬在心頭奔馳而過敖创病羊赵!
用于傳輸?shù)腟ervice自然也要做出對應(yīng)調(diào)整:


public interface RetrofitPostJsonService {

    @POST("{action}")
    Observable<ResponseBody> postResult(@Path("action") String action, @HeaderMap Map<String, String> headerParams, @Body RequestBody requestBody);
}

需要說明的一點(diǎn)是,大家如果與我之前發(fā)的POST傳輸?shù)腟ervice對比一下就會發(fā)現(xiàn)扇谣,這里要比之前少了“@FormUrlEncoded”的配置昧捷,那是因?yàn)檫@個(gè)配置信息本就是將URL格式化為為key-value格式,既然我們設(shè)置了要傳輸JSON格式罐寨,再用這個(gè)參數(shù)靡挥,自然是要報(bào)錯的:

Caused by: java.lang.IllegalArgumentException: @Body parameters cannot be used with form or multi-part encoding. (parameter #3)

這部分搞定了之后,該進(jìn)行的自然就是如何封裝到POST方法中了衩茸,對于這部分呢芹血,我這里總共分為了一下兩種情況:

  1. 普通key-Value形式的封裝
  2. 復(fù)雜JSON的封裝

普通key-Value形式的封裝###

把這一條放在最前面不僅僅是因?yàn)樗拿种杏衅胀▋蓚€(gè)字,顯得比較好處理楞慈,而是因?yàn)樗娴囊攘硪环N情況要好處理得多幔烛。

public <T extends BaseBean> void post(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
        if (showDialog) {
            showLoadDialog();
        }
        initBaseData(false);
        if (jsonService == null) {
            jsonService = retrofit.create(RetrofitPostJsonService.class);
        }

        if (params == null) {
            params = new HashMap<>();
        }

        RequestBody requestBody =
                RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
                        String.valueOf(new JSONObject(params)));

        jsonService.postResult(action, headerParams, requestBody)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ResponseBody>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull ResponseBody responseBody) {
                        hideLoadDialog();
                        try {
                            String responseString = responseBody.string();
                            LogUtil.info("responseString", action + "********** responseString post  " + responseString);
                            callBack.success(action, new Gson().fromJson(responseString, clazz));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        callBack.error(action, e);
                    }

                    @Override
                    public void onComplete() {
                        params = null;
                    }
                });
    }

可以看得出來,這段接受的方法囊蓝,實(shí)際上與之前封裝的POST方法真的卻別不大饿悬。
1、由于需要上傳JSON聚霜,且又是普通的Key-Value轉(zhuǎn)換而成的JSON狡恬,于是我們就可以實(shí)現(xiàn)最最簡單的方法實(shí)現(xiàn)珠叔,那就是直接將原本的之前封裝中用到的Map轉(zhuǎn)換成JSON就好。

String.valueOf(new JSONObject(params))

最外一層的目的是將得到的JSON轉(zhuǎn)換成String類型弟劲,用于上傳祷安。

2、前面說過了兔乞,Service已經(jīng)被改變了汇鞭,所以這里我們也需要將傳輸?shù)膮?shù)做出對應(yīng)修改,那就是創(chuàng)建一個(gè)RequestBody對象庸追,在配合上第一條生成的JSON霍骄,就是如下的代碼:

RequestBody requestBody =
                RequestBody.create(MediaType.parse("application/json; charset=utf-8"),
                        String.valueOf(new JSONObject(params)));

3、就是參數(shù)的傳遞淡溯,這個(gè)也是最簡單的了读整,那就是將我們的RequestBody 對象傳遞給Service中的postResult方法即可:

jsonService.postResult(action, headerParams, requestBody)

如此以來,我們終于可以成功的傳遞JSON了咱娶。

復(fù)雜JSON封裝###

上述方法已經(jīng)能夠?qū)崿F(xiàn)我們大多數(shù)接口的傳輸數(shù)據(jù)的需求米间,可是在大多數(shù),它也是有我們無法完成的部分豺总,哪怕是我下面列舉的簡單又常見的JSON形式:

[
  {
    "name": "",
    "myClass": "",
    "grade": ""
  },
  {
    "name": "",
    "myClass": "",
    "grade": ""
  },
  {
    "name": "",
    "myClass": "",
    "grade": ""
  },
  {
    "name": "",
    "myClass": "",
    "grade": ""
  }
]

傳輸?shù)木褪撬膫€(gè)學(xué)生的姓名车伞、年級择懂、以及班級喻喳,如果用之前的方法嘗試的話,我的嘗試的結(jié)果如下:


這里寫圖片描述

當(dāng)然困曙,除了直接創(chuàng)建JSON對象表伦,我們還可以創(chuàng)建一個(gè)GSON獲取json串,我就賤賤的嘗試了一下Gson解析:

List<BaseBean> beanList = new ArrayList<>();
        for (int i = 0; i < 4; i++) {
            BaseBean baseBean = new BaseBean();
            baseBean.setName("name" + i);
            baseBean.setMyClass("class" + i);
            baseBean.setGrade("grade" + i);
            beanList.add(baseBean);
        }
        Log.i("adada", new Gson().toJson(beanList).toString());

很榮幸的發(fā)現(xiàn)慷丽,竟然能夠轉(zhuǎn)換成功:

[
  {
    "grade": "grade0",
    "myClass": "class0",
    "name": "name0"
  },
  {
    "grade": "grade1",
    "myClass": "class1",
    "name": "name1"
  },
  {
    "grade": "grade2",
    "myClass": "class2",
    "name": "name2"
  },
  {
    "grade": "grade3",
    "myClass": "class3",
    "name": "name3"
  }
]

正興奮呢蹦哼,需求又變了一個(gè)樣子:

{
  "student": [
    {
      "name": "",
      "class": "",
      "grade": ""
    },
    {
      "name": "",
      "class": "",
      "grade": ""
    },
    {
      "name": "",
      "class": "",
      "grade": ""
    },
    {
      "name": "",
      "class": "",
      "grade": ""
    }
  ],
  "pass": [
    {
      "name": ""
    },
    {
      "name": ""
    }
  ]
}

剛剛看到的勝利曙光瞬間又陰云密布了,無奈之下要糊,就只能再創(chuàng)建一個(gè)傳輸?shù)腷ean:

class PostStudentBean{
    private List<BaseBean> student;
    private List<PassBean> pass;

    public void setStudent(List<BaseBean> student){
        this.student = student;
    }

    public List<BaseBean> getStudent(){
        return student;
    }

    public void setPass(List<PassBean> pass){
        this.pass= pass;
    }

    public List<PassBean> getPass(){
        return pass;
    }
}

class PassBean{
    private String name;
    public void setName(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}

如此以來纲熏,就可以通過GSON將Bean轉(zhuǎn)換為JSON串,用于傳輸即可锄俄。
當(dāng)然局劲,上面用一種比較倒霉的方法說明了兩種我們在編程開發(fā)過程中可能會遇到的兩種情況(個(gè)人認(rèn)為如果需要Map配合Bean的不如就直接創(chuàng)建一個(gè)父級別的Bean好了),這部分操作暫時(shí)都是在創(chuàng)建參數(shù)的時(shí)候完成的奶赠,所以在這個(gè)第二種方法中鱼填,post傳參要比上一中方法少一個(gè)使用param傳遞參數(shù)的過程,卻需要通過創(chuàng)建Bean類毅戈,以及為Bean類賦值的步驟苹丸,同樣愤惰,需要重載post方法,多添加一個(gè)(String json)的參數(shù)赘理,然后將RequestBody 的創(chuàng)建改成如下形式即可:

RequestBody requestBody =
                RequestBody.create(MediaType.parse("application/json; charset=utf-8"), json);

這樣宦言,我們的就實(shí)現(xiàn)了Retrofit的JSON傳參,雖然看起來有些麻煩商模,但是……也確實(shí)麻煩蜡励,不過在有這方面需求的時(shí)候,這個(gè)封裝也是必不可少的過程阻桅,所以希望這里可以幫助到大家凉倚。

尾址特殊字符轉(zhuǎn)譯問題

這個(gè)內(nèi)容是在前面封裝的時(shí)候所沒有想到的,在開發(fā)過程中卻狠狠的坑了我一回嫂沉,除了含著淚解決稽寒,我們還有什么辦法呢!
之前我們的傳遞的內(nèi)容都很簡單趟章,比如我前面一直拿來舉例的豆瓣電影查詢杏糙,尾址只需要傳遞“top250”就算完成戰(zhàn)斗,所以也就沒有遇到過尾址特殊字符轉(zhuǎn)譯的問題蚓土,可是還是以豆瓣電影查詢?yōu)槔晔蹋暾埱骍RL為“https://api.douban.com/v2/movie/top250”,可是豆瓣API明顯不只是電影一項(xiàng):
[豆瓣書籍所搜] (https://api.douban.com/v2/book/search?q=python&fields=id,title)
[豆瓣音樂搜索] (https://api.douban.com/v2/music/search)
等等等等蜀漆,有興趣大家可以去豆瓣Api V2看一看還有多少谅河,這么多的內(nèi)容,我們在封裝網(wǎng)絡(luò)訪問框架的時(shí)候确丢,自然不會只使用“https://api.douban.com/v2/movie/”作為baseUrl绷耍,而通過觀察,顯而易見鲜侥,我們所使用的baseUrl將會是“https://api.douban.com/v2/”褂始,而后在根據(jù)具體需求設(shè)置尾址“movie/top250”、“book/search”描函、或者“music/search”,而當(dāng)如此使用的時(shí)候崎苗,我們今天遇到的問題也就來了,那就是尾址特殊字符轉(zhuǎn)譯舀寓,說具體點(diǎn)胆数,就是“/”被Retrofit進(jìn)行了轉(zhuǎn)譯,變成了“%2F”基公,當(dāng)然幅慌,除此之外還會有其他的特殊字符會被轉(zhuǎn)譯,具體請參見網(wǎng)址URL中特殊字符轉(zhuǎn)義編碼轰豆。查詢了一些資料胰伍,有一些解決方法是通過“//”齿诞,將被轉(zhuǎn)譯的字符再強(qiáng)行轉(zhuǎn)譯回來,原理嘛骂租,就好像傳說中的負(fù)負(fù)得正一樣祷杈。
只不過很不幸的是,我嘗試了這個(gè)方法之后渗饮,在上述監(jiān)聽到的URL卻編程了如下的樣子“https://api.douban.com/v2/movie%2F%2Ftop250”(點(diǎn)了一下但汞,豆瓣竟然能識別,不禁讓我“內(nèi)牛滿面”)互站,服務(wù)器果斷給我返回了404私蕾,查了N多資料,真心沒找到Retrofit應(yīng)該怎么解決這個(gè)問題胡桃,而之前使用Volley的時(shí)候踩叭,又沒遇到過這種情況。所以這里只能采用一個(gè)最無腦的解決方法翠胰,如果大家誰有更高端的解決方法容贝,希望能不吝賜教:

首先,將post或者get傳入的action根據(jù)action中最后一個(gè)“/”的坐標(biāo)將其分為兩部分之景,最后一個(gè)“/”后面的字符串被用做尾址斤富,繼續(xù)傳遞到后面Service對應(yīng)的方法中,也就是下面代碼中的“action1 ”锻狗,而其余的部分满力,也就是下面的“action2 ”則被用來當(dāng)做參數(shù)傳入Retrofit的初始化中,拼接到baseUrl中屋谭。

    public <T extends BaseBean> void get(final String action, final Class<T> clazz, boolean showDialog, final ResultCallBack callBack) {
        this.action = action;
        if (showDialog) {
            showLoadDialog();
        }
        String action1 = action.substring(action.lastIndexOf("/") + 1);
        String action2 = action.substring(0, action.lastIndexOf("/") + 1);
        initBaseData(action2, false);

        if (getService == null) {
            getService = retrofit.create(RetrofitGetService.class);
        }
        if (params == null) {
            params = new HashMap<>();
        }

        getService.getResult(action1, headerParams, params)
                .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Observer<ResponseBody>() {
                    @Override
                    public void onSubscribe(@NonNull Disposable d) {

                    }

                    @Override
                    public void onNext(@NonNull ResponseBody responseBody) {
                        hideLoadDialog();
                        try {
                            String responseString = responseBody.string();
                            LogUtil.info("responseString", action + "********** responseString get  " + responseString);
                            callBack.success(action, new Gson().fromJson(responseString, clazz));
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }

                    @Override
                    public void onError(@NonNull Throwable e) {
                        LogUtil.info("responseString", "responseString get  " + e.toString());
                        callBack.error(action, e);
                    }

                    @Override
                    public void onComplete() {
                        params = null;
                    }
                });
    }

下面是動態(tài)拼接baseUrl的方法:

private void initBaseData(final String url, boolean isFromLogin) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.connectTimeout(5, TimeUnit.SECONDS);
        builder.addInterceptor(new Interceptor() {
            @Override
            public okhttp3.Response intercept(Chain chain) throws IOException {
                Request request = chain.request();
                LogUtil.info("zzz", "request====" + action);
                LogUtil.info("zzz", "request====" + request.headers().toString());
                LogUtil.info("zzz", "request====" + request.toString());
                okhttp3.Response proceed = chain.proceed(request);
                LogUtil.info("zzz", "proceed====" + proceed.headers().toString());
                return proceed;
            }
        });

        Retrofit.Builder builder1 = new Retrofit.Builder()
                .client(builder.build())
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(RxJava2CallAdapterFactory.create());
            builder1.baseUrl(baseUrl + url);

        retrofit = builder1.build();
    }

如此脚囊,就可以動態(tài)適配我們的尾址龟糕,同時(shí)也避免了轉(zhuǎn)譯帶來的影響桐磁,雖然說方法簡陋的可以,但是暫時(shí)拿來應(yīng)急還是可以考慮的讲岁,如果后續(xù)發(fā)現(xiàn)更好的方法我擂,我會繼續(xù)更新到后續(xù)的博客上的。

附錄

《一個(gè)Android工程的從零開始》- 目錄

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末缓艳,一起剝皮案震驚了整個(gè)濱河市校摩,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阶淘,老刑警劉巖衙吩,帶你破解...
    沈念sama閱讀 219,110評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異溪窒,居然都是意外死亡坤塞,警方通過查閱死者的電腦和手機(jī)冯勉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評論 3 395
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來摹芙,“玉大人灼狰,你說我怎么就攤上這事「『蹋” “怎么了交胚?”我有些...
    開封第一講書人閱讀 165,474評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長盈电。 經(jīng)常有香客問我蝴簇,道長,這世上最難降的妖魔是什么匆帚? 我笑而不...
    開封第一講書人閱讀 58,881評論 1 295
  • 正文 為了忘掉前任军熏,我火速辦了婚禮,結(jié)果婚禮上卷扮,老公的妹妹穿的比我還像新娘荡澎。我一直安慰自己,他們只是感情好晤锹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評論 6 392
  • 文/花漫 我一把揭開白布摩幔。 她就那樣靜靜地躺著,像睡著了一般鞭铆。 火紅的嫁衣襯著肌膚如雪或衡。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,698評論 1 305
  • 那天车遂,我揣著相機(jī)與錄音封断,去河邊找鬼。 笑死舶担,一個(gè)胖子當(dāng)著我的面吹牛坡疼,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播衣陶,決...
    沈念sama閱讀 40,418評論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼柄瑰,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了剪况?” 一聲冷哼從身側(cè)響起教沾,我...
    開封第一講書人閱讀 39,332評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎译断,沒想到半個(gè)月后授翻,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評論 3 337
  • 正文 我和宋清朗相戀三年堪唐,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了隆箩。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,110評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡羔杨,死狀恐怖捌臊,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情兜材,我是刑警寧澤理澎,帶...
    沈念sama閱讀 35,792評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站曙寡,受9級特大地震影響糠爬,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜举庶,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評論 3 331
  • 文/蒙蒙 一执隧、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧户侥,春花似錦镀琉、人聲如沸烛占。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽檬某。三九已至替梨,卻和暖如春钓试,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背副瀑。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評論 1 272
  • 我被黑心中介騙來泰國打工弓熏, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人糠睡。 一個(gè)月前我還...
    沈念sama閱讀 48,348評論 3 373
  • 正文 我出身青樓挽鞠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親铜幽。 傳聞我的和親對象是個(gè)殘疾皇子滞谢,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評論 2 355

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