springboot2結(jié)合mybatis攔截器實(shí)現(xiàn)主鍵自動(dòng)生成

前言

前陣子和朋友聊天,他說(shuō)他們項(xiàng)目有個(gè)需求抵皱,要實(shí)現(xiàn)主鍵自動(dòng)生成,不想每次新增的時(shí)候辩蛋,都手動(dòng)設(shè)置主鍵呻畸。于是我就問(wèn)他,那你們數(shù)據(jù)庫(kù)表設(shè)置主鍵自動(dòng)遞增不就得了悼院。他的回答是他們項(xiàng)目目前的id都是采用雪花算法來(lái)生成伤为,因此為了項(xiàng)目穩(wěn)定性,不會(huì)切換id的生成方式。

朋友問(wèn)我有沒(méi)有什么實(shí)現(xiàn)思路绞愚,他們公司的orm框架是mybatis叙甸,我就建議他說(shuō),不然讓你老大把mybatis切換成mybatis-plus位衩。mybatis-plus就支持注解式的id自動(dòng)生成裆蒸,而且mybatis-plus只是對(duì)mybatis進(jìn)行增強(qiáng)不做改變。朋友還是那句話糖驴,說(shuō)為了項(xiàng)目穩(wěn)定齿穗,之前項(xiàng)目組沒(méi)有使用mybatis-plus的經(jīng)驗(yàn)唯咬,貿(mào)然切換不知道會(huì)不會(huì)有什么坑讶凉。后面沒(méi)招了问欠,我就跟他說(shuō)不然你用mybatis的攔截器實(shí)現(xiàn)一個(gè)吧。于是又有一篇吹水的創(chuàng)作題材出現(xiàn)跷睦。

前置知識(shí)

在介紹如何通過(guò)mybatis攔截器實(shí)現(xiàn)主鍵自動(dòng)生成之前筷弦,我們先來(lái)梳理一些知識(shí)點(diǎn)

1肋演、mybatis攔截器的作用

mybatis攔截器設(shè)計(jì)的初衷就是為了供用戶在某些時(shí)候可以實(shí)現(xiàn)自己的邏輯而不必去動(dòng)mybatis固有的邏輯

2抑诸、Interceptor攔截器

每個(gè)自定義攔截器都要實(shí)現(xiàn)

org.apache.ibatis.plugin.Interceptor

這個(gè)接口,并且自定義攔截器類(lèi)上添加@Intercepts注解

3爹殊、攔截器能攔截哪些類(lèi)型

  • Executor:攔截執(zhí)行器的方法蜕乡。

  • ParameterHandler:攔截參數(shù)的處理。

  • ResultHandler:攔截結(jié)果集的處理梗夸。

  • StatementHandler:攔截Sql語(yǔ)法構(gòu)建的處理层玲。

4、攔截的順序

a反症、不同類(lèi)型攔截器的執(zhí)行順序

Executor -> ParameterHandler -> StatementHandler -> ResultSetHandler

b辛块、多個(gè)攔截器攔截同種類(lèi)型同一個(gè)目標(biāo)方法,執(zhí)行順序是后配置的攔截器先執(zhí)行

比如在mybatis配置如下

    <plugins>
        <plugin interceptor="com.lybgeek.InterceptorA" />
        <plugin interceptor="com.lybgeek.InterceptorB" />
    </plugins>

則InterceptorB先執(zhí)行铅碍。

如果是和spring做了集成润绵,先注入spring ioc容器的攔截器,則后執(zhí)行胞谈。比如有個(gè)mybatisConfig尘盼,里面有如下攔截器bean配置

 @Bean
    public InterceptorA interceptorA(){
        return new InterceptorA();
    }

    @Bean
    public InterceptorB interceptorB(){
        return new InterceptorB();
    }

則InterceptorB先執(zhí)行。當(dāng)然如果你是直接用@Component注解這形式烦绳,則可以配合@Order注解來(lái)控制加載順序

5卿捎、攔截器注解介紹

