使用egg.js + Vue實現(xiàn)一個簡書(三)---服務(wù)端搭建

寫在前面

完成前端搭建后,我們需要搭建服務(wù)端代碼两芳。
首先獻(xiàn)上代碼倉庫
本文將有以下幾個模塊

  1. egg項目安裝運行
  2. 簡單的egg寫法示例摔寨,寫一個簡單的接口,聯(lián)通前后端
  3. 接入數(shù)據(jù)庫和JWT怖辆,完成登錄注冊的功能
  4. 以用戶信息查詢優(yōu)化為例是复,講講redis的安裝和使用

項目安裝運行

看到第三篇了,想必nodejs已經(jīng)裝上了

// 創(chuàng)建一個文件夾
mkdir myblog
cd myblog
// 初始化一個node項目
npm int
// 安裝egg包
npm i -S egg
npm i -D egg-bin

在package.json的script中添加一句"dev": "egg-bin dev"竖螃,之后我們將使用npm run dev命令來啟動項目淑廊。
egg項目需要按照規(guī)范的文件目錄來建立,否則啟動時會報錯
當(dāng)前新建項目的目錄結(jié)構(gòu)如圖

新建的nodejs項目目錄

egg官網(wǎng)上約定的目錄結(jié)構(gòu)

我們先依照上圖創(chuàng)建必要的文件夾和文件(上圖已做標(biāo)記)
router.js中定義一個路由規(guī)則

// app/router.js
module.exports = app => {
    const { router,  controller } = app;
    router.get('/home/index', controller.home.index); // 首頁
    router.redirect('/', '/home/index', 302); // 當(dāng)沒有指定路由的時候特咆,重定向到首頁
}
// app/controller/home.js
'use strict';
const Controller = require('egg').Controller;
class HomeController extends Controller {
  async index() { // 首頁
    this.ctx.body = 'hello world';
  }
}
module.exports = HomeController;

config.**.js表示不同環(huán)境下的配置季惩,default是在各個環(huán)境下都生效的配置,實際上某個環(huán)境的最終配置,是default和相應(yīng)環(huán)境配置的合并蜀备。
配置中必須要有keys這個屬性关摇,其值是一個自定義的字符串,所以我們寫在config.default.js中碾阁。
config有多種exports的方法输虱,具體可以參見官方文檔,寫的很詳細(xì)脂凶。

// config/config.default.js
exports.keys = 'testblog';

有以上代碼宪睹,項目就可以跑起來了,執(zhí)行npm run dev蚕钦,訪問 http://127.0.0.1:7001
可以看到亭病,路由被重定向到了 http://127.0.0.1:7001/home/index,頁面上顯示hello world嘶居。說明我們的項目已經(jīng)成功搭建并運行起來了

我的第一個接口

下面我們將定義一個post路由罪帖,用于分頁查詢用戶列表,并與前端聯(lián)通邮屁。

// router.js加一條路由配置
router.post('/home/list', controller.home.list); // 獲取用戶列表
// app/controller/home.js中增加一個list方法
async list() { // 用戶列表
    const res = await this.ctx.service.home.list(this.ctx.request);
    this.ctx.body = res;
    this.ctx.status = res.status;
  }

參考上面官方目錄結(jié)構(gòu)整袁,controller負(fù)責(zé)請求的接收和結(jié)果的返回,具體的業(yè)務(wù)邏輯(查詢數(shù)據(jù)庫之類的)應(yīng)該放到service層佑吝。所以我們在app下新建一個service文件夾坐昙,創(chuàng)建home.js∮蠓蓿可以通過this.ctx.service.home.XX訪問到該文件中定義的方法炸客。

// app/service/home.js
'use strict';
const Service = require('egg').Service;
class HomeService extends Service {
    async list(data) { // 用戶列表
        return {
            message: 'ok',
            status: 200,
            result: {
                list: [
                    { id: 1, name: '王大娃' },
                    { id: 2, name: '王二娃' },
                    { id: 3, name: '王三娃' },
                ],
                pageSize: data.pageSize,
                current: data.current,
                totalCount: 3,
                total: 1
            }
        }
    }
}
module.exports = HomeService;

