[.NET项目实战] Elsa开源工作流组件应用(三):实战演练

补充

之前的文章简单介绍了工作流和Elsa工作流库,这里再补充说明两点

  1. 工作流的使用场景非常广泛,几乎涵盖了所有需要进行业务流程自动化管理的领域。

  2. 学习一个开源库,最简单的方法就是看源码,Elsa的工作流引擎源码非常简单易懂,并且提供了非常丰富的示例代码,举一个例子:审批工作流示例.\src\samples\aspnet\Elsa.Samples.AspNet.DocumentApproval

在这里插入图片描述

这个审批流是这样的:
作者发来一个文章,有两个审批人需要全部审批通过,文章才算通过,否则退回。

我们尝试阅读工作流源代码DocumentApprovalWorkflow.cs,并运行此项目,用postman发送请求

第一步:

假设这名叫Amanda的作者要发布文章,请求发送后,作者浏览器显示发送成功稍安勿躁之类的提示

同时后台打印作者信息和4个链接,分别是Jack和Lucy两位审批人“通过”和“退回”的url链接

Activities =
{
    new HttpEndpoint
    {
        Path = new("/documents"),
        SupportedMethods = new(new[] { HttpMethods.Post }),
        ParsedContent = new(documentVariable),
        CanStartWorkflow = true
    },
    new WriteLine(context => $"Document received from {documentVariable.Get<dynamic>(context)!.Author.Name}."),
    new WriteHttpResponse
    {
        Content = new("<h1>Request for Approval Sent</h1><p>Your document has been received and will be reviewed shortly.</p>"),
        ContentType = new(MediaTypeNames.Text.Html),
        StatusCode = new(HttpStatusCode.OK),
        ResponseHeaders = new(new HttpHeaders { ["X-Powered-By"] = new[] { "Elsa 3.0" } })
    },

第二步:

Jack觉得文章不错,通过浏览器请求了“通过”链接,而Lucy觉得文章还不够好,需改进,她在浏览器中请求了“退回”链接。

两位审批人的审批结果存储于approvedVariable变量中

同时他们的浏览器返回的响应内容:Thanks for the approval 或 Sorry to hear that

    new Fork
    {
        JoinMode = ForkJoinMode.WaitAll,
        Branches =
        {
            // Jack
            new Sequence
            {
                Activities =
                {
                    new WriteLine(context => $"Jack approve url: \n {GenerateSignalUrl(context, "Approve:Jack")}"),
                    new WriteLine(context => $"Jack reject url: \n {GenerateSignalUrl(context, "Reject:Jack")}"),
                    new Fork
                    {
                        JoinMode = ForkJoinMode.WaitAny,
                        Branches =
                        {
                            // Approve
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Approve:Jack"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(true)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Thanks for the approval, Jack!"),
                                    }
                                }
                            },

                            // Reject
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Reject:Jack"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(false)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Sorry to hear that, Jack!"),
                                    }
                                }
                            }
                        }
                    }
                }
            },

            // Lucy
            new Sequence
            {
                Activities =
                {
                    new WriteLine(context => $"Lucy approve url: \n {GenerateSignalUrl(context, "Approve:Lucy")}"),
                    new WriteLine(context => $"Lucy reject url: \n {GenerateSignalUrl(context, "Reject:Lucy")}"),
                    new Fork
                    {
                        JoinMode = ForkJoinMode.WaitAny,
                        Branches =
                        {
                            // Approve
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Approve:Lucy"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(true)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Thanks for the approval, Lucy!"),
                                    }
                                }
                            },

                            // Reject
                            new Sequence
                            {
                                Activities =
                                {
                                    new Event("Reject:Lucy"),
                                    new SetVariable
                                    {
                                        Variable = approvedVariable,
                                        Value = new(false)
                                    },
                                    new WriteHttpResponse
                                    {
                                        Content = new("Sorry to hear that, Lucy!"),
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    },

第三步:

根据approvedVariable变量判定文章是否被审核通过。

如果通过则在控制台打印Document document-1 approved!, 否则打印Document document-1 not rejected!

    new WriteLine(context => $"Approved: {approvedVariable.Get<bool>(context)}"),
    new If(context => approvedVariable.Get<bool>(context))
    {
        Then = new WriteLine(context => $"Document ${documentVariable.Get<dynamic>(context)!.Id} approved!"),
        Else = new WriteLine(context => $"Document ${documentVariable.Get<dynamic>(context)!.Id} rejected!")
    }
}

Elsa工作流源码还提供了大量的Sample,这里就不一一列举了,

需求描述

根据不同的时间规则,发送下发问卷给客户填写。

下发问卷给用户填写,且填写有超时时间,期间要提醒用户答题,

如果问卷未在规定的时间内作答则,则作废,并提醒用户。

需求分析

我们将需求尽可能分解成为单一职责的功能单元,并定义这些功能单元的输入输出。

下发问卷任务 PublishQuestionnaireActivity

下发问卷是将问卷(Questionnaire)实例化成问卷实例(Survey),问卷实例绑定用户Id,用户在问卷实例上作答。明确输入和输出:

  • 输入:问卷ID
  • 输出:问卷实例对象SurveyDto

通知任务 NotificationActivity

通知在这个需求中需要发送问卷状态,时间等内容给对应的用户,同通至少包含标题和内容。

  • 输入:标题和内容
  • 输出:无

问卷状态跟踪任务 WaitFillInSurveyActivity

这个任务要追踪问卷实例的状态,当问卷实例状态为已完成时,可以继续执行后续任务。

  • 输入:问卷实例ID
  • 输出:无

定时和延时任务

用于延时执行每个下发问卷的时间,等待问卷超时,以及延时发送通知等。

  • 输入:开始日期,延时日期,间隔时间或cron表达式
  • 输出:无

根任务

根任务包含所有的子任务,完成这个任务后,整个流程结束。在这个需求中根任务只需要知道将什么问卷,发送给哪位用户,以及在何时发送这三个问题。

  • 输入:问卷ID,用户ID,发送时间
  • 输出:无

各子任务参数对于他们的根任务是透明的(Invisible),根任务只需要关心是否完成,而不需要知道任务参数。

代码实现

下发问卷任务可以抽象成为下发问卷活动 PublishQuestionnaireActivity
创建PublishQuestionnaireActivity类并设置输入QuestionnaireId,输出SurveyDto

public class PublishQuestionnaireActivity : Activity<SurveyDto>
{
    public PublishQuestionnaireActivity()
    {

    }
    public PublishQuestionnaireActivity(long questionnaireId)
    {
        QuestionnaireId = new Input<long>(questionnaireId);
    }


    
    public Input<long> QuestionnaireId { get; set; } = default!;
}


重写ExecuteAsync方法,完成问卷下发逻辑

protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
{
    var _surveyAppService = context.GetRequiredService<ISurveyAppService>();
    if (_surveyAppService != null)
    {
        var currentUserId = await context.GetInputValueAsync<Guid>("UserId");
        var survey = await _surveyAppService.PublishAsync(new PublishInput()
        {
            QuestionnaireId = this.QuestionnaireId.Get<long>(context),
            UserId = currentUserId

        }) ?? throw new Exception("创建问卷失败");
        context.SetResult(survey);
    }


    await context.CompleteActivityAsync();

}

如此,其他的任务分别抽象成为相应的活动,这里展示完整代码

通知活动:NotificationActivity

public class NotificationActivity : Activity
{


    public NotificationActivity()
    {

    }
    public NotificationActivity(string title, string content)
    {
        Content = new Input<string>(content);
        Title = new Input<string>(title);
    }

    protected override async ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var notificationManager = context.GetRequiredService<NotificationManager>();

        if (notificationManager != null)
        {
            var title = this.Title.Get(context);
            var content = this.Content.Get(context);
            var currentUserId = await context.GetInputValueAsync<Guid>("UserId");
            var data = new CreatePrivateMessageNotificationEto(currentUserId, title, content);
            await notificationManager.Send(data);
        }


    await context.CompleteActivityAsync();

}

    public Input<string> Title { get; set; } = default!;
    public Input<string> Content { get; set; } = default!;
}

