Ionic百度原生定位插件开发

一、背景啰唆

  这两天被临时要求帮忙解决一个安卓App上的定位问题。其实我是有点抵触心理的,因为我最近刚入手了Koa2和Vue,玩得不亦乐乎,不太想在这个时候被打断。但是作为一个有良好职业素养的搬砖工,同时也本着“自己也是一块砖,哪里需要哪里搬”的精神,同时感受到“这个问题好像只我能解决”的特殊使命感,我愉快地(不得不)接受了这个任务。经过分析,App是用Ionic开发的,用的是百度的Js定位api,多数机型,多数时候定位准确,但是出现不准的概率还是比较多,于是我决定给它做个原生插件,用百度的Android sdk重做定位(说得云淡风清,其实,过程让我好蛋疼)。
  下图是过程总结,先看一下,做到心理有个大概:


Ionic-Android原生插件开发流程.png

一、环境准备

1.1 Android开发环境准备

1.1.1 JDK、Android-sdk、gradle、android-studio(不用as的,也可以用其它IDE)

  以上三件套准备好,安装过程也不再赘述,有不清楚的地方可以百度,最终结果要达下图所示(相关版本是我本机的,不一定要一样):

环境变量

image.png

JDK版本

image.png

JDK下载地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-2133151.html

Gradle环境

image.png

Gradle下载地址:http://www.androiddevtools.cn/

Android SDK

  下图是我本机的情况,实际当然不需要这么多版本的sdk,只需要一个你期望的编译版本就好。

image.png

这里有你想要的关于Android的一切
http://www.androiddevtools.cn/

1.2 Ionic开发环境准备

nodejs、ionic相关、cordova、vsCode/webstorm(由你喜欢)

nodejs环境

装完nodejs后,验证一下:

image.png

从这里下载nodejs
https://nodejs.org/en/

Ionic

通过指令安装:

npm install -g ionic

结果:

image.png

  这个版本只是当时装的时候的最新稳定版本,每个人可能不一样,如果要装指定版本,请用类似“npm install -g ionic@3.0.5”的指令,其中3.0.5就是你要指定的版本号。

三、Ionic项目准备

3.1 生成官方标准项目

这里就用官方的标准项目为例,在cmd定位到你的项目目录,执行以下指令:

ionic start myApp tabs
image.png

  这里选择y,同意给你的Ionic应用添加iOS和Android的cordova依赖平台。如果你是用ionic来做类似微信公众号那样的应用就可以不加这个依赖。
  一堆日志后,会面临如图的第二个要选择的处理:


image.png

  这个就是问你要不要用ionic的在线平台,这里可以选择n,不要。最后跑完之后,生成的项目目录是这样的:


image.png

cmd进入到该目录下:
执行:
ionic server

把项目跑一下,起来后效果如下图:


image.png

image.png

3.2 增加Android编译平台

给你的ionic增加cordova cli

在你的项目目录下,执行指令:

npm install --save -D cordova

安装cordovacli,
还是在你的项目目录下,执行指令:

ionic cordova platform add android@6.4.0

这里要下的东西比较多,时间也比较久。

这里一定要加一个android的版本号指明你要依赖哪个android版本的sdk来编译

成功后,你的项目目录下会多出这两个目录:


image.png

完成!

四、Android插件开发

  我要开发的插件是百度地图的Android原生定位插件。因此要到百度平台去申请账号和创建App应用,到得相关的key,下载相关的so库和jar包
这个过程就不赘述了,百度的文档很清楚:http://lbsyun.baidu.com/

4.1 在Android原生应用上开发功能和自测

  这里啰唆一下,可以先在Androidd原生应用上写一遍你的逻辑,并且自测一下功能,保证你理解了你要做的功能是什么,用原生代码逻辑上怎么实现,自己测试通过,然后再搬到插件项目中去,因为插件项目调试比较麻烦,不要把bug留到那个时候去调试。

4.2 创建cordova插件项目:

安装plugman

在cmd执行以下命令:

npm install plugman -g
image.png

找一个你准备放插件项目的目录:
在该目录下执行:

plugman -create --name BDLocation --plugin_id com.test.bdlocation --plugin_version 1.0.0

  其中,BDLocation是我的插件项目名,com.test.bdlocation是我的主包名,作为插件的唯一标识。生成如下目录结构的项目:

image.png

在该目录下,执行:

plugman platform add --platform_name android

  在项目的src下会创建android文件夹,下面会生成一个文件BDLocation.java,这就是我们cordova插件的入口文件。另外在项目的www文件夹下还有一个js的调用例子。
  这里不详细讨论cordova的api,只作简单的例子介绍,希望后面有时间再详细讨论cordova的api。这是BDLocation.java里的内容:

  package com.test.bdlocation;

import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.CallbackContext;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

/**
 * This class echoes a string called from JavaScript.
 */
public class BDLocation extends CordovaPlugin {
    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if (action.equals("coolMethod")) {
            String message = args.getString(0);
            this.coolMethod(message, callbackContext);
            return true;
        }
        return false;
    }

    private void coolMethod(String message, CallbackContext callbackContext) {
        if (message != null && message.length() > 0) {
            callbackContext.success(message);
        } else {
            callbackContext.error("Expected one non-empty string argument.");
        }
    }
}
  下面来简单说一下execute方法:

  这个方法是插件的入口方法,js通过传入类名来让cordova找到这个插件,找到这个插件后,默认执行入口方法execute。

  参数action:通过js调用传进来,代表你要这个插件干什么,通常代表一个行为,这里默认的例子是调用了一个方法
  参数args:通过js调用传进来,类似java main方法传参,在js中就是传对象数组进来
  参数callbackContext:通过它来把结果回调给js,在js中会传来成功和失败的两个回调函数,对应callbackContext的success和error方法,如这里的coolMethod示例方法所示。
  返回boolean值,根据英文理解是,如果你收到的action是你想要的,就返回true,否则返回false。(这里还可以测试一下,如果首次不处理这个action,并且返回false,下次js调用如果还是这个类的这个action,有可能cordova不帮你调插件了,这一点我还没验证,回头再补)

  下面是wwww/BDLocation.js里调用示例:

var exec = require('cordova/exec');

exports.coolMethod = function (arg0, success, error) {
    exec(success, error, 'BDLocation', 'coolMethod', [arg0]);
};

  这里主要看exec函数的传参。一目了然,sucess和error分别对应BDLocation.java中的callbackContext成功与失败的回调,也就是说callbackContext中调用success方法,那么js中这个success方法就会被回调,error也是同理。
  BDLocation是代表了java中插件的类名(也是插件名,一般保持同名,不同会怎样呢?可以再试。),arg0当然就是你要传给插件的业务参数了。

4.3 编写插件代码

  现在我们回到前面在AndroidStudio上的应用:

  我们把插件项目的BDLocation.java文件直接copy到应用中:

image.png

  我们发现项目里根据还没有Cordova相关的依赖,代码会报错。于是我们还要添加一个模块:
  回到我们之前创建的ionic项目,在项目目录下,进入路径:platforms\android,找到该路径下的CordovaLib目录,记下该目录。
  现在再回到AndroidStudio,我把CordovaLib以模块的形式加入到应用项目依赖中,成功后如下图效果,BDLocation中的报错会消失。


image.png

  根据之前在应用中写的百度地图的定位功能,在BDLocation上编写相应功能:

package com.test.bdlocation;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Build;
import android.support.v4.app.ActivityCompat;
import android.support.v4.content.ContextCompat;

import com.baidu.location.Address;
import com.baidu.location.BDAbstractLocationListener;
import com.baidu.location.LocationClient;
import com.baidu.location.LocationClientOption;

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.util.ArrayList;
import java.util.List;

/**
 * Created by Administrator on 2018/8/8.
 */

