vertx, kotlin, postgrest reactive client demo

build.gradle

plugins {
  id 'java'
  id 'java-library'
  id 'application'
  id 'org.jetbrains.kotlin.jvm' version '1.3.40'
  id 'com.github.johnrengelman.shadow' version '5.2.0'
  id 'maven-publish'
//  id("io.vertx.vertx-plugin") version "0.8.0" apply false
}

group 'citi.rio'
version '1.0-SNAPSHOT'

sourceCompatibility = 1.8
def mainClass = 'citi.ken.Starter'

dependencies {w
  // kotlin
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kt_version"
  implementation "org.jetbrains.kotlin:kotlin-reflect:$kt_version"
  implementation "com.fasterxml.jackson.module:jackson-module-kotlin:$jackson_version"

  implementation "io.vertx:vertx-core:$vertx_version"
  implementation "io.vertx:vertx-web:$vertx_version"
  implementation "io.vertx:vertx-pg-client:$vertx_version"
//    implementation "io.vertx:vertx-redis-client:$vertx_version"
//    implementation "io.vertx:vertx-jdbc-client:$vertx_version"

  // kotlin vertx
  implementation "io.vertx:vertx-lang-kotlin:$vertx_version"
  implementation "io.vertx:vertx-lang-kotlin-coroutines:$vertx_version"

  // log
  implementation "org.slf4j:jul-to-slf4j:$slf4j_version"
  runtime "ch.qos.logback:logback-classic:$logback_version"

  // java -cp .\h2-1.4.199.jar org.h2.tools.Server -tcp -tcpAllowOthers -web -webAllowOthers -browser -ifNotExists
  testImplementation("com.h2database:h2:1.4.199")
  testImplementation "org.junit.jupiter:junit-jupiter:5.4.2"
  testImplementation "io.vertx:vertx-junit5:$vertx_version"
  testImplementation "io.vertx:vertx-web-client:$vertx_version"
  testImplementation("org.testcontainers:postgresql:1.11.3")

  testImplementation "junit:junit:4.12"
}

repositories {
  mavenLocal()
  maven {
    url = uri("https://www.artifactrepository.citigroup.net:443/artifactory/maven-icg-dev")
    credentials {
      username = "ocean-devops"
      password = "APBQW78wqY4oewwXFBtTfcN17ZG"
    }
  }
  mavenCentral()
  jcenter()
}

sourceSets {
  main {
    java {
      srcDirs += 'src/main/kotlin'
      outputDir = file("$buildDir/classes/java/main")
    }
    kotlin {
      outputDir = file("$buildDir/classes/kotin/main")
    }
  }
}

compileKotlin {
  kotlinOptions.jvmTarget = "1.8"
  dependencies {
    compile files("$sourceSets.main.kotlin.outputDir")
  }
}
compileTestKotlin {
  kotlinOptions.jvmTarget = "1.8"
}

def mainVerticleName = 'citi.ken.verticle.MainVerticle'
def watchForChange = 'src/**/*'
def doOnChange = 'gradle classes'

application {
  mainClassName = mainClass
}

run {
  args = ['run', mainVerticleName, "--redeploy=$watchForChange", "--launcher-class=$mainClassName",
          "--on-redeploy=$doOnChange", '--conf=config/config_local.json']
}

task sourceJar(type: Jar) {
  classifier 'sources'
  from sourceSets.main.allJava
}

publishing {
  publications {
    maven(MavenPublication) {
      artifact tasks.sourceJar
      groupId = group
      artifactId = "$project.name"
      from components.java
    }
  }
}

task testRunner() {
  doLast {
    def libs = configurations.runtimeClasspath.findAll {
      it.name.contains('rio_registryservice') || it.name.contains('ocean-common')
    }
    configurations.runtimeClasspath.collect { println it.name }
  }
}

// tasks demo
//task startUmb1(type: Exec, group: 'gemfire', dependsOn: [installGemfire, build]) {
//  workingDir projectDir
//  environment 'GEMFIRE_HOME', installDir
//  environment 'PATH', gemfirePath
////    environment 'JAVA_HOME', 'C:/Users/hw83770/Java/jdk1.8.0_161'
//  if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) {
//    commandLine cmd, "run --file=${projectDir}/scripts/start_umb1.gfsh"
//  } else {
//    commandLine 'sh', '-c', "gfsh run --file=${projectDir}/scripts/start_umb1.gfsh"
//  }
//}
//
//task cleanYumeServer(group: 'gemfire') {
//  doLast {
//    delete 'yume_locator'
//    delete 'yume_server1'
//    delete 'yume_server2'
//  }
//}

gradle.properties

