Camunda 工作流引擎集成

camunda 集成

1.流程引擎集成

由于我的项目不使用spring相关框架, 但是内部也有实例生命周期管理的框架, 集成思路都差不多

首先通过 ProcessEngineConfiguration 来配置流程引擎并且启动,

ProcessEngineConfiguration 分为几种

1,JtaProcessEngineConfiguration 支持流程事务与框架事务整合的配置(当前方案采用)

2.StandaloneProcessEngineConfiguration 标准配置, 可能有些功能会缺失

3.和Spring 可以整的的 ProcessEngineConfiguration, 使用了Sping 框架可以优先选择

ProcessEngineConfiguration 主要的配置项是

1. configuration.setIdGenerator(idGenerator) 
使用 UUID 生成主键

2.configuration.setDataSource(dataSource)
设置数据源(用于保存流程的表结构和数据),当然如果有多数据源,最好jta事务管理多数据源事务
如果流程中调用系统服务时出错保证事务回滚.

3.configuration.setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE)
设置流程引擎数据库生成表策略,我使用的是每次启动自动更新表结构,生产环境初始化一次就可关闭此选项了

4.configuration.setCustomFormTypes(formFieldTypes)
设置流程引擎自定义表单类型,类型需要实现 FormType 接口,后续会说一下如何完成添加自定义表单类型,camunda 有个依赖 camunda-common-typed-values jar包内有一些表单类型可用,但是业务系统所需要的表单类型远不止这些,需要更多的自定义类型支持工作流引擎

5.configuration.setCustomPreVariableSerializers(typeValueSerializers)
设置自定义字段解析器, 流程自定义表单类型需要配合类型解析器来工作,解析器内定义如果将你的自定义类型保存到数据表 variables ,和如果从数据表取出来变成自定义类型.

6.configuration.setClassLoader(classLoader)
设置类加载器, 如果需要使用scriptTask groovy 脚本等,需要使用当前类加载器

7.configuration.setTransactionManager(getTransactionManager())
设置事务管理器,springboot 的configuration 好像有个属性直接集成事务管理器了

8.configuration.setEnableFetchScriptEngineFromProcessApplication(true)
设置脚本执行上下文,主要是使用 serviceTask 或者scriptTask 时有用

9.processEngine = configuration.buildProcessEngine()
启动工作流,并且初始化流程引擎,流程引擎是camunda 各种service 的入口了, 熟悉activitie 的这里应该很熟,基本activiti系列的工作流都是这么搞.

有了流程引擎就可以使用camunda 工作流了, 具体各种services 我不多做赘述, 官方文档写的不是很全面,建议还是下载个源码看,来的快点.

camunda 提供了离线的流程编辑器 ,camunda modeler,流程部署方式是线下设计好,再来部署,其实也可以自己集成一个线上设计器.

具体过程为:

1.jquery集成
bpmn.io 为BPMN2.0 提供了很强大的图形化管理工具,但是文档没有那么全面,我这里弄了一个比较省事比较快速的集成方式,还有react vue 上集成方式.后面说

bpmn.io 示例项目地址 到这里下载示例项目   properties-panel , 这个项目是带属性面板的, 同时也有camunda 的模块内容,

下载的项目就是一个Grunt 前端项目 使用命令  npm run dev 就可以运行, 运行后编译的前端代码在dist 中,

可以在这个基础上简单修改一下,让该项目支持jquery

if (jQuery){
    jQuery.fn.extend({
        bpmnModeler:function (config = {}) {....这里写一下如何初始化流程设计器})

    })

}
记得需要依赖
// 流程模型设计器
import BpmnModeler from "bpmn-js/lib/Modeler";

// 设计器属性面板
import propertiesPanelModule from "bpmn-js-properties-panel";

// 属性面板支持camunda 的module
import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";

// camunda 的Moddle 的配置
import camundaModdleDescriptorfrom "camunda-bpmn-moddle/resources/camunda.json";

// 一个空的流程文件,用于新建流程引用
import diagramXMLfrom "../resources/newDiagram.bpmn";

// 国际化文件,这个得自己来写
import ZhCN from "../resources/ZhCN";

具体代码可以参考当前的前端项目,可以使用grunt 生成的代码作为一个组件,直接引用就可以调用$('XXX').bpmnModeler({....属性})
来初始化一个流程设计器

