文書承認システムのワークフロー

.NET開発者が「プロジェクトにワークフローを追加する必要がある」という言葉を聞いたとき、最初のアイデアはWindows Workflow Foundationを採用することです。

2010年に、WFをワークフローエンジンとして選択しました。

引数は単純です:

WFを使用して1年半(2010年8月から2012年2月まで)、顧客の要件を実装する際にさまざまな問題に遭遇しました。 最終的に、Windows Workflow Foundationを放棄し、ステートマシンの実装を余儀なくされました。

この記事では、遭遇した主な問題と、それらがどのように解決した(または解決しなかった)かについて説明します。

はじめに


私の意見では、ドキュメント承認システムでのWFの使用についてかなりよく説明している記事が2つあります。
WWF 3の場合: 「ドキュメント承認ワークフローシステム」
WWF 4の場合: 「電子ドキュメント管理システムの構築例を含むWindows Workflow Foundationの概要」

彼らはそれをうまく説明していますが、氷山の一角だけを説明しています。

要するに、これらの記事では以下の方法について説明しています。

これらの簡単な操作でさえ、非常に大きな人件費を必要とし、松葉杖やタンバリンとのダンスなしにはできません。 私たちもこのタンバリンで踊りました。

Luxoftの同僚とは異なり、 勇気を出して、ワークフローモジュールの実装をWWF 3.5に「現状のまま」公開しました。

URL: Budget.Server
プロジェクトに関する簡単な情報
プロジェクトは、クライアントWinFormsアプリケーションとサーバー部分の2つの部分で構成されています。
リンクには、サーバー側のソースが含まれています。
サーバー部分は、ドキュメント管理と外部システムとの統合を担当します。

ワークフロースキームはプロジェクトBudget2.Workflowにあります (WWF 3.5を使用しましたが、WWF 4でも同じ問題が残りました)。
ファイル内のワークフローを操作するためのAPI: Budget.Server \ API \ Services \ WorkflowAPI.cs

行きましょう。

Workflow Foundationとの戦い方


WFをプロジェクトに接続し、ルートに沿ってドキュメントを移動する方法を学習し、ステータスを変更するための条件を示しました。 これを行う方法は、上記で引用した記事に記載されています。

その後、楽しみが始まります...



ユーザーが使用可能なコマンドのリストを取得する


WFは、コマンドとアクター(ドキュメント作成者、作成者のチーフ、コントローラー、マネージャー)をサポートしていません。
これは独立して実装する必要があります。 さらに、WWF 4でブックマークリストを取得できる場合、これはバージョン3.5では不可能であり、各状態のコマンドのリストを個別に保存する必要がありました。



上記の記事から著者を引用します。
さらに、一般アクティビティごとに、特定のメタデータセットが個別に保存されます:実行する特権、アクティビティを実行できるドキュメントの種類、Dynamic LINQ-起動できるかどうかを確認するドキュメントへの式など。

アクティビティごとに 、メタデータのセットを個別に指定する必要があります。それに応じて、アクセスを確認する必要があります。
そうです、私たちはまったく同じことをしました。
これを実行すると、最新の状態に保つのは困難です。

受信ドキュメントのリストを取得する


この要件は、回路がWFに実装された後に実装します。
問題は、バナリティの前は簡単でした。 現在の段階のユーザー特定のドキュメントを調整できるかどうかを判断できましたが、この段階でドキュメントを調整できるすべてのユーザーのリストを取得できませんでした 。 システムには約300〜400人のユーザーがいますが、徹底的な検索で問題を解決することはできませんでした。

これにより、ユーザーの役割、ユニットの階層におけるユーザーの位置、ドキュメント属性およびその他のパラメーターに応じて、現在のユーザーが承認できるドキュメントの選択を可能にするフィルターを作成する必要がありました。



フィルターの例
プロセスステータスは、enum BillDemandStateEnumにリストされます。

