首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >流程引擎的两个可编程接口:参数与事件

流程引擎的两个可编程接口:参数与事件

原创
作者头像
驰骋工作流程
发布于 2026-10-18 10:13:52
发布于 2026-10-18 10:13:52
180
举报

参数决定流程往哪走,事件决定流程在走的过程中还能做什么。本文从参数容器、五类来源、事件标记、执行类型四处做机制级拆解,并说明发送前事件为何能反向影响流向。

引子:引擎在「哪些地方」是可编程的

一个 BPM 引擎,如果只会「按图流转」,那它只是一个执行器。真正让引擎嵌入业务系统的,是它对外暴露的可编程接口。

该平台的引擎在两个地方对外开口:

开口

作用

一句话概括

参数

控制流程往哪走、谁来办

输入:外部世界把信息告诉引擎

事件

在流程的关键动作前后插入代码

输出:引擎把控制权交回业务方

参数与事件是一对:参数决定流程「怎么走」,事件决定流程在走的过程中「能做别的什么」。 本文按这个线索,把两块拆开讲清楚。


上篇:流程引擎的参数

一、参数的两大作用

先明确参数是干什么用的。剥开细节,参数只服务两件事:

  1. 控制流程的运转方向——条件分支走哪条、方向条件成不成立;
  2. 控制节点的接收人——这一步该谁办。

这两件事,恰好是流程引擎在运行时唯一需要外部输入的两个决策点。其他的(表单字段、审批意见)都是过程数据,不直接影响流转。

二、参数的定义:键值对

参数的形式很简单:参数键 = 参数值。

代码语言:javascript
复制
QJTS=2
  • QJTS 是参数键
  • 2 是参数值

在该平台里,这个结构由 AtPara 承载。看它的解析逻辑(DA/AtPara.cs):

代码语言:javascript
复制
public AtPara(string para)
{
    if (para == null || DataType.IsNullOrEmpty(para) == true || para.Length <= 3)
        return;
    if (para.Contains("@") == false)
    {
        try
        {
            this._HisHT = BP.Tools.Json.ToHashtable(para);
            return;
        }
        catch { para = "@" + para; }
    }
    string[] strs = para.Split('@');
    foreach (string str in strs)
    {
        // ...
        string[] mystr = str.Split('=');
        if (mystr.Length == 2)
            this.SetVal(mystr[0], mystr[1]);
        // ...
    }
}

有两个细节值得注意:

  • 分隔符默认是 @:@QJTS=2@SQL=...。一行字符串里可以塞多个参数。
  • 不带 @ 时自动尝试 JSON:AtPara 能直接吃 JSON 并转成 Hashtable。这是它兼容老接口(@ 串)和新接口(JSON)的地方。
  • HisHT 是 Hashtable,GetValStrByKey / GetValInt64ByKey 等按类型取值——一个参数容器,多种取值方式。

三、五大类型

该平台把流程参数按来源分成五类。这个分类的价值在于:不同类型的参数,注入时机不同、生命周期不同、作用范围也不同。

#

类型

来源

典型内容

1

系统参数

发起或处理流程时,通过 API 压入

流程编号、WorkID、节点 ID

2

表单参数

用户在表单里输入的数据

主表字段名 → 字段值

3

登录人员参数

当前登录者的信息

账号、名称、部门、组织

4

外部传递参数

URL / HTTP 传进来

QueryString 上的键值

5

系统全局配置参数

全局配置文件

AppSettings 里的项

3.1 系统参数:API 压入

发起或处理流程时,调用方通过 API 把参数压进引擎。对应 FlowEventBase.DoIt 里的处理:

代码语言:javascript
复制
if (atPara != null)
{
    AtPara ap = new AtPara(atPara);
    foreach (string s in ap.HisHT.Keys)
    {
        try { r.Add(s, ap.GetValStrByKey(s)); }
        catch { r[s] = ap.GetValStrByKey(s); }
    }
}

注意 try/catch:已存在就覆盖、不存在就新增。这个写法在处理「同名参数来自不同来源」时很有用——后注入的覆盖先注入的。

3.2 表单参数:主表数据自动成为参数