2.react 集成
react 集成还算比较简单,需要引用的依赖为

"bpmn-js":"^7.4.0",

"bpmn-js-properties-panel":"^0.37.5",

"camunda-bpmn-moddle":"^4.4.1",

"diagram-js":"^6.8.0",

主要就是这几个,当前你也可以添加自定义的流程模块,这里就不讲了,

import {FullScreen,useFullScreenHandle }from 'react-full-screen';

// @ts-ignore

import Modeler from 'bpmn-js/lib/Modeler';

// @ts-ignore

import PropertiesPanelModulefrom 'bpmn-js-properties-panel';

// @ts-ignore

import PropertiesProviderModulefrom 'bpmn-js-properties-panel/lib/provider/camunda';

// @ts-ignore

import CamundaModdlefrom 'camunda-bpmn-moddle/resources/camunda.json';

在react 中弄个组件,应用这些模块

useEffect(() => {

modeler.current =new Modeler({

container:'#modeler-main',

propertiesPanel: {

parent:'#modeler-panel',

},

additionalModules: [

PropertiesPanelModule,

PropertiesProviderModule,

{

translate: ['value',customTranslate],

},

],

moddleExtensions: {

camunda: CamundaModdle,

},

});

handleDiagram();

}, []);

初始化modeler 就可以生成一个流程设计器了


流程设计器提供了几个接口可以帮助完成很多操作
1.缩小流程图

bpmnModeler.get("zoomScroll").zoom(-1, {

x:container.get(0).offsetWidth,

y:container.get(0).offsetHeight,

});

2.放大流程图

zoomOut.click(function (e) {

e.stopPropagation();

e.preventDefault();

bpmnModeler.get("zoomScroll").zoom(1, {

x:container.get(0).offsetWidth,

y:container.get(0).offsetHeight,

});

});

3.快捷键
初始化流程时配置属性
keyboard: {

bindTo:window,

},

完整的jquery 使用流程设计器代码

import {debounce }from "min-dash";

import BpmnModeler from "bpmn-js/lib/Modeler";

import propertiesPanelModule from "bpmn-js-properties-panel";

import propertiesProviderModule from "bpmn-js-properties-panel/lib/provider/camunda";

import camundaModdleDescriptorfrom "camunda-bpmn-moddle/resources/camunda.json";

import diagramXMLfrom "../resources/newDiagram.bpmn";

import ZhCN from "../resources/ZhCN";

// { save:'#saveBtn',reset:'#resetBtn',zoomIn:'#zoomInBtn',zoomOut:'#zoomOutBtn',downloadDiagram:'downloadDiagramBtn',downloadSvg:'downloadSvgBtn'}

