Node + MongoDB 建站 2期 -1

Grant

npm install grunt grant-cli -g全局安装grant及grant命令行工具,

npm install grunt-contrib-watch --save-dev  //文件修改重新执行注册的任务
npm install grunt-nodemon --save-dev    //监听app.js修改
npm install grunt-concurrent --save-dev //慢任务如sass

建立grunt.js文件

module.exports = function(grunt) {

  grunt.initConfig({
    watch: {
      jade: {
        files: ['views/**'],
        options: {
          livereload: true
        }
      },
      js: {
        files: ['public/js/**', 'models/**/*.js', 'schemas/**/*.js'],
        //tasks: ['jshint'],
        options: {
          livereload: true
        }
      }
    },

    nodemon: {
      dev: {
        options: {
          file: 'app.js',
          args: [],
          ignoredFiles: ['README.md', 'node_modules/**', '.DS_Store'],
          watchedExtensions: ['js'],
          watchedFolders: ['app','config'],
          debug: true,
          delayTime: 1,
          env: {
            PORT: 3000
          },
          cwd: __dirname
        }
      }
    },

    concurrent: {
      tasks: ['nodemon', 'watch'],
      options: {
        logConcurrentOutput: true
      }
    }
  })

  grunt.loadNpmTasks('grunt-contrib-watch')
  grunt.loadNpmTasks('grunt-nodemon')
  grunt.loadNpmTasks('grunt-concurrent')
  grunt.option('force', true)  //避免一些错误影响后续执行

  grunt.registerTask('default', ['concurrent'])  //默认任务

}

根目录下执行grunt命令启动

用户登陆注册

1)用户模型及密码处理

安装bcrypt进行密码加盐处理 npm install bcrypt --save-dev,创建schema user.js

var mongoose = require('mongoose')
var bcrypt = require('bcrypt')
var SALT_WORK_FACTOR = 10

var UserSchema = new mongoose.Schema({
  name: {
    unique: true,
    type: String
  },
  password: String,
  meta: {
    createAt: {
      type: Date,
      default: Date.now()
    },
    updateAt: {
      type: Date,
      default: Date.now()
    }
  }
})

UserSchema.pre('save', function(next) {
  var user = this

  if (this.isNew) {
    this.meta.createAt = this.meta.updateAt = Date.now()
  }
  else {
    this.meta.updateAt = Date.now()
  }

  bcrypt.genSalt(SALT_WORK_FACTOR, function(err, salt) {
    if (err) return next(err)

    bcrypt.hash(user.password, salt, function(err, hash) {
      if (err) return next(err)

      user.password = hash
      next()
    })
  })
})

UserSchema.statics = {
  fetch: function(cb) {
    return this
      .find({})
      .sort('meta.updateAt')
      .exec(cb)
  },
  findById: function(id, cb) {
    return this
      .findOne({_id: id})
      .exec(cb)
  }
}

module.exports = UserSchema

2)登录注册前端视图

修改header.jade,添加navbar,添加注册与登陆两个按钮,分别为其添加模态视图

.container
  .row
    .page-header
      h1= title
      small 重度科幻迷

.navbar.navbar-default.navbar-fixed-bottom
  .container
    .navbar-header
      a.navbar-brand(href="/") 重度科幻迷
    if user
      p.navbar-text.navbar-right
        span 欢迎您,#{user.name}
        span  | 
        a.navbar-link(href="/logout") 登出
    else
      p.navbar-text.navbar-right
        a.navbar-link(href="#", data-toggle="modal", data-target="#signupModal") 注册
        span  | 
        a.navbar-link(href="#", data-toggle="modal", data-target="#signinModal") 登录
#signupModal.modal.fade
  .modal-dialog
    .modal-content
      form(method="POST", action="/user/signup")
        .modal-header 注册
        .modal-body
          .form-group
            label(for="signupName") 用户名
            input#signupName.form-control(name="user[name]", type="text")
          .form-group
            label(for="signupPassword") 密码
            input#signupPassword.form-control(name="user[password]", type="text")
        .modal-footer
          button.btn.btn-default(type="button", data-dismiss="modal") 关闭
          button.btn.btn-success(type="submit") 提交