@Intercepts:標(biāo)識(shí)該類(lèi)是一個(gè)攔截器

@Signature:指明自定義攔截器需要攔截哪一個(gè)類(lèi)型,哪一個(gè)方法径密。
@Signature注解屬性中的type表示對(duì)應(yīng)可以攔截四種類(lèi)型(Executor午阵、ParameterHandler、ResultHandler享扔、StatementHandler)中的一種底桂;method表示對(duì)應(yīng)類(lèi)型(Executor括细、ParameterHandler、ResultHandler戚啥、StatementHandler)中的哪類(lèi)方法奋单;args表示對(duì)應(yīng)method中的參數(shù)類(lèi)型

6、攔截器方法介紹

a猫十、 intercept方法

public Object intercept(Invocation invocation) throws Throwable

這個(gè)方法就是我們來(lái)執(zhí)行我們自己想實(shí)現(xiàn)的業(yè)務(wù)邏輯览濒,比如我們的主鍵自動(dòng)生成邏輯就是在這邊實(shí)現(xiàn)。

Invocation這個(gè)類(lèi)中的成員屬性target就是@Signature中的type拖云;method就是@Signature中的method贷笛;args就是@Signature中的args參數(shù)類(lèi)型的具體實(shí)例對(duì)象

b、 plugin方法

public Object plugin(Object target)

這個(gè)是用返回代理對(duì)象或者是原生代理對(duì)象宙项,如果你要返回代理對(duì)象乏苦,則返回值可以設(shè)置為

Plugin.wrap(target, this);
this為攔截器

如果返回是代理對(duì)象,則會(huì)執(zhí)行攔截器的業(yè)務(wù)邏輯尤筐,如果直接返回target汇荐,就是沒(méi)有攔截器的業(yè)務(wù)邏輯。說(shuō)白了就是告訴mybatis是不是要進(jìn)行攔截盆繁,如果要攔截掀淘,就生成代理對(duì)象,不攔截是生成原生對(duì)象

c油昂、 setProperties方法

public void setProperties(Properties properties)

用于在Mybatis配置文件中指定一些屬性

主鍵自動(dòng)生成思路

1革娄、定義一個(gè)攔截器

主要攔截

 `Executor#update(MappedStatement ms, Object parameter)`} 

這個(gè)方法。mybatis的insert冕碟、update拦惋、delete都是通過(guò)這個(gè)方法,因此我們通過(guò)攔截這個(gè)這方法安寺,來(lái)實(shí)現(xiàn)主鍵自動(dòng)生成厕妖。其代碼塊如下

@Intercepts(value={@Signature(type = Executor.class,method = "update",args = {MappedStatement.class,Object.class})})
public class AutoIdInterceptor implements Interceptor {}

2、判斷sql操作類(lèi)型

Executor 提供的方法中我衬,update 包含了 新增叹放,修改和刪除類(lèi)型,無(wú)法直接區(qū)分挠羔,需要借助 MappedStatement 類(lèi)的屬性 SqlCommandType 來(lái)進(jìn)行判斷,該類(lèi)包含了所有的操作類(lèi)型

public enum SqlCommandType {
  UNKNOWN, INSERT, UPDATE, DELETE, SELECT, FLUSH;
}

當(dāng)SqlCommandType類(lèi)型是insert我們才進(jìn)行主鍵自增操作

3俱恶、填充主鍵值

3.1、編寫(xiě)自動(dòng)生成id注解

Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface AutoId {
    /**
     * 主鍵名
     * @return
     */
    String primaryKey();

    /**
     * 支持的主鍵算法類(lèi)型
     * @return
     */
    IdType type() default IdType.SNOWFLAKE;

    enum IdType{
        SNOWFLAKE
    }
}

3.2合是、 雪花算法實(shí)現(xiàn)

我們可以直接拿hutool這個(gè)工具包提供的idUtil來(lái)直接實(shí)現(xiàn)算法了罪。

引入

 <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
        </dependency>
Snowflake snowflake = IdUtil.createSnowflake(0,0);
long value = snowflake.nextId();

