DDD 实战1 - 基础代码模型

本文首先介绍一下 DDD 代码模型的分层策略,然后介绍一下各层的职责,最后基于 DDD 开发一个订单中心,来实践 DDD.

DDD 分层策略

image.png

在 DDD 中,共有四层(领域层、应用层、用户接口层、基础设施层),其层级实际上是环状架构。如上图所示。根据整洁架构思想,在上述环状架构中,越往内层,代码越稳定,其代码不应该受外界技术实现的变动而变动,所以依赖关系是:外层依赖内层。按照这个依赖原则,DDD 代码模块依赖关系如下:

image.png
  • 领域层(domain):位于最内层,不依赖其他任何层;
  • 应用层(application):仅依赖领域层;
  • 用户接口层(interfaces):依赖应用层和领域层;
  • 基础设施层(infrastructure):依赖应用层和领域层;
  • 启动模块(starter):依赖用户接口层和基础设施层,对整个项目进行启动。
    注意:interfaces 和 infrastructure 位于同一个换上,二者没有依赖关系。

DDD 各层职责

领域模型层 domain

包括实体、值对象、领域工厂、领域服务(处理本聚合内跨实体操作)、资源库接口、自定义异常等

应用服务层 application

跨聚合的服务编排,仅编排聚合根。包括:应用服务等

用户接口层 interfaces

本应用的所有流量入口。包括三部分:

  1. web 入口的实现:包括 controller、DTO 定义、DTO 转化类
  2. 消息监听者(消费者):包括 XxxListener
  3. RPC 接口的实现:比如在使用 Dubbo 时,我们的服务需要开放 Dubbo 服务给第三方,此时需要创建单独的模块包,例如 client 模块,包含 Dubbo 接口和 DTO,在用户接口层中,去做 client 中接口的实现以及 DTO 转化类

基础设施层 infrastructure