version=1.0-SNAPSHOT
### remote
#gemfireRepositoryUrl = https://globaldeploymentservicesforunixsystems.citigroup.net/cgi-bin/down.cgi?lookup=pivotal-gemfire-9.8.4.tgz&location=Linux&dl=yes
### local
gemfireRepositoryUrl=C:/Users/hw83770/Desktop/gemfire-9.8.4/pivotal-gemfire-9.8.4.tgz
gemfireReleaseUrl=
# dependency versions
#assertjVersion = 3.6.2
#awaitilityVersion = 1.7.0
#junitVersion = 4.12
#mockitocoreVersion = 2.19.1
#log4jVersion = 2.11.0
#systemrulesVersion = 1.16.1
#lombokVersion = 1.18.8
#guavaVersion = 25.1-jre

kt_version=1.3.40
vertx_version=3.8.2
jackson_version=2.9.10
slf4j_version=1.7.25
logback_version=1.2.3
jackson_version=2.9.10

config_local.json

{
  "http.port": 9991,
  "pgClient": {
    "database": "meta_int",
    "host": "oceanap02d.nam.nsroot.net",
    "user": "admin",
    "password": "password",
    "port": 5432,
    "max_pool_size": 30
  }
}

log-back.xml

<?xml version="1.0" encoding="UTF-8"?>
<configuration>

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
            <Pattern>%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n</Pattern>
        </encoder>
    </appender>

    <logger name="citi.ken" level="info"/>

    <root level="info">
        <appender-ref ref="STDOUT"/>
    </root>

</configuration>

constants.kt

package citi.ken.config

/**
 * @Author hw83770
 * @Date 17:04 2019/10/30
 *
 */

const val item = "account"

const val API_GET = "/$item/:Id"
const val API_LIST_ALL = "/$item"
const val API_CREATE = "/$item"
const val API_UPDATE = "/$item/:Id"
const val API_DELETE = "/$item/:Id"
const val API_DELETE_ALL = "/$item"

package citi.ken.domain

import java.time.LocalDateTime

/**
 * @Author hw83770
 * @Date 17:18 2019/10/30
 *
 */
data class Account(val id: Int) {
  var username: String = ""
  var nickname: String = ""
  var avatar: String = ""
  var createTime: String = LocalDateTime.now().toString()
  var updateTime: String = LocalDateTime.now().toString()

  constructor(
    id: Int,
    username: String,
    nickname: String,
    avatar: String,
    createTime: String,
    updateTime: String
  ) : this(id) {
    this.username = username
    this.nickname = nickname
    this.avatar = avatar
    this.createTime = createTime
    this.updateTime = updateTime
  }
}

package citi.ken.domain

import citi.ken.orm.Table
import citi.ken.orm.column
import io.vertx.sqlclient.Row

/**
 * @Author hw83770
 * @Date 10:08 2019/10/31
 *
 */

object Accounts : Table<Account>(tblName = "accounts") {

  val id = column("id", "int")
  val username = column("username", "String")
  val nickname = column("nickname", "String")
  val avatar = column("avatar", "String")
  val created_at = column("created_at", "timestamp")
  val updated_at = column("updated_at", "timestamp")

  fun mapToEntity(from: Row): Account = run {
    return Account(
      id = from.getInteger(id.col),
      username = from.getString(username.col),
      nickname = from.getString(nickname.col).orEmpty(),
      avatar = from.getString(avatar.col).orEmpty(),
      createTime = from.getLocalDateTime(created_at.col).toString(),
      updateTime = from.getLocalDateTime(updated_at.col).toString()
    )
  }

}


package citi.ken.orm

/**
 * @Author hw83770
 * @Date 18:21 2019/10/30
 *
 */
data class Column(
  val col: String,
  val type: String
)

---

package citi.ken.orm

/**
 * @Author hw83770
 * @Date 10:42 2019/10/31
 *
 */
class Expression {
}


package citi.ken.orm

import java.util.LinkedHashMap
import java.util.concurrent.atomic.AtomicInteger
import kotlin.reflect.KClass
import kotlin.text.StringBuilder

/**
 * @Author hw83770
 * @Date 18:11 2019/10/30
 *
 */
abstract class Table<E : Any>(tblName: String, entityClass: KClass<E>? = null) {
  private val _refCounter = AtomicInteger()
  private val _columns = LinkedHashMap<String, Column>()
  private var _primaryKeyName: String? = null

  val tblName: String = tblName

//  val entityClass: KClass<E>? =
//    (entityClass ?: referencedKotlinType.jvmErasure as KClass<E>).takeIf { it != Nothing::class }

//  protected abstract fun mapToEntity(): E