#signinModal.modal.fade
  .modal-dialog
    .modal-content
      form(method="POST", action="/user/signin")
        .modal-header 登录
        .modal-body
          .form-group
            label(for="signinName") 用户名
            input#signinName.form-control(name="user[name]", type="text")
          .form-group
            label(for="signinPassword") 密码
            input#signinPassword.form-control(name="user[password]", type="text")
        .modal-footer
          button.btn.btn-default(type="button", data-dismiss="modal") 关闭
          button.btn.btn-success(type="submit") 提交

3)注册用户后台存储

添加Model user.js

var mongoose = require('mongoose')
var UserSchema = require('../schemas/user')
var User = mongoose.model('User', UserSchema)

module.exports = User

在app.js中引入user model,并添加路由

var User = require('./models/user')
......
app.post('/user/signup',function(req,res){
    var _user = req.body.user
    var user = new User(_user)

    user.save(function(err,user){
        if (err) {
            console.log(err)
        }
        res.redirect('/')
    })
})

添加用户列表页

extends ../layout

block content
  .container
    .row
      table.table.table-hover.table-bordered
        thead
          tr
            th 名字
            th 时间
            th 查看
            th 修改
            th 删除
        tbody
          each item in users
            tr(class="item-id-#{item._id}")
              td #{item.name}
              td #{moment(item.meta.updateAt).format('MM/DD/YYYY')}
              td: a(target="_blank", href="../movie/#{item._id}") 查看
              td: a(target="_blank", href="../admin/update/#{item._id}") 修改
              td
                button.btn.btn-danger.del(type="button", data-id="#{item._id}") 删除

在app.js中添加路由

//user list page
app.get('/admin/userlist', function(req, res) {
    Movie.fetch(function(err,users){
        if (err) {
            console.log(err)
        }
        res.render('userlist', {
            title: 'imooc 用户列表页',
            users: users
        })
    })
})

登陆时判断用户是否存在,更改app.js signup路由

app.post('/user/signup',function(req,res){
    var _user = req.body.user

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (user) {
            return res.redirect('/')
        } else {
            var user = new User(_user)

            user.save(function(err,user){
                if (err) {
                    console.log(err)
                }        
                res.redirect('/admin/userlist')
            })
        }
    })
})

4)实现登陆逻辑

添加signin路由

app.post('/user/signin',function(req,res){
    var _user = req.body.user
    var name = _user.name
    var password = _user.password

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (!user) {
            return res.redirect('/')
        } 
        user.comparePassword(password, function(err, isMatch){
            if (err) {
                console.log(err)
            }
            if (isMatch){
                console.log('Password is match')
                return res.redirect('/')
            } else {
                console.log('Password not match')
            }
        })
    })
})

在user schema中添加comparePassword方法

UserSchema.methods = {
  comparePassword: function(_password, cb) {
    bcrypt.compare(_password, this.password, function(err, isMatch) {
      if (err) return cb(err)
      cb(null, isMatch)
    })
  }
}

5)保持用户状态

在app.js中安装并引用cookie-session,index中打印当前user,在/user/signin的post中,如果密码匹配,存储user到session

var cookieSession = require('cookie-session')
app.use(cookieSession({
    secret: 'imooc'
}))
......
//index page
app.get('/', function(req, res) {
    console.log('user in session:')
    console.log(req.session.user)
......

if (isMatch){
                console.log('Password is match')
                req.session.user = user
                return res.redirect('/')
            } 

6)利用MongoDB做会话的持久化

安装 express-session connect-mongo

var session = require('express-session')
var MongoStore = require('connect-mongo')(session);

app.use(session({
    secret: 'imooc',
    store: new MongoStore({
        url:'mongodb://localhost/imooc',
        collection: 'sessions'
    })
}))

7)注销功能实现

header.jade中修改,判断user显示

    if user
      p.navbar-text.navbar-right
        span 欢迎您,#{user.name}
        span  | 
        a.navbar-link(href="/logout") 登出
    else
      p.navbar-text.navbar-right
        a.navbar-link(href="#", data-toggle="modal", data-target="#signupModal") 注册
        span  | 
        a.navbar-link(href="#", data-toggle="modal", data-target="#signinModal") 登录

