跳到主要内容
版本:Next

流程节点定制指南

本文档介绍如何在自己的插件中定制新的流程节点,扩展 LMES 流程引擎功能。

详细原理:查看 LMES 流程引擎原理文档


一、创建自定义流程节点

1.1 在自己的插件中创建节点类

在您的插件的 Abstractions 项目中创建自定义节点类,继承自 BusinessActivity 基类。

以下是完整的示例代码(参考 doc/server/src/CMS.Plugin.MyPluginName.Abstractions):

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.Enums;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.MesSuite.Abstractions.Models;
using CMS.Plugin.OrderManagement.Abstractions.Models;
using CMS.Plugin.ProcessManagement.Abstractions.Models;
using CMS.Plugin.TraceManagement.Abstractions.Models.Traces;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
/// <summary>
/// MyPluginName 业务步骤
/// </summary>
[Design("MyPluginName", "MyPluginName 业务步骤", Sort = 99), Category("定制步骤")]
[Serializable]
public class MyPluginNameActivity : BusinessActivity
{
/// <summary>
/// 配置属性1
/// </summary>
[Design("配置属性1", "配置属性1", Sort = 1), Category("配置信息")]
[DataMember]
public string MyProperty1 { get; set; }

/// <summary>
/// 配置属性2
/// </summary>
[Design("配置属性2", "配置属性2", Sort = 2), Category("配置信息")]
[DataMember]
public int MyProperty2 { get; set; }

/// <summary>
/// 流程上下文标识集合
/// </summary>
public override List<FlowItemKey> FlowItemKeys => GetFlowItemKeys();

/// <summary>
/// 工艺流程处理
/// </summary>
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// 工艺模型
var processModel = Flow.DataItems.ApplicationData as ProcessModel;

// 工单模型
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

// 产品模型
var productModel = Flow.DataItems[FlowItemCollection.ProductModel] as AssociationProductModel;

// 追溯模型
var traceModel = Flow.DataItems[FlowItemCollection.TraceModel] as TraceModel;

Flow.Logger.LogInformation($"执行流程:实例={Flow.Instance.ProcID} -> {Flow.Name} -> {Name}");

// 业务处理
var myPluginNameFlowService = Flow.ServiceProvider.GetService<IMyPluginNameFlowService>();
if (myPluginNameFlowService != null)
{
await myPluginNameFlowService.ProcessAsync(args);
}
}

/// <summary>
/// Gets the flow item keys.
/// </summary>
protected virtual List<FlowItemKey> GetFlowItemKeys()
{
var result = new List<FlowItemKey>();
return result;
}
}
}

关键说明:

元素说明
[Design] 特性定义节点在流程设计器中的显示名称、描述和排序
[Category] 特性定义节点在设计器中的分类
[DataMember] 特性标记可在流程设计器中配置的属性
[Serializable] 特性节点类必须可序列化
ProcessAsync 方法节点的核心业务逻辑,在节点执行时调用
FlowItemKeys 属性定义节点使用的流程上下文键

1.2 项目引用配置

在您的插件项目文件(.csproj)中添加必要的引用:

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<EnableDynamicLoading>true</EnableDynamicLoading>
<ProduceReferenceAssembly>false</ProduceReferenceAssembly>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
</PropertyGroup>

<ItemGroup>
<!-- 推荐:包含所有业务插件的 Abstractions 引用,版本号根据项目使用的 LMES 版本确定 -->
<PackageReference Include="CMS.Plugin.MesSuite.Activitys" Version="x.x.x.*" />
</ItemGroup>

</Project>

注意:尽量减少外部依赖,如果必须引用其他程序集,需要在主插件模块中配置共享程序集(见 1.5 节)。

1.3 配置 AssemblyInfo.cs

Abstractions 项目的 Properties/AssemblyInfo.cs 中添加流程扩展标记:

using SYC.Flow.Kernel;

[assembly: ProcessflowExtention(true)]

这个特性告诉流程引擎扫描此程序集中的自定义流程节点。

1.4 定义服务接口(可选)

如果业务逻辑较复杂,建议将业务逻辑抽取到服务中:

using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
/// <summary>
/// MyPluginName 流程服务
/// </summary>
public interface IMyPluginNameFlowService
{
/// <summary>
/// 工艺流程处理
/// </summary>
Task ProcessAsync(ProcessflowEventArgs args);
}
}

1.5 配置共享程序集

在主插件模块(CMS.Plugin.MyPluginName/CMSPluginModule.cs)中配置共享程序集:

using System.Reflection;
using CMS.Extensions.Abp;
using CMS.Plugin.MyPluginName.Abstractions;
using Volo.Abp.Modularity;

