SSM架构之高并发秒杀之Service详解

SSM架构之高并发秒杀之Dao http://www.jianshu.com/p/15ccf298d486

一、Service 层基本项目项目开发

1、enums

枚举常量

  • SeckillStateEnum.java

使用枚举表示我们的常量数据字典

package org.seckill.enums;

/**
 * 使用枚举表示我们的常量数据字典
 * Created by wangxf on 2017/2/25.
 */
public enum SeckillStateEnum {
    SUCCESS(1,"秒杀成功"),
    END(0,"秒杀结束"),
    REPEAT_KILL(-1,"重复秒杀"),
    INNER_ERROR(-2,"系统异常"),
    DATA_REWRITE(-3,"数据篡改");

    private int state;
    private String stateInfo;

    public static SeckillStateEnum stateOf(int index){
        for (SeckillStateEnum stateEnum : values()) {
            if (stateEnum.getState() == index) {
                return stateEnum;
            }
        }
        return null;
    }

    SeckillStateEnum(int state, String stateInfo) {
        this.state = state;
        this.stateInfo = stateInfo;
    }

    public int getState() {
        return state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public void setState(int state) {
        this.state = state;
    }

    public void setStateInfo(String stateInfo) {
        this.stateInfo = stateInfo;
    }
}

2、dto

  • Exposer.java

用于暴露秒杀地址的DTO

package org.seckill.dto;

/**
 * 用于暴露秒杀地址的DTO
 * Created by wangxf on 2017/2/24.
 */
public class Exposer {
    private boolean exposed;        // 用户判断秒杀接口是否开启
    private String md5;             // 一种加密机制
    private long seckillId;         // 秒杀id
    private long nowTime;           // 系统的当前时间(毫秒)
    private long startTime;         // 秒杀的开启时间(毫秒)
    private long endTime;           // 秒杀的结束时间(毫秒)

    public Exposer() {

    }

    public Exposer(boolean exposed, String md5, long seckillId) {
        this.exposed = exposed;
        this.md5 = md5;
        this.seckillId = seckillId;
    }

    public Exposer(boolean exposed, long nowTime, long startTime, long endTime) {
        this.exposed = exposed;
        this.nowTime = nowTime;
        this.startTime = startTime;
        this.endTime = endTime;
    }

    public Exposer(boolean exposed, long seckillId) {
        this.exposed = exposed;
        this.seckillId = seckillId;
    }

    public Exposer(boolean exposed, long seckillId, long nowTime, long startTime, long endTime) {
        this.exposed = exposed;
        this.seckillId = seckillId;
        this.nowTime = nowTime;
        this.startTime = startTime;
        this.endTime = endTime;
    }

    public boolean isExposed() {
        return exposed;
    }