等待问卷完成活动:WaitFillInSurveyActivity

public class WaitFillInSurveyActivity : Activity
{
    public WaitFillInSurveyActivity()
    {

    }
    public WaitFillInSurveyActivity(Func<ExpressionExecutionContext, long?> surveyId)
: this(Expression.DelegateExpression(surveyId))
    {
    }

    public WaitFillInSurveyActivity(long surveyId) => SurveyId = new Input<long>(surveyId);

    public WaitFillInSurveyActivity(Expression expression) => SurveyId = new Input<long>(expression, new MemoryBlockReference());


    /// <inheritdoc />
    protected override ValueTask ExecuteAsync(ActivityExecutionContext context)
    {
        var surveyId = SurveyId.Get(context);
        if (surveyId == default)
        {
            var survey = context.ExpressionExecutionContext.GetLastResult<SurveyDto>();
            surveyId = survey.Id;
        }
        var payload = new WaitFillInSurveyBookmarkPayload(surveyId);
        context.CreateBookmark(new CreateBookmarkArgs
        {
            Payload = payload,
            Callback = Resume,
            BookmarkName = Type,
            IncludeActivityInstanceId = false
        });
        return ValueTask.CompletedTask;
    }

    private async ValueTask Resume(ActivityExecutionContext context)
    {
        await context.CompleteActivityAsync();
    }