private string GetFilter() { List<Guid> deputyIds = DeputyEmployeeRepository.GetReplaceableEmployeeIdentityIds(DocumentType.BillDemand,EmployeeRepository.CurrentEmployee, true); string idsString = StringUtil.GetString(deputyIds); string opSubfilter = string.Format( "SELECT dbd.{0} FROM {1} dbd INNER JOIN {2} ON dbd.{0} = {3} INNER JOIN {4} ON {5} = {6} LEFT OUTER JOIN {7} ON dbd.{0} = {8} AND {9} = {10} WHERE {11} IS NULL", BillDemandTableBase.SelectColumn_Id, BillDemandTableBase.DEFAULT_NAME, BillDemandDistributionTableBase.DEFAULT_NAME, BillDemandDistributionTableBase.FilterColumn_BillDemandDistribution_BillDemandId, DemandTableBase.DEFAULT_NAME, BillDemandDistributionTableBase.FilterColumn_BillDemandDistribution_DemandId, DemandTableBase.FilterColumn_Demand_Id, WorkflowSightingTableBase.DEFAULT_NAME, WorkflowSightingTableBase.FilterColumn_WorkflowSighting_EntityId, DemandTableBase.FilterColumn_Demand_ExecutorStructId, WorkflowSightingTableBase.FilterColumn_WorkflowSighting_ItemId, WorkflowSightingTableBase.FilterColumn_WorkflowSighting_Id ); string limitSubfilter = string.Format( "SELECT {0} FROM {1} WHERE {2} = {3} AND {4} = {5} AND {6} IS NULL AND {7} IN ({8}) ", WorkflowSightingTableBase.SelectColumn_Id, WorkflowSightingTableBase.DEFAULT_NAME, BillDemandTableBase.FilterColumn_BillDemand_Id, WorkflowSightingTableBase.FilterColumn_WorkflowSighting_EntityId, WorkflowSightingTableBase.SelectColumn_StateId, BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, WorkflowSightingTableBase.SelectColumn_SightingTime, WorkflowSightingTableBase.SelectColumn_SighterId, idsString); string filter = string.Format( "({0} IN ({1},{2}) AND {3} IN ({4})) OR ( EXISTS ({5}) )", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.Draft, (int) BillDemandStateEnum.PostingAccounting, BillDemandTableBase.FilterColumn_BillDemand_AuthorId, idsString, limitSubfilter); if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.ControllerRoleId)) filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.UPKZControllerSighting); if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.CuratorRoleId)) filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.UPKZCuratorSighting); if (SecurityHelper.IsPrincipalsInRole(deputyIds, SecurityHelper.UPKZHeadRoleId)) filter += string.Format(" OR {0} = {1}", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.UPKZHeadSighting); if (SecurityHelper.IsPrincipalsAccountant(deputyIds, BudgetPart)) { if (CommonSettings.CheckAccountingInFilial) { filter += string.Format(" OR ({0} = {1} AND {2} = '{3}')", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.InAccounting, BillDemandTableBase.FilterColumn_BillDemand_FilialId, EmployeeRepository.CurrentEmployeeFilialId); } else { filter += string.Format(" OR ({0} = {1})", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int)BillDemandStateEnum.InAccounting ); } } List<Guid> deputyDivisionHeads = SecurityHelper.GetPrincipalsDivisionHead(deputyIds, BudgetPart); if (deputyDivisionHeads.Count > 0) { string currentEmployeeChildrenStructs = EmployeeRepository.CurrentEmployeeChildrenStructs.Replace("(", "").Replace(")", ""); string deputyDevisionHeadString = StringUtil.GetString(deputyDivisionHeads); filter += string.Format( " OR ({0} = {1} AND EXISTS (SELECT vp.Id FROM [dbo].[vStructDivisionParentsAndThis] vp INNER JOIN {2} e ON vp.ParentId = e.{3} WHERE vp.Id = {4} AND e.{5} IN ({6})))", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int)BillDemandStateEnum.HeadInitiatorSighting, EmployeeRepository.DEFAULT_NAME, EmployeeRepository.SelectColumn_StructDivisionId, BillDemandTableBase.FilterColumn_AuthorStructDivision_Id, EmployeeRepository.SelectColumn_SecurityTrusteeId, deputyDevisionHeadString ); filter += string.Format( " OR ({0} = {1} AND {2} > 0 AND EXISTS ({3} AND {4} IN ({5}) AND dbd.{6} = {7}))", BillDemandTableBase.FilterColumn_BillDemand_BillDemandStateId, (int) BillDemandStateEnum.LimitManagerSighting, BillDemandTableBase.FilterColumn_BillDemand_BudgetPartId, opSubfilter, DemandTableBase.FilterColumn_Demand_ExecutorStructId, currentEmployeeChildrenStructs, BillDemandTableBase.SelectColumn_Id, BillDemandTableBase.FilterColumn_BillDemand_Id); } return filter; } 