if (jQuery) {

jQuery.fn.extend({

bpmnModeler:function (config = {}) {

// 初始化元素

      const container =jQuery(this);

// 添加元素

      const canvas =jQuery('<div class="bpmn-canvas"></div>');

const panel =jQuery('<div class="bpmn-panel"></div>');

const messageInfo =jQuery(

'

放入文件加载工作流 或 新建工作流 开始工作流设计
'

      );

const messageError =jQuery(

'

无法显示 BPMN 2.0 流程图.

错误详细信息

'

      );

const url =container.get(0).dataset.url;

const form =container.get(0).dataset.form;

const method =container.get(0).dataset.method;

const resourceId =container.get(0).dataset.resourceId;

const deploymentId =container.get(0).dataset.deploymentId;

const sessionToken =container.get(0).dataset.sessionToken;

const initUrl =container.get(0).dataset.initUrl;

const backUrl =container.get(0).dataset.backUrl;

const save = config.save ?jQuery(config.save) :undefined;

const reset = config.reset ?jQuery(config.reset) :undefined;

const zoomIn = config.zoomIn ?jQuery(config.zoomIn) :undefined;

const zoomOut = config.zoomOut ?jQuery(config.zoomOut) :undefined;

const downloadDiagram = config.downloadDiagram

        ?jQuery(config.downloadDiagram)

:undefined;

const downloadSvg = config.downloadSvg

        ?jQuery(config.downloadSvg)

:undefined;

container.append(canvas);

container.append(panel);

container.append(messageInfo);

container.append(messageError);

// 初始化编辑器

      const bpmnModeler =new BpmnModeler({

container:canvas,

keyboard: {

bindTo:window,

},

propertiesPanel: {

parent:panel,

},

additionalModules: [

propertiesPanelModule,

propertiesProviderModule,

{

translate: ["value",ZhCN],

},

],

moddleExtensions: {

camunda: camundaModdleDescriptor,

},

});

container.addClass("bpmn-content");

container.removeClass("with-diagram");

if (save)save.addClass("disabled");

if (reset)reset.addClass("disabled");

if (zoomIn)zoomIn.addClass("disabled");

if (zoomOut)zoomOut.addClass("disabled");

// 含有流程定义ID,加载流程文件

      if (resourceId &&initUrl) {

jQuery(function () {

jQuery.ajax({

url:initUrl,

type:"get",

headers: {

Accept:"application/json; charset=utf-8",

zkitSessionToken:sessionToken,

},

data: {deploymentId,resourceId },

success:function (res) {

openDiagram(res.data);

},

error:function (e) {

container.removeClass("with-diagram").addClass("with-error");

messageError.text("加载工作流模型失败");

},

});

});

}

const exportArtifacts =debounce(async function () {

if (downloadSvg) {

try {

const {svg } =await bpmnModeler.saveSVG();

setEncoded(downloadSvg,"diagram.svg",svg);

}catch (err) {

console.error("Error happened saving SVG: ", err);

setEncoded(downloadSvg,"diagram.svg",null);

}

}

if (downloadDiagram) {

try {

const {xml } =await bpmnModeler.saveXML({format:true });

setEncoded(downloadDiagram,"diagram.bpmn",xml);

}catch (err) {

console.log("保存 XML 错误: ", err);

setEncoded(downloadDiagram,"diagram.bpmn",null);

}

}

},500);

bpmnModeler.on("commandStack.changed",exportArtifacts);

// 处理按钮事件

      jQuery("#bpmn-create-diagram").click(function (e) {

e.stopPropagation();

e.preventDefault();

openDiagram(diagramXML);

});

if (reset) {

reset.click(function (e) {

e.stopPropagation();

e.preventDefault();

const bpmnCanvas =bpmnModeler.get("canvas");

bpmnCanvas.zoom("fit-viewport");

});

}

if (zoomIn) {

zoomIn.click(function (e) {

e.stopPropagation();

e.preventDefault();

bpmnModeler.get("zoomScroll").zoom(-1, {

x:container.get(0).offsetWidth,

y:container.get(0).offsetHeight,

});

});

}

if (zoomOut) {

zoomOut.click(function (e) {

e.stopPropagation();

e.preventDefault();

bpmnModeler.get("zoomScroll").zoom(1, {

x:container.get(0).offsetWidth,

y:container.get(0).offsetHeight,

});

});

}

if (save) {

save.click(function (e) {

e.stopPropagation();

e.preventDefault();

if (form &&url &&method) {

const formData =new FormData(jQuery(form).get(0));

const deploymentName =formData.get("deploymentName");

const source =formData.get("source");

// 表单验证

            bpmnModeler.saveXML({format:true },function (err, xml) {

if (err) {

messageError.text(err);

return;

}

let fileName ="";

if (source) {

fileName =source +".bpmn";

}else if (deploymentName) {

fileName =deploymentName +".bpmn";

}

formData.append(

"file",

new Blob([xml], {type:"text/xml" }),

fileName

              );

jQuery.ajax({

url,

type:method,

data:formData,

cache:false,

headers: {

zkitSessionToken:sessionToken,

},

processData:false,

contentType:false,

success:function (res) {

config.onSave && config.onSave();

if (res && res.deploymentId) {

backUrl &&jQuery(location).prop("href",backUrl);

}else {

alert("决策表部署失败,请检查设计模型");

}

},

});

});

}

});

}

// drop 文件引入

      if (!window.FileList || !window.FileReader) {

window.alert(

"浏览器版本不支持拖动文件加载. " +

"请尝试使用 Chrome, Firefox 或 the Internet Explorer > 10 以上重试."

        );

}else {

registerFileDrop(container, openDiagram);

}

function setEncoded(link, name, data) {

var encodedData =encodeURIComponent(data);

if (data) {

link.removeClass("disabled").attr({

href:"data:application/bpmn20-xml;charset=UTF-8," +encodedData,

download: name,

});

}else {

link.addClass("disabled");

}

}

function registerFileDrop(element, callback) {

function handleFileSelect(e) {

e.stopPropagation();

e.preventDefault();

const files = e.dataTransfer.files;

const file =files[0];

const reader =new FileReader();

reader.onload =function (e) {

const xml = e.target.result;

callback(xml);

};

reader.readAsText(file);

}

function handleDragOver(e) {

e.stopPropagation();

e.preventDefault();

e.dataTransfer.dropEffect ="copy";// Explicitly show this is a copy.

        }

element.get(0).addEventListener("dragover", handleDragOver,false);

element.get(0).addEventListener("drop", handleFileSelect,false);

}

async function openDiagram(xml) {

try {

await bpmnModeler.importXML(xml);

// zoom to fit full viewport

          container.removeClass("with-error").addClass("with-diagram");

const bpmnCanvas =bpmnModeler.get("canvas");

bpmnCanvas.zoom("fit-viewport");

if (save)save.removeClass("disabled");

if (reset)reset.removeClass("disabled");

if (zoomIn)zoomIn.removeClass("disabled");

if (zoomOut)zoomOut.removeClass("disabled");

}catch (err) {

container.removeClass("with-diagram").addClass("with-error");

messageError.text(err.message);

if (save)save.addClass("disabled");

if (reset)reset.addClass("disabled");

if (zoomIn)zoomIn.addClass("disabled");

if (zoomOut)zoomOut.addClass("disabled");

}

}

},

});

}else {

console.log("流程设计器需要加载 jquery");

}