添加登出路由,在index路由中,赋予locals user值

    console.log('user in session:')
    console.log(req.session.user)
    var _user = req.session.user
    if(_user) {
        app.locals.user = _user
    }
    Movie.fetch(function(err,movies){
......
app.get('/logout',function(req,res){
    delete req.session.user
    delete app.locals.user  
    res.redirect('/')
})

8)会话持久逻辑预处理

将index路由中的赋值移到预处理逻辑中

app.use(function(req,res,next){
    var _user = req.session.user
    if(_user) {
        app.locals.user = _user
    }
    return next()
})

9)调整目录,独立路由

添加config/routes.js,将路由相关代码移到该文件

var _ = require('underscore')
var Movie = require('../models/movie')
var User = require('../models/user')

module.exports = function(app){

    app.use(function(req,res,next){
        var _user = req.session.user
        if(_user) {
            app.locals.user = _user
        }
        return next()
    })
//index page
app.get('/', function(req, res) {
    console.log('user in session:')
    console.log(req.session.user)

    Movie.fetch(function(err,movies){
        if (err) {
            console.log(err)
        }

        res.render('index', {
            title: 'imooc 首页',
            movies: movies
        })
    })

})

app.post('/user/signup',function(req,res){
    var _user = req.body.user

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (user) {
            return res.redirect('/')
        } else {
            var user = new User(_user)

            user.save(function(err,user){
                if (err) {
                    console.log(err)
                }
        
                res.redirect('/admin/userlist')
            })
        }
    })

})

app.post('/user/signin',function(req,res){
    var _user = req.body.user
    var name = _user.name
    var password = _user.password

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (!user) {
            return res.redirect('/')
        } 
        user.comparePassword(password, function(err, isMatch){
            if (err) {
                console.log(err)
            }
            if (isMatch){
                console.log('Password is match')
                req.session.user = user
                return res.redirect('/')
            } else {
                console.log('Password not match')
            }
        })
    })

})

app.get('/logout',function(req,res){
    delete req.session.user
    delete app.locals.user  
    res.redirect('/')
})

//user list page
app.get('/admin/userlist', function(req, res) {
    User.fetch(function(err,users){
        if (err) {
            console.log(err)
        }

        res.render('userlist', {
            title: 'imooc 用户列表页',
            users: users
        })
    })
})


//detail page
app.get('/movie/:id', function(req, res) {
    var id = req.params.id
    Movie.findById(id,function(err,movie){
        res.render('detail', {
            title: 'imooc 详情页',
            movie: movie
        })
    })

})

//admin page
app.get('/admin/movie', function(req, res) {
    res.render('admin', {
        title: 'imooc 后台录入页',
        movie: {
            title: '',
            doctor: '',
            country: '',
            year: '',
            poster: '',
            flash: '',
            summary: '',
            language: ''
        }
    })
})

//admin update movie
app.get('/admin/update/:id',function(req,res){
    var id = req.params.id

    if (id) {
        Movie.findById(id,function(err,movie){
            res.render('admin',{
                title: 'Imooc 后台更新页',
                movie: movie
            })
        })
    }
})

//admin post movie
app.post('/admin/movie/new', function(req,res){
    var id = req.body.movie._id
    var movieObj = req.body.movie
    var _movie

    if (id !=='undefined') {
        Movie.findById(id, function(err,movie){
            if (err){
                console.log(err)
            }

            _movie = _.extend(movie, movieObj)
            _movie.save(function(err,movie) {
                if (err){
                    console.log(err)
                }
                res.redirect('/movie/'+movie._id)
            })
        })
    } else {
        _movie = new Movie({
            dector: movieObj.dector,
            title: movieObj.title,
            country: movieObj.country,
            language: movieObj.language,
            year: movieObj.year,
            poster: movieObj.poster,
            summary: movieObj.summary,
            flash: movieObj.flash,
        })

        _movie.save(function(err,movie) {
            if (err){
                console.log(err)
            }
            res.redirect('/movie/'+movie._id)
        })
    }
})

//list page
app.get('/admin/list', function(req, res) {
    Movie.fetch(function(err,movies){
        if (err) {
            console.log(err)
        }

        res.render('list', {
            title: 'imooc 列表页',
            movies: movies
        })
    })
})