list方法需要傳入一個參數(shù)data,從controller中可以看到戈钢,我們將客戶端的入?yún)his.ctx.request直接傳給了service的方法痹仙。
這里我們直接寫死假數(shù)據(jù)返回,先聯(lián)通前后端殉了,數(shù)據(jù)庫操作后面慢慢展開开仰。

接下來測試一下接口能不能正常訪問

我們可以先在瀏覽器控制臺中使用fetch方法寫個簡單的異步調(diào)用測試一下

// 瀏覽器控制臺中
fetch('http://127.0.0.1:7001/home/list', {
    method: 'POST',
    headers: {'content-type': 'application/json'},
    body: JSON.stringify({ pageSize: 10, current: 1 })
})

敲回車,發(fā)現(xiàn)報了csrf token出錯


403報錯

服務(wù)端控制臺報錯

出這個錯誤的原因是:egg 框架內(nèi)置了安全系統(tǒng)宣渗,默認(rèn)開啟防止 XSS 攻擊 和 CSRF 攻擊抖所,每次請求得時候請求頭必須攜帶csrfToken字段梨州。
解決方法是痕囱,在config中指定校驗用的header屬性名稱,前端從cookie中取到csrfToken的值暴匠,將其放到請求header中鞍恢,即可通過驗證。

// config.default.js 添加一段
exports.security = {
    csrf: {
        headerName: 'x-csrf-token'
    }
}

在瀏覽器控制臺中查到cookie中csrfToken對應(yīng)的值,將其傳入header中帮掉,然后重新請求弦悉,發(fā)現(xiàn)接口調(diào)通了。

// 瀏覽器控制臺中
fetch('http://127.0.0.1:7001/home/list', {
    method: 'POST',
    headers: {'content-type': 'application/json', 'x-csrf-token': 'V9357i1wopen1uSDoakpY7Ex'},
    body: JSON.stringify({ pageSize: 10, current: 1 })
})
接口正確返回

前端代碼中會用axios封裝請求蟆炊,需要在請求前傳入header


前端axios封裝

接入數(shù)據(jù)庫和JWT稽莉,實現(xiàn)登陸注冊功能

  1. 首先要安裝mysql和navicat(可視化的數(shù)據(jù)庫管理工具)
    大家可以去官網(wǎng)下載,mysql推薦用社區(qū)版涩搓,不收費污秆。navicat下載后需要破解,百度很容易找到攻略昧甘,相信難不倒你
  2. 軟件安裝完畢后良拼,在egg項目中使用mysql,還得安裝egg-mysql這個包
    npm i -S egg-mysql
  3. 在config中添加mysql相關(guān)配置 plugin.js中指定插件的使用情況
// config/config.default.js中添加一段
exports.mysql = {
    client: {
        // host
        host: 'localhost',
        // 端口號
        port: '3306',
        // 用戶名
        user: 'root',
        // 密碼
        password: '你的密碼',
        // 數(shù)據(jù)庫名
        database: 'blog',
    },
    // 是否加載到 app 上充边,默認(rèn)開啟
    app: true,
    // 是否加載到 agent 上庸推,默認(rèn)關(guān)閉
    agent: false,
};
// config/plugin.js
exports.mysql = {
    enable: true, // 是否啟用mysql插件
    package: 'egg-mysql', // 插件對應(yīng)的node包
};
  1. 在數(shù)據(jù)庫中創(chuàng)建表格,我們創(chuàng)建一個用戶表


    用戶表數(shù)據(jù)結(jié)構(gòu)
  2. controller文件夾中創(chuàng)建user.js
'use strict';
const Controller = require('egg').Controller;
class UserController extends Controller {
  async register() { // 注冊
    const res = await this.ctx.service.user.register(this.ctx.request.body);
    this.ctx.body = res;
    this.ctx.status = res.status;
  }
  async login() { // 登錄
    const res = await this.ctx.service.user.login(this.ctx.request.body);
    this.ctx.body = res;
    this.ctx.status = res.status;
  }
}
module.exports = UserController;
  1. service文件夾中創(chuàng)建user.js