  fun registerColumn(colums: Column): Unit {
    if (colums.col in _columns.keys) {
      throw IllegalArgumentException("Duplicate column name: ${colums.col}")
    }
    _columns[colums.col] = colums
//    return ColumnRegistration(name)
  }

  fun select(vararg colums: Column): Query {
    val cols = colums.asList()
    return Query(cols = cols, from = this.tblName)
  }

  fun insert(cols: List<Column>, values: List<String>, source: String): String {
    val expression = StringBuilder("insert into $source (")
    val str = cols.map { it.col }
    expression.append(str.joinToString())
    expression.append(") values (")
    expression.append(values.joinToString())
    expression.append(")")
    return expression.toString()
  }

  fun insertPrepare(cols: List<Column>, source: String): String {
    val expression = StringBuilder("insert into $source (")
    val str = cols.map { it.col }
    // generate $1 $2 ...
    val seqs = generateSequence(1) { it + 1 }.map { "$$it" }
    val values = seqs.take(str.size).toList()

    expression.append(str.joinToString())
    expression.append(") values (")
    expression.append(values.joinToString())
    expression.append(")")
    return expression.toString()
  }

}

data class Query(
  val cols: List<Column>,
  val from: String
) {
  fun where(block: () -> Boolean) {
    block()
  }
}


fun <E : Any> Table<E>.column(name: String, type: String): Column {
  val col = Column(name, type)
  this.registerColumn(colums = col)
  return col
}

package citi.ken.service

import citi.ken.domain.Account
import citi.ken.domain.Accounts
import io.vertx.core.Vertx
import io.vertx.pgclient.PgPool
import org.slf4j.LoggerFactory
import io.vertx.kotlin.pgclient.pgConnectOptionsOf
import io.vertx.kotlin.pgclient.preparedQueryAwait
import io.vertx.kotlin.pgclient.queryAwait
import io.vertx.kotlin.sqlclient.poolOptionsOf
import io.vertx.sqlclient.Row
import io.vertx.sqlclient.Tuple
import java.time.LocalDateTime


/**
 * @Author hw83770
 * @Date 17:14 2019/10/30
 *
 */
class PgclientService(val vertx: Vertx) {

  val logger = LoggerFactory.getLogger(PgclientService::class.java)
  private val pgClient by lazy(LazyThreadSafetyMode.PUBLICATION) { createPgClient() }


  suspend fun getAll(): List<Account>? = runSafely<List<Account>> {
    val queryAwait = pgClient.queryAwait("select * from ${Accounts.tblName}")
    return queryAwait.toList().map { Accounts.mapToEntity(it) }
  }

  suspend fun getCertain(accountId: String): Account? = runSafely<Account> {
    val queryAwait = pgClient.queryAwait("select * from ${Accounts.tblName} where ${Accounts.id.col} = $accountId")
    val res: Row = queryAwait.iterator().next()
    return Accounts.mapToEntity(res)
  }

  suspend fun insert(account: Account): Account? {
    val sql = Accounts.insertPrepare(
      cols = listOf(Accounts.username, Accounts.nickname, Accounts.created_at),
//      values = listOf(account.username, account.nickname, LocalDateTime.now()),
      source = Accounts.tblName
    )
    logger.info("sql: $sql")
    return runSafely {
      val queryAwait = pgClient
        .preparedQueryAwait(sql, Tuple.of(account.username, account.nickname, LocalDateTime.now()))
      if (queryAwait.rowCount() > 0) account else null
    }
  }

  private fun createPgClient(): PgPool {
    val config = Vertx.currentContext().config().getJsonObject("pgClient")

    val connectOptions = pgConnectOptionsOf(
      database = config.getString("database"),
      host = config.getString("host"),
      user = config.getString("user"),
      password = config.getString("password"),
      port = config.getInteger("port")
    ).addProperty("search_path", "test")

    val poolOptions = poolOptionsOf(maxSize = config.getInteger("max_pool_size"))
    return PgPool.pool(vertx, connectOptions, poolOptions)
  }

  inline fun <T> runSafely(block: () -> T?): T? {
    return try {
      block()
    } catch (e: Throwable) {
      this.logger.error(e.message)
      null
    }
  }

}

package citi.ken.utils

import io.vertx.core.http.HttpHeaders
import io.vertx.core.json.Json
import io.vertx.ext.web.RoutingContext
import io.vertx.kotlin.coroutines.awaitResult
import io.vertx.pgclient.PgPool
import io.vertx.pgclient.impl.RowImpl
import io.vertx.sqlclient.Row
import io.vertx.sqlclient.RowSet
import io.vertx.sqlclient.SqlResult
import java.util.stream.Collector