表单参数不需要单独传,主表数据本身就是参数。事件分发时,实体行被直接摊进参数表:

代码语言:javascript
复制
Row r = en.Row;                          // 实体数据行
if (r.ContainsKey("FK_MapData") == true)
    r["FK_MapData"] = en.ClassID;
else
    r.Add("FK_MapData", en.ClassID);

也就是说,用户在表单里填的每个字段,都自动成为一个键值对,可以在条件、接收人规则、事件里按字段名直接引用。

3.3 登录人员参数

当前登录者的账号、名称、部门编号、组织编号、组织名称等。这类参数不出现在调用方传参里,而是引擎运行时按当前用户补齐。

FlowEventBase 暴露了 WorkID、OID、FID、Title、FK_Mapdata、Nos 等一组属性,事件代码里可以直接取。人员相关的表达式(如 @WebUser.No、@WebUser.DeptNo)能在条件与接收人规则里直接用。

3.4 外部传递参数:URL / HTTP 进来

BS 系统下,引擎会把当前请求的 QueryString 全量并入参数表(FlowEventBase.DoIt):

代码语言:javascript
复制
if (BP.Difference.SystemConfig.isBSsystem == true)
{
    /*如果是bs系统, 就加入外部url的变量.*/
    foreach (string key in HttpContextHelper.RequestParamKeys)
    {
        if (key == null) continue;
        switch (key)
        {
            case "WorkID":
            case "NodeID":
            case "FrmID":
            case "IsReadonly":
            case "__VIEWSTATE":
            case "__EVENTVALIDATION":
            case "OID":
                continue;     // 这些是引擎自己的参数,跳过
            default: break;
        }
        string val = HttpContextHelper.RequestParams(key);
        if (r.ContainsKey(key) == true)
            r[key] = val;
        else
            r.Add(key, val);
    }
}

这里有个细节:白名单式的排除。WorkID、NodeID、OID 这类引擎内部参数被显式跳过,避免外部请求覆盖引擎状态——是安全考量,也是稳定性考量。

所以从 URL 上带一个 ?QJTS=2 进来,这个 QJTS 就自动成为流程参数,能被条件分支和接收人规则读到。

3.5 系统全局配置参数

放在全局配置文件里,比如连接串、路径、Token、语言、默认密码等。看 SystemConfig:

代码语言:javascript
复制
public static NameValueCollection AppSettings
{
    get
    {
        // ...
        _app_setting = System.Configuration.ConfigurationManager.AppSettings;
        // ...
    }
}

读取方式统一:

代码语言:javascript
复制
string s = BP.Difference.SystemConfig.AppSettings["TokenModel"];
string str = BP.Difference.SystemConfig.AppSettings["UserDefaultPass"];

这类参数不随流程实例变化,是整个系统级的存在——典型如 HostURL、MobileURL、DataUserDirPath、该引擎AppPath。

3.6 五类参数汇总

参数来源

进入方式

汇入位置

系统参数

发起 / 处理时 API 压入

参数表

表单参数

主表字段自动摊入

参数表

登录人员参数

运行时按当前用户补齐

参数表

外部传递参数

URL / HTTP 全量并入

参数表

系统全局配置

AppSettings 读取

参数表

五类参数最终在同一个参数表里汇合,然后被两个消费方使用:方向判断和接收人计算。

四、参数怎么用:交给事件

参数本身只是数据。它真正发挥作用是通过两个消费方:

  1. 引擎内置的判断——方向条件、接收人规则直接读参数;
  2. 节点事件里的自定义代码——这是参数能做的事情最多的地方。

所以接下来必须讲事件。


下篇:流程引擎设计之节点事件

五、什么是事件

事件是对流程的操作点击所发生的动作。

用户在界面上的每一个动作,都会在引擎里触发一个事件标记:

动作

事件

发送前

SendWhen

发送成功后

SendSuccess

发送失败

SendError

工作到达

WorkArrive

退回前 / 退回后

ReturnBefore / ReturnAfter

撤销前 / 撤销后

UndoneBefore / UndoneAfter

移交后

ShitAfter

加签后 / 加签答复后

AskerAfter / AskerReAfter

流程删除前 / 删除后