react 流程设计器完整代码

import React, {useEffect,useRef }from 'react';

import {message,Space, Spin }from 'antd';

import {useRequest }from 'umi';

import {makeStyles, Theme,Toolbar,Typography }from '@material-ui/core';

import { Button,useLang }from '@/components';

import {useHotkeys }from 'react-hotkeys-hook';

import {FullScreen,useFullScreenHandle }from 'react-full-screen';

// @ts-ignore

import Modeler from 'bpmn-js/lib/Modeler';

// @ts-ignore

import PropertiesPanelModulefrom 'bpmn-js-properties-panel';

// @ts-ignore

import PropertiesProviderModulefrom 'bpmn-js-properties-panel/lib/provider/camunda';

// @ts-ignore

import CamundaModdlefrom 'camunda-bpmn-moddle/resources/camunda.json';

import customTranslate from './lang.zh';

import 'bpmn-js-properties-panel/styles/properties.less';

import 'bpmn-js/dist/assets/diagram-js.css';

import 'bpmn-js/dist/assets/bpmn-font/css/bpmn-embedded.css';

const blankBpmn =

'<?xml version="1.0" encoding="UTF-8"?>\n' +

'<bpmn2:definitions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:bpmn2="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xsi:schemaLocation="http://www.omg.org/spec/BPMN/20100524/MODEL BPMN20.xsd" id="sample-diagram" targetNamespace="http://bpmn.io/schema/bpmn">\n' +

'  <bpmn2:process id="Process_1" isExecutable="false">\n' +

'    <bpmn2:startEvent id="StartEvent_1"/>\n' +

'  </bpmn2:process>\n' +

'  <bpmndi:BPMNDiagram id="BPMNDiagram_1">\n' +

'    <bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="Process_1">\n' +

'      <bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">\n' +

'        <dc:Bounds height="36.0" width="36.0" x="412.0" y="240.0"/>\n' +

'      </bpmndi:BPMNShape>\n' +

'    </bpmndi:BPMNPlane>\n' +

'  </bpmndi:BPMNDiagram>\n' +

'</bpmn2:definitions>';

const useStyles =makeStyles((theme: Theme) => ({

root: {

flexGrow:1,

display:'flex',

flexDirection:'column',

flexWrap:'nowrap',

position:'relative',

background:'#fff',

'& .fullscreen': {

flexGrow:1,

display:'flex',

flexDirection:'column',

flexWrap:'nowrap',

},

},

main: {

flexGrow:1,

'& .bjs-powered-by': {

display:'none',

},

},

toolbar: {

paddingLeft: theme.spacing(2),

paddingRight: theme.spacing(2),

background: theme.palette.background.default,

},

title: {

flexGrow:1,

},

panel: {

position:'absolute',

top:0,

bottom:0,

right:0,

width:'260px',

zIndex:10,

borderLeft:'1px solid #ccc',

overflow:'auto',

'&:empty': {

display:'none',

},

'& > .djs-properties-panel': {

paddingBottom:'70px',

minHeight:'100%',

},

},

}));

