Workflow 通知的站内信事实已并入流程命令的事务边界:平台 INotificationChannel 实现逐接收人调用 Notifications 模块写入收件箱并产生 CAP Outbox 记录,任一接收人失败都会让整个流转命令失败回滚。“审批成功但通知悄悄丢了”不再是合法状态;跨网络的外部投递(邮件、钉钉等)仍由 Notifications 模块的消费者异步完成——送达确认属于它的职责域。
1. 分发链
EventDispatcher 按注册顺序调用 Task、Execution 和 WorkflowEvent listener;监听器异常记录日志后向上重抛。
2. 事件与接收人
| 事件 | 默认接收人 |
|---|---|
| TaskCreated | Task.Assignee |
| TaskCompleted | 当前申请人 |
| TaskRejected | 当前申请人 |
| TaskTransferred / Delegated | data.recipientUserIds |
| ParticipantAdded | 新参与人 |
| InstanceCompleted/Cancelled/Terminated/Withdrawn | 当前申请人 |
| CcCreated | 抄送人 |
| TaskRemind | 当前 Pending Assignee |
IApplicantResolver 可动态解析申请人;失败回退 CurrentApplicantId/StarterId。TaskCreated 每个解析用户已有独立 Task,因此通知按具体 Assignee。
3. 模板
NotificationDispatcher 可使用 IWorkflowNotificationTemplateResolver 按 eventType、businessType、语言解析模板;不存在或失败时回退内置中文默认文案(如 "您有新的审批任务:{title}")。变量只做受控花括号插值。
var engine = new WorkflowEngineBuilder() // 直接 Builder 才会把模板与偏好端口完整传给 Dispatcher。 .UsePersistence(store) .UseExpressionEvaluator(new DefaultExpressionEvaluator()) .UseNotificationChannel(channel) .UseNotificationTemplateResolver(templateResolver) .UseNotificationPreferenceStore(preferenceStore) // Build 只验证必需端口,不提供通知持久化与重试。 .Build();4. 偏好
若 Builder 直接接入 IWorkflowNotificationPreferenceStore,只有 InboxEnabled 或 ExternalDeliveryEnabled 任一为 true 才保留接收人。偏好查询失败时 fail-open,保留接收人并写日志。
平台 NotificationService 也有自身偏好与外部投递抑制。双层策略需要统一,否则可能出现引擎层丢弃、平台层再判断,或 UI 配置与实际分发不一致。
5. 平台适配
WorkflowNotificationAdapter 实现引擎的 INotificationChannel,底层调用 Notifications 模块的 INotificationComposer:code 为 workflow.{EventType},严重度映射 Urgent→Error、High→Warning、其余 Info,类型映射把审批类事件归 Approval、终态事件归 Business、抄送归 Todo;Metadata JSON 携带 instanceId、businessKey、eventType、definitionKey、officeId。任一接收人的 Compose 返回失败或抛异常都会累计计数:
foreach (string userId in notification.RecipientUserIds){ try { var result = await _notificationComposer.ComposeAsync( tenantId: notification.TenantId, userId: userId, code: $"workflow.{notification.EventType}", title: notification.Title, body: notification.Body, type: type, category: notification.BusinessType, severity: severity, linkUrl: null, linkText: null, metadataJson: metadataJson, cancellationToken: cancellationToken);
// 站内信写失败只是计数:通道要在遍历完所有接收人后统一决策。 if (!result.IsSuccess) { failedRecipients++; } } catch (OperationCanceledException) { throw; } catch (Exception exception) when (exception is not OutOfMemoryException) { failedRecipients++; }}
if (failedRecipients > 0){ // 任一接收人落库失败都升级为通道异常,交由上层令整个流转失败回滚。 throw new InvalidOperationException( $"Workflow notification persistence failed for {failedRecipients} recipient(s).");}这保证”成功提交 ⇒ 站内信与 Outbox 已写”;代价是通知通道故障会阻断审批动作本身——按事务一致性设计监控告警是运维义务,不是可选项。
6. 节点 DSL 通知
NodeListenerDispatcher 对 type=notification 的 listener 读取 __listenerRecipients 变量,Title 使用 channel 或 node-listener,Body 使用 expression。它与标准 NotificationDispatcher 是两条不同路径。
接收人列表为空或变量缺失时直接跳过发送(不发空通知);而任何真实发送失败的异常会沿新的失败链条抛出并使流转失败。
7. JobHost 接线
后台催办不再是缺口。WorkflowBackgroundJobsExtensions.AddWorkflowBackgroundJobs 在 hasSqlSugar 时注册完整处理链:
SqlSugarWorkflowPersistenceStore与计时器租户执行作用域;- 定时任务专用系统权限检查器
WorkflowTimerSystemPermissionChecker; INotificationChannel → WorkflowNotificationAdapter;- 作 业身份
BackgroundJobIdentities.WorkflowTimer.JobName = "workflow-timer",Quartz 端[DisallowConcurrentExecution],默认每 5 分钟扫描,可用BackgroundJobs:workflow-timer:IntervalSeconds覆盖。
WorkflowTimerProcessor 以 ILicenseGate 为必选依赖推进到期 Timer、超时升级(escalate)与超时转办(transfer)。执行作用域保证业务动作、通知 Outbox 与 Fired 状态在租户上下文内原子提交。无 SqlSugar 的纯内存宿主则整链不注册,作业也不会被描述符登记。
8. 失败语义
| 失败点 | 当前结果 |
|---|---|
| 模板解析 | 记录警告,使用默认模板 |
| 偏好查询 | 记录警告,保留接收人 |
| 单个接收人 Compose | 计入失败计数,继续其他接收人 |
| 接收人整体结束且有失败 | 抛 InvalidOperationException,分发器重抛,流转失败回滚 |
| 监听器异常 | EventDispatcher 日志 + 重抛 |
| CAP 外部投递 | 由 Notifications 模块异步消费,Workflow 内不等待 |
9. 外部投递的对账边界
站内信与 Outbox 已随事务原子化;尚无跨网络的 delivery receipt、attempt 台账和死信对账——它们是 Notifications 模块消费侧的关注点。需要强 SLA 的产品应在消费侧补充:
模板版本与最终渲染内容也应作为证据保存,避免重试时模板变更产生不同文案。
10. 安全与隐私
Variables 可能含敏感数据,NotificationDispatcher 会把变量带入 WorkflowNotification;平台 Metadata 当前只选择少数字段,但标题/正文仍可能插值泄露。模板 allowlist、长度、HTML/URL 上下文编码、收件人租户校验和日志脱敏都必须测试。
11. 测试清单
- 每种事件的接收人和无接收人路径;
- 申请人解析失败回退;
- 模板缺失/异常与偏好 fail-open;
- 多接收人部分失败导致整单回滚;
- 相同 Transition 重放不重复站内信;
- JobHost Reminder 实际发送与 Fired 原子提交;
- CAP 停机、恢复、重复投递和对账;
- 模板注入、超长变量和跨租户用户。