namespace CMS.Plugin.MyPluginName
{
[DependsOn(typeof(CMSPluginAbpModule))]
public class CMSPluginModule : AbpStartupModule
{
/// <inheritdoc />
public override Assembly[]? GetSharedAssemblies()
{
// 将 Abstractions 程序集设置为共享程序集
// 这样流程引擎才能正确加载自定义节点
return base.GetSharedAssemblies().Concat(new[]
{
typeof(CMSPluginMyPluginNameAbstractionsModule).Assembly,
}).ToArray();
}
}
}

重要:由于 ServiceProvider 来源于 Flow 组件,如果在流程节点中调用自定义服务,必须将 Abstractions 程序集设置为共享程序集,否则无法正确解析服务。

1.6 在流程设计器中使用

完成节点注册后,您的自定义节点就可以在 LMES 流程设计器中使用了。

打开流程设计器

  1. 登录 LMES 系统
  2. 进入 流程配置 组件
  3. 选择一个流程或新建流程,点击 流程设计 按钮

详细的流程管理操作请参考:流程管理

配置自定义节点

在流程设计器中,您的自定义节点会出现在节点面板中:

  1. 从左侧节点面板拖拽 CustomDevice 节点到画布
  2. 选中节点,在右侧属性面板配置节点属性:
    • 节点名称:发送设备指令
    • DeviceAddress:192.168.1.100
    • Command:START_PROCESS
  3. 连接节点,设置迁移条件
  4. 保存流程定义

流程设计器的详细使用方法请参考:流程设计


二、常见节点开发场景

2.1 数据采集节点

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.Enums;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Variables;
using Microsoft.Extensions.DependencyInjection;
using SYC.Flow.Kernel;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("数据采集", "从变量中采集数据", Sort = 101), Category("定制步骤")]
[Serializable]
public class DataCollectionActivity : BusinessActivity
{
[Design("变量名称", "要采集的变量名称"), Category("采集配置")]
[DataMember]
public string VariableName { get; set; }

public override List<FlowItemKey> FlowItemKeys => new List<FlowItemKey>
{
new FlowItemKey("CollectedValue", "采集值", "采集到的变量值", typeof(string))
};

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
if (string.IsNullOrWhiteSpace(VariableName))
{
Flow.Logger.LogWarningMessage("变量名称为空", Name);
return;
}

// 使用变量服务读取变量值
var variableService = Flow.ServiceProvider.GetRequiredService<IFlowVariableService>();
var traceId = Flow.Instance.GetTraceId();
var values = await variableService.ReadValueAsync(this, new[] { VariableName }, traceId: traceId);

// 保存到流程上下文
if (values.TryGetValue(VariableName, out var value))
{
Flow.DataItems["CollectedValue"] = value;
Flow.Logger.LogMessage($"采集变量:{VariableName}={value},TraceId={traceId}", Name);
}
}
}
}

2.2 数据验证节点

using System.ComponentModel;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using SYC.Flow.Kernel;
using Volo.Abp;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("数据验证", "验证流程数据", Sort = 102), Category("定制步骤")]
[Serializable]
public class DataValidationActivity : BusinessActivity
{
[Design("数据键", "要验证的流程上下文数据键"), Category("验证配置")]
[DataMember]
public string DataKey { get; set; }

[Design("验证规则", "正则表达式验证规则"), Category("验证配置")]
[DataMember]
public string ValidationRule { get; set; }

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
var data = Flow.DataItems[DataKey]?.ToString();

if (string.IsNullOrWhiteSpace(data))
{
throw new UserFriendlyException($"数据验证失败:{DataKey} 为空");
}

if (!string.IsNullOrWhiteSpace(ValidationRule) && !Regex.IsMatch(data, ValidationRule))
{
throw new UserFriendlyException($"数据验证失败:{data} 不符合规则 {ValidationRule}");
}

Flow.DataItems["ValidationResult_Value"] = "Pass";
Flow.Logger.LogMessage($"数据验证通过:{DataKey}={data}", Name);

await Task.CompletedTask;
}
}
}

2.3 第三方系统集成节点

using System.ComponentModel;
using System.Runtime.Serialization;
using CMS.Plugin.FlowManagement.Abstractions.FlowBusiness.Activitys;
using CMS.Plugin.OrderManagement.Abstractions.Models;
using CMS.Plugin.MesSuite.Abstractions.Models;
using Microsoft.Extensions.DependencyInjection;
using SYC.Flow.Kernel;
using Volo.Abp;

