HarmonyOS开发:审批流程——工作流引擎

举报
Jack20 发表于 2026/06/26 16:47:25 2026/06/26
【摘要】 HarmonyOS开发:审批流程——工作流引擎📌 核心要点:工作流引擎是OA系统的"心脏"——审批节点、流转规则、条件分支、并行审批,全靠状态机驱动。搞懂状态机,审批流程就搞懂了一大半。 背景与动机你提交了一个请假申请,然后呢?等。等你的组长审批,等部门经理审批,等HR备案。如果请假超过3天,还得加一个副总审批。你有没有想过,这个"等"的背后是什么?是一套规则在驱动——谁先审、谁后审、什...

HarmonyOS开发:审批流程——工作流引擎

📌 核心要点:工作流引擎是OA系统的"心脏"——审批节点、流转规则、条件分支、并行审批,全靠状态机驱动。搞懂状态机,审批流程就搞懂了一大半。

背景与动机

你提交了一个请假申请,然后呢?等。等你的组长审批,等部门经理审批,等HR备案。如果请假超过3天,还得加一个副总审批。

你有没有想过,这个"等"的背后是什么?是一套规则在驱动——谁先审、谁后审、什么条件下加人、什么条件下跳过、全部通过才算通过、任何一人拒绝就打回。

这就是工作流。

工作流看起来简单——不就是A审完B审,B审完C审嘛?但真实的企业审批流程远比这复杂:

  • 条件分支:请假3天以内组长批就行,3天以上要加部门经理
  • 并行审批:采购申请要财务和法务同时审批,都通过才行
  • 会签:5个部门经理都要签字,全同意才算通过
  • 或签:5个部门经理中任意1个同意就行
  • 加签:审批到一半,发现需要另一个人也审一下
  • 转办:我太忙了,把这个审批转给副手处理
  • 撤回:提交后发现填错了,趁还没人审批赶紧撤回来
  • 驳回:不通过,打回修改后重新提交

这些场景,你打算用if-else写?写到最后你自己都看不懂。

工作流引擎要解决的核心问题:把审批流程从代码里抽出来,变成可配置的规则,让流程跟着规则走,而不是跟着代码走。

核心原理

工作流引擎的本质是一个有限状态机(FSM)——审批单在各个节点之间流转,每个节点有明确的进入条件、处理动作和离开规则。

flowchart TD
    A[提交申请] --> B[发起节点]
    B --> C{条件判断}
    
    C -->|金额<5000| D[直属领导审批]
    C -->|5000≤金额<50000| E[直属领导审批]
    C -->|金额≥50000| F[直属领导审批]
    
    D --> G{审批结果}
    E --> G
    F --> G
    
    G -->|通过| H{是否需要下一节点?}
    G -->|拒绝| I[驳回至发起人]
    
    H -->|| J{条件判断}
    H -->|| K[流程结束-通过]
    
    J -->|金额<5000| K
    J -->|5000≤金额<50000| L[部门经理审批]
    J -->|金额≥50000| M[并行审批<br/>部门经理+财务总监]
    
    L --> N{审批结果}
    M --> O{全部通过?}
    
    N -->|通过| K
    N -->|拒绝| I
    
    O -->|| P[副总审批]
    O -->|| I
    
    P --> Q{审批结果}
    Q -->|通过| K
    Q -->|拒绝| I
    
    I --> R{是否重新提交?}
    R -->|| B
    R -->|| S[流程结束-终止]
    
    classDef start fill:#1565C0,color:#fff,stroke:#0D47A1
    classDef process fill:#2E7D32,color:#fff,stroke:#1B5E20
    classDef decision fill:#E65100,color:#fff,stroke:#BF360C
    classDef end_pass fill:#00897B,color:#fff,stroke:#004D40
    classDef end_fail fill:#C62828,color:#fff,stroke:#B71C1C
    classDef parallel fill:#6A1B9A,color:#fff,stroke:#4A148C
    
    class A,B start
    class D,E,F,L,P process
    class C,G,H,J,N,O,Q,R decision
    class K end_pass
    class I,S end_fail
    class M parallel

核心概念