    public Input<long> SurveyId { get; set; } = default!;
}

此任务需要等待,我们创建一个Bookmark,注意创建Bookmark时,我们根据问卷实例SurveyId判断是否完成问卷的回答,因此指定IncludeActivityInstanceIdfalse,创建携带SurveyId的Payload类型:

public record WaitFillInSurveyBookmarkPayload(long SurveyId);

在回调OnResumeAsync中,我们使用context.CompleteActivityAsync来完成任务。

public QuestionnaireActivity(long questionnaireId, TimeSpan fillInTimeout)
{
    this.QuestionnaireId = questionnaireId;
    this.FillInTimeout = fillInTimeout;
    var currentSurvey = new Variable<SurveyDto>();
    Variables.Add(currentSurvey);
    Activities = new List<IActivity>()
    {
        new WriteLine("Start"),

        new PublishQuestionnaireActivity(QuestionnaireId)
        {
            Name="PublishQuestionnaire",
            Result=new Output<Questionnaire.Survey.Dto.SurveyDto> (currentSurvey)
        },
        new NotificationActivity("新问卷提醒", "您有新的问卷,请查收"),

        new Fork
        {
            JoinMode = ForkJoinMode.WaitAny,
            Branches =
            {
                new Sequence
                {
                    Activities =
                    {
                        new Delay
                        {
                            Id = "RemindDelay",
                            TimeSpan = new(RemindDelay)
                        },
                        new NotificationActivity("问卷即将超时", "问卷即将超时,请尽快回答")
                    }
                },

                new Sequence
                {
                    Activities =
                    {
                        new WriteLine("问卷计时器开始"),
                        new Delay
                        {
                            Id = "TimeoutDelay",
                            TimeSpan = new(FillInTimeout)
                        },

                        new NotificationActivity("问卷已过期", "问卷已过期,请等待工作人员处理"),

                        // This should break the while loop, not matter how high up in the tree it is.
                        new Fault()
                        {
                            Message=new ("问卷回答超时")
                        }
                    }
                },
                new Sequence
                {
                    Activities =
                    {
                        new WriteLine("开始等待问卷提交信号"),
                        new WaitFillInSurveyActivity(context => currentSurvey.Get<SurveyDto>(context)?.Id)

                    }
                }
            }
        },
        new WriteLine("完成问卷流程"),
        new Finish(),
    };
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/472406.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Flutter 面试题】讲一讲 Dart 的一些重要概念?

【Flutter 面试题】讲一讲 Dart 的一些重要概念&#xff1f; 文章目录 写在前面口述回答补充说明完整代码运行结果详细说明 写在前面 &#x1f64b; 关于我 &#xff0c;小雨青年 &#x1f449; CSDN博客专家&#xff0c;GitChat专栏作者&#xff0c;阿里云社区专家博主&#…

【Leetcode每日一题】 递归 - 反转链表(难度⭐)(36)

1. 题目解析 题目链接&#xff1a;206. 反转链表 这个问题的理解其实相当简单&#xff0c;只需看一下示例&#xff0c;基本就能明白其含义了。 2.算法原理 一、递归函数的核心任务 递归函数的主要职责是接受一个链表的头指针&#xff0c;并返回该链表逆序后的新头结点。递归…

mysql迁移达梦数据库 Java踩坑合集

达梦数据库踩坑合集 文章目录 安装达梦设置大小写不敏感Spring boot引入达梦驱动&#xff08;两种方式&#xff09;将jar包打入本地maven仓库使用国内maven仓库&#xff08;阿里云镜像&#xff09; 达梦驱动yml配置springboot mybatis-plus整合达梦,如何避免指定数据库名&…

如何在Windows系统使用VS Code制作游戏网页并实现无公网IP远程访问

文章目录 前言1. 编写MENJA小游戏2. 安装cpolar内网穿透3. 配置MENJA小游戏公网访问地址4. 实现公网访问MENJA小游戏5. 固定MENJA小游戏公网地址 前言 本篇教程&#xff0c;我们将通过VS Code实现远程开发MENJA小游戏&#xff0c;并通过cpolar内网穿透发布到公网&#xff0c;分…

YZ系列工具之YZ08:窗体加载图片后进行放大查看

我给VBA下的定义&#xff1a;VBA是个人小型自动化处理的有效工具。利用好了&#xff0c;可以大大提高自己的工作效率&#xff0c;而且可以提高数据的准确度。我的教程一共九套一部VBA手册&#xff0c;教程分为初级、中级、高级三大部分。是对VBA的系统讲解&#xff0c;从简单的…

Linux学习总结下

vim\vi编辑器 什么是vi\vim编辑器&#xff1f; 1、vi、vim编辑器&#xff0c;就是命令模式下的文本编辑器&#xff0c;用来编辑文件 2、vim是vi的升级版&#xff0c;一般用vim即可&#xff0c;包含vi所有功能 基础命令&#xff1f; vi 文件路径 vim 文件路径 运行模式 …

二、yocto 集成ros2(基于raspberrypi 4B)

yocto 集成ros2 yocto 集成ros21. 下载ros layer2. 编译集成ros3. 功能验证 yocto 集成ros2 本篇文章为基于raspberrypi 4B单板的yocto实战系列的第二篇文章。 一、yocto 编译raspberrypi 4B并启动 本节我们将ros2机器人操作系统移植到我们的yocto系统里面。 1. 下载ros laye…

LLM如何处理长上下文:Lost in the middle

论文地址&#xff1a;Lost in the Middle: How Language Models Use Long Contexts 论文总结&#xff1a;写prompt的时候&#xff0c;需要注意内容的顺序&#xff0c;把重要的信息放在最前面或者最后面。 大型语言模型大有用处&#xff0c;在设计 prompt 方面&#xff0c;人们…

当OKR无法按时完成或达成时,应如何进行调整?

在企业管理中&#xff0c;OKR&#xff08;Objectives and Key Results&#xff0c;目标与关键成果&#xff09;作为一种有效的管理工具&#xff0c;被广泛用于设定和跟踪目标。然而&#xff0c;在实际执行过程中&#xff0c;OKR无法按时完成或达成的情况时有发生。面对这种情况…

IO多分复用

#include<myhead.h> #define SER_PORT 8888 //服务器端口号 #define SER_IP "192.168.65.131" //服务器IPint main(int argc, const char *argv[]) {//1、创建一个套接字int sfd -1;sfd socket(AF_INET, SOCK_STREAM, 0); //参数1&#xff1a;…

高效快捷的快递查询助手,让您随时随地掌握包裹最新状态

面对一堆快递单号&#xff0c;您是否还在手忙脚乱地逐个复制粘贴到网上查询物流信息&#xff1f;是否还在为如何保存查询好的物流信息而犯愁&#xff1f;别担心&#xff0c;固乔快递查询助手来帮您解决这些烦恼&#xff01; 固乔快递查询助手是一款功能强大的快递单号查询与管理…

【Linux Day17 Libevent库】

Libevent 1.介绍 Libevent 是一个轻量级的开源高性能网络库&#xff0c;有几个显著的亮点&#xff1a; 事件驱动&#xff08;event-driven&#xff09;&#xff0c;高性能;轻量级&#xff0c;专注于网络&#xff0c;不如 ACE 那么臃肿庞大&#xff1b;线程安全。Libevent 使…

Java IO流之Netty实现聊天通信功能

文章目录 1 Netty1.1 概要设计1.1.1 技术选型1.1.2 数据库设计1.1.3 通信设计1.1.3.1 报文协议格式1.1.3.2 报文交互场景 1.2 Netty简单示例1.2.1 pom.xml1.2.2 发送和接收1.2.3 示例说明1.2.3.1 线程阻塞问题1.2.3.2 服务端和接收端 EventLoopGroup 1.3 Netty中handler概述1.4…

python中字典相关知识点总结

1.字典的定义 字典&#xff1a;在Python中&#xff0c;字典是一系列键-值对。每个键都与一个值相关联&#xff0c;程序员可以通过键来访问与之相关联的值。 实际举例&#xff1a; student{name:xincun,age:18} 通过实例我们可以发现&#xff0c;键-值对是两个相关联的值。指…

Qualcomm AI Hub-示例(二)模型性能分析

文章介绍 模型性能分析&#xff08;Profiling&#xff09; 当模型尝试部署到设备时&#xff0c;会面临许多重要问题&#xff1a; 目标硬件的推理延迟是多少&#xff1f;该模型是否符合一定的内存预算&#xff1f;模型能够利用神经处理单元吗&#xff1f; 通过在云端的物理设…

邮件客户端 Thunderbird 简单配置

1. 基本情况介绍 原来使用的邮箱客户端是 Office 365 自带的 Outlook 365切换原因&#xff1a;新装电脑&#xff0c;发现原 Outlook 中的账号信息无法迁移&#xff0c;需要耗费大量时间手动配置邮箱使用的邮箱&#xff1a;微软 O365 邮箱、qq 邮箱、163 邮箱、公司私有邮箱 …

【计算机网络篇】计算机网络的定义和分类

文章目录 &#x1f354;什么是计算机网络&#x1f5c3;️计算机网络的分类⭐按交换方式分类⭐按使用者分类⭐按传输介质分类⭐按覆盖范围分类⭐按拓扑结构分类 &#x1f6f8;小结 &#x1f354;什么是计算机网络 计算机网络是指将多台计算机或其他网络设备通过通信链路连接起来…

55、服务攻防——数据库安全RedisHadoopMysql未授权访问RCE

文章目录 常见服务应用的安全测试&#xff1a; 配置不当——未授权访问安全机制——特定安全漏洞安全机制——弱口令爆破攻击 应用服务安全测试流程&#xff1a; 判断服务开放情况——端口扫描&组合猜解等 端口扫描&#xff1a;服务开放&#xff0c;绑定端口没开放&#…

关于继承是怎么样的?那当然是很好理解之

本文描述了关于继承的大部分知识&#xff0c;但是并不全&#xff0c;每篇博客之间的知识都有互串&#xff0c;所以需要把几篇文章合起来看&#xff0c;学会融会贯通&#xff01; 温馨提示&#xff1a;使用PC端观看&#xff0c;效果更佳&#xff01; 目录 1.继承是什么 2.什…

es 聚合操作(一)

前言 Elasticsearch除搜索以外&#xff0c;提供了针对ES 数据进行统计分析的功能。聚合(aggregations)可以让我们极其方便的实现对数据的统计、分析、运算。例如&#xff1a; 衣服品牌的受欢迎程度这些衣服的平均价格、最高价格、最低价格这些衣服的每天、每月销量如何 使用…