本文目录导读:

- 核心概念:从“硬编码”到“流程引擎”
- 总体架构方案
- 步骤一:流程设计器(前端实现)
- 步骤二:流程定义存储(后端数据库)
- 步骤三:流程引擎(核心PHP逻辑)
- 步骤四:集成机器人(钉钉/飞书/企业微信)
- 安全性、性能与扩展性考量
- 简化实现:基于“角色/用户”的硬编码 + 配置表
- 总结建议
这是一个比较复杂的工程问题,在PHP项目中实现“用户自定义审批机器人”,核心在于将审批流程的“逻辑”与“代码”解耦,让用户通过图形化界面或配置来定义流程,而不是让开发人员为每个流程写死代码。
下面我为你梳理一个完整的技术实现思路和架构方案,分为几个核心环节。
核心概念:从“硬编码”到“流程引擎”
传统方式:if (请假天数 > 3) { 找总监审批; } else { 找经理审批; }
用户自定义方式:用户自己画一个流程图:“请假 -> 条件判断天数 > 3 -> 总监审批 -> 结束” 或 “请假 -> 经理审批 -> 结束”。
总体架构方案
一个完整的审批机器人系统通常包含以下模块:
- 流程设计器(前端):用户画流程图的界面。
- 流程定义存储(后端):存储用户设计的流程图数据(JSON/XML)。
- 流程引擎(核心后端):解析流程图,驱动流程流转,调用通知、审批等动作。
- 机器人/通知模块(集成):连接钉钉、飞书、企业微信或邮件,发送审批任务和通知。
流程设计器(前端实现)
用户需要能“画”出审批流程,这通常使用可视化拖拽库实现。
- 技术选型:推荐使用
bpmn-js(集成了BPMN 2.0标准,比较专业) 或国内更开箱即用的LogicFlow、GGEditor。 - 要存储的数据结构:设计器最终会生成一个JSON对象,用于描述整个流程,一个简单的自定义格式例子如下:
// 一个简单的请假流程定义
{
"id": "leave_flow_001",
"name": "普通员工请假流程",
"nodes": [
{ "id": "start", "type": "start", "name": "发起申请", "assignee": "user:{{initiator}}" },
{ "id": "seq_1", "type": "condition", "name": "判断天数", "expression": "{{leave_days}} <= 3" },
{ "id": "task_manager", "type": "approval", "name": "经理审批", "assignee": "role:manager" },
{ "id": "task_director", "type": "approval", "name": "总监审批", "assignee": "role:director" },
{ "id": "end", "type": "end", "name": "结束" }
],
"edges": [
{ "from": "start", "to": "seq_1" },
{ "from": "seq_1", "to": "task_manager", "condition": true },
{ "from": "seq_1", "to": "task_director", "condition": false },
{ "from": "task_manager", "to": "end" },
{ "from": "task_director", "to": "end" }
]
}
Node节点类型:start, end, condition (条件/排他网关), approval (审批人节点), notify (通知节点) 等。
流程定义存储(后端数据库)
将用户设计的流程图JSON存入数据库。
- 表结构设计:
workflow_definitions表:idnamedescriptiondefinition_json(TEXT, 存储上面的JSON)status(draft/active/archived)created_atupdated_atcreated_by
workflow_instances表: (每发起一次审批,生成一条记录)iddefinition_idinitiator_user_idform_data(TEXT, 表单填写的数据)status(pending/approved/rejected/cancelled)created_atupdated_at
workflow_tasks表: (审批节点实例)idinstance_idnode_id(对应流程定义里的node id)nameassignee_user_idstatus(pending/approved/rejected)commentoperated_at
流程引擎(核心PHP逻辑)
这是最难啃的骨头,你需要一个PHP服务来解析流程定义JSON,并根据当前状态驱动流程到下一个节点。
核心逻辑(伪代码):
<?php
class WorkflowEngine
{
public function advanceWorkflow($instanceId, $action, $userId, $comment)
{
// 1. 加载实例和工作流定义
$instance = $this->getWorkflowInstance($instanceId);
$definition = $this->getWorkflowDefinition($instance->definition_id);
$flow = json_decode($definition->definition_json, true);
// 2. 找到当前在等待的审批任务 (pending状态的task)
$currentTask = $this->getCurrentPendingTask($instanceId);
if ($currentTask == null) { /* 流程已结束 */ return; }
// 3. 处理当前节点的动作 (通过/拒绝)
if ($action == 'approve') {
// 更新当前task为approved
$this->updateTaskStatus($currentTask->id, 'approved', $userId, $comment);
// 4. 寻找下一个节点
$nextNodes = $this->findNextNodes($flow, $currentTask->node_id, $instance->form_data);
foreach ($nextNodes as $nextNode) {
if ($nextNode['type'] == 'end') {
// 流程结束,更新instance状态为approved
$this->updateInstanceStatus($instanceId, 'approved');
$this->sendNotification('审批通过', $instance);
} else if ($nextNode['type'] == 'approval') {
// 创建新的审批任务 (pending)
$assignee = $this->resolveAssignee($nextNode['assignee'], $instance->form_data);
$this->createTask($instanceId, $nextNode['id'], $nextNode['name'], $assignee);
$this->sendNotification('新审批任务', $task);
} else if ($nextNode['type'] == 'condition') {
// 条件节点自动判断,继续向前走
$this->advanceToNextNodeAfterCondition($instance, $flow, $nextNode, $instance->form_data);
}
}
} else if ($action == 'reject') {
// 拒绝逻辑:更新当前task为rejected,然后实例状态变为rejected。
$this->updateTaskStatus($currentTask->id, 'rejected', $userId, $comment);
$this->updateInstanceStatus($instanceId, 'rejected');
$this->sendNotification('审批被拒绝', $instance);
}
}
private function findNextNodes($flow, $currentNodeId, $formData)
{
$nextNodes = [];
foreach ($flow['edges'] as $edge) {
if ($edge['from'] == $currentNodeId) {
// 如果是条件边,需要计算表达式的值
if ($edge['condition'] !== null) {
$conditionResult = $this->evaluateExpression($edge['expression'], $formData);
if ($conditionResult == $edge['condition']) {
$nextNodes[] = $this->getNodeById($flow, $edge['to']);
}
} else {
$nextNodes[] = $this->getNodeById($flow, $edge['to']);
}
}
}
return $nextNodes;
}
// 该函数解析用户定义的表达式,"{{leave_days}} <= 3"
private function evaluateExpression($expression, $formData)
{
// 简单实现:用正则替换占位符,eval (有安全风险,需严格控制输入)
// 生产环境建议使用 symfony/expression-language 或自定义沙盒解析器
$search = ['{{leave_days}}'];
$replace = [$formData['leave_days']];
$resolvedExpression = str_replace($search, $replace, $expression);
// 注意:这里使用了eval,需要确保expression来源安全且不能执行任意PHP代码
return eval("return $resolvedExpression;");
}
}
集成机器人(钉钉/飞书/企业微信)
机器人负责推送消息给审批人,并在消息中携带操作按钮(同意/拒绝)。
- 构建消息卡片(Rich Text / 消息卡片):
- “您有一个审批任务待处理”
- 申请人、申请事由、表单数据摘要。
- 按钮:
同意、拒绝。
- 处理按钮回调:
- 用户在机器人消息中点击“同意”或“拒绝”。
- 平台(钉钉/飞书)会发一个 HTTP回调请求 到你的后端。
- 你定义的回调URL需要接收这个请求,解析出
实例ID、任务ID、操作、用户ID。 - 调用
步骤三中的WorkflowEngine::advanceWorkflow()方法。
安全性、性能与扩展性考量
- 表达式安全:
eval()是危险的,对于用户自定义的条件(如{{days}} > 3),建议使用专门的数学表达式库(如php-math-expression)或写一个简单的解析器来安全计算,绝对不要直接用eval执行用户输入的字符串。 - 错误处理:流程引擎必须有完善的日志和异常处理,如通知发送失败,需有重试机制。
- 并发锁:处理回调时(特别是异步消息),必须对实例ID加锁(如Redis锁),防止重复操作或状态错乱。
- 可视化设计器:建议直接使用成熟的BPMN设计器库,不要自己从头造轮子,成本极高且易用性差。
- 低代码方案:如果项目复杂度高,可考虑集成现成的流程引擎(如:Camunda BPM 或 Flowable 的PHP客户端,但不推荐,因为复杂)或使用国内云厂商的低代码平台(如微搭、简道云等,但这会脱离PHP生态)。
简化实现:基于“角色/用户”的硬编码 + 配置表
如果不想实现通用的流程引擎,只想让用户“自定义”审批人,那么可以简化方案:
- 用户配置:在后台创建一个审批规则表:
approval_rules(trigger_type,trigger_value,approver_roles,level)。 trigger_type='leave_days',trigger_value='>3',approver_roles='["director"]'。- 代码中:用户提交表单后,根据规则表查找到对应的审批人,生成任务。
- 优点:实现简单,适合80%的固定审批场景。
- 缺点:无法实现并发、会签、条件分支、动态路由等复杂流程。
总结建议
- 小步快跑:如果业务不太复杂,先采用简化实现(配置表),快速上线验证。
- 确定核心:如果确需通用引擎,先确保
流程设计器和流程引擎这两个核心模块的接口和数据结构设计清晰,不急于接入机器人。 - 复用生态:PHP不是这类复杂工作流引擎的首选语言(Java有Activiti等),如果团队没有大坑经验,建议考虑使用Node.js或Go实现核心引擎,PHP只做API的胶水层和后台管理,或者使用BPMN标准的第三方服务(如Zeebe/Flowable Cloud),PHP调用其API。
- 测试先行:流程引擎的状态机逻辑非常容易出错,务必为每个分支、每个回调、每个节点类型编写充分的单元测试和集成测试。
如果你能提供更具体的场景(只需要一个简单的“接力审批”还是需要“会签、或签、分支”),我可以给出更针对性的代码结构和示例。