概念 说明 示例
流程定义 审批流程的模板,定义有哪些节点、怎么流转 请假审批流程、报销审批流程
流程实例 一次具体的审批,按流程定义创建 张三的请假审批(3天)
节点 流程中的一个审批环节 直属领导审批、部门经理审批
连线 节点之间的流转路径和条件 金额≥5000 → 部门经理审批
变量 流程中用到的业务数据 金额、天数、部门
动作 节点上可以执行的操作 同意、拒绝、转办、加签

状态机设计

审批单的状态流转:

草稿 → 待审批 → 审批中 → 已通过
                  ↓
                已拒绝 → 已撤回
                  ↓
                已终止

每个审批节点的状态:

未到达 → 待处理 → 已通过
          ↓
        已拒绝
          ↓
        已转办

流转规则引擎

流转规则决定了审批单"下一步去哪"。核心逻辑:

  1. 当前节点审批完成
  2. 检查所有出线(从当前节点出发的连线)
  3. 评估每条出线的条件表达式
  4. 满足条件的出线指向的节点就是下一个节点
  5. 如果多条出线都满足,走"并行审批"模式

代码实战

基础用法:审批节点与状态机

先定义审批流程的数据模型,再实现状态机驱动。

// WorkflowEngine.ets - 工作流引擎核心
import { relationalStore } from '@kit.ArkData';
import { emitter } from '@kit.BasicServicesKit';

// ========== 数据模型 ==========

// 审批节点类型
export enum NodeType {
  START = 'start',           // 发起节点
  APPROVAL = 'approval',     // 审批节点(单人审批)
  COUNTERSIGN = 'countersign', // 会签节点(多人审批,全通过才算通过)
  OR_SIGN = 'or_sign',       // 或签节点(多人审批,任一通过即可)
  CONDITION = 'condition',   // 条件判断节点
  END = 'end',               // 结束节点
}

// 审批结果
export enum ApprovalAction {
  APPROVE = 'approve',       // 同意
  REJECT = 'reject',         // 拒绝
  TRANSFER = 'transfer',     // 转办
  ADD_SIGN = 'add_sign',     // 加签
  WITHDRAW = 'withdraw',     // 撤回
}

// 流程状态
export enum ProcessStatus {
  DRAFT = 'draft',           // 草稿
  PENDING = 'pending',       // 待审批
  APPROVING = 'approving',   // 审批中
  APPROVED = 'approved',     // 已通过
  REJECTED = 'rejected',     // 已拒绝
  WITHDRAWN = 'withdrawn',   // 已撤回
  TERMINATED = 'terminated', // 已终止
}

// 节点定义
export interface NodeDefinition {
  nodeId: string;             // 节点ID
  nodeName: string;           // 节点名称
  nodeType: NodeType;         // 节点类型
  approverIds: string[];      // 审批人ID列表
  conditionExpression?: string; // 条件表达式(条件节点用)
  timeoutHours?: number;      // 超时时间(小时)
}

// 连线定义
export interface EdgeDefinition {
  edgeId: string;             // 连线ID
  fromNodeId: string;         // 起始节点
  toNodeId: string;           // 目标节点
  conditionExpression?: string; // 条件表达式
  label?: string;             // 连线标签
}

// 流程定义
export interface ProcessDefinition {
  processId: string;          // 流程定义ID
  processName: string;        // 流程名称
  nodes: NodeDefinition[];    // 节点列表
  edges: EdgeDefinition[];    // 连线列表
  variables: Record<string, Object>; // 流程变量
}

// 流程实例——一次具体的审批
export interface ProcessInstance {
  instanceId: string;         // 实例ID
  processId: string;          // 流程定义ID
  title: string;              // 审批标题
  applicantId: string;        // 申请人ID
  status: ProcessStatus;      // 当前状态
  currentNodeId: string;      // 当前节点ID
  variables: Record<string, Object>; // 流程变量
  createTime: number;         // 创建时间
  updateTime: number;         // 更新时间
}