3.3聪全、填充主鍵值

其實(shí)現(xiàn)核心是利用反射。其核心代碼片段如下

   ReflectionUtils.doWithFields(entity.getClass(), field->{
                    ReflectionUtils.makeAccessible(field);
                    AutoId autoId = field.getAnnotation(AutoId.class);
                    if(!ObjectUtils.isEmpty(autoId) && (field.getType().isAssignableFrom(Long.class))){
                        switch (autoId.type()){
                            case SNOWFLAKE:
                                SnowFlakeAutoIdProcess snowFlakeAutoIdProcess = new SnowFlakeAutoIdProcess(field);
                                snowFlakeAutoIdProcess.setPrimaryKey(autoId.primaryKey());
                                finalIdProcesses.add(snowFlakeAutoIdProcess);
                                break;
                        }
                    }
                });
public class SnowFlakeAutoIdProcess extends BaseAutoIdProcess {

    private static Snowflake snowflake = IdUtil.createSnowflake(0,0);


    public SnowFlakeAutoIdProcess(Field field) {
        super(field);
    }

    @Override
    void setFieldValue(Object entity) throws Exception{
        long value = snowflake.nextId();
        field.set(entity,value);
    }
}

如果項(xiàng)目中的mapper.xml已經(jīng)的insert語(yǔ)句已經(jīng)含有id难礼,比如

insert into sys_test( `id`,`type`, `url`,`menu_type`,`gmt_create`)values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})

則只需到填充id值這一步娃圆。攔截器的任務(wù)就完成。如果mapper.xml的insert不含id蛾茉,形如

insert into sys_test( `type`, `url`,`menu_type`,`gmt_create`)values( #{type}, #{url},#{menuType},#{gmtCreate})

則還需重寫(xiě)insert語(yǔ)句以及新增id參數(shù)

4、重寫(xiě)insert語(yǔ)句以及新增id參數(shù)(可選)

4.1 重寫(xiě)insert語(yǔ)句

方法一:
從 MappedStatement 對(duì)象中獲取 SqlSource 對(duì)象悦屏,再?gòu)膹?SqlSource 對(duì)象中獲取獲取 BoundSql 對(duì)象键思,通過(guò) BoundSql#getSql 方法獲取原始的sql,最后在原始sql的基礎(chǔ)上追加id

方法二:

引入

<dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid.version}</version>
        </dependency>

通過(guò)

com.alibaba.druid.sql.dialect.mysql.parser.MySqlStatementParser

獲取相應(yīng)的表名稚机、需要insert的字段名获搏。然后重新拼湊出新的insert語(yǔ)句

4.2 把新的sql重置給Invocation

其核心實(shí)現(xiàn)思路是創(chuàng)建一個(gè)新的MappedStatement,新的MappedStatement綁定新sql纬乍,再把新的MappedStatement賦值給Invocation的args[0],代碼片段如下

 private void resetSql2Invocation(Invocation invocation, BoundSqlHelper boundSqlHelper,Object entity) throws SQLException {
        final Object[] args = invocation.getArgs();
        MappedStatement statement = (MappedStatement) args[0];
        MappedStatement newStatement = newMappedStatement(statement, new BoundSqlSqlSource(boundSqlHelper));
        MetaObject msObject =  MetaObject.forObject(newStatement, new DefaultObjectFactory(), new DefaultObjectWrapperFactory(),new DefaultReflectorFactory());
        msObject.setValue("sqlSource.boundSqlHelper.boundSql.sql", boundSqlHelper.getSql());

            args[0] = newStatement;

    }

4.3 新增id參數(shù)

其核心是利用

org.apache.ibatis.mapping.ParameterMapping

核心代碼片段如下

  private void setPrimaryKeyParaterMapping(String primaryKey) {
           ParameterMapping parameterMapping = new ParameterMapping.Builder(boundSqlHelper.getConfiguration(),primaryKey,boundSqlHelper.getTypeHandler()).build();
           boundSqlHelper.getBoundSql().getParameterMappings().add(parameterMapping);
       }