'use strict';

const Service = require('egg').Service;
const utility = require('utility');
const JWT = require('jsonwebtoken');

class UserService extends Service {
    async register(data) { // 注冊
        // 開啟事務(wù)
        const result = await this.app.mysql.beginTransactionScope(async conn => {
            const userList = [];
            const now = Date.now();
            data.password = utility.md5(data.password);
            userList.push({
                nick_name: data.nickName,
                account: data.account,
                password: data.password,
                create_time: now,
                update_time: now
            });
            try {
                // 先校驗邀請碼是否正確
                const res = await conn.select('invite', {
                    where: {
                        invite_code: data.code
                    }
                });
                if (!res || res.length === 0) {
                    return {
                        message: '該邀請碼不存在浇冰,請確認(rèn)后再試',
                        status: 501,
                        result: null
                    };
                }

                // 檢查用戶名是否重復(fù)
                const accountCheck = await conn.query(`select id from user where account='${data.account}'`);
                if (accountCheck && accountCheck.length > 0) {
                    return {
                        message: '用戶名重復(fù)贬媒,請修改后重試',
                        status: 501,
                        result: null
                    }
                }

                // 檢查昵稱是否重復(fù)
                const nickNameCheck = await conn.query(`select id from user where nick_name='${data.nick_name}'`);
                if (nickNameCheck && nickNameCheck.length > 0) {
                    return {
                        message: '昵稱重復(fù),請修改后重試',
                        status: 501,
                        result: null
                    }
                }

                // 寫入
                const insertOpe = await conn.insert('user', userList);
                if (insertOpe.affectedRows !== 1) {
                    console.log('寫入用戶信息出錯');
                    return {
                        message: '系統(tǒng)異常',
                        status: 501,
                        result: null
                    }
                }

                // 更新invite表
                const updateRecord = await conn.query(`update invite set invite_count='${res[0].invite_count + 1}' where user_id='${res[0].user_id}'`);
                if (updateRecord.affectedRows !== 1) {
                    console.log('更新邀請表出錯');
                    return {
                        message: '系統(tǒng)異常',
                        status: 501,
                        result: null
                    }
                }

                // 寫入邀請關(guān)系表
                const insertRelation = await conn.insert('invite_relation', [{
                    user_id: res[0].user_id,
                    be_invited_user_id: insertOpe.insertId
                }]);
                if (insertRelation.affectedRows !== 1) {
                    console.log('寫入邀請關(guān)系表出錯');
                    return {
                        message: '系統(tǒng)異常',
                        status: 501,
                        result: null
                    }
                }

                return {
                    message: '注冊成功',
                    status: 200,
                    result: true
                }
            } catch (err) {
                console.log(err);
                return {
                    message: '系統(tǒng)異常湖饱,請稍后再試',
                    status: 501,
                    result: null
                };
            }
        }, this.ctx);
        return result;
    }