// 审批记录
export interface ApprovalRecord {
  recordId: string;           // 记录ID
  instanceId: string;         // 实例ID
  nodeId: string;             // 节点ID
  approverId: string;         // 审批人ID
  action: ApprovalAction;     // 审批动作
  comment: string;            // 审批意见
  timestamp: number;          // 审批时间
}

进阶用法:条件分支与并行审批

状态机的核心逻辑——根据条件决定流转方向,处理并行审批。

// WorkflowStateMachine.ets - 工作流状态机
import {
  ProcessDefinition, ProcessInstance, ProcessStatus,
  NodeDefinition, NodeType, EdgeDefinition,
  ApprovalAction, ApprovalRecord
} from './WorkflowEngine';

export class WorkflowStateMachine {
  private definition: ProcessDefinition;

  constructor(definition: ProcessDefinition) {
    this.definition = definition;
  }

  // 创建流程实例
  createInstance(
    applicantId: string,
    title: string,
    variables: Record<string, Object>
  ): ProcessInstance {
    // 找到起始节点
    const startNode = this.definition.nodes.find(n => n.nodeType === NodeType.START);
    if (!startNode) {
      throw new Error('流程定义缺少起始节点');
    }

    // 找到起始节点的下一个节点
    const nextNode = this.getNextNode(startNode.nodeId, variables);

    return {
      instanceId: `inst_${Date.now()}_${Math.random().toString(36).substring(2, 8)}`,
      processId: this.definition.processId,
      title,
      applicantId,
      status: ProcessStatus.PENDING,
      currentNodeId: nextNode?.nodeId || '',
      variables: { ...this.definition.variables, ...variables },
      createTime: Date.now(),
      updateTime: Date.now(),
    };
  }

  // 处理审批动作
  processApproval(
    instance: ProcessInstance,
    approverId: string,
    action: ApprovalAction,
    comment: string
  ): ProcessInstance {
    const updatedInstance = { ...instance, updateTime: Date.now() };

    switch (action) {
      case ApprovalAction.APPROVE:
        return this.handleApprove(updatedInstance, approverId, comment);
      case ApprovalAction.REJECT:
        return this.handleReject(updatedInstance, approverId, comment);
      case ApprovalAction.TRANSFER:
        return this.handleTransfer(updatedInstance, approverId, comment);
      case ApprovalAction.WITHDRAW:
        return this.handleWithdraw(updatedInstance, approverId);
      default:
        return updatedInstance;
    }
  }

  // 处理"同意"
  private handleApprove(
    instance: ProcessInstance,
    approverId: string,
    comment: string
  ): ProcessInstance {
    const currentNode = this.getNode(instance.currentNodeId);
    if (!currentNode) return instance;

    if (currentNode.nodeType === NodeType.COUNTERSIGN) {
      // 会签:需要所有人同意
      // 简化实现:检查是否所有审批人都已同意
      const allApproved = this.checkCountersignComplete(instance, approverId);
      if (!allApproved) {
        // 会签未完成,状态不变
        return { ...instance, status: ProcessStatus.APPROVING };
      }
    }

    // 当前节点通过,查找下一个节点
    const nextNode = this.getNextNode(instance.currentNodeId, instance.variables);

    if (!nextNode || nextNode.nodeType === NodeType.END) {
      // 没有下一个节点或下一个是结束节点,流程通过
      return {
        ...instance,
        status: ProcessStatus.APPROVED,
        currentNodeId: '',
        updateTime: Date.now(),
      };
    }

    // 流转到下一个节点
    return {
      ...instance,
      status: ProcessStatus.APPROVING,
      currentNodeId: nextNode.nodeId,
      updateTime: Date.now(),
    };
  }

  // 处理"拒绝"
  private handleReject(
    instance: ProcessInstance,
    approverId: string,
    comment: string
  ): ProcessInstance {
    return {
      ...instance,
      status: ProcessStatus.REJECTED,
      updateTime: Date.now(),
    };
  }

  // 处理"转办"
  private handleTransfer(
    instance: ProcessInstance,
    approverId: string,
    comment: string
  ): ProcessInstance {
    // 转办不改变流程状态,只是换了审批人
    // 实际实现中需要解析comment获取转办目标人
    return {
      ...instance,
      status: ProcessStatus.APPROVING,
      updateTime: Date.now(),
    };
  }

