Activiti 工作流引擎的初步使用

最近领导让我研究下工作流,于是查啊查就查到了Activiti,特么刚开始一直查的是Activity,查出来一堆Android的东西,我也是醉了。话不多说,下面就记录下这2天的研究成果吧。

所用环境

Maven工程
 JDK:jdk1.8.0_73
 IDE:eclipse Mars.2 Release (4.5.2)
 数据库:mysql 5.1.39-ndb-7.0.9-cluster-gpl
 SSM框架:(spring + spring-mvc)4.3.2.RELEASE + mybatis3.4.1
 Activiti:5.21.0

spring+mvc+mybatis整合就不贴了,网上一大堆了

eclipse安装流程设计插件

eclipse依次点击 Help -> Install New Software -> Add:
 Name:Activiti Designer
 Location:http://activiti.org/designer/update/
 点击OK选中插件安装即可

添加 Activiti 到项目中

  1. 在 pom.xml 中添加 Activiti 依赖
    <activiti.version>5.21.0</activiti.version>

     <dependency>
         <groupId>org.activiti</groupId>
         <artifactId>activiti-engine</artifactId>
         <version>${activiti.version}</version>
     </dependency>
     
     <dependency>
         <groupId>org.activiti</groupId>
         <artifactId>activiti-spring</artifactId>
         <version>${activiti.version}</version>
     </dependency>
     
     <dependency>
         <groupId>org.activiti</groupId>
         <artifactId>activiti-rest</artifactId>
         <version>${activiti.version}</version>
     </dependency>
    

2.新建 applicationContext-activiti.xml,别忘了在主配置文件中将其import

<?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:aop="http://www.springframework.org/schema/aop" 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/aop http://www.springframework.org/schema/aop/spring-aop-4.1.xsd
            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd
            http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">

        <!-- 流程配置 -->
        <bean id="processEngineConfiguration" class="org.activiti.spring.SpringProcessEngineConfiguration">
            <property name="dataSource" ref="dataSource" />
            <property name="transactionManager" ref="transactionManager" />
            <property name="databaseSchemaUpdate" value="true" />
            <property name="jobExecutorActivate" value="true" />
            <!-- 以下2个是为了防止生成流程图片时出现乱码 -->
            <property name="activityFontName" value="宋体"/>  
            <property name="labelFontName" value="宋体"/>  
        </bean>

        <!-- 流程引擎 -->
        <bean id="processEngine" class="org.activiti.spring.ProcessEngineFactoryBean">
            <property name="processEngineConfiguration" ref="processEngineConfiguration" />
        </bean>

        <!-- 流程服务 -->
        <bean id="repositoryService" factory-bean="processEngine"
            factory-method="getRepositoryService" />
        <bean id="runtimeService" factory-bean="processEngine"
            factory-method="getRuntimeService" />
        <bean id="taskService" factory-bean="processEngine"
            factory-method="getTaskService" />
        <bean id="historyService" factory-bean="processEngine"
            factory-method="getHistoryService" />
        <bean id="managementService" factory-bean="processEngine"
            factory-method="getManagementService" />
        <bean id="IdentityService" factory-bean="processEngine"
            factory-method="getIdentityService" />
    
    </beans>

3.启动项目,如果未出现错误,Activiti会在连接的数据库中自动新建25张表,如下:

25张表.png

至此Activiti流程引擎添加完毕,下面开始使用Activiti,实现一次请假流程

设计流程

流程设计插件安装成功后会在eclipse新建向导中出现Activiti向导,如图

Paste_Image.png

1.我们新建一个 Activiti Diagram 命名为 leave.bpmn,完成时如下图:

Paste_Image.png

主要就是拖拖拉拉,从左至右控件分别为
StartEvent,UserTask,ExlusiveGateway,UserTask,EndEvent;连线都是SequenceFlow

属性可在Properties视图中设置,如果没有这个视图,可在 eclipse 中依次点击
Window->Show View->Other 搜索Properties点击OK即可

  • 流程属性:一般设置一个Id,Name,NameSpace就可以了,此处为分别为leaveProcess、Leave Process、http://www.mario.com; 这个Id在之后启动流程时会用到,不同流程Id不可相同
  • 开始事件(StartEvent):流程开始
  • 结束事件(EndEvent):流程结束
  • 用户任务(UserTask):主要用到Id,Name,Assignee
    Assignee为此任务的办理人,在查找任务时需要用到,三个任务分别指派给 apply,pm,boss
Paste_Image.png
Paste_Image.png
  • 排他网关(ExlusiveGateway):只会寻找唯一一条能走完的顺序流
  • 顺序流(SequenceFlow):主要用到Id,Name,Condition
    Condition用于指定该顺序流表达式 ,day的值会在调用执行任务方法时传入
Paste_Image.png