interface BpmnModelerProps {

title?: React.ReactNode;

service:any;

params:any;

onDeploy?: (xml:any) =>void;

}

const BpmnModeler: React.FC = ({ title, service, params, onDeploy }) => {

const lang =useLang();

const classes =useStyles();

const modeler =useRef();

const handle =useFullScreenHandle();

const {loading,run } =useRequest(service, {

manual:true,

onSuccess: ({data: res }) => {

if (res.data) {

modeler.current.importXML(res.data, (error:any) => {

if (error) {

message.error(error);

}

});

}else {

modeler.current.importXML(blankBpmn);

}

},

});

// 打开流程设计器

  const handleDiagram =async () => {

if (params.resourceId || params.deploymentId) {

run(params);

}else {

modeler.current.importXML(blankBpmn);

}

};

// 保存流程

  const handleSave = () => {

if (modeler.current) {

modeler.current.saveXML({format:true }, (err:any, xml:any) => {

if (err) {

message.error(err);

return;

}

onDeploy &&onDeploy(xml);

});

}

};

// 重置

  const handleReset = () => {

if (modeler.current) {

const canvas =modeler.current.get('canvas');

canvas?.resized();

canvas?.zoom('fit-viewport','auto');

}

};

// 放大

  const handleZoomIn = () => {

if (modeler.current) {

// eslint-disable-next-line no-underscore-dangle

      const container =modeler.current._container;

modeler.current.get('zoomScroll').zoom(1, {

x:container.offsetWidth,

y:container.offsetHeight,

});

}

};

// 缩小

  const handleZoomOut = () => {

if (modeler.current) {

// eslint-disable-next-line no-underscore-dangle

      const container =modeler.current._container;

modeler.current.get('zoomScroll').zoom(-1, {

x:container.offsetWidth,

y:container.offsetHeight,

});

}

};

const handleUndo = () => {

if (modeler.current) {

modeler.current.get('commandStack').undo();

}

};

const handleRedo = () => {

if (modeler.current) {

modeler.current.get('commandStack').redo();

}

};

useHotkeys('ctrl+z', handleUndo);

useHotkeys('ctrl+shift+z', handleRedo);

useEffect(() => {

modeler.current =new Modeler({

container:'#modeler-main',

propertiesPanel: {

parent:'#modeler-panel',

},

additionalModules: [

PropertiesPanelModule,

PropertiesProviderModule,

{

translate: ['value',customTranslate],

},

],

moddleExtensions: {

camunda: CamundaModdle,

},

});

handleDiagram();

}, []);

return (

<div className={classes.root}>

<FullScreen handle={handle}>

<Spin spinning={loading}>

<Toolbar variant="dense" className={classes.toolbar}>

<Typography variant="h6" color="inherit" className={classes.title}>

{title}

</Typography>

<Space>

<Button

                icon="expand"

                size="small"

                onClick={handle.enter}                tooltip={lang('fullScreen')}              />

<Button icon="undo" size="small" onClick={handleUndo} tooltip={lang('undo')} />

<Button icon="redo" size="small" onClick={handleRedo} tooltip={lang('redo')} />

<Button icon="reset" size="small" onClick={handleReset} tooltip={lang('reset')} />

<Button

                icon="minus-square"

                size="small"

                onClick={handleZoomOut}                tooltip={lang('zoom.out')}              />

<Button

                icon="plus-square"

                size="small"

                onClick={handleZoomIn}                tooltip={lang('zoom.in')}              />

<Button icon="file-upload" size="small" tooltip={lang('upload')} />

<Button icon="file-download" size="small" tooltip={lang('download')} />

<Button icon="save" size="small" tooltip={lang('deploy')} onClick={handleSave} />

</Space>

</Toolbar>

<div className={classes.root}>

<div id="modeler-main" className={classes.main} />

<div id="modeler-panel" className={classes.panel} />

</div>

</Spin>

</FullScreen>

</div>

);

};

export default BpmnModeler;

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

推荐阅读更多精彩内容