  // 处理"撤回"
  private handleWithdraw(
    instance: ProcessInstance,
    approverId: string
  ): ProcessInstance {
    // 只有申请人可以撤回,且只能在第一个审批节点之前
    if (instance.applicantId !== approverId) {
      console.error('[Workflow] 非申请人无法撤回');
      return instance;
    }

    return {
      ...instance,
      status: ProcessStatus.WITHDRAWN,
      updateTime: Date.now(),
    };
  }

  // 获取下一个节点
  private getNextNode(
    currentNodeId: string,
    variables: Record<string, Object>
  ): NodeDefinition | null {
    // 找到从当前节点出发的所有连线
    const outEdges = this.definition.edges.filter(
      edge => edge.fromNodeId === currentNodeId
    );

    if (outEdges.length === 0) return null;

    // 评估条件,找到满足条件的连线
    for (const edge of outEdges) {
      if (!edge.conditionExpression) {
        // 无条件连线,直接走
        return this.getNode(edge.toNodeId);
      }

      // 评估条件表达式
      if (this.evaluateCondition(edge.conditionExpression, variables)) {
        return this.getNode(edge.toNodeId);
      }
    }

    // 没有满足条件的连线,走默认(无条件)连线
    const defaultEdge = outEdges.find(e => !e.conditionExpression);
    if (defaultEdge) {
      return this.getNode(defaultEdge.toNodeId);
    }

    return null;
  }

  // 评估条件表达式
  // 支持简单表达式如: "amount >= 5000", "days > 3"
  private evaluateCondition(
    expression: string,
    variables: Record<string, Object>
  ): boolean {
    try {
      // 简化实现:替换变量后用Function求值
      // 生产环境应该用专门的表达式引擎,避免安全风险
      let evalExpr = expression;
      for (const [key, value] of Object.entries(variables)) {
        evalExpr = evalExpr.replace(new RegExp(`\\b${key}\\b`, 'g'), String(value));
      }

      // 安全检查:只允许数字、比较运算符、逻辑运算符
      if (!/^[\d\s><=!&|().]+$/.test(evalExpr)) {
        console.error(`[Workflow] 不安全的表达式: ${expression}`);
        return false;
      }

      // 使用Function安全求值
      const result = new Function(`return ${evalExpr}`)();
      return Boolean(result);
    } catch (error) {
      console.error(`[Workflow] 条件评估失败: ${expression}, ${error}`);
      return false;
    }
  }

  // 检查会签是否完成
  private checkCountersignComplete(
    instance: ProcessInstance,
    currentApproverId: string
  ): boolean {
    // 简化实现:实际需要查询所有审批记录
    // 这里假设会签节点需要所有approverIds都审批通过
    const currentNode = this.getNode(instance.currentNodeId);
    if (!currentNode) return true;

    // 生产环境中从数据库查询已审批记录
    // 这里简化返回true
    console.info(`[Workflow] 会签检查: 节点${instance.currentNodeId}`);
    return true;
  }

  // 根据节点ID获取节点定义
  private getNode(nodeId: string): NodeDefinition | null {
    return this.definition.nodes.find(n => n.nodeId === nodeId) || null;
  }

  // 获取当前节点的审批人列表
  getCurrentApprovers(instance: ProcessInstance): string[] {
    const node = this.getNode(instance.currentNodeId);
    return node?.approverIds || [];
  }

  // 判断用户是否可以审批当前节点
  canApprove(instance: ProcessInstance, userId: string): boolean {
    const approvers = this.getCurrentApprovers(instance);
    return approvers.includes(userId);
  }
}

完整示例:审批流程页面

把状态机、条件分支、审批操作串起来,做一个完整的审批页面。

// ApprovalPage.ets - 审批流程页面
import {
  WorkflowStateMachine, ProcessDefinition, ProcessInstance,
  ProcessStatus, NodeType, ApprovalAction, NodeDefinition
} from '../workflow/WorkflowEngine';
import { relationalStore } from '@kit.ArkData';