public class BDLocation extends CordovaPlugin{
    private LocationClientOption option=new LocationClientOption();
    private LocationClient ml=null;
    private CallbackContext callbackContext;
    private List<String> permissionNeedCheck=null;
    public BDAbstractLocationListener myL=new BDAbstractLocationListener() {
        @Override
        public void onReceiveLocation(com.baidu.location.BDLocation bdLocation) {
            if(bdLocation!=null){
                JSONObject data=new JSONObject();
                try {
                    JSONObject object=new JSONObject();
                    object.put("type", bdLocation.getLocType());
                    object.put("latitude",bdLocation.getLatitude());
                    object.put("longitude",bdLocation.getLongitude());
                    JSONObject address=new JSONObject();
                    address.put("adcode",bdLocation.getAddress().adcode);
                    address.put("address",bdLocation.getAddress().address);
                    address.put("city",bdLocation.getAddress().city);
                    address.put("cityCode",bdLocation.getAddress().cityCode);
                    address.put("country",bdLocation.getAddress().country);
                    address.put("countryCode",bdLocation.getAddress().countryCode);
                    address.put("district",bdLocation.getAddress().district);
                    address.put("province",bdLocation.getAddress().province);
                    address.put("street",bdLocation.getAddress().street);
                    address.put("streetNumber",bdLocation.getAddress().streetNumber);
                    object.put("address",address);
                    object.put("city",bdLocation.getCity());
                    object.put("cityCode",bdLocation.getCityCode());
                    data.put("status","success");
                    data.put("location",object);
                    data.put("code","000");
                    data.put("errMsg","");
                    PluginResult pluginResult=new PluginResult(PluginResult.Status.OK,data);
                    callbackContext.sendPluginResult(pluginResult);
                } catch (JSONException e) {
                    e.printStackTrace();
                }
                ml.stop();
            }
        }
    };
    @Override
    public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException {
        if(Build.VERSION.SDK_INT>=23){
            permissionNeedCheck=new ArrayList<String>();
            permissionNeedCheck.add(Manifest.permission.ACCESS_COARSE_LOCATION);
            permissionNeedCheck.add(Manifest.permission.ACCESS_FINE_LOCATION);
            this.callbackContext=callbackContext;
            if (action.equals("location")) {
                this.location();
                return true;
            }
            return false;
        }else{
            if(action.equals("location")){
                this.startLocate();
                return true;
            }
            return false;
        }
    }
    private boolean permissionCheck(){
        List<String> unGrantedPermission=new ArrayList<String>();
        for(String permission:permissionNeedCheck){
            if(PackageManager.PERMISSION_GRANTED!=ContextCompat.checkSelfPermission(cordova.getActivity(),permission)){
                unGrantedPermission.add(permission);
            }
        }
        this.permissionNeedCheck=unGrantedPermission;
        return unGrantedPermission.size() > 0 ? false : true;
    }
    private void askPermission(){
        String[] unGrantedPer=new String[permissionNeedCheck.size()];
        for(int i=0;i<permissionNeedCheck.size();i++){
            unGrantedPer[i]=permissionNeedCheck.get(i);
        }
        ActivityCompat.requestPermissions(cordova.getActivity(),unGrantedPer,1);
    }

    @Override
    public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) throws JSONException {
        super.onRequestPermissionResult(requestCode, permissions, grantResults);
        location();
    }

    private void location(){
        if(permissionCheck()){
            startLocate();
        }else{
            askPermission();
        }

    }
    private void startLocate(){
        option.setLocationMode(LocationClientOption.LocationMode.Hight_Accuracy);
        option.setCoorType("bd09ll");
        option.setOpenGps(true);
        option.setIsNeedAddress(true);
        option.setIgnoreKillProcess(false);
        ml=new LocationClient(cordova.getActivity());
        ml.setLocOption(option);
        ml.registerLocationListener(myL);
        ml.start();
    }
}

  这里不再详细说明里面的代码了,主要是定位和权限相关的内容。

  这里唯一可以说明一下的就是PluginResult,当你要从插件返回一个复杂结果给js的时候,就用它。

五、打包与调试

  最后一个大环节就是打包与调试

5.1首先要进行文件准备

  到目前为止我们只写了代码,我们在应用中调试了相关的逻辑,我们so库,jar包,等相关的东西都还没有放进插件相关的目录。

  现在我们回到插件项目的目录

把在应用中写好的BDLocation.java文件Copy到插件项目的src/android文件夹,把初始生成的BDLocation.java覆盖掉:

image.png

库处理——在项目目录的src/android下新建libs文件夹,把相关的so库,jar包放到libs下:

image.png

plugin.xml文件处理:

默认生成的plugin.xml文件内容如下:

<?xml version='1.0' encoding='utf-8'?>
<plugin 
    id="com.test.bdlocation" 
    version="1.0.0" 
    xmlns="http://apache.org/cordova/ns/plugins/1.0" 
    xmlns:android="http://schemas.android.com/apk/res/android">
        <name>BDLocation</name>
        <js-module name="BDLocation" src="www/BDLocation.js">
            <clobbers target="cordova.plugins.BDLocation" />
        </js-module>
        <platform name="android">
            <config-file parent="/*" target="res/xml/config.xml">
                <feature name="BDLocation"><param name="android-package" value="com.test.bdlocation.BDLocation" />
                </feature>
            </config-file>
            <config-file parent="/*" target="AndroidManifest.xml">
                
            </config-file>
            <source-file src="src/android/BDLocation.java" target-dir="src/com/test/bdlocation/BDLocation" />
        </platform>
</plugin>

配置完成后,如下:

<?xml version='1.0' encoding='utf-8'?>
<plugin 
    id="com.test.bdlocation" 
    version="1.0.0" 
    xmlns="http://apache.org/cordova/ns/plugins/1.0" 
    xmlns:android="http://schemas.android.com/apk/res/android">
        <name>BDLocation</name>
        <js-module name="BDLocation" src="www/BDLocation.js">
            <clobbers target="cordova.plugins.BDLocation" />
        </js-module>
        <platform name="android">
            <config-file parent="/*" target="res/xml/config.xml">
                <!--这里的value值要对应你的ionic项目的android应用主包名-->
                <feature name="BDLocation"><param name="android-package" value="你的ionic项目对应用android应用主包名,如com.jianshu.app" />
                </feature>
            </config-file>
            <!--这个标签内的配置,将会copy到ionic项目的android主配置文件下<manifest>标签内,所以这里可以放权限-->
            <config-file parent="/*" target="AndroidManifest.xml">
                <!-- 这个权限用于进行网络定位-->
                <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"></uses-permission>
                <!-- 这个权限用于访问GPS定位-->
                <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION"></uses-permission>
                <!-- 用于访问wifi网络信息,wifi信息会用于进行网络定位-->
                <uses-permission android:name="android.permission.ACCESS_WIFI_STATE"></uses-permission>
                <!-- 获取运营商信息,用于支持提供运营商信息相关的接口-->
                <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"></uses-permission>
                <!-- 这个权限用于获取wifi的获取权限,wifi信息会用来进行网络定位-->
                <uses-permission android:name="android.permission.CHANGE_WIFI_STATE"></uses-permission>
                <!-- 用于读取手机当前的状态-->
                <uses-permission android:name="android.permission.READ_PHONE_STATE"></uses-permission>
                <!-- 写入扩展存储,向扩展卡写入数据,用于写入离线定位数据-->
                <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"></uses-permission>
                <!-- 访问网络,网络定位需要上网-->
                <uses-permission android:name="android.permission.INTERNET" />
                <!-- SD卡读取权限,用户写入离线定位数据-->
                <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"></uses-permission>
            </config-file>
            <!--同理,这个标签内的配置,将会copy到ionic项目的android主配置文件下的<application>标签内-->
            <config-file parent="/manifest/application" target="AndroidManifest.xml">
                <meta-data
                    android:name="com.baidu.lbsapi.API_KEY"
                    android:value="TVBiUUUYv8koeL2jysdG6GHU3TW0jcgM"></meta-data>
                <service android:name="com.baidu.location.f" android:enabled="true" android:process=":remote"> </service>
            </config-file>
            <!--想必聪明的你已经看出来了,下面的配置就是把你插件的java文件,jar包,so库文件或者其它你的插件要用的文件,copy到你的ionic项目的android打包环境(就是我们的android原生项目结构)对应的目录下-->
            <!--这里要特别留意,保持你的BDLocation.java文件内package声明的包名和你将要把它放置的包名要一致-->
            <source-file src="src/android/BDLocation.java" target-dir="src/com/test/bdlocation/BDLocation" />
            <source-file src="src/android/libs/arm64-v8a/libindoor.so" target-dir="libs/arm64-v8a" />
            <source-file src="src/android/libs/arm64-v8a/liblocSDK7b.so" target-dir="libs/arm64-v8a" />
            <source-file src="src/android/libs/armeabi/libindoor.so" target-dir="libs/armeabi" />
            <source-file src="src/android/libs/armeabi/liblocSDK7b.so" target-dir="libs/armeabi" />
            <source-file src="src/android/libs/armeabi-v7a/libindoor.so" target-dir="libs/armeabi-v7a" />
            <source-file src="src/android/libs/armeabi-v7a/liblocSDK7b.so" target-dir="libs/armeabi-v7a" />
            <source-file src="src/android/libs/x86/libindoor.so" target-dir="libs/x86" />
            <source-file src="src/android/libs/x86/liblocSDK7b.so" target-dir="libs/x86" />
            <source-file src="src/android/libs/x86_64/libindoor.so" target-dir="libs/x86_64" />
            <source-file src="src/android/libs/x86_64/liblocSDK7b.so" target-dir="libs/x86_64" />
            <source-file src="src/android/libs/BaiduLBS_Android.jar" target-dir="libs" />
        </platform>