namespace CMS.Plugin.MyPluginName.Abstractions
{
[Design("ERP集成", "调用ERP系统接口", Sort = 103), Category("定制步骤")]
[Serializable]
public class ErpIntegrationActivity : BusinessActivity
{
[Design("接口地址", "ERP接口URL"), Category("接口配置")]
[DataMember]
public string ApiEndpoint { get; set; }

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// 从流程上下文获取工单信息
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

if (orderModel == null)
{
Flow.Logger.LogWarningMessage("工单信息为空,跳过ERP同步", Name);
return;
}

// 获取自定义服务
var erpService = Flow.ServiceProvider.GetService<IErpIntegrationService>();
if (erpService == null)
{
throw new UserFriendlyException("ERP集成服务未配置");
}

try
{
var result = await erpService.SyncOrderAsync(orderModel.Code, ApiEndpoint);
Flow.DataItems["ErpSyncResult_Value"] = result;
Flow.Logger.LogMessage($"ERP同步成功:{orderModel.Code}", Name);
}
catch (Exception ex)
{
Flow.Logger.LogExceptionMessage(ex, Name);
throw new UserFriendlyException($"ERP接口调用异常:{ex.Message}");
}
}
}
}

三、流程上下文数据操作

流程上下文(Flow.DataItems)是节点间传递数据的核心机制。

3.1 基本数据操作

// 设置数据
Flow.DataItems["MyData_Value"] = "P001";
Flow.DataItems["Quantity_Value"] = 100;

// 获取数据
var myData = Flow.DataItems["MyData_Value"]?.ToString();
var quantity = Convert.ToInt32(Flow.DataItems["Quantity_Value"]);

// 检查数据是否存在
if (Flow.DataItems.ContainsKey("MyData_Value"))
{
// 数据存在
}

3.2 获取常用模型

LMES 在流程上下文中预置了常用的业务模型:

// 工艺模型(包含工序、工位配置)
var processModel = Flow.DataItems.ApplicationData as ProcessModel;
var workSectionName = processModel?.WorkSection?.Name;
var workStationName = processModel?.WorkStation?.Name;

// 工单模型
var orderModel = Flow.DataItems[FlowItemCollection.OrderModel] as OrderModel;

// 产品模型
var productModel = Flow.DataItems[FlowItemCollection.ProductModel] as AssociationProductModel;

// 追溯模型
var traceModel = Flow.DataItems[FlowItemCollection.TraceModel] as TraceModel;

3.3 FlowItemCollection 常用键

FlowItemCollection 类定义了常用的流程上下文键:

键名说明
FlowItemCollection.SerialNumber产品序列号/条码
FlowItemCollection.OrderModel工单模型
FlowItemCollection.ProductModel产品模型
FlowItemCollection.TraceModel追溯模型
FlowItemCollection.FormulaApplyModel配方应用模型
FlowItemCollection.QualityResultValue合格判断结果

3.4 使用服务和日志

// 获取依赖注入的服务
var variableService = Flow.ServiceProvider.GetRequiredService<IFlowVariableService>();
var orderProvider = Flow.ServiceProvider.GetRequiredService<IOrderProvider>();

// 获取 TraceId(用于日志追踪)
var traceId = Flow.Instance.GetTraceId();

// 记录日志
Flow.Logger.LogMessage("普通日志", Name);
Flow.Logger.LogWarningMessage("警告日志", Name);
Flow.Logger.LogErrorMessage("错误日志", Name);
Flow.Logger.LogExceptionMessage(exception, Name);

四、最佳实践

4.1 节点设计原则

  • 单一职责:每个节点只做一件事
  • 可配置性:通过 [Design][DataMember] 特性暴露可配置属性
  • 异常处理:使用 UserFriendlyException 抛出业务异常
  • 日志记录:使用 Flow.Logger 记录关键操作

4.2 使用依赖注入

通过 Flow.ServiceProvider 获取服务:

[Design("自定义节点", "使用依赖注入的节点", Sort = 104), Category("自定义步骤")]
[Serializable]
public class CustomActivity : BusinessActivity
{
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
// 获取服务
var orderProvider = Flow.ServiceProvider.GetRequiredService<IOrderProvider>();
var productProvider = Flow.ServiceProvider.GetRequiredService<IProductProvider>();

// 使用服务
var order = await orderProvider.GetCurrentAsync();
Flow.Logger.LogMessage($"当前工单:{order?.Code}", Name);
}
}

4.3 异常处理

public override async Task ProcessAsync(ProcessflowEventArgs args)
{
try
{
// 业务逻辑
await DoBusinessLogicAsync();
}
catch (UserFriendlyException)
{
// 业务异常,直接抛出,流程会中断并显示错误信息
throw;
}
catch (Exception ex)
{
// 记录异常日志
Flow.Logger.LogExceptionMessage(ex, Name);

// 转换为业务异常
throw new UserFriendlyException($"节点执行失败:{ex.Message}");
}
}

五、调试与测试

5.1 本地调试