BeforeFlowDel / AfterFlowDel

流程结束前 / 结束后

FlowOverBefore / FlowOverAfter

完整的清单在 ORM 组件/Sys/EvenListNode.cs 里,逐条带注释。

5.1 事件的本质:在代码里插入代码片段

事件机制的实现方式很朴素:流程引擎在执行过程中,在一些关键位置留出「钩子」,到点就调用配置好的代码片段。

看 FlowEventBase.DoIt 的 switch 分发:

代码语言:javascript
复制
switch (eventType)
{
    case EventListNode.SendWhen:              // 节点事件 - 发送前
        this.ItIsStopFlow = false;
        string str = this.SendWhen();
        if (this.ItIsStopFlow == true)
            return "@Info=" + str + "@IsStopFlow=1";
        if (this.JumpToNodeID == 0 && this.JumpToEmps == null)
            return str;
        return "@Info=" + str + "@ToNodeID=" + this.JumpToNodeID
             + "@ToEmps=" + this.JumpToEmps;

    case EventListNode.SendSuccess:
        return this.SendSuccess();
    case EventListNode.ReturnBefore:
        return this.ReturnBefore();
    // ...
}

关键点在返回值。发送前事件的返回是可以反向影响流程的:

  • @IsStopFlow=1 → 停止发送;
  • @ToNodeID= / @ToEmps= → 改变流向、改变接收人。

这意味着发送前事件不只是「通知」,而是「参与决策」——它可以直接把流程导向别的节点、或者干脆拦住不让发。

5.2 事件的表达方式:三种

这是本次要讲的重点。同一个事件,可以用三种不同方式实现——对应三类不同的人、三种不同的复杂度。

方式一:配置模式

在界面上配置,填一段 SQL、一个 URL、一个类名。

存储表是 Sys_FrmEvent,定义见 ORM 组件/Sys/FrmEvent.cs:

代码语言:javascript
复制
Map map = new Map("Sys_FrmEvent", "外部自定义事件(表单,从表,流程,节点)");
//0=表单事件,1=流程,2=节点事件.
map.AddTBInt(FrmEventAttr.EventSource, 0, "事件类型", true, true);
map.AddTBString(FrmEventAttr.EventID, null, "事件标记", ...);
map.AddTBString(FrmEventAttr.DoDoc, null, "执行内容", ...);
map.AddTBString(FrmEventAttr.FK_DBSrc, "local", "数据源", ...);
map.AddTBString(FrmEventAttr.EventDoType, null, "事件执行类型", ...);

配置模式的核心优势:改事件不用重新编译。

字段说明:

字段

含义

EventSource

0=表单事件,1=流程事件,2=节点事件

EventID

事件标记(SendWhen、SendSuccess…)

EventDoType

执行类型(见下)

DoDoc

执行内容

FK_DBSrc

数据源,默认 local

方式二:内置事件实体类

继承 FlowEventBase,重写需要的方法。

FlowEventBase 定义了一大批 virtual 方法,每个对应一个事件点:

代码语言:javascript
复制
public virtual string FrmLoadAfter()   { ... }
public virtual string SendWhen()       { ... }
public virtual string SendSuccess()    { ... }
public virtual string SendError()      { ... }
public virtual string ReturnBefore()   { return null; }
public virtual string ReturnAfter()    { return null; }
public virtual string WorkArrive()     { ... }
public virtual string FlowOverBefore() { ... }
public virtual string FlowOverAfter()  { ... }
// ...

同时它把常用上下文都准备好了,子类直接取:

代码语言:javascript
复制
public Node HisNode            { get; }   // 当前节点
public GenerWorkFlow HisGenerWorkFlow { get; }  // 流程实例
public Entity HisEn            { get; }   // 数据实体
public Row SysPara             { get; }   // 参数表 ← 参数与事件在此汇合
public string JumpToEmps       { get; }
public int JumpToNodeID        { get; }
public bool ItIsStopFlow       { get; set; }

注意 SysPara 与 ItIsStopFlow / JumpToNodeID / JumpToEmps——前者读参数,后三者写决策。这就是「参数进、决策出」的完整闭环。