</plugin>

配置的注释我都尽量写详细了,我这里主要就是相关权限、地图的配置、对应jar、so、java文件的对位copy。

插件package.json文件生成与配置

在我们的插件主目录下,执行cmd指令:

npm init

生成初始package.json文件:


image.png

package.json初始内容如下:

{
  "name": "bdlocation",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

配置完成后:

{
  "name": "bdlocation",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "cordova": {
    "id": "com.test.bdlocation",
    "platforms": [
      "android"
    ]
  },
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "author": "",
  "license": "ISC"
}

  主要是加了cordova插件相关配置。
  其中id是指你给你的插件自定义的唯一id,platform就是你的插件的运行平台,如果支持ios,你可以加上ios。
  到此文件准备就完成了!!!

5.2给Ionic项目添加插件

  现在回到我们的Ionic项目目录

添加自定义插件到Ionic项目

  在ionic项目目录下,执行cmd指令:

ionic cordova plugin add ../BDLocation

  其中,../BDLocation是自定义插件的项目的相对目录,完成后,Ionic项目目录下,会产生一个plugins文件夹,里面有我们安装好的插件:

image.png
image.png

5.3打包debug apk装备进行真机调试

  在Ionic项目目录下,执行cmd指令:

ionic cordova build android --debug

  这个过程相对比较久,第一次的时候还要联网下载不少东西,完成后在Ionic项目的platforms\android\build\outputs\apk的目录下,生成了相应的android-debug.apk文件就是了。

5.4真机调试

  把上面的apk copy到手机上安装,或者通过adb指令安装都可以。这里我们用chrome的DevTools进行真机调试。在Chrome地址中输入chrome://inspect/#devices,用USB接上你的Android手机,打开调试:

image.png

如上图,我的pro5就被找到了。
运行你的app,你们看到多了一个Inspect标签,可以点击:
image.png

点击进去后,发现:
image.png

  有日志输出了!!!!
  当然你完全也可以用Android Studio来看日志,还可以在cmd用adb相关命令来看日志。

5.4打生产包与签名

  在Ionic项目目录下,执行cmd指令:

ionic cordova build android --release

得到未签名的apk文件:android-unsign.apk(根据打包的输出日志,可以看到apk包被打出到哪个目录,叫什么名字)
  把未签名apk,keystore文件放到同一个目录,在该目录下,执行cmd指令:

jarsigner -verbose -keystore your.keystore -signedjar complete.apk android-unsign.apk your.keystore

得到签名好的apk,complete.apk

  虽然,还有不少是我还想写的详细一点的内容,还有一些是我原本计划要写但是又被我砍掉的内容,但是就写到这里吧,等下还要再校正一遍,我的原力快用完了!!!

  有错误的地方欢迎指正。

最后我的环境如下:


QQ图片20181008111658.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 213,014评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,796评论 3 386
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,484评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,830评论 1 285
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,946评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,114评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,182评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,927评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,369评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,678评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,832评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,533评论 4 335
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,166评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,885评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,128评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,659评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,738评论 2 351

推荐阅读更多精彩内容