    async login(data) {
        // 開啟事務(wù)
        const result = await this.app.mysql.beginTransactionScope(async conn => {
            try {
                // 檢查賬號是否存在
                const checkAccount = await conn.select('user', {
                    where: {
                        account: data.account
                    }
                });
                let password = '';
                if (checkAccount && checkAccount.length > 0) {
                    password = checkAccount[0].password;
                    let inputPassword = utility.md5(data.password);
                    if (inputPassword === password) {
                        // 簽發(fā)jwt token
                        const token = JWT.sign({
                            userId: checkAccount[0].id,
                            nickName: checkAccount[0].nick_name,
                            account: checkAccount[0].account,
                            avatar: checkAccount[0].avatar
                        }, this.config.jwt.secret, {
                            expiresIn: this.config.jwt.expiresIn
                        });
                        return {
                            message: '登錄成功',
                            status: 200,
                            result: {
                                data: {
                                    id: checkAccount[0].id,
                                    nickName: checkAccount[0].nick_name,
                                    account: checkAccount[0].account,
                                    avatar: checkAccount[0].avatar
                                },
                                token
                            }
                        }
                    } else {
                        return {
                            message: '密碼錯誤掖蛤,請檢查拼寫',
                            status: 501,
                            result: null
                        }
                    }
                } else {
                    return {
                        message: '該用戶名不存在,請檢查拼寫或者注冊新賬號',
                        status: 501,
                        result: null
                    }
                }
            } catch (err) {
                return {
                    message: '系統(tǒng)異常井厌,請稍后再試',
                    status: 501,
                    result: null
                };
            }
        });
        return result;
    }
}
module.exports = UserService;
  • 由于注冊前有一步邀請碼驗證的操作蚓庭,所以注冊方法中有兩個關(guān)于邀請的表的操作,大家可以忽略掉先仅仆。
  • service中的方法結(jié)構(gòu)是一樣的器赞,首先使用this.app.mysql.beginTransactionScope()方法新建一個事務(wù),然后在事務(wù)中進(jìn)行數(shù)據(jù)庫操作墓拜,當(dāng)有多個數(shù)據(jù)庫操作的時候港柜,其中一個出錯,之前的操作都會回滾咳榜,避免臟數(shù)據(jù)的出現(xiàn)夏醉。
  • try...catch...語句進(jìn)行異常處理
  • service中方法的返回格式是統(tǒng)一的
{
    message: '返回的文案',
    status: 'http狀態(tài)碼',
    result: '操作的處理結(jié)果'
}
  • 數(shù)據(jù)庫的增刪改查使用conn提供的select、delete涌韩、insert等方法畔柔,如果sql比較復(fù)雜,可以用conn.query()方法臣樱,直接傳入完整的sql語句進(jìn)行數(shù)據(jù)庫處理靶擦。更詳細(xì)的使用方法腮考,可以參考egg-mysql的官方文檔。
  1. JWT的使用
  • JWT是json web token的縮寫玄捕,是為了在網(wǎng)絡(luò)應(yīng)用環(huán)境間傳遞聲明而執(zhí)行的一種基于JSON的開放標(biāo)準(zhǔn)(RFC 7519).該token被設(shè)計為緊湊且安全的踩蔚,特別適用于分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務(wù)提供者間傳遞被認(rèn)證的用戶身份信息枚粘,以便于從資源服務(wù)器獲取資源馅闽,也可以增加一些額外的其它業(yè)務(wù)邏輯所必須的聲明信息,該token也可直接被用于認(rèn)證馍迄,也可被加密捞蛋。
  • jwt提供了signverify兩個方法,sign用于簽發(fā)token柬姚,verify用于校驗token是否有效拟杉。
  • 簽發(fā)的代碼可以參考上面service/user.js中的login方法,簽發(fā)時可以傳入一些非敏感用戶信息量承,在校驗的時候可以被解析出來搬设,用于確認(rèn)當(dāng)前訪問的用戶。
  • 校驗的代碼可以參考app/extend/helper.js中的checkLogin方法
  • 關(guān)于JWT的詳細(xì)解析可以點這里撕捍,就不展開了拿穴。
jwt的安裝和使用
  1. 服務(wù)端要支持CORS(跨來源資源共享)策略
    npm i -S jsonwebtoken
    npm i -S egg-cors
  2. 啟用插件