// 请假审批流程定义
const LEAVE_PROCESS: ProcessDefinition = {
  processId: 'leave_approval',
  processName: '请假审批',
  variables: { days: 0, type: '' },
  nodes: [
    { nodeId: 'start', nodeName: '发起', nodeType: NodeType.START, approverIds: [] },
    { nodeId: 'leader', nodeName: '直属领导审批', nodeType: NodeType.APPROVAL, approverIds: ['leader_001'] },
    { nodeId: 'manager', nodeName: '部门经理审批', nodeType: NodeType.APPROVAL, approverIds: ['manager_001'], conditionExpression: 'days > 3' },
    { nodeId: 'hr', nodeName: 'HR备案', nodeType: NodeType.APPROVAL, approverIds: ['hr_001'] },
    { nodeId: 'end', nodeName: '结束', nodeType: NodeType.END, approverIds: [] },
  ],
  edges: [
    { edgeId: 'e1', fromNodeId: 'start', toNodeId: 'leader' },
    { edgeId: 'e2', fromNodeId: 'leader', toNodeId: 'manager', conditionExpression: 'days > 3', label: '请假超过3天' },
    { edgeId: 'e3', fromNodeId: 'leader', toNodeId: 'hr', label: '请假3天以内' },
    { edgeId: 'e4', fromNodeId: 'manager', toNodeId: 'hr' },
    { edgeId: 'e5', fromNodeId: 'hr', toNodeId: 'end' },
  ],
};

@Entry
@Component
struct ApprovalPage {
  @State currentInstance: ProcessInstance | null = null;
  @State approvalRecords: ApprovalRecord[] = [];
  @State commentText: string = '';
  @State isLoading: boolean = false;

  private stateMachine: WorkflowStateMachine = new WorkflowStateMachine(LEAVE_PROCESS);

  aboutToAppear(): void {
    // 模拟加载一个审批实例
    this.loadProcessInstance();
  }

  // 加载审批实例
  private loadProcessInstance(): void {
    this.currentInstance = this.stateMachine.createInstance(
      'user_001',
      '张三的请假申请',
      { days: 5, type: '年假' }
    );
    this.approvalRecords = [];
  }

