流行框架源碼分析(18)-UnifyStorage統(tǒng)一的數(shù)據(jù)庫(kù)存儲(chǔ),key-value存儲(chǔ)扮饶,mock網(wǎng)絡(luò)數(shù)據(jù)的一個(gè)庫(kù)

主目錄見(jiàn):Android高級(jí)進(jìn)階知識(shí)(這是總目錄索引)
項(xiàng)目目錄:https://github.com/yuzhijun/UnifyStorage

這里先給大家道歉一下具练,最近因?yàn)橐獙W(xué)習(xí)的方向?qū)嵲谑潜容^大,所以文章已經(jīng)好久沒(méi)有更新甜无,如果有什么需要可以留言問(wèn)我扛点,有什么東西很想要了解的也可以交流,文筆生疏了岂丘,見(jiàn)諒陵究。

一.目標(biāo)

寫(xiě)這個(gè)庫(kù)的開(kāi)始是源于一個(gè)小的需求,當(dāng)然奥帘,這個(gè)庫(kù)也是小巧的铜邮。而且做這個(gè)庫(kù)的初衷就是為了能將網(wǎng)絡(luò),數(shù)據(jù)庫(kù)存儲(chǔ)翩概,本地key-value存儲(chǔ)統(tǒng)一起來(lái)牲距,這樣管理和擴(kuò)展都會(huì)更加方便,對(duì)于用戶來(lái)說(shuō)是統(tǒng)一的接口钥庇。

如果不知道怎么使用牍鞠,這個(gè)庫(kù)有詳細(xì)的入門(mén)文檔:https://www.kancloud.cn/sharkchao/unifystorage/864979#65_summinmaxaverage_324,看完文檔你一定知道是怎么操作的评姨,所以我這里就講講怎么實(shí)現(xiàn)的吧难述,其實(shí)也是簡(jiǎn)單的,主要是借助retrofit的方式吐句,結(jié)合realm數(shù)據(jù)庫(kù)和mmkv來(lái)做的胁后。

二.源碼分析

1.基本使用

首先還是跟其他源碼的入手點(diǎn)一樣,我們先來(lái)看看最基本的使用方法嗦枢,依賴這方面文檔里面已經(jīng)很清楚了攀芯,我就直接講使用部分:

public class ApiServiceModule {
    private volatile static ApiServiceModule mInstance;

    private ApiServiceModule(){

    }
    public static ApiServiceModule getInstance(){
        if (null == mInstance){
            synchronized (ApiServiceModule.class){
                if (null == mInstance){
                    mInstance = new ApiServiceModule();
                }
            }
        }
        return mInstance;
    }

    private UStorage provideUStorage(){
        return new UStorage.Builder()
                .setSchemaVersion(1)
                .build();
    }

    <T> T provideApiService(Class<T> apiDataBase){
        return provideUStorage().create(apiDataBase);
    }
}

很簡(jiǎn)單,用過(guò)retrofit的人都知道文虏,需要先獲取業(yè)務(wù)接口類侣诺,這里的provideUStorage().create(apiDataBase)方法就是獲取接口類的方法,那我們先看下接口類的實(shí)現(xiàn)氧秘,這里接口類以一個(gè)查詢?yōu)槔暝В剂信e出來(lái)太多了,不利于查看:

public interface ApiDataBase {
    @DB(table = User.class)
    @FIND(where = "name = ? and (age > ? or sex = ?)",limit = 10,orderBy = "age desc")
    DbResult<User> findUser(String name, int age, String sex);
}

可以看到這里的接口類跟retrofit極其相似丸相,不同的是這里進(jìn)行的是數(shù)據(jù)庫(kù)的存儲(chǔ)搔确,然后我們看最終的調(diào)用使用代碼:

   mApiDataBase.findUser("sharkchao", 20, "男")
                .registerDbFindCallBack(new DbResult.DbFindCallBack<User>() {
                    @Override
                    public void onFirstFindResult(RealmResults<User> realmResults) {
                        UserData.setmResults(realmResults);
                        Toast.makeText(MainActivity.this, "成功!"+ realmResults.size(), Toast.LENGTH_SHORT).show();
                    }

                    @Override
                    public void onChange(RealmResults<User> realmResults) {

                    }
                });

這就是查詢的使用方法了,我們以這個(gè)入口點(diǎn)進(jìn)行源碼分析。

2.代碼框架分析

我們看到使用的時(shí)候先要進(jìn)行UStorage的建造,這里使用的是建造者模式膳算,主要是為了構(gòu)造數(shù)據(jù)庫(kù)所需的必要參數(shù)座硕,如代碼所示:

new UStorage.Builder()
                .setSchemaVersion(1)
                .build();

我們直接跟進(jìn)代碼里面查看,首先查看UStorage類中的Builder

 public static final class Builder {
        private static final String DEFAULT_DB_NAME = "winningStorage.realm";

        private String dbName;
        private int schemaVersion = 0;
        private BaseMigration migration;
        private Realm realmDefault;

        public Builder() {
        }

        public Builder setDbName(String dbName) {
            this.dbName = dbName;
            return this;
        }

        public Builder setSchemaVersion(int schemaVersion) {
            this.schemaVersion = schemaVersion;
            return this;
        }

        public Builder setMigration(BaseMigration migration) {
            this.migration = migration;
            return this;
        }

        public UStorage build() {
            configDB();

            return new UStorage(this);
        }

        private void configDB(){
            RealmConfiguration.Builder otherConfigBuilder = new RealmConfiguration.Builder()
                    .name(CommonUtil.isEmptyStr(dbName) ? DEFAULT_DB_NAME : dbName)
                    .schemaVersion(schemaVersion);

            if (null == migration){
                otherConfigBuilder.deleteRealmIfMigrationNeeded();
            }else {
                otherConfigBuilder.migration(migration);
            }

            RealmConfiguration otherConfig = otherConfigBuilder.build();
            Realm.setDefaultConfiguration(otherConfig);

            realmDefault = Realm.getDefaultInstance();
        }
    }

上面的代碼主要是對(duì)realm數(shù)據(jù)庫(kù)的初始化,沒(méi)有什么有難度的代碼畦幢,接著我們看provideUStorage().create(apiDataBase)中的create()方法:

//這里的代碼跟retrofit是一摸一樣的
  public <T>  T create(final Class<T> service) {
        CommonUtil.validateServiceInterface(service);

        return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[]{service},
                new InvocationHandler() {
                    private final Object[] emptyArgs = new Object[0];

                    @Override
                    public Object invoke(Object o, Method method, Object[] args) throws Throwable {
                        // If the method is a method from Object then defer to normal invocation.
                        if (method.getDeclaringClass() == Object.class) {
                            return method.invoke(this, args);
                        }

                        return loadServiceMethod(method).invoke(args != null ? args : emptyArgs);
                    }
                });
    }

    ServiceMethod<?> loadServiceMethod(Method method) {
        ServiceMethod<?> result = serviceMethodCache.get(method);
        if (result != null) return result;

        synchronized (serviceMethodCache) {
            result = serviceMethodCache.get(method);
            if (result == null) {
              //這里是主要的解析注解的代碼
                result = ServiceMethod.parseAnnotations(this, method);
                if (result == null){
                    throw new IllegalArgumentException("annotation is not exits! please check your code");
                }
                serviceMethodCache.put(method, result);
            }
        }
        return result;
    }

我們看到上面的代碼用的跟retrofit一樣的代理模式坎吻,關(guān)于代理模式的文章可以查看流行框架源碼分析(14)-Proxy代理設(shè)計(jì)模式,我們接著看關(guān)鍵的代碼ServiceMethod.parseAnnotations(this, method),這個(gè)代碼調(diào)用了ServiceMethod類的靜態(tài)方法:

  @SuppressWarnings("unchecked")
    static <T> ServiceMethod<T> parseAnnotations(UStorage storage, Method method) {
        StorageFactory storageFactory = StorageFactory.parseAnnotations(storage, method);

        return storageFactory.getServiceMethod();
    }

我們看到這里代碼又調(diào)用了StorageFactory.parseAnnotations(storage, method)方法,這個(gè)方法主要是根據(jù)注解@DB@JSON@GETJSON來(lái)創(chuàng)建不同的ServiceMethod對(duì)象宇葱,進(jìn)行不同的策略選擇瘦真,這也是策略模式的一個(gè)變種,我們跟進(jìn)StorageFactory類中的方法:

    static StorageFactory parseAnnotations(UStorage storage, Method method) {
        return new Builder(storage, method).build();
    }

