PHP项目实现用户自定义审批条件:从零搭建灵活工作流引擎
目录导读
-
为什么需要用户自定义审批条件?

-
核心设计思路:条件表达式与规则引擎
-
数据库表结构设计(含ER图说明)
-
PHP实现代码骨架:条件解析与匹配
-
前端交互:如何让用户“拖拽式”配置条件
-
高级扩展:多级审批、会签、条件分支
-
安全与性能考量(防注入、缓存策略)
-
常见问题问答(基于实际开发痛点)
为什么需要用户自定义审批条件?
在企业级PHP项目中,审批流程往往不是固定的。
- 费用报销:金额小于1000元由部门经理审批,超过5000元需要总经理介入。
- 采购订单:根据供应商等级、采购品类、紧急程度自动跳转不同审批节点。
如果将这些条件硬编码在代码中,每次业务调整都需要修改PHP文件并重新上线,成本极高。用户自定义审批条件的核心价值在于:让非技术用户(HR、财务、运营)通过可视化界面配置“....”逻辑,PHP系统自动解析执行,无需开发介入。
核心设计思路:条件表达式与规则引擎
1 条件的三要素
- 字段(Field):如
amount(金额)、department(部门)、supplier_level(供应商等级)。 - 运算符(Operator):
>、<、、IN、BETWEEN、LIKE。 - 值(Value):1000、'研发部'、[1,2,3]。
2 规则引擎架构
我们采用 “表达式树” 模式:每个条件是一个节点,多个节点通过 AND/OR 组合成条件组。
- 单体条件:
[field:amount, operator:>=, value:5000] - 条件组:
{AND: [条件1, 条件2]}或{OR: [条件3, 条件4]}
为什么不直接用PHP eval?
eval存在严重安全风险,我们使用语义化解析:将JSON条件转换为可信任的PHP逻辑,仅允许预设字段和运算符。
数据库表结构设计(含ER图说明)
我们使用4张核心表来存储自定义条件:
approval_flow (审批流主表)
├── id (PK)
├── name (流程名称,如“报销审批”)
├── status (启用/停用)
└── created_at
approval_node (审批节点表)
├── id (PK)
├── flow_id (FK)
├── node_name (节点名称,如“部门经理审批”)
├── order_index (节点排序)
├── assignee_type (审批人类型:user/role/dept)
├── assignee_id (具体审批人ID)
└── condition_group (存储条件树的JSONB字段)
approval_node_condition (条件明细表,可选,替代JSONB)
├── id
├── node_id
├── field_name
├── operator
├── value
├── logical_operator (AND/OR,用于组内连接)
└── parent_group_id (嵌套分组,支持多级)
approval_record (审批记录表)
├── id
├── flow_id
├── submitter_id
├── current_node_id
├── form_data (JSON,存储提交的具体字段值)
└── status
关键设计点:
condition_group 字段存储结构如:
{
"groups": [
{
"logic": "AND",
"conditions": [
{"field": "amount", "op": ">=", "value": 5000},
{"field": "dept", "op": "=", "value": "finance"}
]
},
{
"logic": "OR",
"conditions": [
{"field": "urgent", "op": "=", "value": 1}
]
}
],
"rootLogic": "AND"
}
这种结构支持无限层级嵌套,且易于PHP递归解析。
PHP实现代码骨架:条件解析与匹配
1 条件解析器类
class ConditionParser {
private $allowedFields = ['amount', 'dept', 'urgent', 'supplier_level']; // 白名单
private $allowedOperators = ['>=', '<=', '=', '!=' , 'in', 'between', 'like'];
public function evaluate($conditionGroup, $formData) {
$rootLogic = $conditionGroup['rootLogic'] ?? 'AND';
$results = [];
foreach ($conditionGroup['groups'] as $group) {
$results[] = $this->evaluateGroup($group, $formData);
}
return $this->applyLogic($results, $rootLogic);
}
private function evaluateGroup($group, $formData) {
$logic = $group['logic'] ?? 'AND';
$conditionResults = [];
foreach ($group['conditions'] as $cond) {
// 检查字段白名单
if (!in_array($cond['field'], $this->allowedFields)) {
throw new \Exception("Field {$cond['field']} not allowed");
}
$conditionResults[] = $this->evaluateSingle($cond, $formData);
}
return $this->applyLogic($conditionResults, $logic);
}
private function evaluateSingle($cond, $formData) {
$fieldValue = $formData[$cond['field']] ?? null;
$op = $cond['operator'];
$targetValue = $cond['value'];
// 匹配运算符
switch ($op) {
case '>=': return $fieldValue >= $targetValue;
case '=': return $fieldValue == $targetValue;
case 'in': return in_array($fieldValue, $targetValue);
// ... 其他运算符
}
return false;
}
}
2 在审批流程中调用
class ApprovalEngine {
public function findNextNode($flowId, $formData) {
$nodes = ApprovalNode::where('flow_id', $flowId)->orderBy('order_index')->get();
foreach ($nodes as $node) {
$conditionGroup = json_decode($node->condition_group, true);
if ($conditionGroup && !empty($conditionGroup['groups'])) {
$parser = new ConditionParser();
$matched = $parser->evaluate($conditionGroup, $formData);
if ($matched) {
return $node; // 命中条件,返回该节点
}
} else {
// 无条件的节点作为默认节点
return $node;
}
}
return null;
}
}
前端交互:如何让用户“拖拽式”配置条件
推荐使用成熟的表单生成器库(如 form-builder 或 react-json-schema-form),构建一个“条件配置面板”:
- 左侧字段池:动态拉取系统预定义的字段列表(如
金额、部门、紧急程度)。 - 中间画布:用户拖入字段,选择运算符,填写值,每个条件块可拖拽排序、嵌套(点击“添加条件组”)。
- 右侧预览:实时生成JSON结构,并可模拟测试(输入测试数据,点击“验证”)。
关键注意:
- 字段动态来源:通过API
GET /api/fields?flow_type=expense返回可用字段列表。 - 运算符约束:根据字段类型显示对应运算符(如
金额只显示>=、<=、BETWEEN;部门显示 、IN)。
高级扩展:多级审批、会签、条件分支
1 多级审批链
在 approval_node 中添加 parent_node_id 自关联,形成“审批链”,条件引擎按序查找,若当前节点不符合条件,则检查 parent_node_id 指向上级节点。
2 会签(多人审批)
在节点配置中增加 sign_type(any 任一通过 / all 全部通过),当条件匹配进入该节点后,系统生成多个审批任务,根据会签规则统计通过数。
3 条件分支 + 并行节点
借助 condition_group 的嵌套分组,实现类似 IF-ELSE 逻辑:group1 条件->节点A;group2 条件->节点B,若所有条件均不匹配,则走默认节点(无条件的回退节点)。
安全与性能考量(防注入、缓存策略)
1 安全防护
- 字段白名单:不允许用户输入任意字段名,仅限预先注册的字段。
- 值类型校验:金额必须为数字,部门必须匹配预设枚举值。
- 避免
eval:绝对禁止使用eval动态执行条件。
2 性能优化
- 条件缓存:每次用户更新条件后,将解析后的条件表达式(如
$amount >= 5000 AND $dept == 'finance')转化为PHP闭包函数,存入Redis。 - 批量匹配:提交表单时,一次性加载流的所有节点条件到内存,避免频繁DB查询。
- 索引设计:
approval_node.flow_id和approval_node.order_index建立联合索引。
常见问题问答(基于实际开发痛点)
Q1:条件缓存突然失效,如何保证一致性?
A:采用“事件驱动+缓存标记”模式,当用户修改审批流条件时,删除该流的缓存key,并在下次请求时重建,同时设置缓存过期时间(如30分钟),防止缓存雪崩。
Q2:嵌套条件太复杂,用户配置出错怎么办?
A:前端做“条件模拟测试”按钮:用户输入模拟表单数据,系统实时解析并显示“匹配节点A”,限制最大嵌套层数(建议不超过5层)。
Q3:条件中支持动态变量(如当前时间、发起人)吗?
A:支持,在字段列表中加入系统变量(如{submitter.dept}、{current_time}),解析时动态替换,需注意性能:每次条件匹配都需重新计算动态值。
Q4:PHP处理大量条件匹配时性能够吗?
A:日常OA场景完全足够,若面对高并发(例如每天10万+审批提交),建议将条件解析逻辑下沉到C扩展(如使用Swoole自定义进程)或直接使用Redis+Lua脚本进行条件匹配。
通过以上设计,你的PHP项目将实现一个灵活、安全、可维护的用户自定义审批条件系统,核心原则是:前端负责生成结构化JSON,PHP负责白名单解析和执行,数据库以JSONB或关系表存储逻辑,缓存层提升效率,这不仅满足SEO优化中的“用户意图匹配”,更能在实际业务中让非技术人员自主调整流程,真正实现办公自动化。