  build() {
    Column() {
      // 顶部导航
      this.TitleBar()

      // 审批内容
      Scroll() {
        Column() {
          // 审批信息卡片
          this.ApprovalInfoCard()

          // 审批流程进度
          this.ApprovalProgress()

          // 审批记录
          this.ApprovalHistory()

          // 审批操作区
          if (this.currentInstance?.status === ProcessStatus.APPROVING ||
              this.currentInstance?.status === ProcessStatus.PENDING) {
            this.ApprovalActions()
          }
        }
        .padding(16)
      }
      .layoutWeight(1)
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
  }

  // 顶部导航栏
  @Builder
  TitleBar() {
    Row() {
      Image($r('app.media.ic_back'))
        .width(24)
        .height(24)
        .fillColor('#333333')
        .onClick(() => {
          // 返回上一页
        })

      Text('审批详情')
        .fontSize(18)
        .fontWeight(FontWeight.Bold)
        .margin({ left: 12 })

      Blank()

      Text(this.getStatusText(this.currentInstance?.status || ProcessStatus.DRAFT))
        .fontSize(14)
        .fontColor(this.getStatusColor(this.currentInstance?.status || ProcessStatus.DRAFT))
    }
    .width('100%')
    .height(56)
    .padding({ left: 16, right: 16 })
    .backgroundColor(Color.White)
  }

  // 审批信息卡片
  @Builder
  ApprovalInfoCard() {
    Column() {
      Row() {
        Text('请假申请')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .layoutWeight(1)

        Text(this.currentInstance?.title || '')
          .fontSize(14)
          .fontColor('#666666')
      }
      .width('100%')

      // 申请详情
      GridRow({ columns: 2, gutter: 12 }) {
        GridCol() {
          this.InfoItem('申请人', '张三')
        }
        GridCol() {
          this.InfoItem('请假类型', '年假')
        }
        GridCol() {
          this.InfoItem('请假天数', '5天')
        }
        GridCol() {
          this.InfoItem('申请时间', '2026-06-25')
        }
      }
      .margin({ top: 16 })
    }
    .width('100%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
  }

  // 信息项
  @Builder
  InfoItem(label: string, value: string) {
    Column() {
      Text(label)
        .fontSize(12)
        .fontColor('#999999')
      Text(value)
        .fontSize(15)
        .fontColor('#333333')
        .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
  }

  // 审批流程进度
  @Builder
  ApprovalProgress() {
    Column() {
      Text('审批流程')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .margin({ bottom: 16 })

      // 流程节点时间线
      ForEach(LEAVE_PROCESS.nodes.filter(n => n.nodeType !== NodeType.START && n.nodeType !== NodeType.END),
        (node: NodeDefinition) => {
          Row() {
            // 节点状态指示器
            Column() {
              Circle()
                .width(12)
                .height(12)
                .fill(this.getNodeColor(node.nodeId))
                .margin({ top: 4 })
              
              if (node.nodeId !== 'hr') {
                // 连接线
                Line()
                  .width(2)
                  .height(40)
                  .backgroundColor('#E0E0E0')
                  .margin({ top: 4 })
              }
            }
            .alignItems(HorizontalAlign.Center)

            // 节点信息
            Column() {
              Text(node.nodeName)
                .fontSize(15)
                .fontWeight(FontWeight.Medium)
              Text(this.getNodeStatusText(node.nodeId))
                .fontSize(12)
                .fontColor('#999999')
                .margin({ top: 2 })
            }
            .alignItems(HorizontalAlign.Start)
            .margin({ left: 12 })
          }
        }
      )
    }
    .width('100%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .margin({ top: 12 })
  }

  // 审批历史记录
  @Builder
  ApprovalHistory() {
    Column() {
      Text('审批记录')
        .fontSize(16)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .margin({ bottom: 12 })

      if (this.approvalRecords.length === 0) {
        Text('暂无审批记录')
          .fontSize(14)
          .fontColor('#999999')
          .width('100%')
          .textAlign(TextAlign.Center)
          .padding(24)
      } else {
        ForEach(this.approvalRecords, (record: ApprovalRecord) => {
          Row() {
            Column() {
              Text(record.approverId)
                .fontSize(14)
                .fontWeight(FontWeight.Medium)
              Text(record.comment)
                .fontSize(13)
                .fontColor('#666666')
                .margin({ top: 4 })
            }
            .alignItems(HorizontalAlign.Start)
            .layoutWeight(1)

            Text(this.getActionText(record.action))
              .fontSize(13)
              .fontColor(record.action === ApprovalAction.APPROVE ? '#2E7D32' : '#C62828')
          }
          .width('100%')
          .padding({ top: 8, bottom: 8 })
        })
      }
    }
    .width('100%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .margin({ top: 12 })
  }

  // 审批操作区
  @Builder
  ApprovalActions() {
    Column() {
      // 审批意见输入
      TextArea({ placeholder: '请输入审批意见...' })
        .width('100%')
        .height(80)
        .fontSize(14)
        .borderRadius(8)
        .backgroundColor('#F5F5F5')
        .onChange((value: string) => {
          this.commentText = value;
        })

      // 操作按钮
      Row() {
        Button('拒绝')
          .width('45%')
          .height(44)
          .fontSize(16)
          .fontColor('#C62828')
          .backgroundColor('#FFEBEE')
          .borderRadius(8)
          .onClick(() => this.handleApproval(ApprovalAction.REJECT))

        Button('同意')
          .width('45%')
          .height(44)
          .fontSize(16)
          .fontColor('#FFFFFF')
          .backgroundColor('#1565C0')
          .borderRadius(8)
          .onClick(() => this.handleApproval(ApprovalAction.APPROVE))
      }
      .width('100%')
      .justifyContent(FlexAlign.SpaceBetween)
      .margin({ top: 12 })

      // 更多操作
      Row() {
        Text('转办')
          .fontSize(14)
          .fontColor('#666666')
          .padding(8)
          .onClick(() => this.handleApproval(ApprovalAction.TRANSFER))

        Text('加签')
          .fontSize(14)
          .fontColor('#666666')
          .padding(8)
          .onClick(() => this.handleApproval(ApprovalAction.ADD_SIGN))
      }
      .width('100%')
      .justifyContent(FlexAlign.End)
      .margin({ top: 8 })
    }
    .width('100%')
    .padding(16)
    .borderRadius(12)
    .backgroundColor(Color.White)
    .margin({ top: 12 })
  }

  // 处理审批操作
  private handleApproval(action: ApprovalAction): void {
    if (!this.currentInstance) return;

    this.isLoading = true;

    // 记录审批操作
    const record: ApprovalRecord = {
      recordId: `rec_${Date.now()}`,
      instanceId: this.currentInstance.instanceId,
      nodeId: this.currentInstance.currentNodeId,
      approverId: 'current_user',
      action,
      comment: this.commentText,
      timestamp: Date.now(),
    };

    this.approvalRecords.push(record);

    // 状态机处理
    this.currentInstance = this.stateMachine.processApproval(
      this.currentInstance,
      'current_user',
      action,
      this.commentText
    );

    this.commentText = '';
    this.isLoading = false;

    // 发送事件通知
    emitter.emit({ eventId: 1001 }, {
      data: {
        action: 'approval_updated',
        instanceId: this.currentInstance.instanceId,
        status: this.currentInstance.status,
      }
    });
  }

  // ========== 辅助方法 ==========

  private getStatusText(status: ProcessStatus): string {
    const map: Record<string, string> = {
      [ProcessStatus.DRAFT]: '草稿',
      [ProcessStatus.PENDING]: '待审批',
      [ProcessStatus.APPROVING]: '审批中',
      [ProcessStatus.APPROVED]: '已通过',
      [ProcessStatus.REJECTED]: '已拒绝',
      [ProcessStatus.WITHDRAWN]: '已撤回',
      [ProcessStatus.TERMINATED]: '已终止',
    };
    return map[status] || '未知';
  }

  private getStatusColor(status: ProcessStatus): string {
    const map: Record<string, string> = {
      [ProcessStatus.APPROVED]: '#2E7D32',
      [ProcessStatus.REJECTED]: '#C62828',
      [ProcessStatus.APPROVING]: '#E65100',
      [ProcessStatus.PENDING]: '#1565C0',
    };
    return map[status] || '#999999';
  }

  private getNodeColor(nodeId: string): string {
    if (!this.currentInstance) return '#E0E0E0';
    // 当前节点之前的节点显示绿色,当前节点显示蓝色,之后的显示灰色
    const nodeIndex = LEAVE_PROCESS.nodes.findIndex(n => n.nodeId === nodeId);
    const currentIndex = LEAVE_PROCESS.nodes.findIndex(n => n.nodeId === this.currentInstance!.currentNodeId);
    if (nodeIndex < currentIndex) return '#2E7D32';
    if (nodeIndex === currentIndex) return '#1565C0';
    return '#E0E0E0';
  }

  private getNodeStatusText(nodeId: string): string {
    if (!this.currentInstance) return '等待中';
    const nodeIndex = LEAVE_PROCESS.nodes.findIndex(n => n.nodeId === nodeId);
    const currentIndex = LEAVE_PROCESS.nodes.findIndex(n => n.nodeId === this.currentInstance!.currentNodeId);
    if (nodeIndex < currentIndex) return '已通过';
    if (nodeIndex === currentIndex) return '审批中';
    return '等待中';
  }

  private getActionText(action: ApprovalAction): string {
    const map: Record<string, string> = {
      [ApprovalAction.APPROVE]: '同意',
      [ApprovalAction.REJECT]: '拒绝',
      [ApprovalAction.TRANSFER]: '转办',
      [ApprovalAction.ADD_SIGN]: '加签',
      [ApprovalAction.WITHDRAW]: '撤回',
    };
    return map[action] || '未知';
  }
}

踩坑与注意事项

坑1:条件表达式别用eval

上面的示例中用了new Function()来评估条件表达式,这在生产环境是安全隐患——恶意注入一个表达式就能执行任意代码。

正确做法:自己写一个简单的表达式解析器,只支持变量名 + 比较运算符 + 常量这种格式。或者用ANTLR之类的解析器生成工具,生成一个安全的表达式引擎。

坑2:会签的"全部通过"判断

会签节点需要所有人同意才算通过,但"所有人"是动态的——有人请假了怎么办?有人离职了怎么办?

建议:会签节点的审批人列表在流程启动时就确定,中途不增不减。如果需要加人,走"加签"操作,加签的人单独处理。

坑3:并行审批的回退问题

并行审批中,财务已经同意了,法务还在审,这时候申请人要撤回——财务的审批记录怎么处理?

方案一:不允许撤回,只能等所有人审完再整体处理
方案二:允许撤回,但已审批的记录标记为"已失效"
方案三:不允许撤回,但可以由审批人主动"退回"

大多数企业选方案三——审批人可以退回,申请人不能撤回。因为审批人已经做了决策,撤回等于决策白做。

坑4:流程定义的版本管理

流程定义改了怎么办?旧流程还在跑的实例用新定义还是旧定义?

原则已启动的流程用旧定义,新启动的流程用新定义。流程定义必须有版本号,流程实例记录它用的是哪个版本。

坑5:超时提醒别忘做

审批节点设置了超时时间(比如24小时),超时了要自动提醒审批人。但超时提醒不是一次性的——第1小时提醒一次,第4小时提醒一次,第24小时升级到上级。

建议:用定时任务扫描超时的审批节点,按超时时长分级提醒。

坑6:审批意见不能为空

很多审批人习惯只点"同意"不写意见。出了问题追溯时,审批记录里只有"同意"两个字,完全不知道审批依据是什么。

建议:拒绝时强制要求填写意见,同意时可以选填但给出提示。关键审批节点(如财务、法务)必须填写意见。

HarmonyOS 6适配说明

HarmonyOS 6对工作流相关能力的增强:

  1. 后台任务增强:审批超时提醒需要后台定时任务。HarmonyOS 6增强了BackgroundTaskManager,支持更灵活的定时任务调度,不再受10分钟限制。
// HarmonyOS 6 后台定时任务
import { backgroundTaskManager } from '@kit.BackgroundTasksKit';

// 注册审批超时检查任务
async function registerTimeoutCheck(): Promise<void> {
  const request: backgroundTaskManager.ContinuousTaskRequest = {
    taskName: 'approval_timeout_check',
    taskParam: {
      interval: 3600000, // 每小时检查一次
      action: 'check_timeout',
    },
  };
  await backgroundTaskManager.startContinuousTask(request);
}
  1. 通知增强:审批提醒支持富文本通知,可以直接在通知栏显示审批详情和快捷操作按钮,不用打开App就能同意或拒绝。

  2. 分布式流转:审批操作可以跨设备流转。你在手机上看到审批通知,流转到PC上打开审批详情,操作体验更流畅。

  3. AI辅助审批:新增AI审批建议能力,根据历史审批记录和规则,自动推荐"同意"或"拒绝",减少审批人的决策负担。

总结

工作流引擎的核心是状态机——搞清楚节点、连线、条件、动作这四个概念,审批流程就理顺了。条件分支决定"去哪",并行审批处理"同时去多个地方",会签/或签处理"多人怎么决策"。

核心记住三点:

  • 流程定义和流程实例分离,定义是模板,实例是具体的审批单,别混在一起
  • 条件表达式要安全,别用eval/Function,自己写解析器或用安全引擎
  • 并行审批的回退要提前想清楚,等出了问题再补逻辑,代码就成了一团乱麻
评估维度 说明
学习难度 ⭐⭐⭐⭐ 状态机概念不难,但条件分支和并行审批的组合场景很复杂
使用频率 ⭐⭐⭐⭐⭐ OA系统的核心,每个企业都有审批需求
重要程度 ⭐⭐⭐⭐⭐ 工作流做不好,整个OA就是摆设

审批流程是OA的灵魂。没有工作流引擎的OA,就像没有发动机的汽车——壳子再好看也跑不起来。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。