HarmonyOS开发:审批流程——工作流引擎
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 → 部门经理审批 |
| 变量 | 流程中用到的业务数据 | 金额、天数、部门 |
| 动作 | 节点上可以执行的操作 | 同意、拒绝、转办、加签 |
状态机设计
审批单的状态流转:
草稿 → 待审批 → 审批中 → 已通过
↓
已拒绝 → 已撤回
↓
已终止
每个审批节点的状态:
未到达 → 待处理 → 已通过
↓
已拒绝
↓
已转办
流转规则引擎
流转规则决定了审批单"下一步去哪"。核心逻辑:
- 当前节点审批完成
- 检查所有出线(从当前节点出发的连线)
- 评估每条出线的条件表达式
- 满足条件的出线指向的节点就是下一个节点
- 如果多条出线都满足,走"并行审批"模式
代码实战
基础用法:审批节点与状态机
先定义审批流程的数据模型,再实现状态机驱动。
// 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对工作流相关能力的增强:
- 后台任务增强:审批超时提醒需要后台定时任务。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);
}
-
通知增强:审批提醒支持富文本通知,可以直接在通知栏显示审批详情和快捷操作按钮,不用打开App就能同意或拒绝。
-
分布式流转:审批操作可以跨设备流转。你在手机上看到审批通知,流转到PC上打开审批详情,操作体验更流畅。
-
AI辅助审批:新增AI审批建议能力,根据历史审批记录和规则,自动推荐"同意"或"拒绝",减少审批人的决策负担。
总结
工作流引擎的核心是状态机——搞清楚节点、连线、条件、动作这四个概念,审批流程就理顺了。条件分支决定"去哪",并行审批处理"同时去多个地方",会签/或签处理"多人怎么决策"。
核心记住三点:
- 流程定义和流程实例分离,定义是模板,实例是具体的审批单,别混在一起
- 条件表达式要安全,别用eval/Function,自己写解析器或用安全引擎
- 并行审批的回退要提前想清楚,等出了问题再补逻辑,代码就成了一团乱麻
| 评估维度 | 说明 |
|---|---|
| 学习难度 | ⭐⭐⭐⭐ 状态机概念不难,但条件分支和并行审批的组合场景很复杂 |
| 使用频率 | ⭐⭐⭐⭐⭐ OA系统的核心,每个企业都有审批需求 |
| 重要程度 | ⭐⭐⭐⭐⭐ 工作流做不好,整个OA就是摆设 |
审批流程是OA的灵魂。没有工作流引擎的OA,就像没有发动机的汽车——壳子再好看也跑不起来。
- 点赞
- 收藏
- 关注作者
评论(0)