これらのフィルターには2週間かかりました。

バージョン管理スキーマ


Windows Workflow Foundation 3.5には、組み込みのバージョン管理および更新プロセス図はありません。
WF 4では、状況は変わっていません-Workflow Foundation 4のバージョン処理



プロセスが実行されている場合、マーチスキームの更新はそのようには動作しません。 スキームを更新するには、古いスキームを持ち、タンバリンと少し踊る必要があります。 彼らは約1〜2週間踊りましたが、サーキットを更新するための多かれ少なかれ機能するメカニズムを作りました。 現在、プロジェクトはWorkflow.xxx.dllという名前のDDLで定期的に更新されています。xxxは古いバージョンの番号です。

調整の履歴...将来のステージのリスト


和解の歴史の実装は些細なことです。 誰が、いつ、どのボタンが押されたかをタブレットに保存する必要があります。 しかし、顧客の承認の簡単な話は合っていません。

クライアントは、承認のために残っているステップ(将来のステップ)のリストと、そのようなステップごとに、ドキュメントを調整できるユーザーのコンマ区切りリストをシステムに表示することを望みました。



このタンバリンがWFを中心に踊るのは飽き飽きしています。 彼らは私たちがどのように手放すかを考え始めました... WF。

ところで、この問題を1つまたは2つ解決しています。製品には特別なモード(実行前モード)があります。 これにより、ルートに沿って空走し、将来のステージと潜在的なコーディネーターを形成することができます。


「デザイナーをください」


明らかな理由から、クライアントにWFのデザイナーを提供できませんでした。 方法は覚えていませんが、どういうわけか、この段階でこれを行うべきではないとクライアントに納得させました。



状態をスキーマに動的に追加する


1年後、クライアントは、特定の条件の下で特別なディレクトリからの新しい条件をドキュメントルートに追加することを望んでいました。



プロセス図を生成するメカニズムを示す単一の例を見つけることができませんでした。 したがって、彼らはそれをしようとさえしませんでした。 クライアントにWFから開発に移行するまで数か月待つように依頼しました。 顧客は理解をもって扱われます。 どうもありがとうございます。

WFで誰かが同様のケースを実装した場合、例を共有すると、それを見るのは非常に興味深いです。

サポート


実装が成功した後、システムの開発は停止しませんでした。 新しい要件が定期的に受信されています。
ルートスキームに変更を加え、更新のたびにシリーズからバグを受け取りました。


これは、ロジックが重複する場合の典型的な状況です(WFの条件の一部、メタデータの一部、受信用のSQLフィルターの一部)。

これに、WFが明確に解釈できない不可解なエラーを生成したという事実が追加されました。 何度か私はクライアントのサイトに行かなければなりませんでした。

まとめると


調整機能がある情報システムを作成している場合、99%の確率で、上記のほとんどのケースが発生します。 すべての企業がこれをWFに実装できるわけではありません。 すべての顧客が喜んでそれを支払うわけではありません。

私たちは自分自身で選択を行いました-独自のWorkflow Engine .NETエンジンを作成し、プロジェクトで正常に使用しました。
その中で、クラスシステムの実装の経験を考慮に入れました-ドキュメント承認システム。

Source: https://habr.com/ru/post/J248525/


All Articles