// config/plugin.js 增加一段
exports.cors = {
    enable: true,
    package: 'egg-cors'
}
// config/config.default.js 增加一段
exports.cors = {
    origin: '*', // 不限制來源
    allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS',
}
exports.jwt = {
    enable: true,
    secret: 'myblog', // 自定義的秘鑰,用于token的簽發(fā)和校驗忧风,不應(yīng)該流露出去
    expiresIn: 60 * 60 // 有效時間默色,單位秒
};
  1. jwt工作流程
  • 登錄接口中,當(dāng)用戶名密碼校驗通過后狮腿,用jwt簽發(fā)token腿宰,帶入上面配置的secret,expiresIn缘厢,以及用戶id吃度、用戶名、昵稱贴硫、頭像等非敏感信息椿每,然后將token作為接口結(jié)果返回給客戶端。
  • 客戶端拿到token后英遭,在之后的接口調(diào)用時间护,將token傳到header的authorization字段中里,當(dāng)有些接口需要登錄信息時挖诸,會去header中獲取token汁尺,并進(jìn)行jwt校驗,如果校驗不通過或者token不存在税灌,說明用戶未登錄或者登錄狀態(tài)過期均函,需要重新登錄。
  • 服務(wù)端jwt校驗通過菱涤,可以獲得當(dāng)前訪問的用戶信息苞也,并進(jìn)行接口相應(yīng)的操作。
    以上粘秆,登錄注冊的功能就完成了如迟。前端的代碼可以去前端項目倉庫中拉取細(xì)看。

redis的使用

我們可以通過緩存一些常用的數(shù)據(jù)攻走,從而達(dá)到減少數(shù)據(jù)庫操作的優(yōu)化目的殷勘。以獲取用戶信息的接口為例。

  1. 服務(wù)端收到接口請求昔搂,先判斷是否存在該用戶信息的緩存玲销。如果存在,直接將緩存作為接口結(jié)果返回摘符。
  2. 如果緩存不存在(沒有存或者緩存過期了)贤斜,則請求接口,并將結(jié)果存到緩存逛裤,返回請求的結(jié)果瘩绒。
  3. 如果用戶的信息發(fā)生變更,需要更新緩存带族。

接下來講講如何安裝使用redis

  • 安裝和配置
    本地下載redis工具锁荔,并啟動
// redis目錄下
redis-server.exe redis.windows.conf   // 按照配置啟動redis
redis啟動后的控制臺

在node項目中配置

npm i -S egg-redis  // 安裝redis的node包
// config/config.default.js 增加一段
exports.redis = {
    client: {
        port: 6379,
        host: '127.0.0.1',
        password: 'auth',
        db: 0
    }
}
// config/plugin.js 增加一段
exports.redis = {
    enable: true,
    package: 'egg-redis'
}

注意,當(dāng)項目中用到redis后蝙砌,再啟動本地服務(wù)前阳堕,要先啟動redis,否則會報錯

  • 封裝redis的方法
    redis提供了獲取择克、寫入鍵值對辉川,查詢長度赋兵、值自增等方法,我將它們封裝進(jìn)一個app/service/cache.js中。redis更多方法可以參考egg-redis的文檔脐瑰。
// 以app/service/user.js中的porfile方法為例
async profile(id) {
        const userId = id ? id : this.ctx.user.userId;
        const userCache = await this.ctx.service.cache.get(`profile_${userId}`);
        let result = null;
        if (!userCache) {
            // 開啟事務(wù)
            result = await this.app.mysql.beginTransactionScope(async conn => {
                try {
                    // 查詢用戶信息
                    const res = await conn.select('user', {
                        where: {
                            id: userId
                        }
                    });
                    if (res && res.length > 0) {
                        const obj = {
                            id: res[0].id,
                            nickName: res[0].nick_name,
                            account: res[0].account,
                            gender: res[0].gender,
                            birth: res[0].birth,
                            brief: res[0].brief,
                            website: res[0].website,
                            avatar: res[0].avatar,
                            userEmail: res[0].email,
                            telephone: res[0].telephone,
                            total_words: res[0].total_words,
                            article_count: res[0].article_count,
                            focus_count: res[0].focus_count,
                            fans_count: res[0].fans_count
                        }
                        await this.ctx.service.cache.set(`profile_${res[0].id}`, obj, 60 * 60);
                        return {
                            message: 'ok',
                            status: 200,
                            result: obj
                        };
                    } else {
                        return {
                            message: '查詢的用戶不存在',
                            status: 501,
                            result: null
                        };
                    }
                } catch (err) {
                    return {
                        message: '系統(tǒng)異常,請稍后再試',
                        status: 501,
                        result: null
                    };
                }
            });
        } else {
            result = {
                message: '讀取緩存',
                status: 200,
                result: userCache
            }
        }
        return result;
    }