除了使用可视化组件,我们也可以通过xml来设计流程,以上流程的xml定义如下:

<?xml version="1.0" encoding="UTF-8"?>
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:activiti="http://activiti.org/bpmn" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC" xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI" typeLanguage="http://www.w3.org/2001/XMLSchema" expressionLanguage="http://www.w3.org/1999/XPath" targetNamespace="http://www.mario.com">
  <process id="leaveProcess" name="Leave Process" isExecutable="true">
    <startEvent id="startevent1" name="开始"></startEvent>
    <userTask id="usertask1" name="请假申请" activiti:assignee="apply"></userTask>
    <exclusiveGateway id="exclusivegateway1" name="Exclusive Gateway"></exclusiveGateway>
    <sequenceFlow id="flow2" name="天数判断" sourceRef="usertask1" targetRef="exclusivegateway1"></sequenceFlow>
    <userTask id="usertask2" name="审批(项目经理)" activiti:assignee="pm"></userTask>
    <sequenceFlow id="flow3" name="小于等于三天" sourceRef="exclusivegateway1" targetRef="usertask2">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${day<=3}]]></conditionExpression>
    </sequenceFlow>
    <userTask id="usertask3" name="审批(老板)" activiti:assignee="boss"></userTask>
    <sequenceFlow id="flow4" name="大于三天" sourceRef="exclusivegateway1" targetRef="usertask3">
      <conditionExpression xsi:type="tFormalExpression"><![CDATA[${day>3}]]></conditionExpression>
    </sequenceFlow>
    <endEvent id="endevent1" name="End"></endEvent>
    <sequenceFlow id="flow5" sourceRef="usertask2" targetRef="endevent1"></sequenceFlow>
    <sequenceFlow id="flow6" sourceRef="usertask3" targetRef="endevent1"></sequenceFlow>
    <sequenceFlow id="flow7" sourceRef="startevent1" targetRef="usertask1"></sequenceFlow>
  </process>
  <bpmndi:BPMNDiagram id="BPMNDiagram_leaveProcess">
    <bpmndi:BPMNPlane bpmnElement="leaveProcess" id="BPMNPlane_leaveProcess">
      <bpmndi:BPMNShape bpmnElement="startevent1" id="BPMNShape_startevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="30.0" y="211.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask1" id="BPMNShape_usertask1">
        <omgdc:Bounds height="55.0" width="105.0" x="110.0" y="201.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="exclusivegateway1" id="BPMNShape_exclusivegateway1">
        <omgdc:Bounds height="40.0" width="40.0" x="285.0" y="208.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask2" id="BPMNShape_usertask2">
        <omgdc:Bounds height="55.0" width="105.0" x="400.0" y="120.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="usertask3" id="BPMNShape_usertask3">
        <omgdc:Bounds height="55.0" width="105.0" x="400.0" y="290.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNShape bpmnElement="endevent1" id="BPMNShape_endevent1">
        <omgdc:Bounds height="35.0" width="35.0" x="560.0" y="211.0"></omgdc:Bounds>
      </bpmndi:BPMNShape>
      <bpmndi:BPMNEdge bpmnElement="flow2" id="BPMNEdge_flow2">
        <omgdi:waypoint x="215.0" y="228.0"></omgdi:waypoint>
        <omgdi:waypoint x="285.0" y="228.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="48.0" x="230.0" y="228.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow3" id="BPMNEdge_flow3">
        <omgdi:waypoint x="305.0" y="208.0"></omgdi:waypoint>
        <omgdi:waypoint x="452.0" y="175.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="100.0" x="295.0" y="180.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow4" id="BPMNEdge_flow4">
        <omgdi:waypoint x="305.0" y="248.0"></omgdi:waypoint>
        <omgdi:waypoint x="452.0" y="290.0"></omgdi:waypoint>
        <bpmndi:BPMNLabel>
          <omgdc:Bounds height="14.0" width="48.0" x="285.0" y="257.0"></omgdc:Bounds>
        </bpmndi:BPMNLabel>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow5" id="BPMNEdge_flow5">
        <omgdi:waypoint x="452.0" y="175.0"></omgdi:waypoint>
        <omgdi:waypoint x="577.0" y="211.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow6" id="BPMNEdge_flow6">
        <omgdi:waypoint x="452.0" y="290.0"></omgdi:waypoint>
        <omgdi:waypoint x="577.0" y="246.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
      <bpmndi:BPMNEdge bpmnElement="flow7" id="BPMNEdge_flow7">
        <omgdi:waypoint x="65.0" y="228.0"></omgdi:waypoint>
        <omgdi:waypoint x="110.0" y="228.0"></omgdi:waypoint>
      </bpmndi:BPMNEdge>
    </bpmndi:BPMNPlane>
  </bpmndi:BPMNDiagram>
