ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

EasyAdminBlazor 审批模块实战:从提交、审批到最终完成

EasyAdminBlazor 审批模块实战:从提交、审批到最终完成 审批是 2.3 新增的重要能力。这篇文章不讨论要不要上工作流引擎只讲一件事用状态机 审批记录表把固定 2~3 级审批做扎实以及怎么在半小时内把它接进一个已经有了AdminTable的模块。一、先划清边界这个模块的定位在源码注释里写得很清楚轻量多级审批状态机状态存放在业务表 审批记录表流水 待办索引。能做的固定串联的 1~N 级审批常见 2~3 级审批人来源部门负责人 / 角色 / 指定用户 / 单据字段或签同一节点多人任一人处理即通过提交、同意、驳回、撤回、转交待办 / 已办 / 我的申请 / 全部轮次与时间线、变更重审、通过后锁定不做源码里也明说了会签、并行、条件分支、回退到任意节点。这些属于工作流引擎如 Elsa的范畴硬套状态机只会得到一个越来越难维护的switch。二、状态与动作1. 五个状态publicenumApprovalStatus{Draft0,// 草稿未提交审批Pending1,// 审批中Approved2,// 已通过Rejected3,// 已驳回Revoked4// 已撤回}关键在于第几级不放在状态里而是单独一个CurrentLevel字段publicinterfaceIApprovalBill{/// summary审批状态/summaryApprovalStatusApprovalStatus{get;set;}/// summary当前待审级次0未提交1/2/3第几级/summaryintCurrentLevel{get;set;}}这样状态只有 5 个可枚举、可穷举测试级次可以任意扩展。2. 五个动作publicenumApprovalAction{Submit,// 提交Approve,// 同意Reject,// 驳回Revoke,// 撤回Transfer// 转交}状态机是纯函数不碰数据库所以能穷举单测publicstatic(ApprovalStatus Status,intLevel)Next(ApprovalActionaction,intcurrentLevel,intmaxLevel){if(maxLevel1)thrownewArgumentOutOfRangeException(nameof(maxLevel),审批流程至少需要一级);returnactionswitch{ApprovalAction.Submit(ApprovalStatus.Pending,1),ApprovalAction.ApprovewhencurrentLevelmaxLevel(ApprovalStatus.Pending,currentLevel1),ApprovalAction.Approve(ApprovalStatus.Approved,0),ApprovalAction.Reject(ApprovalStatus.Rejected,0),ApprovalAction.Revoke(ApprovalStatus.Revoked,0),ApprovalAction.Transfer(ApprovalStatus.Pending,currentLevel),_thrownewInvalidOperationException($未知的审批动作{action})};}把状态流转画出来草稿 ──提交── 一级审批中 ──同意── 二级审批中 ──同意── 已通过 │ │ ├──驳回───────────────┴── 已驳回 ──修改后提交── 一级新一轮 └──撤回无人审批前─── 已撤回 ──提交── 一级新一轮 审批中 ──转交── 级次不变只换审批人三、五步接入第一步安装并注册dotnetaddpackage EasyAdminBlazor.Approvalbuilder.AddEasyAdminBlazor(newEasyAdminBlazorOptions{...}).AddEasyAdminBlazorApproval(o{o.Flows.Add(newApprovalFlowConfig{BillTypetypeof(Article).FullName!,Levels[newApprovalLevelConfig{Level1,Name部门主管审批,KindApproverKind.OrgLeader,Offset1},newApprovalLevelConfig{Level2,Name管理员审批,KindApproverKind.Role,ValueAdministrator}]});});注册做了什么ServiceCollectionExtensions.cs注册项说明IApprovalFlowProvider→SysConfigApprovalFlowProvider流程配置来源代码注册 or 后台参数配置IApproverResolver→DefaultApproverResolver审批人解析IApprovalUserProvider→AdminApprovalUserProvider当前用户 / 权限 / 组织IApprovalNotifier→AdminMessageApprovalNotifier站内信 SignalR 通知IApprovalGateway→ApprovalService替换核心包的NullApprovalGatewayApprovalInitializerHostedService启动时建表、修复历史轮次这张表里最重要的一行是最后一行核心包里的AdminTable只依赖IApprovalGateway接口未安装扩展时是空实现所以审批是可选扩展不装也不会影响其他功能。流程也可以放到后台参数配置里Code 为APPROVAL_FLOW_{单据全名}值是流程 JSON——改了不用重启。第二步业务实体接入审批把基类换掉即可审批字段由基类提供// 需要软删除publicpartialclassArticle:ApprovalEntityFull{// 原有字段不动}// 不需要软删除publicpartialclassOrder:ApprovalEntity{}两个基类的唯一区别是继承链EntityFull/EntityCreated字段完全一致publicabstractclassApprovalEntityFull:EntityFull,IApprovalBill{[Column(Position-8)][DisplayName(审批状态)]publicvirtualApprovalStatusApprovalStatus{get;set;}ApprovalStatus.Draft;[Column(Position-7)][DisplayName(审批级次)]publicvirtualintCurrentLevel{get;set;}/// summary列表展示用不落库/summary[Column(IsIgnoretrue)][DisplayName(审批状态)]publicstringApprovalStatusTextApprovalStatusswitch{ApprovalStatus.Draft草稿,ApprovalStatus.PendingCurrentLevel0?${CurrentLevel}级审批中:审批中,ApprovalStatus.Approved已通过,ApprovalStatus.Rejected已驳回,ApprovalStatus.Revoked已撤回,_string.Empty};}ApprovalStatusText是[Column(IsIgnore true)]的展示属性——表格里直接绑它就能显示2 级审批中这种可读文案。如果实体已经继承了别的基类直接实现接口 手写两个字段也可以publicclassMyBill:EntityCreated,IApprovalBill{[Column(Position-8)]publicApprovalStatusApprovalStatus{get;set;}[Column(Position-7)]publicintCurrentLevel{get;set;}}注意源码里的提醒接口只提供契约字段仍需由实体或基类声明否则 ORM 不会建列。发起人和提交时间复用IEntityCreated的CreatedUserId/CreatedTime不需要额外字段。第三步页面接入方式一AdminTable 自动接管。实体实现IApprovalBill后AdminTable会自动识别// 实体实现 IApprovalBill 时自动接管审批未安装 Approval 扩展时空实现直接跳过_autoApprovaltypeof(IApprovalBill).IsAssignableFrom(typeof(TItem));自动做的事保存后按配置决定是否自动提交审批审批中的单据禁止编辑、删除如果你在列里绑了ApprovalStatus/ 状态文本就能显示审批状态。方式二审批面板组件。更常见也更直观的做法是在编辑模板里加一个审批选项卡inject CommonLocalizer CommonLocalizer Tab IsCard TabItem TextCommonLocalizer[随笔] !-- 业务字段 -- /TabItem TabItem TextCommonLocalizer[审批] ApprovalActions TItemArticle BillModel OnChangedOnApprovalChanged / /TabItem /Tab code { [Parameter][NotNull] public Article? Model { get; set; } [Parameter] public EventCallback OnApprovalChanged { get; set; } }ApprovalActions组件内部Components/ApprovalActions.razor会显示当前审批状态徽章按CanSubmitAsync/CanCurrentUserApproveAsync决定显示哪些按钮提供审批意见输入框显示审批轨迹ShowHistory默认 true操作成功后把最新状态同步回传入的单据对象再触发OnChanged让宿主刷新列表。最后那一步有个容易踩的坑源码里专门写了注释/// summary/// 把最新的审批状态同步回传进来的单据对象。/// 服务改的是数据库而列表/表单持有的是同一个实体实例不同步的话界面上会一直显示旧状态。/// /summaryprivateasyncTaskSyncBillStateAsync(){if(Billisnull)return;varviewawaitApprovalGateway.GetBillViewAsync(BillType,Bill.Id);if(view?.Statusisnull)return;Bill.ApprovalStatusview.Status.Value;Bill.CurrentLevelview.CurrentLevel;}宿主页面再加一个状态列并让OnChanged刷新表格TableColumn bind-Fieldcontext.ApprovalStatus Filterabletrue Template Contextvv.Row.ApprovalStatusText/Template /TableColumn ... EditTemplate ArticleEditTemplate Modelcontext OnApprovalChangedReloadAsync / /EditTemplate code { private AdminTableArticle, long _table default!; private async Task ReloadAsync() { if (_table is not null) await _table.Reload(); } }第四步配好审批人四种审批人来源publicenumApproverKind{[Display(Name部门负责人)]OrgLeader0,// 按 SysOrg.ResponsibleUserId 逐级向上查找[Display(Name角色)]Role1,// 该角色下全部有效用户或签[Display(Name指定用户)]User2,[Display(Name单据字段)]FormField3// 字段值作为审批人 Id}解析规则DefaultApproverResolver来源配置行为部门负责人Offset 1从发起人所在组织开始向上找第 N 个负责人中间层缺负责人自动继续向上角色Value 财务取该角色下所有IsEnabled ! false的用户或签指定用户Value 10086用户必须存在且未停用否则视为无人可审单据字段Field AuditorId支持long/long?/long[]/ 可解析为 long 的字符串两个容易被忽略的细节1默认禁止自审。/// summary是否允许自己审自己提交人成为自己单据的审批人默认 false。/summarypublicboolAllowSelfApproval{get;set;}默认不允许时部门负责人这一级会继续向上找单人部门很常见角色 / 单据字段会把提交人从名单里排除。排除后没人可审提交时就会提示未找到「XX」的审批人请检查审批流程配置。要允许自审比如单人部门负责人就是唯一员工显式打开.AddEasyAdminBlazorApproval(o{o.AllowSelfApprovaltrue;...})2审批人在提交时一次性解析并落库。// 审批人在提交时一次性解析并落库固定 2~3 级流程// 这样我的待办只需查审批记录一张表不用遍历各业务表。这是设计上的关键取舍解析结果存进sys_approval_record待办查询就变成查一张表 两个索引不需要为了显示待办去 union 所有业务表。第五步跑通流程把上面的配好之后用户新增一条 Article → 状态是Draft草稿保存与提交分离在审批选项卡点提交审批→ 状态变PendingCurrentLevel 1一级审批人收到站内信待您审批xxx点进去到审批中心审批人点同意→ 若还有下一级则CurrentLevel 1否则Approved期间发起人可以在无人审批前撤回审批人可以转交给别人被驳回后修改再提交会进入新一轮Round 1旧轮次记录完整保留。四、sys_approval_record一张表干三件事审批记录表的表结构注释写得非常清楚一张表同时承担三个角色 1) 审批流水按单据查询即为完整审批轨迹Timeline 展示 2) 待办索引IsCurrent and StatusPending and ApproverUserId我 即我的待办无需跨业务表查询 3) 通知目标提交/过级时按本表审批人推送站内信。对应两个索引[Index(idx_approval_record_bill,BillType,BillId,false)][Index(idx_approval_record_todo,ApproverUserId,IsCurrent,false)]几个语义要点字段含义Level0 发起记录1…N 审批节点Round轮次驳回/撤回后重新发起会 1IsCurrent是否当前待处理节点待办查询的过滤条件Status节点结果Pending/Submitted/Approved/Rejected/Revoked/Transferred/SkippedApproverUserId“该节点应由谁处理”OperatorUserId“实际是谁操作的”转交、代审时两者不同BeforeStatus/AfterStatus审计用的状态变更前后快照InstanceKey{BillType}#{BillId}#{Round}把实例、节点、历史关联起来Skipped状态是或签场景的产物A 先同意了同节点其他待处理记录会被置为Skipped避免节点被完成两次。五、待办、通知与审批中心1. 审批中心页面框架内置了/Admin/ApprovalTodo支持四种视角publicenumApprovalListScope{Todo0,// 待我处理Done1,// 我处理过的已办Submitted2,// 我发起的我的申请All3// 全部仅管理员}菜单和按钮由ApprovalMenuProvisioner自动补齐且是幂等的1) 没有审批菜单就创建一级菜单「审批中心」 2) 老结构审批中心分组 我的待办子菜单升级成单一一级菜单 3) 补齐 approve/reject/revoke 三个按钮权限。这三步是旧项目能平滑升级的关键框架的SeedData只在菜单表为空时插入老项目装了扩展后菜单表非空新菜单不会自动出现所以由扩展在启动时补齐。按钮权限同样要勾给角色——非管理员用户如果没有approve/reject/revoke权限审批按钮不会显示服务端也会拒绝。管理员自动放行。2. 通知默认通知走站内信含 SignalR 实时推送publicinterfaceIApprovalNotifier{/// summary推送待办通知给当前节点审批人/summaryTaskNotifyPendingAsync(IReadOnlyCollectionSysApprovalRecordrecords);/// summary把审批结果通知发起人/summaryTaskNotifyResultAsync(SysApprovalRecordrecord,ApprovalStatusstatus);}待办通知的收件人计算有个值得学习的细节/// summary/// 计算待办通知的收件人只认审批节点——级次大于 0、状态为待处理、且有审批人。/// 发起记录Level0里同样存了 ApproverUserId但那个值是提交人/// 不能被当成审批人否则提交人就会收到本该发给审批人的待办通知。/// /summarypublicstaticlong[]ResolveRecipients(IEnumerableSysApprovalRecordrecords)records.Where(xx.Level0x.StatusApprovalRecordStatus.Pendingx.ApproverUserIdis0).Select(xx.ApproverUserId!.Value).Distinct().ToArray();通知里的跳转地址统一是/Admin/ApprovalTodo点击直接进审批中心。还有一条原则通知失败不影响审批流程本身try/catch 日志告警并且通知只在事务提交之后发送——第 18 篇会展开。六、策略配置速查ApprovalOptions里的全局开关流程配置优先配置默认含义Enabledtrue是否启用审批扩展AutoCreateMenutrue菜单缺失时自动创建审批中心AllowSelfApprovalfalse是否允许自己审自己AutoSubmitOnSavefalse保存后是否自动提交默认保存与提交分开ResubmitOnEdittrue已通过的单据被修改后是否重新走审批AllowEditAfterApprovedfalse已通过的单据是否还允许修改默认通过后锁定RequireCommentOnRejecttrue驳回是否必须填意见RequireCommentOnApprovefalse同意是否必须填意见AllowRevoketrue发起人能否在无人审批前撤回EnableNotificationtrue是否推送站内信通知流程级可覆盖的ResubmitOnEdit、AutoSubmitOnSave、AllowEditAfterApproved、ResubmitFields。其中变更重审的判定方式值得单独提一句提交时把ResubmitFields里各字段的值按字段名排序拼成文本做SHA256存进该轮发起记录的ResubmitHash再次保存时重新算一遍比对一致就跳过审批。只存指纹不存原值不会把业务数据抄一份进审批记录。七、常见问题现象原因处理提交时提示未找到「XX」的审批人审批人解析结果为空检查组织负责人是否维护、角色下是否有有效用户或打开AllowSelfApproval非管理员看不到审批按钮角色没勾approve/reject/revoke审批中心菜单下勾按钮权限或确认ApprovalMenuProvisioner已补齐保存后没有进入审批默认保存与提交分离打开AutoSubmitOnSave或让用户点提交审批已通过的单据改不了默认通过后锁定按需打开AllowEditAfterApproved改了单据却没有重审关键字段没变ResubmitFields指纹一致确认改的是关键字段或调整ResubmitFields待办列表看不到单据扩展未安装 / 菜单未创建确认AddEasyAdminBlazorApproval已注册且AutoCreateMenu true提交人收到了待办通知早期版本的收件人计算问题2.3 已修复为只认审批节点Level 0八、小结EasyAdminBlazor 2.3 的审批模块核心就三样东西状态机ApprovalStateMachine解决怎么流转纯函数、可穷举测试审批记录表sys_approval_record解决轨迹、待办、通知三件事一张表 两个索引流程配置ApprovalFlowConfig解决谁能审四种来源 或签 禁止自审。接入成本很低装包、注册流程、换个实体基类、编辑模板加一个审批选项卡。剩下的复杂部分并发、事务、通知时序框架已经处理好了——那正是后面两篇要讲的内容。如果你的 .NET 10 Blazor 后台需要多级审批又不想引入完整工作流引擎EasyAdminBlazor 的轻量审批可以先用起来状态存在业务表上审批记录独立成表扩展和降级都容易。文档https://easyadmin.wang-zhan.com.cn/doc源码https://gitee.com/gudufy/EasyAdminBlazor
返回列表