再配合 GetValStr / GetValInt / GetValBoolen / GetValDecimal 系列方法,子类里读参数很直接。

方式三:外挂模式

不改引擎源码,把逻辑挂在外部。OverrideEvent.cs 就是这类模式的落点,里面是一堆静态方法:

代码语言:javascript
复制
public static string DoIt(string eventMark, WorkNode wn, string paras,
                          string checkNote, int returnToNodeID,
                          string returnToEmps, string returnMsg)
public static string SendWhen(WorkNode wn)
public static string SendSuccess(WorkNode wn)
public static string ReturnAfter(WorkNode wn, int toNodeID, string toEmp, string info)
public static string FlowOverAfter(WorkNode wn)

同时它还承担了邮件、钉钉、企业微信、CCApp、短信等推送的分流:

代码语言:javascript
复制
public static void SendToEmail(SMS sms)
public static void SendToCCApp(SMS sms)
public static void SendToDingDing(SMS sms)
public static void SendToWeiXin(SMS sms)
public static void MessageIsEnableSelf(SMS sms)

外挂模式的价值:把「引擎行为的扩展」与「引擎源码」解耦。 升级引擎时,外部代码不动。

5.3 三种方式对照

维度

配置模式

内置事件实体类

外挂模式

落地位置

Sys_FrmEvent 表

继承 FlowEventBase

静态方法(如 OverrideEvent)

改完要不要编译

不用

要

要

适合谁

实施 / 业务配置人员

二次开发人员

集成 / 平台维护人员

适合什么

SQL、URL、简单调用

复杂业务逻辑

全局行为、外部推送分流

主要字段/入口

EventDoType + DoDoc

重写 virtual 方法

OverrideEvent.DoIt 等

六、事件的执行类型:EventDoType

配置模式下,EventDoType 决定了这段配置怎么执行。完整枚举在 ORM 组件/Sys/EnumLib.cs:

代码语言:javascript
复制
public enum EventDoType
{
    Disable     = 0,   // 禁用
    SP          = 1,   // 执行存储过程
    SQL         = 2,   // 运行 SQL
    URLOfSelf   = 3,   // 自定义 URL
    WSOfSelf    = 4,   // 自定义 WS
    SpecClass   = 5,   // 执行 dll 文件的类与方法
    EventBase   = 6,   // 基类
    BuessUnit   = 7,   // 执行的业务单元
    WebApi      = 8,   // 自定义 WebApi
    SFProc      = 9    // SFProduces
}

这份枚举很说明问题:它把「事件能干什么」铺成了一个从简单到复杂的谱系——

  • 数据库侧:SP(存储过程)、SQL;
  • 网络侧:URLOfSelf、WSOfSelf、WebApi;
  • 代码侧:SpecClass(反射调用 dll 里的类与方法)、EventBase(基类)、BuessUnit、SFProc。

同一张配置表,可以表达从「执行一条 SQL」到「调用一个外部 WebApi」的全部跨度。 这是配置模式能覆盖大多数场景的原因。

七、事件是与外部系统交互的媒介

这是本次要讲的最后一点,也是最重要的一点。

事件是流程引擎与外部系统的接口。

流程引擎本身只管流转。它要跟 ERP、MES、OA、财务系统、消息平台打交道,靠的就是事件:

参数来源

进入方式

汇入位置

系统参数

发起 / 处理时 API 压入

参数表

表单参数

主表字段自动摊入

参数表

登录人员参数

运行时按当前用户补齐

参数表

外部传递参数

URL / HTTP 全量并入

参数表

系统全局配置

AppSettings 读取

参数表

  • 流程走到某一步,通知外部系统(SendSuccess);
  • 流程到达某人,推送消息(WorkArrive → OverrideEvent.SendToDingDing);
  • 流程发起前,让外部系统校验,不通过就拦住(SendWhen + IsStopFlow)。

OverrideEvent 里那一组 SendToEmail / SendToCCApp / SendToDingDing / SendToWeiXin,本身就是「事件作为外部交互媒介」最直接的证据——引擎不知道钉钉和企业微信的区别,它只发事件,谁来接是外挂层的事。


八、把参数与事件串起来

