流程节点定制指南
本文档介绍如何在自己的插件中定制新的流程节点,扩展 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 流程设计器中使用了。
打开流程设计器
- 登录 LMES 系统
- 进入 流程配置 组件
- 选择一个流程或新建流程,点击 流程设计 按钮
详细的流程管理操作请参考:流程管理
配置自定义节点
在流程设计器中,您的自定义节点会出现在节点面板中:
- 从左侧节点面板拖拽 CustomDevice 节点到画布
- 选中节点,在右侧属性面板配置节点属性:
- 节点名称:发送设备指令
- DeviceAddress:192.168.1.100
- Command:START_PROCESS
- 连接节点,设置迁移条件
- 保存流程定义
流程设计器的详细使用方法请参考:流程设计
二、常见节点开发场景
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 本地调试
启动流程进行调试
查看流程日志
流程执行日志存储在 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:如何实现并行流程?
在流程设计器中设置节点的迁出模式:
- 选中需要并行分支的节点
- 在属性面板中设置 迁出模式 为
SplitAND - 连接多个后续节点,这些节点将并行执行
详细的迁入/迁出模式请参考:迁入/迁出
Q3:自定义节点需要引用哪些 NuGet 包?
<!-- 推荐:包含所有业务插件的 Abstractions 引用,版本号根据项目使用的 LMES 版本确定 -->
<PackageReference Include="CMS.Plugin.MesSuite.Activitys" Version="x.x.x.*" />
七、学习资源
示例代码
完整的流程节点定制示例代码位于:doc/server/src/CMS.Plugin.MyPluginName.Abstractions/
流程引擎相关
- LMES 流程引擎原理 - 流程引擎详细原理
开发相关
- 后端组件开发 - 后端开发基础
总结
在自己的插件中定制流程节点的步骤:
- 创建节点类:在
Abstractions项目中继承BusinessActivity基类,实现ProcessAsync方法 - 添加特性:使用
[Design]、[Category]、[DataMember]、[Serializable]特性 - 配置引用:在 .csproj 中添加
CMS.Plugin.FlowManagement.Abstractions等包引用 - 使用节点:在流程设计器中拖拽配置节点属性
- 测试调试:通过流程日志和断点调试