    public String getMd5() {
        return md5;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public long getNowTime() {
        return nowTime;
    }

    public long getStartTime() {
        return startTime;
    }

    public long getEndTime() {
        return endTime;
    }

    public void setExposed(boolean exposed) {
        this.exposed = exposed;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public void setNowTime(long nowTime) {
        this.nowTime = nowTime;
    }

    public void setStartTime(long startTime) {
        this.startTime = startTime;
    }

    public void setEndTime(long endTime) {
        this.endTime = endTime;
    }

    @Override
    public String toString() {
        return "Exposer{" +
                "exposed=" + exposed +
                ", md5='" + md5 + '\'' +
                ", seckillId=" + seckillId +
                ", nowTime=" + nowTime +
                ", startTime=" + startTime +
                ", endTime=" + endTime +
                '}';
    }
}
  • SeckillExcution.java

封装执行秒杀后的数据

package org.seckill.dto;

import org.seckill.bean.SuccessKilled;
import org.seckill.enums.SeckillStateEnum;

/**
 * 封装执行秒杀后的数据
 * Created by wangxf on 2017/2/24.
 */
public class SeckillExcution {

    private long seckillId;                 // 秒杀信息id
    private int state;                      // 秒杀执行结构状态
    private String stateInfo;               // 执行结果状态标识
    private SuccessKilled successKilled;    // 秒杀成功的对象

    public SeckillExcution() {
    }

    public SeckillExcution(long seckillId, SeckillStateEnum stateEnum) {
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getStateInfo();
    }

    public SeckillExcution(long seckillId, SeckillStateEnum stateEnum, SuccessKilled successKilled) {
        this.seckillId = seckillId;
        this.state = stateEnum.getState();
        this.stateInfo = stateEnum.getStateInfo();
        this.successKilled = successKilled;
    }

    public long getSeckillId() {
        return seckillId;
    }

    public int getState() {
        return state;
    }

    public String getStateInfo() {
        return stateInfo;
    }

    public SuccessKilled getSuccessKilled() {
        return successKilled;
    }

    public void setSeckillId(long seckillId) {
        this.seckillId = seckillId;
    }

    public void setState(int state) {
        this.state = state;
    }

    public void setStateInfo(String stateInfo) {
        this.stateInfo = stateInfo;
    }

    public void setSuccessKilled(SuccessKilled successKilled) {
        this.successKilled = successKilled;
    }

    @Override
    public String toString() {
        return "SeckillExcution{" +
                "seckillId=" + seckillId +
                ", state=" + state +
                ", stateInfo='" + stateInfo + '\'' +
                ", successKilled=" + successKilled +
                '}';
    }
}

2、exception

异常定义

  • SeckillException.java

所有秒杀业务相关的异常(运行时异常)

package org.seckill.exception;

/**
 * 所有秒杀业务相关的异常(运行时异常)
 * Created by wangxf on 2017/2/24.
 */
    public class SeckillException extends RuntimeException{

    public SeckillException() {
    }

    public SeckillException(String message) {
        super(message);
    }

    public SeckillException(String message, Throwable cause) {
        super(message, cause);
    }
}

  • SeckillCloseException.java

秒杀关闭时异常(运行时异常)

package org.seckill.exception;

/**
 * 秒杀关闭时异常(运行时异常)
 * Created by wangxf on 2017/2/24.
 */
public class SeckillCloseException extends SeckillException{

    public SeckillCloseException() {
    }

    public SeckillCloseException(String message) {
        super(message);
    }

    public SeckillCloseException(String message, Throwable cause) {
        super(message, cause);
    }
}
  • RepeatKillException.java

重复秒杀异常(运行时异常)

package org.seckill.exception;

/**
 * 重复秒杀异常(运行时异常)
 * Created by wangxf on 2017/2/24.
 */
public class RepeatKillException extends SeckillException{

    public RepeatKillException() {
    }

    public RepeatKillException(String message) {
        super(message);
    }

    public RepeatKillException(String message, Throwable cause) {
        super(message, cause);
    }
}

3、Service

  • ISeckillService.java

业务接口,站在用户的角度设计开发接口, 三个方面:方法定义粒度、参数、返回类型/异常

package org.seckill.service.interfaces;

import org.seckill.bean.Seckill;
import org.seckill.bean.SuccessKilled;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;

import java.util.List;

/**
 *  业务接口,站在用户的角度设计开发接口
 *  三个方面:方法定义粒度、参数、返回类型/异常
 * Created by wangxf on 2017/2/23.
 */
public interface ISeckillService {

    /**
     * 查询所有的秒杀记录
     * @return List<Seckill>
     */
    public List<Seckill> selectSeckillList();

    /**
     * 通过 id 精确查询秒杀记录信息
     * @param seckillId 秒杀信息id
     * @return Seckill
     */
    public Seckill selectSeckillById(long seckillId);

    /**
     * 秒杀开启时输出秒接口地址
     * 否则,输出系统时间或者秒杀时间
     * @param seckillId 秒杀信息id
     * @return Exposer
     */
    public Exposer exportSeckillUrlException(long seckillId);

    /**
     * 用户执行秒杀操作
     * @param seckillId 秒杀信息id
     * @param userPhone 用户手机号码
     * @param md5 密文
     * @return SeckillExcution
     */
    public SeckillExcution excuteSeckill(long seckillId, long userPhone, String md5) throws SeckillException,RepeatKillException,SeckillCloseException;
}
  • SeckillServiceImpl.java

业务接口,站在用户的角度设计开发接口,三个方面:方法定义粒度、参数、返回类型/异常

package org.seckill.service.impl;

import org.seckill.bean.Seckill;
import org.seckill.bean.SuccessKilled;
import org.seckill.dao.interfaces.ISeckillDao;
import org.seckill.dao.interfaces.ISuccessKilledDao;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.enums.SeckillStateEnum;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.seckill.exception.SeckillException;
import org.seckill.service.interfaces.ISeckillService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;

import java.util.Date;
import java.util.List;

/**
 *  业务接口,站在用户的角度设计开发接口
 *  三个方面:方法定义粒度、参数、返回类型/异常
 *
 *  Spring 的类注解主要有:@Component 所有的注解、@Controller @Service @Dao
 *  * Created by wangxf on 2017/2/23.
 */
@Service
public class SeckillServiceImpl implements ISeckillService{

    private Logger logger = LoggerFactory.getLogger(this.getClass());       // 日志对象

    // 注入 service 依赖
    // @Autowired 自动加载依赖 @Resource @Inject 注入一些规范等
    @Autowired
    private ISeckillDao seckillDao;                 // dao层秒杀对象
    @Autowired
    private ISuccessKilledDao successKilledDao;     // Dao秒杀成功后对象
    // MD5 腌制字符串,用于混洗 MD5
    private final String slat = "diasj29er2ur734tuei89u34efdfi30q7u5834tdphf056=-251758";

    /**
     * 查询所有的秒杀记录
     * @return List<Seckill>
     */
    public List<Seckill> selectSeckillList() {
        return seckillDao.selectProductAll(0,4);
    }

    /**
     * 通过 id 精确查询秒杀记录信息
     * @param seckillId 秒杀信息id
     * @return Seckill
     */
    public Seckill selectSeckillById(long seckillId) {
        return seckillDao.selectProductById(seckillId);
    }

    /**
     * 秒杀开启时输出秒接口地址
     * 否则,输出系统时间或者秒杀时间
     * @param seckillId 秒杀信息id
     * @return Exposer
     */
    public Exposer exportSeckillUrlException(long seckillId) {

        // 根据seckillId查询秒杀信息
        Seckill seckill = seckillDao.selectProductById(seckillId);

        // 判断秒杀对象是否为空,如果为空,返回 Exposer 对象为:false
        if ( seckill == null ) {
            return new Exposer(false, seckillId);
        }

        // 获取秒杀的开始、结束时间
        Date startTime = seckill.getStartTime();
        Date endTime = seckill.getEndTime();

        // 获取系统当前时间
        Date nowTime = new Date();

        // 判断当前产品是否可以进行秒杀,即是否在秒杀的开始、结束时间内
        if ( nowTime.getTime() < startTime.getTime() || nowTime.getTime() > endTime.getTime() ) {
            return new Exposer(false, seckillId,nowTime.getTime(), startTime.getTime(), endTime.getTime());
        }

        // 生成 md5, md5 就是转换特定字符串的过程,这个过程是不可逆的
        String md5 = getMd5(seckillId);
        return new Exposer(true, md5, seckillId);
    }

    /**
     * 用户执行秒杀操作,正真秒杀操作的实现
     * @param seckillId 秒杀信息id
     * @param userPhone 用户手机号码
     * @param md5 密文
     * @return
     * @throws SeckillException
     * @throws RepeatKillException
     * @throws SeckillCloseException
     */
    /*
     * 使用注解控制事务方法的优点
     * 1、开发团队打成一个约定,明确标注事务方法的编程风格
     * 2、保证食物方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求等,如果需要剥离到事务方法外部
     * 3、不是所有的方法都是需要声明式事务的,如:只有一条修改操作、只读操作不需要事务控制
     */
    @Transactional
    public SeckillExcution excuteSeckill(long seckillId, long userPhone, String md5) throws SeckillException, RepeatKillException, SeckillCloseException {

        // 这里try ... catch 是为了:防止除我们自定义之外的异常
        try {
            // 判断用户传入的 md5 是否与生成的md5 相匹配
            // 当不匹配时,抛出异常
            if (null == md5 || !md5.equals(getMd5(seckillId))) {
                throw new SeckillException("秒杀数据有误:seckill data rewrite!");
            }

            // 秒杀的业务逻辑 : 减库存 + 记录购买行为
            // 获取当前时间
            Date nowTime = new Date();
            // 减库存
            int updateProductNumber = seckillDao.updateProductNumber(seckillId, nowTime);

            // 判断修改数据库行数,如果 小于等于 0 :说明修改失败
            if (updateProductNumber <= 0) {
                throw new SeckillCloseException("秒杀还未开始或已经结束或者库存不足!");
            } else {
                // 记录购买行为  主键为:seckillId + userPhone 防止重复秒杀
                int insertSuccessKilled = successKilledDao.insertSuccessKilled(seckillId, userPhone);
                // 如果未能成功插入,即 返回结果为0时,表示主键冲突,即已经秒杀过了
                if (insertSuccessKilled <= 0) {
                    throw new RepeatKillException("不能够重复秒杀!");
                } else {
                    // 秒杀成功,返回秒杀记录对象
                    SuccessKilled successKilled = successKilledDao.selectByIdWithSeckill(seckillId, userPhone);

                    if (null != successKilled) {
                        return new SeckillExcution(seckillId, SeckillStateEnum.SUCCESS,successKilled);
                    }
                }
            }
            // 再抛出异常时,为了避免将我们自定义的异常转换成其他异常,顾,先抛出我们自己的异常
        } catch (SeckillCloseException e1) {
            throw e1;
        } catch (RepeatKillException e2) {
            throw e2;
        } catch (Exception e) {
            // 记录日志
            logger.error(e.getMessage(), e);
            // 将所有的编译时异常转变成运行时异常
            throw new SeckillException("秒杀时异常:" + e.getMessage());
        }


        return null;
    }

    /**
     * 获取 md5 密文信息
     * @param seckillId 秒杀产品id
     * @return String
     */
    private String getMd5(long seckillId) {
        String base = seckillId + "/" + slat;
        String md5 = DigestUtils.md5DigestAsHex(base.getBytes());
        return md5;
    }
}

二、基于Spring的Service依赖

  • Spring IOC 功能详解
Spring-IOC
业务依赖
使用Spring-IOC的原因
Spring-IOC的使用方式
本项目IOC使用方式

1、Service 配置

  • spring-service.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
                           http://www.springframework.org/schema/beans/spring-beans.xsd
                           http://www.springframework.org/schema/context
                           http://www.springframework.org/schema/context/spring-context.xsd
                           http://www.springframework.org/schema/tx
                           http://www.springframework.org/schema/tx/spring-tx.xsd">
    <!-- 自动扫描 Service 包下的所有的注解类型 -->
    <context:component-scan base-package="org.seckill.service"/>

    <!-- 配置Spring的声明式事务管理器 -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <!-- 注入数据库的连接池 -->
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置基于注解的声明式事务,默认使用注解来管理事务行为 -->
    <tx:annotation-driven transaction-manager="transactionManager"/>
</beans>

2、Spring 声明式事务

传统数据库操作

Spring来自动的管理事务的开启、提交、回滚,这种方式叫做声明式事务

声明式事务的使用方式

推荐使用第三种

Spring 声明式事务
Spring 声明式事务回滚的的条件

1)声明式事务的配置