</definitions>

流程部署

有了流程图,我们就可以部署该流程了,Activiti提供多种部署方法(有自动部署,手动部署等),这里演示两种手动部署方法

Workflow.java代码片段

private static ProcessEngine processEngine = ProcessEngines.getDefaultProcessEngine();
/**
 * 通过定义好的流程图文件部署,一次只能部署一个流程
 */
public static void deploy() {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    Deployment deployment = repositoryService.createDeployment()
            .addClasspathResource("death/note/lawliet/web/workflow/leave.bpmn").deploy();
}
/**
 * 将多个流程文件打包部署,一次可以部署多个流程
 */
public void deployByZip() {
    InputStream is = this.getClass().getClassLoader().getResourceAsStream("diagrams/bpm.zip");
    ZipInputStream zip = new ZipInputStream(is);
    Deployment deployment = processEngine
            .getRepositoryService()
            .createDeployment()
            .addZipInputStream(zip)
            .deploy();
}

方便起见,通过一个Deploy按钮来部署流程

Paste_Image.png

部署成功后,会分别在 act_ge_bytearray,act_re_deployment,act_re_procdef三张表插入相应数据,多次部署同一流程的话会增加版本号,以此获取最新的流程

启动流程

我们通过一个表单来用于请假申请

Paste_Image.png

我们定义一个 Leave 对象用于保存请假信息,相应的数据表为 leave 。Result 对象用于封装一些返回信息。这里的 "leaveProcess" 就是在流程属性中定义的Id。启动成功后会返回一个 ProcessInstance 对象,这个对象主要是一些流程的基本信息,具体可以查看文档或源码。这里将返回的流程实例 id 存入到我们的 leave 表,以便以后可以通过这个 id 查询相关的流程。

@ResponseBody
@RequestMapping(value = "/save", method = RequestMethod.POST)
public Result save(@RequestBody Leave user) {
    Result result = new Result();
    ProcessInstance pi = Workflow.startInstanceByKey("leaveProcess");
    user.setInstaceId(pi.getId());
    leaveService.insert(user);
    result.info(true, 0, "保存成功");
    return result;
}

Workflow.java代码片段

public static ProcessInstance startInstanceByKey(String instanceByKey) {
    RuntimeService runtimeService = processEngine.getRuntimeService();
    ProcessInstance instance = runtimeService.startProcessInstanceByKey(instanceByKey);
    return instance;
}

流程启动成功后会在 act_hi_actinst,act_hi_identitylink,act_hi_procinst,act_hi_taskinst,act_ru_execution,act_ru_identitylink,act_ru_task 表中插入相应数据。我们比较关心的就是 act_ru_task 表了,它存储了任务的相关信息。

查看任务

流程启动完毕后,应该就是进入了请假申请任务,我们可以通过请假申请的办理人 apply 来查询该任务(当然还有其他方法),这里可以指定不同查询条件和过滤条件。返回 taskList 就是当前的任务列表了,Task是Activiti为我们定义好的接口对象,主要封装了任务的信息。
 这里由于 Activiti 的Task是接口对象无法转换为json,所以自定义了一个Task对象,来转换成json

@ResponseBody
@RequestMapping(value = "/data/{assignee}")
public List<death.note.lawliet.web.model.Task> data(@PathVariable String assignee){
    List<Task> tasks = Workflow.findTaskByAssignee(assignee);
    
    List<death.note.lawliet.web.model.Task> list = new ArrayList<>();
    for(Task task : tasks){
        death.note.lawliet.web.model.Task t = new death.note.lawliet.web.model.Task();
        t.setTaskId(task.getId());
        t.setName(task.getName());
        t.setAssignee(task.getAssignee());
        t.setExecutionId(task.getExecutionId());
        t.setProcessInstanceId(task.getProcessInstanceId());
        t.setProcessDefinitionId(task.getProcessDefinitionId());
        list.add(t);
    }
    return list;
}

Workflow.java代码片段

 public static List<Task> findTaskByAssignee(String assignee) {
    TaskService taskService = processEngine.getTaskService();
    List<Task> taskList = taskService.createTaskQuery().taskAssignee(assignee).list();
    return taskList;
}
Paste_Image.png

查看流程图

我们可以通过流程定义ID(processDefinitionId)来获取流程图

@RequestMapping(value = "/shwoImg/{procDefId}")  
public void shwoImg(@PathVariable String procDefId,HttpServletResponse response){  
    try {  
        InputStream pic = Workflow.findProcessPic(procDefId);  
          
        byte[] b = new byte[1024];  
        int len = -1;  
        while((len = pic.read(b, 0, 1024)) != -1) {  
            response.getOutputStream().write(b, 0, len);  
        }  
    } catch (Exception e) {  
        e.printStackTrace();  
    }  
}  