我們看到這個(gè)方法又調(diào)用了Builder類中的build()方法:

 static final class Builder {
        final UStorage storage;
        final Method method;
        final Annotation[] methodAnnotations;

        ServiceMethod<?> serviceMethod;


        Builder(UStorage storage, Method method) {
            this.storage = storage;
            this.method = method;
            this.methodAnnotations = method.getAnnotations();
        }

        StorageFactory build() {
            for (Annotation annotation : methodAnnotations) {
                parseMethodAnnotation(annotation);
            }

            return new StorageFactory(this);
        }

        private void parseMethodAnnotation(Annotation annotation){
            if (annotation instanceof DB){
                DB db = (DB) annotation;
                this.serviceMethod = DBServiceMethod.parseAnnotations(this.storage, this.method, db.table());
            }else if (annotation instanceof JSON){
                JSON json = (JSON) annotation;
                this.serviceMethod = JSONServiceMethod.parseAnnotations(this.storage, this.method, json.key(), json.convert());
            }else if (annotation instanceof GETJSON){
                GETJSON json = (GETJSON) annotation;
                this.serviceMethod = JSONServiceMethod.parseAnnotations(this.storage, this.method, json.key(), json.convert());
            }
        }

我們看到這里parseMethodAnnotation方法里面就是判斷是哪個(gè)注解,然后分別初始化serviceMethod這個(gè)全局變量黍瞧,最終會(huì)返回回去這個(gè)變量給外部的create方法中存進(jìn)map中诸尽。因?yàn)楂@取到了ServiceMethod對(duì)象了,我們看create方法后面干了啥loadServiceMethod(method).invoke(args != null ? args : emptyArgs);,這里直接就調(diào)用了方法invoke方法印颤,這個(gè)方法就是具體的每個(gè)ServiceMethod實(shí)現(xiàn)類中的方法您机,我們還是以查詢?yōu)槔?/p>

 private void parseMethodAnnotation(Annotation annotation){
            if (annotation instanceof DB){
                DB db = (DB) annotation;
                this.serviceMethod = DBServiceMethod.parseAnnotations(this.storage, this.method, db.table());
            }else if (annotation instanceof JSON){
                JSON json = (JSON) annotation;
                this.serviceMethod = JSONServiceMethod.parseAnnotations(this.storage, this.method, json.key(), json.convert());
            }else if (annotation instanceof GETJSON){
                GETJSON json = (GETJSON) annotation;
                this.serviceMethod = JSONServiceMethod.parseAnnotations(this.storage, this.method, json.key(), json.convert());
            }
        }

從上面的代碼入手,我們走DB注解這條路徑年局,調(diào)用DBServiceMethod.parseAnnotations(this.storage, this.method, db.table());方法:

static <ReturnT> DBServiceMethod<ReturnT> parseAnnotations(
            UStorage storage, Method method, Class<? extends RealmObject> table) {

        return new DBServiceMethod<>(method, table);
    }

這里就是一句簡(jiǎn)單的代碼际看,初始化了DBServiceMethod對(duì)象,我們跟進(jìn)構(gòu)造函數(shù)看看:

    private DBServiceMethod(Method method, Class<? extends RealmObject> table){
        this.parameterTypes = method.getGenericParameterTypes();
        this.parameterAnnotationsArray = method.getParameterAnnotations();
        this.table = table;

        if (null != method){
            for (Annotation annotation : method.getAnnotations()){
                parseHandler(annotation, method.getAnnotations());
            }
        }
    }

我們看到前幾行都是賦值操作矢否,將方法里面的參數(shù)類型仲闽,方法的參數(shù)注解,table注解里面的表名這些基本信息賦值僵朗,然后遍歷方法上面的注解赖欣,調(diào)用parseHandler方法:

  private void parseHandler(Annotation annotation, Annotation[] annotations) {
        if (annotation instanceof FIND){
            this.storageHandler = FindHandler.parseAnnotations(annotations, this.table);
        }else if(annotation instanceof SAVE){
            this.storageHandler = SaveHandler.parseAnnotations(annotations, this.table);
        }else if(annotation instanceof SAVEORUPDATE){
            this.storageHandler = SaveOrUpdateHandler.parseAnnotations(annotations, this.table);
        }else if(annotation instanceof UPDATE){
            this.storageHandler = UpdateHandler.parseAnnotations(annotations, this.table);
        }else if(annotation instanceof DELETE){
            this.storageHandler = DeleteHandler.parseAnnotations(annotations, this.table);
        }
    }

這個(gè)代碼和前面的代碼有點(diǎn)類似,這個(gè)地方也是分別判斷注解是哪個(gè)注解验庙,然后進(jìn)行什么樣的操作顶吮,這里因?yàn)槲覀兪侨〔樵優(yōu)槔晕覀兛吹谝粋€(gè)FindHandler.parseAnnotations(annotations, this.table);方法:

  private FindHandler(Annotation[] annotations, Class<? extends RealmObject> table){
        this.table = table;
        buildField(annotations);
    }

    private void buildField(Annotation[] annotations) {
        if (null != annotations){
            for (Annotation annotation : annotations){
                if (annotation instanceof FIND){
                    FIND find = (FIND) annotation;
                    this.orderBy = find.orderBy();
                    this.where = find.where();
                    this.distinct = find.distinct();
                    this.limit = find.limit();
                    this.eager = find.eager();
                }
            }
        }
    }

    public static HandlerAdapter parseAnnotations(Annotation[] annotations,final Class<? extends RealmObject> table){
        return new FindHandler(annotations, table);
    }