//list delete movie
app.delete('/admin/list',function(req,res){
    var id = req.query.id

    if (id) {
        Movie.remove({_id: id},function(err,movie){
            if (err) {
                console.log(err)
            } else {
                res.json({success: 1})
            }
        })
    }
})
}

app.js中引入routes文件

app.use(session({
    secret: 'imooc',
    store: new MongoStore({
        url:'mongodb://localhost/imooc',
        collection: 'sessions'
    })
}))

require('./config/routes')(app)

10)配置入口文件

安装morgan,

var logger = require('morgan')

if('development' === app.get('env')){
    app.set('showStackError', true)
    app.use(logger(':method :url :status'))
    app.locals.pretty = true
    mongoose.set('debug',true)
}

11)调整目录,分离mvc

新建app文件夹,将models/schemas/views文件夹移到该文件夹,添加controllers文件夹,将routes中代码移到新建的文件中
index.js

var Movie = require('../models/movie')

exports.index = function(req, res) {
    console.log('user in session:')
    console.log(req.session.user)

    Movie.fetch(function(err,movies){
        if (err) {
            console.log(err)
        }
        res.render('index', {
            title: 'imooc 首页',
            movies: movies
        })
    })
}

user.js

var User = require('../models/user')


exports.signup = function(req,res){
    var _user = req.body.user

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (user) {
            return res.redirect('/')
        } else {
            var user = new User(_user)

            user.save(function(err,user){
                if (err) {
                    console.log(err)
                }
        
                res.redirect('/admin/userlist')
            })
        }
    })

}

exports.signin = function(req,res){
    var _user = req.body.user
    var name = _user.name
    var password = _user.password

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (!user) {
            return res.redirect('/')
        } 
        user.comparePassword(password, function(err, isMatch){
            if (err) {
                console.log(err)
            }
            if (isMatch){
                console.log('Password is match')
                req.session.user = user
                return res.redirect('/')
            } else {
                console.log('Password not match')
            }
        })
    })

}

exports.logout = function(req,res){
    delete req.session.user
    // delete app.locals.user  
    res.redirect('/')
}

//user list page
exports.list = function(req, res) {
    User.fetch(function(err,users){
        if (err) {
            console.log(err)
        }

        res.render('userlist', {
            title: 'imooc 用户列表页',
            users: users
        })
    })
}

movie.js

var _ = require('underscore')
var Movie = require('../models/movie')

//detail page
exports.detail = function(req, res) {
    var id = req.params.id
    Movie.findById(id,function(err,movie){
        res.render('detail', {
            title: 'imooc 详情页',
            movie: movie
        })
    })

}

//admin page
exports.new =  function(req, res) {
    res.render('admin', {
        title: 'imooc 后台录入页',
        movie: {
            title: '',
            doctor: '',
            country: '',
            year: '',
            poster: '',
            flash: '',
            summary: '',
            language: ''
        }
    })
}

//admin update movie
exports.update = function(req,res){
    var id = req.params.id

    if (id) {
        Movie.findById(id,function(err,movie){
            res.render('admin',{
                title: 'Imooc 后台更新页',
                movie: movie
            })
        })
    }
}

//admin post movie
exports.save =  function(req,res){
    var id = req.body.movie._id
    var movieObj = req.body.movie
    var _movie

    if (id !=='undefined') {
        Movie.findById(id, function(err,movie){
            if (err){
                console.log(err)
            }

            _movie = _.extend(movie, movieObj)
            _movie.save(function(err,movie) {
                if (err){
                    console.log(err)
                }
                res.redirect('/movie/'+movie._id)
            })
        })
    } else {
        _movie = new Movie({
            dector: movieObj.dector,
            title: movieObj.title,
            country: movieObj.country,
            language: movieObj.language,
            year: movieObj.year,
            poster: movieObj.poster,
            summary: movieObj.summary,
            flash: movieObj.flash,
        })

        _movie.save(function(err,movie) {
            if (err){
                console.log(err)
            }
            res.redirect('/movie/'+movie._id)
        })
    }
}

//list page
exports.list =  function(req, res) {
    Movie.fetch(function(err,movies){
        if (err) {
            console.log(err)
        }

        res.render('list', {
            title: 'imooc 列表页',
            movies: movies
        })
    })
}