Workflow.java代码片段

public static InputStream findProcessPic(String procDefId) throws Exception {
    RepositoryService repositoryService = processEngine.getRepositoryService();
    ProcessDefinition procDef = repositoryService.createProcessDefinitionQuery().processDefinitionId(procDefId)
            .singleResult();
    String diagramResourceName = procDef.getDiagramResourceName();
    InputStream imageStream = repositoryService.getResourceAsStream(procDef.getDeploymentId(), diagramResourceName);
    return imageStream;
}

然后通过processDefinitionId,executionId 来获取当前正在执行任务的位置坐标,以便用于标识流程图上的位置。ActivityImpl 对象封装了任务的位置信息,包括xy坐标,长和宽。自定义 Rect 对象用于转换json

@ResponseBody
@RequestMapping(value = "/showImg/{procDefId}/{executionId}")
public Rect showImg(@PathVariable String procDefId,@PathVariable String executionId ) {
    Rect rect = new Rect();
    try {
        ActivityImpl img = Workflow.getProcessMap(procDefId,executionId );
        rect.setX(img.getX());
        rect.setY(img.getY());
        rect.setWidth(img.getWidth());
        rect.setHeight(img.getHeight());
    } catch (Exception e) {
        e.printStackTrace();
    }
    return rect;
}

Workflow.java代码片段

public static ActivityImpl getProcessMap(String procDefId, String executionId) throws Exception {
    ActivityImpl actImpl = null;
    RepositoryService repositoryService = processEngine.getRepositoryService();
    //获取流程定义实体
    ProcessDefinitionEntity def = (ProcessDefinitionEntity) ((RepositoryServiceImpl) repositoryService)
            .getDeployedProcessDefinition(procDefId);
    RuntimeService runtimeService = processEngine.getRuntimeService();
    //获取执行实体
    ExecutionEntity execution = (ExecutionEntity) runtimeService.createExecutionQuery().executionId(executionId)
            .singleResult();
    // 获取当前任务执行到哪个节点
    String activitiId = execution.getActivityId();
    // 获得当前任务的所有节点
    List<ActivityImpl> activitiList = def.getActivities();
    for (ActivityImpl activityImpl : activitiList) {
        String id = activityImpl.getId();
        if (id.equals(activitiId)) {
            actImpl = activityImpl;
            break;
        }
    }
    return actImpl;
}

最终生成图片时分别调用2个 showImg 方法即可,红框可以根据返回的 Rect 来绘制,起始坐标根据自己的布局自行调整

LeavePicController.js片段

var pic.rect = {};
var pic.procDefId = $stateParams.procDefId;
$http.get('workflow/showImg/'+$stateParams.procDefId +'/'+$stateParams.executionId) 
          .success(function(data) {
              pic.rect.x = data.x;
              pic.rect.y = data.y;
              pic.rect.width = data.width;
              pic.rect.height = data.height;
          });

picture.html

<div class="container-fluid" ng-controller="LeavePicController as pic">
  <img src="workflow/showImg/{{pic.procDefId}}">
  <div style="position:absolute; border:2px solid red;
      left:{{pic.rect.x + 20 }}px;
      top:{{pic.rect.y + 88 }}px;
      width:{{pic.rect.width }}px;
      height:{{pic.rect.height }}px;">
  </div>  
</div>
Paste_Image.png

流程审批

通过 taskId 就可以对当前执行的任务进行审批,这里的 day 应该从 leave 表中查询出来,方便起见就写死了,这个day也就是在顺序流的Condition中指定的变量,小于等于3就会流向项目经理(pm)审批任务了

@ResponseBody
@RequestMapping(value = "/check/{taskId}")
public Result check(@PathVariable String taskId) {
    Result result = new Result();
    Map<String, Object> map = new HashMap<>();
    map.put("day", 3);
    Workflow.completeTask(taskId,map);
    result.info(true, 0, "审批成功");
    return result;
}

Workflow.java代码片段

public static void completeTask(String taskid,Map<String, Object> map map) {
    TaskService taskService = processEngine.getTaskService();
    taskService.complete(taskid, map);
}

审批成功后,就需要通过办理人(pm)来查询任务了,并且请假办理(apply)中的任务被移除了

Paste_Image.png
Paste_Image.png

同时流程图也流向了下一节点

Paste_Image.png

项目经理再执行一次审批方法,这个流程就算走完了

最后

以上就是一次相对简单的流程:部署,启动,查询任务,显示流程图,审批。Activiti流程引擎还有很多核心操作,包括驳回、会签、转办、中止、挂起等,等有空的时候再深入研究吧
 最后的最后,初来乍到,不喜勿喷或轻喷 - -!Thanks...

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

推荐阅读更多精彩内容