  • 配置声明式事务的管理器

    • 在Spring相关的配置文件中配置声明式事务管理器
    • @Transactional 是Spring唯一的声明式事务注入标签
  • 使用注解控制事务方法的优点

    • 1、开发团队打成一个约定,明确标注事务方法的编程风格
    • 2、保证食物方法的执行时间尽可能短,不要穿插其他的网络操作,RPC/HTTP请求等,如果需要剥离到事务方法外部
    • 3、不是所有的方法都是需要声明式事务的,如:只有一条修改操作、只读操作不需要事务控制
  • logback.xml

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

    <appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
        <!-- encoder 默认配置为PatternLayoutEncoder -->
        <encoder>
            <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
        </encoder>
    </appender>

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

</configuration>

3、测试类

  • ISeckillServiceTest.java
package org.seckill.service.interfaces;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.seckill.bean.Seckill;
import org.seckill.dto.Exposer;
import org.seckill.dto.SeckillExcution;
import org.seckill.exception.RepeatKillException;
import org.seckill.exception.SeckillCloseException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

import java.util.List;

import static org.junit.Assert.*;

/**
 * ISeckillService 接口的测试类
 * Spring 和 junit 整合
 * 目的:视为了让 junit 在启动时加载 Spring IOC 容器
 * 原因:因为 Dao 接口的实现是由 Spring 完成的
 *
 * 实现:通过 JUnit 的@RunWith(SpringJUnit4ClassRunner.class) 接口来加载Spring 的SpringJUnit4ClassRunner
 *       在加载时,用Spring 的ContextConfiguration来加载验证 MyBatis 与 Spring 的整合文件
 * spring-test、junit{}
 * Created by wangxf on 2017/2/22.
 */
 @RunWith(SpringJUnit4ClassRunner.class)
 @ContextConfiguration({"classpath:spring/spring-service.xml","classpath:spring/spring-dao.xml"})
public class ISeckillServiceTest {
    // 日志的定义
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private ISeckillService seckillService;