本应用的所有流量出口。包括:

  1. 资源库接口的实现
  2. 数据库操作接口、数据库实现(如果使用mybatis,则包含 resource/*.xml)、数据库对象 DO、DO 转化类
  3. 中间件的实现、文件系统实现、缓存实现、消息实现 等
  4. 第三方服务接口的实现

基于 DDD 开发订单中心

需求:基于 DDD 开发一个订单中心,实现下订单、查询订单等功能
代码:https://github.com/zhaojigang/ordercenter

ordercenter 根模块
├── order-application 应用模块
├── order-domain 领域模块
├── order-infrastructure 基础设施模块
├── order-interfaces 用户接口模块
├── order-starter 启动模块
└── pom.xml 根模块

领域层代码模型

image.png

包依赖

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>

引入 spring-boot-autoconfigure:2.4.2,在领域工厂中需要用到 Spring 注解

DDD 标识注解 common.ddd.AggregateRoot

/**
 * 标注一个实体是聚合根
 */
@Documented
@Retention(SOURCE)
@Target(TYPE)
public @interface AggregateRoot {
}

自定义异常 common.exception.OrderException

/**
 * 自定义异常
 */
@Data
public class OrderException extends RuntimeException {
    private Integer code;
    private String message;

    public OrderException(Integer code, String message) {
        this.code = code;
        this.message = message;
    }
}

将自定义异常放在领域层,因为 DDD 推荐使用充血模型,在领域实体、值对象或者领域服务中,也会做一些业务逻辑,在业务逻辑中,可以根据需要抛出自定义异常

资源库接口 io.study.order.repository.OrderRepository

/**
 * 订单资源库接口
 */
public interface OrderRepository {
    /**
     * 保存订单
     *
     * @param order 订单
     */
    void add(Order order);

    /**
     * 根据订单ID获取订单
     * @param orderId
     */
    Order orderOfId(OrderId orderId);
}
  1. 资源库接口放置在领域层,实现领域对象自持久化,同时实现依赖反转。
  2. 依赖反转:将依赖关系进行反转,假设 Order 要做自持久化,那么需要拿到资源库的实现 OrderRepositoryImpl 才行,那么 domain 包就要依赖 infrastructure 包,但是这不符合 外层依赖内层 的原则,所以需要进行依赖反转,由 infrastructure 包依赖 domain 包。实现依赖反转的方式就是在被依赖方中添加接口(例如,在 domain 包中添加 OrderRepository 接口),依赖包对接口进行实现(infrastructure 包中对 OrderRepository 进行实现),这样的好处是,domain 可以完全仅关注业务逻辑,不要关心具体技术细节,不用去关心,到底是存储到 mysql,还是 oracle,使用的数据库框架是 mybatis 还是 hibernate,技术细节的实现由 infrastructure 来完成,真正实现了业务逻辑和技术细节的分离
  3. 资源库的命名推荐:对于资源库,推荐面向集合进行设计,即资源库的方法名采用与集合相似的方法名,例如,保存和更新是 add、addAll,删除时 remove、removeAll,查询是 xxxOfccc,例如 orderOfId,ordersOfCondition,复数使用 xxxs 的格式,而不是 xxxList 这样的格式
  4. 一个聚合具有一个资源库:比如订单聚合中,Order 主订单是聚合根,OrderItem 子订单是订单聚合中的一个普通实体,那么在订单聚合中只能存在 OrderRepository,不能存在 OrderItemRepository,OrderItem 的 CRUD 都要通过 OrderRepository 先获得 Order,再从 Order 中获取 List<OrderItem>,再做逻辑。这样的好处,保证了聚合根值整个聚合的入口,对聚合内的其他实体和值对象的方访问,只能通过聚合根,保证了聚合的封装性

领域工厂 io.study.order.factory.OrderFactory

/**
 * 订单工厂
 */
@Component
public class OrderFactory {
    private static OrderRepository orderRepository;

    @Autowired
    public OrderFactory(OrderRepository repository) {
        orderRepository = repository;
    }

    public static Order createOrder() {
        return new Order(orderRepository);
    }
}

工厂的作用:创建聚合。
工厂的好处:

  1. 创建复杂的聚合,简化客户端的使用。例如 Order 的创建需要注入资源库,订单创建后,可以直接发布订单创建事件。
  2. 可读性好(更加符合通用语言),比如 对于创建订单,createOrder 就比 new Order 的语义更加明确
  3. 更好的保证一致性,防止出错,假设创建两个主订单 Order,两个主订单下分别还要创建多个子订单 OrderItem,每个子订单中需要存储主订单的ID,如果由客户端来设置 OrderItem 中的主订单ID,可能会将A主订单的ID设置给B主订单下的子订单,可能出现数据不一致的问题,具体的示例见 《实现领域驱动》P183。

实体唯一标识 io.study.order.domain.OrderId

import lombok.Value;

/**
 * 订单ID
 */
@Value
public class OrderId {
    private Long id;

    public static OrderId of(Long id) {
        return new OrderId(id);
    }

    public void validId(){
        if (id == null || id <= 0) {
            throw new OrderException(400, "id 为空");
        }
    }
}
  1. 推荐使用强类型的对象作为实体的唯一标识,好处有两个:
    a. 用来避免传参混乱,同时提升接口的可读性,例如 xxx(Long orderId, Long goodsId),假设上述接口第一个参数传了 goodsId,第二个传了 orderId,那么编译期是无法发现的,改为 xxx(OrderId orderId, GoodsId, goodsId) 即可避免,同时可读性也较高。
    b. 唯一标识中会有一些其他行为方法,如果唯一标识使用弱类型,那么这些行为方法将会泄露在实体中
  2. 唯一标识类是一个值对象,推荐值对象设置为不可变对象,使用 @lombok.Value 标注值对象,既可标识该对象为值对象,也可以是该类变为不可变类。例如,表示后的 OrderId 没有 setXxx 方法。
  3. 值对象的行为函数都是无副作用函数(即不能影响值对象本身的状态,例如 OrderId 对象被创建后,不能再使用 setXxx 修改其属性值),如果确实有属性需要变动,值对象需要整个换掉(例如,重新创建一个 OrderId 对象)

聚合根 io.study.order.domain.Order

/**
 * 订单聚合根
 */
@Setter
@Getter
@AggregateRoot
public class Order {
    /**
     * 订单 ID
     */
    private OrderId id;
    /**
     * 订单名称
     */
    private String name;
    /**
     * 订单资源库
     */
    private OrderRepository orderRepository;

    protected Order(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    /**
     * 创建订单
     *
     * @param order
     */
    public void saveOrder(Order order) {
        orderRepository.add(order);
    }

    public void setName(String name) {
        if (name == null) {
            throw new OrderException(400, "name 不能为空");
        }
        this.name = name;
    }

    public void setGoodsId(Long goodsId) {
        if (goodsId == null) {
            throw new OrderException(400, "goodsId 不能为空");
        }
        this.goodsId = goodsId;
    }

    public void setBuyQuality(Integer buyQuality) {
        if (buyQuality == null) {
            throw new OrderException(400, "buyQuality 不能为空");
        }
        this.buyQuality = buyQuality;
    }
}
  1. 聚合根是一个特殊的实体,是整个聚合对外的使者,其他聚合与改聚合沟通的方式只能是通过聚合根
  2. 由于使用工厂来创建 Order,那么 Order 的构造器需要设置为 protected,防止外界直接使用进行创建
  3. 实体单个属性的校验需要在 setXxx 中完成自校验
  4. 实体是可变的、具有唯一标识,其唯一标识通常需要设计成强类型
  5. 聚合中的 XxxRepository 可以通过上述的工厂进行注入,也可以使用“双委派”机制,即提供类似方法:createOrder(Order order, XxxRepository repository),然后应用层在调用该方法时,传入注入好的 repository 实例即可。但是这样的方式,提高了客户端使用的复杂性。

应用层代码模型

image.png

包依赖

    <dependencies>
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

应用服务 io.study.order.app.service.OrderAppService

/**
 * 订单应用服务
 */
@Service
public class OrderAppService {
    /**
     * 创建一个订单
     *
     * @param order
     */
    public void createOrder(Order order) {
        /**
         * 存储订单
         */
        order.saveOrder(order);
        /**
         * 扣减库存
         */
    }
}

应用服务用于服务编排,如上述先存储订单,然后再调用库存服务减库存。(库存服务属于第三方服务,第三方服务的集成见下一小节)

基础设施层代码模型

image.png

包依赖

    <dependencies>
        <!-- 领域模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mapstruct -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mybatis -->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!-- mysql -->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>
  1. mapstruct 用于实现模型映射器,关于其使用见 https://www.jianshu.com/p/53aac78e7d60
  2. 数据存储采用 mysql,数据库操作框架使用 mybatis,可以看到,领域层对具体的技术实现并不关注,仅关注业务,通过 DDD 实现了技术细节与业务逻辑的解耦。

资源库实现 io.study.order.repository.impl.OrderRepositoryImpl

/**
 * 订单资源库实现类
 */
@Repository
public class OrderRepositoryImpl implements OrderRepository {
    @Resource
    private OrderDAO orderDAO;

    @Override
    public void add(Order order) {
        orderDAO.insertSelective(OrderDOConverter.INSTANCE.toDO(order));
    }

    @Override
    public Order orderOfId(OrderId orderId) {
        OrderDO orderDO = orderDAO.selectByPrimaryKey(orderId.getId());
        return OrderDOConverter.INSTANCE.fromDO(orderDO);
    }
}

数据库操作接口 io.study.order.data.OrderDAO

/**
 * 订单 DAO
 * 使用 mybatis-generator 自动生成
 */
@org.apache.ibatis.annotations.Mapper
public interface OrderDAO {
    int insertSelective(OrderDO record);
    OrderDO selectByPrimaryKey(Long id);
}

数据库实现类 resources/mapper/OrderDAO.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="io.study.order.data.OrderDAO">
    <resultMap id="BaseResultMap" type="io.study.order.data.OrderDO">
        <id column="id" property="id" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
    </resultMap>
    <sql id="Base_Column_List">
    id, name
    </sql>
    <select id="selectByPrimaryKey" resultMap="BaseResultMap" parameterType="java.lang.Long">
        select
        <include refid="Base_Column_List"/>
        from `order`
        where id = #{id,jdbcType=BIGINT}
    </select>
    <insert id="insertSelective" parameterType="io.study.order.data.OrderDO">
        insert into `order`
        <trim prefix="(" suffix=")" suffixOverrides=",">
            <if test="id != null">
                id,
            </if>
            <if test="name != null">
                name,
            </if>
        </trim>
        <trim prefix="values (" suffix=")" suffixOverrides=",">
            <if test="id != null">
                #{id,jdbcType=BIGINT},
            </if>
            <if test="name != null">
                #{name,jdbcType=VARCHAR},
            </if>
        </trim>
    </insert>
</mapper>

数据对象

/**
 * 订单数据库对象
 */
@Data
public class OrderDO {
    /**
     * 订单 ID
     */
    private Long id;
    /**
     * 订单名称
     */
    private String name;
}

数据对象转换器 io.study.order.data.OrderDOConverter

/**
 * OrderDO 转换器
 */
@org.mapstruct.Mapper
public interface OrderDOConverter {
    OrderDOConverter INSTANCE = Mappers.getMapper(OrderDOConverter.class);

    @Mapping(source = "id.id", target = "id")
    OrderDO toDO(Order order);

    @Mapping(target = "id", expression = "java(OrderId.of(orderDO.getId()))")
    void update(OrderDO orderDO, @MappingTarget Order order);

    default Order fromDO(OrderDO orderDO) {
        Order order = OrderFactory.createOrder();
        INSTANCE.update(orderDO, order);
        return order;
    }
}

在创建实体对象时,需要使用工厂进行创建,这样才能为实体注入资源库实现。

用户接口层代码模型

image.png

包依赖

    <dependencies>
        <!-- 领域模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-domain</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- 应用模块 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-application</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- mapstruct -->
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mapstruct</groupId>
            <artifactId>mapstruct-processor</artifactId>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok-mapstruct-binding</artifactId>
            <scope>provided</scope>
        </dependency>
        <!-- springboot-web -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- springfox -->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-boot-starter</artifactId>
        </dependency>
    </dependencies>

引入 springfox-boot-starter:3.0.0 来实现自动化可测试的 Rest 接口文档

Controller io.study.order.web.OrderController

/**
 * Order 控制器
 */
@Api("订单控制器")
@RestController
@RequestMapping("order")
public class OrderController {
    @Resource
    private OrderAppService orderAppService;
    @Resource
    private OrderRepository orderRepository;

    /**
     * 创建一个订单
     *
     * @param orderDto
     */
    @ApiOperation("创建订单")
    @PostMapping("/create")
    public void createOrder(@RequestBody OrderDto orderDto) {
        orderAppService.createOrder(OrderDtoAssembler.INSTANCE.fromDTO(orderDto));
    }

    /**
     * 查询一个订单
     *
     * @param id 订单ID
     * @return
     */
    @ApiOperation("根据订单ID获取订单")
    @GetMapping("/find/{id}")
    public OrderDto findOrder(@PathVariable Long id) {
        Order order = orderRepository.orderOfId(OrderId.of(id));
        return OrderDtoAssembler.INSTANCE.toDTO(order);
    }
}

数据传输对象 io.study.order.web.dto.OrderDto

/**
 * 订单数据传输对象
 */
@ApiModel("订单")
@Data
public class OrderDto {
    /**
     * 订单 ID
     */
    @ApiModelProperty("订单ID")
    private Long id;
    /**
     * 订单名称
     */
    @ApiModelProperty("订单名称")
    private String name;
}

DTO 转换类 io.study.order.web.assembler.OrderDtoAssembler

/**
 * OrderDTO<=>Order 转换器
 */
@Mapper
public interface OrderDtoAssembler {
    OrderDtoAssembler INSTANCE = Mappers.getMapper(OrderDtoAssembler.class);
    /**
     * DTO 转 Entity
     * @param dto
     * @return
     */
    default Order fromDTO(OrderDto dto) {
        Order order = OrderFactory.createOrder();
        INSTANCE.update(dto, order);
        return order;
    }

    /**
     * Entity 转 DTO
     * @param order
     * @return
     */
    @Mapping(source = "id.id", target = "id")
    OrderDto toDTO(Order order);

    @Mapping(target = "id", expression = "java(OrderId.of(orderDto.getId()))")
    void update(OrderDto orderDto, @MappingTarget Order order);
}

转换器应该写在外层还是内层,比如 OrderDtoAssembler 是应该写在 interfaces 层,还是写在 application 层,从依赖关系来考虑:假设写在 application 层,由于 DTO 是定义在 interfaces 层,那么 application 需要依赖 interfaces,与 外层依赖内层 的原则不符,那么 DTO 是否可以写在 application 层,假设现在有个需要对外提供的 Dubbo 接口,该接口中存在的 DTO 是需要打包给第三方的,所以并不适合写在 application 层。

启动模块代码模型

image.png

包依赖

    <dependencies>
        <!-- 基础设施层 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-infrastructure</artifactId>
            <version>${project.version}</version>
        </dependency>
        <!-- 用户接口层 -->
        <dependency>
            <groupId>io.study</groupId>
            <artifactId>order-interfaces</artifactId>
            <version>${project.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

启动器 io.study.order.OrderApplication

/**
 * 应用启动器
 */
@EnableOpenApi
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

springfox3.x 通过注解 @EnableOpenApi 来启动自动配置

配置文件 resource/application.properties

mybatis.mapper-locations=/mapper/*.xml

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

推荐阅读更多精彩内容