最后回答一个自然的问题:参数和事件,在运行时是什么关系?

答案是:参数是事件的输入,事件是参数的消费者与生产者。

参数来源

进入方式

汇入位置

系统参数

发起 / 处理时 API 压入

参数表

表单参数

主表字段自动摊入

参数表

登录人员参数

运行时按当前用户补齐

参数表

外部传递参数

URL / HTTP 全量并入

参数表

系统全局配置

AppSettings 读取

参数表

  • 进:五类参数汇入 SysPara;
  • 用:控制流向(条件)与接收人(DeliveryWay),或在事件里自定义;
  • 出:事件通过返回值(@ToNodeID、@ToEmps、@IsStopFlow)反过来影响流程。

顺带提一句发送前事件的返回格式,它是参数与事件共用一套语法的体现:

代码语言:javascript
复制
@Info=xxx@ToNodeID=102@ToEmps=zhangsan

返回的也是参数格式。引擎用同一套 AtPara 解析它。输入是参数,输出也是参数,中间是事件——这就是这个设计的对称性。

另外还有一个双轨机制值得注意:FrmEvents(Sys_FrmEvent 表)与 FlowEventBase(代码继承)是两条并列的路径。FrmEvents.DoEventNode 的注释里写着一句话:

不再使用节点事件 2019-08-27 zl 原调用点有两处:(1)FrmEvent.cs 中的 DoEventNode();(2)Flow.cs 中的 DoFlowEventEntity() 方法中。

这段注释反映了一次架构演进:配置表驱动的事件与代码继承的事件,在历史上有过并行,后来调用点发生了变化。理解这一点对升级和排障很重要——不能想当然认为「配了 Sys_FrmEvent 就一定会在每个事件点被调用」,具体以版本为准。

参数命名的一点实务提醒

回到最初的例子 QJTS=2。参数键叫什么、值是什么,引擎不关心——它只做键值匹配。但工程上要注意:

  • 参数键避免与系统保留字冲突(WorkID、NodeID、OID、FK_MapData 等),否则会被引擎的参数处理逻辑覆盖或跳过;
  • 同名参数要清楚覆盖顺序:Entity.Row → AtPara → URL 参数,后处理的会覆盖先处理的;
  • URL 传参是「全量并入」,意味着外部请求能影响流程——这在开放环境下需要考虑校验。

九、结语

把两块合起来看,该平台在引擎上留的两个开口,设计思路是一致的:

参数:五类来源(系统 / 表单 / 人员 / 外部 / 全局)汇成一张参数表,服务于「走哪条路、谁来办」两个决策点。

事件:二十多个事件标记覆盖从表单保存、节点发送、退回撤销到流程结束的全过程;三种表达方式(配置 / 实体类 / 外挂)对应三类人的投入成本;十种执行类型覆盖从一条 SQL 到一次 WebApi 调用的跨度。

而把两者连起来的,是同一套 @key=value 参数语法——输入用它,输出也用它。

引擎只负责流转,业务通过参数与事件接进来。 这条边界划得越清楚,引擎就越不容易被业务细节污染,也就越经得起长期的版本迭代。这也解释了为什么这套核心结构能沿用至今:接口是稳的,变化留给参数和事件。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

目录
  • 引子:引擎在「哪些地方」是可编程的
  • 上篇:流程引擎的参数
    • 一、参数的两大作用
    • 二、参数的定义:键值对
    • 三、五大类型
      • 3.1 系统参数:API 压入
      • 3.2 表单参数:主表数据自动成为参数
      • 3.3 登录人员参数
      • 3.4 外部传递参数:URL / HTTP 进来
      • 3.5 系统全局配置参数
      • 3.6 五类参数汇总
    • 四、参数怎么用:交给事件
  • 下篇:流程引擎设计之节点事件
    • 五、什么是事件
      • 5.1 事件的本质:在代码里插入代码片段
      • 5.2 事件的表达方式:三种
      • 5.3 三种方式对照
    • 六、事件的执行类型:EventDoType
    • 七、事件是与外部系统交互的媒介
    • 八、把参数与事件串起来
      • 参数命名的一点实务提醒
    • 九、结语
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档