/**
 * @Author hw83770
 * @Date 17:54 2019/10/30
 *
 */

fun RoutingContext.toJson(obj: Any?) {
  response()
    .putHeader(HttpHeaders.CONTENT_TYPE, "application/json; charset=utf-8")
    .end(if (obj is String) obj else Json.encode(obj))
}

package citi.ken.verticle

import citi.ken.config.API_CREATE
import citi.ken.config.API_GET
import citi.ken.config.API_LIST_ALL
import citi.ken.domain.Account
import citi.ken.service.PgclientService
import citi.ken.utils.toJson
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.vertx.core.Handler
import io.vertx.core.http.HttpHeaders
import io.vertx.core.http.HttpMethod
import io.vertx.ext.web.Router
import io.vertx.ext.web.RoutingContext
import io.vertx.ext.web.handler.BodyHandler
import io.vertx.ext.web.handler.CorsHandler
import io.vertx.ext.web.handler.LoggerHandler
import io.vertx.kotlin.coroutines.CoroutineVerticle
import io.vertx.kotlin.coroutines.dispatcher
import kotlinx.coroutines.launch
import org.slf4j.LoggerFactory

/**
 * @Author hw83770
 * @Date 17:46 2019/10/30
 *
 */
class AccountRestVerticleCo : CoroutineVerticle() {
  private val logger = LoggerFactory.getLogger(AccountRestVerticleCo::class.java)

  private val defaultPort = 9991
  private val router by lazy(LazyThreadSafetyMode.NONE) { createRouter() }
  val mapper = jacksonObjectMapper()

  private lateinit var pgclientService: PgclientService


  private val cors: CorsHandler = CorsHandler.create("*")
    .allowedHeaders(
      setOf(
        HttpHeaders.CONTENT_TYPE.toString(),
        HttpHeaders.ORIGIN.toString(),
        HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN.toString()
      )
    )
    .allowedMethods(
      setOf(
        HttpMethod.GET,
        HttpMethod.POST,
        HttpMethod.PUT,
        HttpMethod.PATCH,
        HttpMethod.DELETE,
        HttpMethod.OPTIONS
      )
    )

  override suspend fun start() {
    val port = config.getInteger("http.port", defaultPort)
    pgclientService = PgclientService(vertx)
    vertx.createHttpServer().requestHandler(router).listen(port)
  }

  private fun createRouter(): Router = Router.router(vertx).apply {
    // cors handle
    route().handler(cors)
    route().handler(BodyHandler.create())
    route().handler(LoggerHandler.create())

    get("/configs").handler { it.toJson(config.toString()) }
    get(API_GET).handler(getCertain)
    get(API_LIST_ALL).handler(getAll)
    post(API_CREATE).handler(createOne)

    //todo:
//      post(API_UPDATE).handler(updateTodo)
//      delete(API_DELETE).handler(deleteTodo)
//      delete(API_DELETE_ALL).handler(deleteAll)

  }


  private val getAll = Handler<RoutingContext> { ctx ->
    doAsync(ctx) { pgclientService.getAll() }
  }

  private val getCertain = Handler<RoutingContext> { ctx ->
    val id = ctx.request().getParam("Id")
    doAsync(ctx) { pgclientService.getCertain(id) }
  }

  private val createOne = Handler<RoutingContext> { ctx ->
    val data: ByteArray = ctx.body.bytes
    val acc: Account = mapper.readValue(data)
    doAsync(ctx) { pgclientService.insert(acc) }
  }


  fun <T> doAsync(ctx: RoutingContext, block: suspend () -> T) {
    launch(vertx.dispatcher()) {
      logger.info("doAsync1: ${Thread.currentThread().name}")
      val t = block.invoke()
//      Thread.sleep(1000)
      logger.info("doAsync2: ${Thread.currentThread().name}")
      ctx.toJson(t ?: "no data")
    }
    logger.info("doAsync3: ${Thread.currentThread().name}")
  }

}


package citi.ken.verticle

/**
 * @Author hw83770
 * @Date 16:48 2019/10/30
 * MainVerticle to deploy other component verticles
 */
import io.vertx.core.AbstractVerticle
import io.vertx.core.DeploymentOptions
import org.slf4j.LoggerFactory

class MainVerticle : AbstractVerticle() {
  val log = LoggerFactory.getLogger(MainVerticle::class.java)

  override fun start() {
    log.info("MainVerticle started")
    vertx.deployVerticle(AccountRestVerticleCo(), DeploymentOptions().setConfig(config()))
  }
}

package citi.ken