启动流程进行调试

  1. 在节点的 ProcessAsync 方法中设置断点
  2. 确保流程已设置为 启用 状态(参考:流程管理
  3. 启动流程服务或使用流程控制台手动启动流程(参考:流程执行
  4. 触发流程执行,观察断点是否命中

查看流程日志

流程执行日志存储在 logs/flow_logs/{工序名_工位名}/ 目录下:

  • flowlog_xxxx-xx-xx.log:流程实例运行日志
  • tracelog_xxxx-xx-xx.log:变量跟踪日志

详细的日志分析方法请参考:流程日志

通过文件初始化流程

定制流程文件应放置在宿主程序(Host)的 Flows 目录下,系统启动时会自动扫描并同步到数据库。

注意:定制流程请勿放在 LMES 插件目录(src/cms.plugin.messuite/Flows)中,该目录仅存放系统内置流程。

文件存放位置

{Host}/
└── Flows/
├── FlowInfo/ # 流程信息配置目录
│ └── 20001.xml # 定制流程的详细配置
└── FlowPfd/ # 流程定义文件目录
└── 20001_1.pfd # 定制流程定义文件

FlowInfo/{flowType}.xml 示例

<?xml version="1.0" encoding="utf-8"?>
<Root>
<FlowInfo>
<Type>20001</Type>
<Name>我的定制流程</Name>
<Version>1</Version>
<FilePath>FlowPfd/20001_1.pfd</FilePath>
<BusinessType>
<Name>Standard</Name>
<Value>1</Value>
<Description>进出站交互</Description>
</BusinessType>
<IsStatic>True</IsStatic>
</FlowInfo>
</Root>

文件命名规则

  • 流程信息文件:FlowInfo/\{flowType\}.xml,如 FlowInfo/20001.xml
  • 流程定义文件:FlowPfd/\{flowType\}_\{flowVersion\}.pfd,如 FlowPfd/20001_1.pfd

建议:定制流程的 flowType 建议使用 20000-79999 的编号,避免与系统流程冲突。

文件同步机制

  • 系统每 3 秒检查一次文件变化
  • .xml 文件变化会触发所有流程定义的全量同步
  • .pfd 文件变化只更新对应版本的流程定义内容
  • 同步完成后会自动清除流程定义缓存

5.2 单元测试

由于 BusinessActivity 依赖流程引擎运行时环境,建议通过集成测试或在实际流程中调试。

如需单元测试业务逻辑,可以将核心逻辑提取到独立的服务类中:

// 将业务逻辑提取到服务类
public class DeviceCommandService : IDeviceCommandService
{
public async Task<string> SendCommandAsync(string address, string command)
{
// 业务逻辑
return "Success";
}
}

// 节点中调用服务
public override async Task ProcessAsync(ProcessflowEventArgs args)
{
var service = Flow.ServiceProvider.GetRequiredService<IDeviceCommandService>();
var result = await service.SendCommandAsync(DeviceAddress, Command);
Flow.DataItems["Result_Value"] = result;
}

// 单元测试服务类
public class DeviceCommandServiceTests
{
[Fact]
public async Task SendCommand_Should_Return_Success()
{
var service = new DeviceCommandService();
var result = await service.SendCommandAsync("192.168.1.100", "START");
Assert.Equal("Success", result);
}
}

六、常见问题

Q1:如何在流程中实现条件分支?

在流程设计器中配置迁移条件,使用 属性条件 来判断流程上下文数据:

  • 在流程设计器中,选中连接线(迁移)
  • 在属性面板中配置条件:
    • 参数名:Quality(流程上下文中的数据键)
    • 比较符:Equal(等于)
    • 常量值:OK

详细的条件配置请参考:属性条件

Q2:如何实现并行流程?

在流程设计器中设置节点的迁出模式:

  1. 选中需要并行分支的节点
  2. 在属性面板中设置 迁出模式SplitAND
  3. 连接多个后续节点,这些节点将并行执行

详细的迁入/迁出模式请参考:迁入/迁出

Q3:自定义节点需要引用哪些 NuGet 包?

<!-- 推荐:包含所有业务插件的 Abstractions 引用,版本号根据项目使用的 LMES 版本确定 -->
<PackageReference Include="CMS.Plugin.MesSuite.Activitys" Version="x.x.x.*" />

七、学习资源

示例代码

完整的流程节点定制示例代码位于:doc/server/src/CMS.Plugin.MyPluginName.Abstractions/

流程引擎相关

开发相关


总结

在自己的插件中定制流程节点的步骤:

  1. 创建节点类:在 Abstractions 项目中继承 BusinessActivity 基类,实现 ProcessAsync 方法
  2. 添加特性:使用 [Design][Category][DataMember][Serializable] 特性
  3. 配置引用:在 .csproj 中添加 CMS.Plugin.FlowManagement.Abstractions 等包引用
  4. 使用节点:在流程设计器中拖拽配置节点属性
  5. 测试调试:通过流程日志和断点调试