    @Test
    public void selectSeckillList() throws Exception {
        List<Seckill> seckillList = seckillService.selectSeckillList();
        logger.info("list={}" + seckillList);
    }

    @Test
    public void selectSeckillById() throws Exception {
        long seckillId = 1000L;
        Seckill seckill = seckillService.selectSeckillById(seckillId);
        logger.info("seckill={}" + seckill);
    }

    /**
     * exportSeckillUrlException + excuteSeckill 的联合测试
     * @throws Exception
     */
    @Test
    public void exportSeckillUrlException() throws Exception {
        long seckillId = 1000L;
        Exposer exposer = seckillService.exportSeckillUrlException(seckillId);

        // 判断秒杀接口是否开启
        if (exposer.isExposed()) {
            logger.info("exposer={ }" + exposer);

            long userPhone = 18779118283L;
            String md5 = exposer.getMd5();
            try {
                SeckillExcution seckillExcution = seckillService.excuteSeckill(seckillId, userPhone, md5);
                logger.info("seckillExcution ={}" + seckillExcution);
            } catch (SeckillCloseException e) {
                logger.error(e.getMessage());
            } catch (RepeatKillException e1) {
                logger.error(e1.getMessage());
            }
        } else {
            logger.warn("exposer={}" + exposer + "秒杀还外开启");
        }
        // md5 = 0460b09c6e8028d6b2620c1d75c34f4b
    }

    @Test
    public void excuteSeckill() throws Exception {
        long seckillId = 1000L;
        long userPhone = 18779118283L;
        String md5 = "0460b09c6e8028d6b2620c1d75c34f4b";
        try {
            SeckillExcution seckillExcution = seckillService.excuteSeckill(seckillId, userPhone, md5);
            logger.info("seckillExcution ={}" + seckillExcution);
        } catch (SeckillCloseException e) {
            logger.error(e.getMessage());
        } catch (RepeatKillException e1) {
            logger.error(e1.getMessage());
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,176评论 5 469
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,190评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,232评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,953评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,879评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,177评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,626评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,295评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,436评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,365评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,414评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,096评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,685评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,771评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,987评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,438评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,032评论 2 341

推荐阅读更多精彩内容