因?yàn)榇a較為簡(jiǎn)單粪薛,所以我都貼出來(lái)悴了,這里就是取到注解里面的東西,進(jìn)行賦值违寿,最重要的方法還是在FindHandler類中的invoke方法让禀,因?yàn)檫@個(gè)就是create方法里面調(diào)用的invoke方法:

  @SuppressWarnings("unchecked")
    @Override
    public DbResult invoke(Object[] args, Type[] parameterTypes, Annotation[][] parameterAnnotationsArray) {
        dbResult = new DbResult();
        try{
            RealmQuery<? extends RealmObject> query = UStorage.realm.where(this.table);
            RealmQuery<? extends RealmObject> whereFilteredQuery = FindConditionUtil.whereFilter(where, query, args , parameterTypes);

            RealmQuery<? extends RealmObject> otherFilteredQuery = FindConditionUtil.otherFilter(whereFilteredQuery, orderBy, limit, distinct);

            RealmResults result = otherFilteredQuery.findAllAsync();//這個(gè)地方所有的查詢操作都是用異步的方式
            result.addChangeListener(new OrderedRealmCollectionChangeListener<RealmResults>() {
                @Override
                public void onChange(RealmResults realmResults, OrderedCollectionChangeSet changeSet) {
                    dbResult.setDbFindCallBack(realmResults, changeSet);
                }
            });
        }catch (Exception e){
            e.printStackTrace();
        }

        return dbResult;
    }

這里面就是最重要的查詢操作了,首先我們回顧下我們查詢的業(yè)務(wù)接口實(shí)現(xiàn):

  @DB(table = User.class)
    @FIND(where = "name = ? and (age > ? or sex = ?)",limit = 10,orderBy = "age desc")
    DbResult<User> findUser(String name, int age, String sex);

查詢方法里面主要就是解析出來(lái)where條件陨界,主要的解析方法是應(yīng)用了正則表達(dá)式,方法實(shí)現(xiàn)主要在whereFilter方法痛阻,這個(gè)方法會(huì)再調(diào)用setFilter方法菌瘪,具體如下:

 public static RealmQuery<? extends RealmObject> setFilter(String set, String where, RealmQuery<? extends RealmObject> query, Object[] args, Type[] parameterTypes){
        linkCondition.clear();
        if (!CommonUtil.isEmptyStr(where)){//判斷where條件是否為空,如果不為空才需要添加條件查詢
            Pattern linkPattern = Pattern.compile(AND_OR);
            Matcher linkMatcher = linkPattern.matcher(where);

            while (linkMatcher.find()){//查找出來(lái)看有多少個(gè)and或者or,存儲(chǔ)到ArrayList中
                linkCondition.add(linkMatcher.group());
            }

            int whereLength;
            //說(shuō)明有復(fù)合條件查詢
            if (linkCondition.size() > 0){
                String[] whereArray = where.split(AND_OR);//將And或者or兩邊的條件分割出來(lái)
                whereLength = whereArray.length;
                if (CommonUtil.isEmptyStr(set)){
                    if (args.length != whereArray.length || parameterTypes.length != whereArray.length){
                        throw new IllegalArgumentException("parameter size is not equal to ?");
                    }
                }

                for (int i = 0;i < whereArray.length;i ++){//對(duì)每一個(gè)語(yǔ)句進(jìn)行構(gòu)建查詢
                    String whereCondition = whereArray[i];
                    Object parameter = args[i];
                    Type parameterType = parameterTypes[i];
                    //構(gòu)造查詢條件
                    buildWhereCondition(query, whereCondition, parameter, parameterType);

                    if (linkCondition.size() - 1 >= i){
                        String condition = linkCondition.get(i);
                        if ("and".equalsIgnoreCase(condition)){
                            query.and();
                        }else {
                            query.or();
                        }
                    }

                    if (!CommonUtil.isEmptyStr(whereCondition) && whereCondition.contains(")")){
                        query.endGroup();
                    }
                }
            }else {//說(shuō)明是單一條件
                buildWhereCondition(query, where, args.length == 0 ? null : args[0],parameterTypes.length == 0 ? null : parameterTypes[0]);
                whereLength = 1;
            }

            buildSetFilter(set, args,whereLength,parameterTypes);
        }

        return query;
    }