5仿贬、將mybatis攔截器注入到spring容器

可以直接在攔截器上加

@org.springframework.stereotype.Component

注解墓贿。也可以通過(guò)

 @Bean
    public AutoIdInterceptor autoIdInterceptor(){
        return new AutoIdInterceptor();
    }

6、在需要實(shí)現(xiàn)自增主鍵的實(shí)體字段上加如下注解

@AutoId(primaryKey = "id")
    private Long id;

測(cè)試

1队伟、對(duì)應(yīng)的測(cè)試實(shí)體以及單元測(cè)試代碼如下

@Data
public class TestDO implements Serializable {
    private static final long serialVersionUID = 1L;

    @AutoId(primaryKey = "id")
    private Long id;
    private Integer type;
    private String url;
    private Date gmtCreate;
    private String menuType;
}
@Autowired
    private TestService testService;

    @Test
    public void testAdd(){
        TestDO testDO = new TestDO();
        testDO.setType(1);
        testDO.setMenuType("1");
        testDO.setUrl("www.test.com");
        testDO.setGmtCreate(new Date());
        testService.save(testDO);
        testService.get(110L);
    }

    @Test
    public void testBatch(){
        List<TestDO> testDOList = new ArrayList<>();
        for (int i = 0; i < 3; i++) {
            TestDO testDO = new TestDO();
            testDO.setType(i);
            testDO.setMenuType(i+"");
            testDO.setUrl("www.test"+i+".com");
            testDO.setGmtCreate(new Date());
            testDOList.add(testDO);
        }

        testService.saveBatch(testDOList);
    }