將用戶信息的緩存癌瘾,以profile_ + 用戶id的格式存儲更扁。
當(dāng)調(diào)用profile接口時,先檢查緩存是否存在勺鸦,用到了cache.js中的get方法并巍。
如果不存在緩存,則調(diào)接口换途,并寫入緩存懊渡,有效期一小時刽射。
如果存在,直接將緩存中的結(jié)果返回剃执。

總結(jié)

在本文中誓禁,我們從無到有搭建了egg項目,按規(guī)范組織了目錄結(jié)構(gòu)肾档,嘗試寫了第一個接口摹恰,了解了egg的基本寫法。然后接入mysql數(shù)據(jù)庫和JWT怒见,實現(xiàn)了登錄注冊功能俗慈。接著又以用戶信息查詢優(yōu)化展開,學(xué)習(xí)了redis緩存的安裝及使用遣耍。希望對大家能有幫助闺阱。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市舵变,隨后出現(xiàn)的幾起案子馏颂,更是在濱河造成了極大的恐慌,老刑警劉巖棋傍,帶你破解...
    沈念sama閱讀 206,311評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件救拉,死亡現(xiàn)場離奇詭異,居然都是意外死亡瘫拣,警方通過查閱死者的電腦和手機(jī)亿絮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,339評論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來麸拄,“玉大人派昧,你說我怎么就攤上這事÷G校” “怎么了蒂萎?”我有些...
    開封第一講書人閱讀 152,671評論 0 342
  • 文/不壞的土叔 我叫張陵,是天一觀的道長淮椰。 經(jīng)常有香客問我五慈,道長,這世上最難降的妖魔是什么主穗? 我笑而不...
    開封第一講書人閱讀 55,252評論 1 279
  • 正文 為了忘掉前任泻拦,我火速辦了婚禮,結(jié)果婚禮上忽媒,老公的妹妹穿的比我還像新娘争拐。我一直安慰自己,他們只是感情好晦雨,可當(dāng)我...
    茶點故事閱讀 64,253評論 5 371
  • 文/花漫 我一把揭開白布架曹。 她就那樣靜靜地躺著隘冲,像睡著了一般。 火紅的嫁衣襯著肌膚如雪绑雄。 梳的紋絲不亂的頭發(fā)上展辞,一...
    開封第一講書人閱讀 49,031評論 1 285
  • 那天,我揣著相機(jī)與錄音绳慎,去河邊找鬼。 笑死漠烧,一個胖子當(dāng)著我的面吹牛杏愤,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播已脓,決...
    沈念sama閱讀 38,340評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼珊楼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了度液?” 一聲冷哼從身側(cè)響起厕宗,我...
    開封第一講書人閱讀 36,973評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎堕担,沒想到半個月后已慢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,466評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡霹购,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,937評論 2 323
  • 正文 我和宋清朗相戀三年佑惠,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片齐疙。...
    茶點故事閱讀 38,039評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡膜楷,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出贞奋,到底是詐尸還是另有隱情赌厅,我是刑警寧澤,帶...
    沈念sama閱讀 33,701評論 4 323
  • 正文 年R本政府宣布轿塔,位于F島的核電站特愿,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏勾缭。R本人自食惡果不足惜洽议,卻給世界環(huán)境...
    茶點故事閱讀 39,254評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望漫拭。 院中可真熱鬧亚兄,春花似錦、人聲如沸采驻。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,259評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至膳叨,卻和暖如春洽洁,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背菲嘴。 一陣腳步聲響...
    開封第一講書人閱讀 31,485評論 1 262
  • 我被黑心中介騙來泰國打工饿自, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人龄坪。 一個月前我還...
    沈念sama閱讀 45,497評論 2 354
  • 正文 我出身青樓昭雌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親健田。 傳聞我的和親對象是個殘疾皇子烛卧,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,786評論 2 345

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