import io.vertx.core.Launcher
import io.vertx.core.VertxOptions
import org.slf4j.bridge.SLF4JBridgeHandler

/**
 * @Author hw83770
 * @Date 16:50 2019/10/30
 *
 */
object Starter {

  /**
   *  @param program argument:
   *  deploy kotlin verticle:
   *  run citi.ken.verticle.MainVerticle -conf=config/config_local.json
   *
   *  deploy java verticle:
   *  run citi.ken.verticle.JavaMainVerticle -conf=config/config_local.json
   */
  @JvmStatic
  fun main(args: Array<String>) {
    SLF4JBridgeHandler.removeHandlersForRootLogger()
    SLF4JBridgeHandler.install()
    MyLauncher().dispatch(args)
  }
}

class MyLauncher : Launcher() {

  override fun beforeStartingVertx(options: VertxOptions?) {
    options?.apply {
      eventLoopPoolSize = 4
      workerPoolSize = 4
    }
  }

}

rest_test.http


### hello
GET http://localhost:9991/configs HTTP/1.1

### get all
GET http://localhost:9991/account HTTP/1.1

### get one
GET http://localhost:9991/account/1 HTTP/1.1 

###
POST http://localhost:9991/account HTTP/1.1
content-type: application/json

{
    "username": "sample1",
    "nickname": "test_nick",
    "avatar": "test/url_avatar",
    "createTime":  "2015-11-11 18:27:50"
}


###
https://localhost:8080/geode/v1/publisher?limit=50


###
https://localhost:8080/geode/v1/publisher?limit=50
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Basic aHc4Mzc3MDoxMjM=
Connection: keep-alive
Host: localhost:8080
Referer: https://localhost:8080/geode/swagger-ui.html
Sec-Fetch-Mode: cors
Sec-Fetch-Site: same-origin
User-Agent: Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/78.0.3904.87 Safari/537.36


###
https://localhost:8080/geode/v1/publisher/publisher%3Akey%3A0
accept: application/json;charset=UTF-8
Accept-Encoding: gzip, deflate, br
Accept-Language: en-US,en;q=0.9
Authorization: Basic aHc4Mzc3MDoxMjM=
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末止潮,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌地啰,老刑警劉巖黄虱,帶你破解...
    沈念sama閱讀 206,968評論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡沙庐,警方通過查閱死者的電腦和手機桐款,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評論 2 382
  • 文/潘曉璐 我一進店門咸这,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人鲁僚,你說我怎么就攤上這事炊苫。” “怎么了冰沙?”我有些...
    開封第一講書人閱讀 153,220評論 0 344
  • 文/不壞的土叔 我叫張陵侨艾,是天一觀的道長。 經(jīng)常有香客問我拓挥,道長唠梨,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評論 1 279
  • 正文 為了忘掉前任侥啤,我火速辦了婚禮当叭,結果婚禮上茬故,老公的妹妹穿的比我還像新娘。我一直安慰自己蚁鳖,他們只是感情好磺芭,可當我...
    茶點故事閱讀 64,425評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著醉箕,像睡著了一般钾腺。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上讥裤,一...
    開封第一講書人閱讀 49,144評論 1 285
  • 那天放棒,我揣著相機與錄音,去河邊找鬼己英。 笑死间螟,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的损肛。 我是一名探鬼主播厢破,決...
    沈念sama閱讀 38,432評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼荧关!你這毒婦竟也來了溉奕?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 37,088評論 0 261
  • 序言:老撾萬榮一對情侶失蹤忍啤,失蹤者是張志新(化名)和其女友劉穎加勤,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體同波,經(jīng)...
    沈念sama閱讀 43,586評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡鳄梅,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,028評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了未檩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片戴尸。...
    茶點故事閱讀 38,137評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖冤狡,靈堂內(nèi)的尸體忽然破棺而出孙蒙,到底是詐尸還是另有隱情,我是刑警寧澤悲雳,帶...
    沈念sama閱讀 33,783評論 4 324
  • 正文 年R本政府宣布挎峦,位于F島的核電站,受9級特大地震影響合瓢,放射性物質發(fā)生泄漏坦胶。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,343評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望顿苇。 院中可真熱鬧峭咒,春花似錦、人聲如沸纪岁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽蜂科。三九已至顽决,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間导匣,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評論 1 262
  • 我被黑心中介騙來泰國打工茸时, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留贡定,地道東北人。 一個月前我還...
    沈念sama閱讀 45,595評論 2 355
  • 正文 我出身青樓可都,卻偏偏與公主長得像缓待,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子渠牲,可洞房花燭夜當晚...
    茶點故事閱讀 42,901評論 2 345