//list delete movie
exports.del = function(req,res){
    var id = req.query.id

    if (id) {
        Movie.remove({_id: id},function(err,movie){
            if (err) {
                console.log(err)
            } else {
                res.json({success: 1})
            }
        })
    }
}

routes.js修改为如下

var Index = require('../app/controllers/index')
var User = require('../app/controllers/user')
var Movie = require('../app/controllers/movie')

module.exports = function(app){

    app.use(function(req,res,next){
        var _user = req.session.user
        // if(_user) {
            app.locals.user = _user
        // }
        next()
    })
//index page
app.get('/', Index.index)
//user
app.post('/user/signup',User.signup)
app.post('/user/signin',User.signin)
app.get('/logout',User.logout)
app.get('/admin/userlist', User.list)

//movie
app.get('/movie/:id', Movie.detail)
app.get('/admin/movie', Movie.new)
app.get('/admin/update/:id',Movie.update)
app.post('/admin/movie/new', Movie.save)
app.get('/admin/list', Movie.list)
app.delete('/admin/list',Movie.del)
}

修改app.js文件中路径app.set('views', './app/views/pages')

12)添加注册登陆跳转页面

添加signin与signup jade文件

extends ../layout

block content
  .container
    .row
      .col-md-5
        form(method="POST", action="/user/signin")
          .modal-body
            .form-group
              label(for="signinName") 用户名
              input#signinName.form-control(name="user[name]", type="text")
            .form-group
              label(for="signinPassword") 密码
              input#signinPassword.form-control(name="user[password]", type="text")
          .modal-footer
            button.btn.btn-default(type="button", data-dismiss="modal") 关闭
            button.btn.btn-success(type="submit") 提交

添加路由

app.get('/signin', User.showSignin)
app.get('/signup', User.showSignup)

添加controller方法,修改signup signin重定向地址

// signup
exports.showSignup = function(req, res) {
    res.render('signup', {
      title: '注册页面'
    })
  }
  
exports.showSignin = function(req, res) {
    res.render('signin', {
      title: '登录页面'
    })
  }
exports.signup = function(req,res){
    var _user = req.body.user

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (user) {
            return res.redirect('/signin')
        } else {
            var user = new User(_user)

            user.save(function(err,user){
                if (err) {
                    console.log(err)
                }
        
                res.redirect('/')
            })
        }
    })

}

exports.signin = function(req,res){
    var _user = req.body.user
    var name = _user.name
    var password = _user.password

    User.findOne({name:_user.name}, function(err,user){
        if (err) {
            console.log(err)
        }
        if (!user) {
            return res.redirect('/signup')
        } 
        user.comparePassword(password, function(err, isMatch){
            if (err) {
                console.log(err)
            }
            if (isMatch){
                console.log('Password is match')
                req.session.user = user
                return res.redirect('/')
            } else {
                return res.redirect('/signin')
                console.log('Password not match')
            }
        })
    })
}

13)用户权限管理

在user schema中添加role字段

  password: String,
  // 0: nomal user
  // 1: verified user
  // 2: professonal user
  // >10: admin
  // >50: super admin
  role: {
    type: Number,
    default: 0
  },

在routes.js文件中,访问userlist添加参数 app.get('/admin/userlist', User.signinRequired, User.adminRequired, User.list),在user controller中添加方法,如果没有登陆则调到signin,如果没有权限则调到signin

// midware for user
exports.signinRequired = function(req, res, next) {
  var user = req.session.user
  if (!user) {
    return res.redirect('/signin')
  }
  next()
}

exports.adminRequired = function(req, res, next) {
  var user = req.session.user
  if (user.role <= 10) {
    return res.redirect('/signin')
  }
  next()
}

给其他路由也加上admin权限管理

//index page
app.get('/', Index.index)
//user
app.post('/user/signup',User.signup)
app.post('/user/signin',User.signin)
app.get('/signin', User.showSignin)
app.get('/signup', User.showSignup)
app.get('/logout',User.logout)
app.get('/admin/userlist', User.signinRequired, User.adminRequired, User.list)

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

推荐阅读更多精彩内容