2嗜侮、當(dāng)mapper的insert語(yǔ)句中含有id,形如下

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`)
        values( #{id},#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            ( #{test.id},#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制臺(tái)sql打印語(yǔ)句

15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`id`,`type`, `url`,`menu_type`,`gmt_create`) values( ?,?, ?,?,? ) 
15:52:04 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 356829258376544258(Long), 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:52:04.738(Timestamp)
15:52:04 [main] DEBUG com.nlybgeek.dao.TestDao.save - <==    Updates: 1
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test( `id`,`gmt_create`,`type`,`url`,`menu_type`) values ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) , ( ?,?,?, ?, ?) 
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 356829258896637961(Long), 2020-09-11 15:52:04.847(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356829258896637960(Long), 2020-09-11 15:52:04.847(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356829258896637962(Long), 2020-09-11 15:52:04.847(Timestamp), 2(Integer), www.test2.com(String), 2(String)
15:52:04 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

查看數(shù)據(jù)庫(kù)


sql語(yǔ)句.png

3、當(dāng)mapper的insert語(yǔ)句中不含id击吱,形如下

<insert id="save" parameterType="com.lybgeek.TestDO" useGeneratedKeys="true" keyProperty="id">
        insert into sys_test(`type`, `url`,`menu_type`,`gmt_create`)
        values(#{type}, #{url},#{menuType},#{gmtCreate})
    </insert>

以及批量插入sql

<insert id="saveBatch"  parameterType="java.util.List" useGeneratedKeys="false">
        insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`)
        values
        <foreach collection="list" item="test" index="index" separator=",">
            (#{test.gmtCreate},#{test.type}, #{test.url},
            #{test.menuType})
        </foreach>
    </insert>

查看控制臺(tái)sql打印語(yǔ)句

15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==>  Preparing: insert into sys_test(`type`,`url`,`menu_type`,`gmt_create`,id) values (?,?,?,?,?) 
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - ==> Parameters: 1(Integer), www.test.com(String), 1(String), 2020-09-11 15:59:46.741(Timestamp), 356831196144992264(Long)
15:59:46 [main] DEBUG com.lybgeek.dao.TestDao.save - <==    Updates: 1
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==>  Preparing: insert into sys_test(`gmt_create`,`type`,`url`,`menu_type`,id) values (?,?,?,?,?),(?,?,?,?,?),(?,?,?,?,?) 
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - ==> Parameters: 2020-09-11 15:59:46.845(Timestamp), 0(Integer), www.test0.com(String), 0(String), 356831196635725829(Long), 2020-09-11 15:59:46.845(Timestamp), 1(Integer), www.test1.com(String), 1(String), 356831196635725828(Long), 2020-09-11 15:59:46.845(Timestamp), 2(Integer), www.test2.com(String), 2(String), 356831196635725830(Long)
15:59:46 [main] DEBUG c.n.lybgeek.dao.TestDao.saveBatch - <==    Updates: 3

從控制臺(tái)我們可以看出覆醇,當(dāng)mapper.xml沒(méi)有配置id字段時(shí),則攔截器會(huì)自動(dòng)幫我們追加id字段

查看數(shù)據(jù)庫(kù)


sql語(yǔ)句1.png

總結(jié)

本文雖然是介紹mybatis攔截器實(shí)現(xiàn)主鍵自動(dòng)生成柴罐,但文中更多講解如何實(shí)現(xiàn)一個(gè)攔截器以及主鍵生成思路革屠,并沒(méi)把intercept實(shí)現(xiàn)主鍵方法貼出來(lái)排宰。其原因主要是主鍵自動(dòng)生成在mybatis-plus里面就有實(shí)現(xiàn),其次是有思路后党瓮,大家就可以自己實(shí)現(xiàn)了盐类。最后對(duì)具體實(shí)現(xiàn)感興趣的朋友,可以查看文末中demo鏈接

參考文檔

mybatis攔截器
mybatis插件實(shí)現(xiàn)自定義改寫(xiě)表名
mybatis攔截器枪萄,動(dòng)態(tài)修改sql語(yǔ)句

demo鏈接

https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatis-autoId

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猫妙,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子齐帚,更是在濱河造成了極大的恐慌彼哼,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,692評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件饥伊,死亡現(xiàn)場(chǎng)離奇詭異琅豆,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)茫因,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,482評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)冻押,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人括袒,你說(shuō)我怎么就攤上這事稿茉。” “怎么了恃慧?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,995評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵渺蒿,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我怠蹂,道長(zhǎng)训唱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,223評(píng)論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮澳骤,結(jié)果婚禮上澜薄,老公的妹妹穿的比我還像新娘。我一直安慰自己颊艳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,245評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布白修。 她就那樣靜靜地躺著重斑,像睡著了一般。 火紅的嫁衣襯著肌膚如雪祖很。 梳的紋絲不亂的頭發(fā)上漾脂,一...
    開(kāi)封第一講書(shū)人閱讀 51,208評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音拆融,去河邊找鬼啊终。 笑死,一個(gè)胖子當(dāng)著我的面吹牛趟脂,可吹牛的內(nèi)容都是我干的例衍。 我是一名探鬼主播,決...
    沈念sama閱讀 40,091評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼硼一,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼梦抢!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起哼蛆,我...
    開(kāi)封第一講書(shū)人閱讀 38,929評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤霞赫,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后叠洗,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,346評(píng)論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡十艾,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,570評(píng)論 2 333
  • 正文 我和宋清朗相戀三年疟羹,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了禀倔。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,739評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡愧杯,死狀恐怖鞋既,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情跌前,我是刑警寧澤陡舅,帶...
    沈念sama閱讀 35,437評(píng)論 5 344
  • 正文 年R本政府宣布,位于F島的核電站灾炭,受9級(jí)特大地震影響颅眶,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜铡原,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,037評(píng)論 3 326
  • 文/蒙蒙 一煤杀、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦辜妓、人聲如沸忌怎。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,677評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至库说,卻和暖如春片择,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背字管。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,833評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工嘲叔, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人锰什。 一個(gè)月前我還...
    沈念sama閱讀 47,760評(píng)論 2 369
  • 正文 我出身青樓掏愁,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親沦泌。 傳聞我的和親對(duì)象是個(gè)殘疾皇子辛掠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,647評(píng)論 2 354