這個(gè)方法比較長(zhǎng)俏扩,實(shí)現(xiàn)也比較負(fù)責(zé)糜工,我這里簡(jiǎn)要說(shuō)下思路,這里首先用AND_OR這個(gè)正則表達(dá)式把where條件拆分開(kāi)來(lái)录淡,這樣多個(gè)and和or中間的部分拆出來(lái)了捌木,然后把這個(gè)中間部分分別用正則表達(dá)式去匹配,主要在buildWhereCondition(query, whereCondition, parameter, parameterType);方法里面嫉戚,當(dāng)然有時(shí)會(huì)有()這個(gè)操作為了區(qū)別優(yōu)先級(jí)的操作刨裆,我們這里會(huì)判斷遇到(或者)的時(shí)候進(jìn)行添加上query.beginGroup();或者query.endGroup();用來(lái)包裝成是一個(gè)模塊的條件。

到這里數(shù)據(jù)庫(kù)查詢的功能代碼講解已經(jīng)說(shuō)完彬檀,代碼算是比較簡(jiǎn)單帆啃,以后我們改變數(shù)據(jù)庫(kù),或者key-value的實(shí)現(xiàn)窍帝,只要在各種的Handler中實(shí)現(xiàn)就可以努潘,這個(gè)庫(kù)實(shí)現(xiàn)簡(jiǎn)單,但是思路還是非常好的坤学,易于擴(kuò)展疯坤,降低了耦合。當(dāng)然代碼還有mock網(wǎng)絡(luò)的方式深浮,這里就不分析了压怠,因?yàn)樯婕暗揭v解retrofit的源碼,這里推薦講解retrofit源碼的地方:流行框架源碼分析(9)-Retrofit2源碼解析看完這篇應(yīng)該就可以看懂工程UnifyStorage里面的unifystorage_mock子庫(kù)的代碼了略号,當(dāng)然不會(huì)的可以留言問(wèn)我刑峡,或者加我都可以。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末玄柠,一起剝皮案震驚了整個(gè)濱河市突梦,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌羽利,老刑警劉巖宫患,帶你破解...
    沈念sama閱讀 206,126評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異这弧,居然都是意外死亡娃闲,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門(mén)匾浪,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)皇帮,“玉大人,你說(shuō)我怎么就攤上這事蛋辈∈羰埃” “怎么了将谊?”我有些...
    開(kāi)封第一講書(shū)人閱讀 152,445評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)渐白。 經(jīng)常有香客問(wèn)我尊浓,道長(zhǎng),這世上最難降的妖魔是什么纯衍? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 55,185評(píng)論 1 278
  • 正文 為了忘掉前任栋齿,我火速辦了婚禮,結(jié)果婚禮上襟诸,老公的妹妹穿的比我還像新娘瓦堵。我一直安慰自己,他們只是感情好励堡,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,178評(píng)論 5 371
  • 文/花漫 我一把揭開(kāi)白布谷丸。 她就那樣靜靜地躺著,像睡著了一般应结。 火紅的嫁衣襯著肌膚如雪刨疼。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 48,970評(píng)論 1 284
  • 那天鹅龄,我揣著相機(jī)與錄音揩慕,去河邊找鬼。 笑死扮休,一個(gè)胖子當(dāng)著我的面吹牛迎卤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播玷坠,決...
    沈念sama閱讀 38,276評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼蜗搔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了八堡?” 一聲冷哼從身側(cè)響起樟凄,我...
    開(kāi)封第一講書(shū)人閱讀 36,927評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎兄渺,沒(méi)想到半個(gè)月后缝龄,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挂谍,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,883評(píng)論 2 323
  • 正文 我和宋清朗相戀三年叔壤,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片口叙。...
    茶點(diǎn)故事閱讀 37,997評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡炼绘,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出妄田,到底是詐尸還是另有隱情俺亮,我是刑警寧澤仗哨,帶...
    沈念sama閱讀 33,646評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站铅辞,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏萨醒。R本人自食惡果不足惜斟珊,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,213評(píng)論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望富纸。 院中可真熱鬧囤踩,春花似錦、人聲如沸晓褪。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,204評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)涣仿。三九已至勤庐,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間好港,已是汗流浹背愉镰。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,423評(píng)論 1 260
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留钧汹,地道東北人丈探。 一個(gè)月前我還...
    沈念sama閱讀 45,423評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像拔莱,于是被迫代替她去往敵國(guó)和親碗降。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,722評(píng)論 2 345

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