本文目录导读:

在PHP项目中实现“流程加签”(即流程审批过程中,当前环节的处理人临时增加额外的审批人)是一个常见且复杂的需求,这需要结合工作流引擎(symfony/workflow、state-machine 或自研引擎)以及灵活的数据库设计来实现。
下面我将从数据库设计、核心逻辑、代码示例 和 注意事项 四个层面,为你提供一套完整的实现方案。
核心思路与数据库设计
加签的本质是:在不改变原始流程定义的情况下,动态地在当前节点插入一个或多个审批人,并要求他们处理完才能流转到下一节点。
数据库表设计(关键)
你需要设计至少两张核心表:
表1: wf_process_node (流程节点实例表)
记录一个审批单在某个节点的状态。
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
INT | 主键 |
process_instance_id |
INT | 流程实例ID |
node_id |
VARCHAR | 节点定义ID (如 "manager_approve") |
node_name |
VARCHAR | 节点名称 (如 "经理审批") |
status |
TINYINT | 0:待处理, 1:通过, 2:拒绝, 3:加签中 |
parent_id |
INT | 关键字段:父节点ID,如果是加签产生的子节点,此字段指向原始节点ID。 |
表2: wf_node_assignee (节点处理人表)
记录每个节点具体由谁处理。
| 字段名 | 类型 | 说明 |
|---|---|---|
id |
INT | 主键 |
node_instance_id |
INT | 关联 wf_process_node.id |
user_id |
INT | 处理人ID |
status |
TINYINT | 0:待处理, 1:已处理, 2:跳过 |
type |
VARCHAR | 关键字段:区分 normal (正常) 还是 countersign (加签人) |
实现逻辑流程图
用户A发起申请 (状态: 经理审批 待处理)
|
v
经理B审批 -> 点击“加签”按钮
|
v
系统操作:
1. 创建子节点 (parent_id = 当前节点ID, status = 加签中)
2. 将当前节点状态改为“加签中”
3. 将部门经理C添加到 子节点的 assignee 表中
|
v
加签人C登录 -> 看到待办 -> 审批通过/拒绝
|
v
系统判断:该子节点下所有加签人是否处理完毕?
|
+--- 是:将父节点状态恢复为“待处理”,子节点状态变为“已完成”,经理B继续审批。
|
+--- 否:等待其他加签人。
PHP 核心代码实现示例 (ThinkPHP/Laravel 风格)
假设我们有 ProcessInstanceController 和 NodeService。
加签操作 (Controller 层)
<?php
namespace App\Http\Controllers;
use App\Services\NodeService;
use Illuminate\Http\Request;
class ApprovalController extends Controller
{
protected $nodeService;
public function __construct(NodeService $nodeService)
{
$this->nodeService = $nodeService;
}
/**
* 执行加签操作
* @param Request $request
* @return \Illuminate\Http\JsonResponse
*/
public function addCounterSign(Request $request)
{
// 1. 参数校验
$request->validate([
'node_instance_id' => 'required|integer', // 当前审批节点实例ID
'user_ids' => 'required|array', // 要加签的多个用户ID
'user_ids.*' => 'integer',
]);
$nodeInstanceId = $request->input('node_instance_id');
$counterSignUserIds = $request->input('user_ids');
$currentUserId = auth()->id(); // 当前操作人 (经理B)
// 2. 调用服务层
$result = $this->nodeService->createCounterSignNode($nodeInstanceId, $currentUserId, $counterSignUserIds);
if ($result) {
return response()->json(['code' => 200, 'msg' => '加签成功']);
} else {
return response()->json(['code' => 400, 'msg' => '加签失败,可能无权限']);
}
}
}
核心服务层 (NodeService)
<?php
namespace App\Services;
use App\Models\ProcessNode;
use App\Models\NodeAssignee;
use DB;
class NodeService
{
/**
* 创建加签节点
* @param int $parentNodeInstanceId 当前节点实例ID (经理B的节点)
* @param int $currentUserId 当前操作人ID (经理B)
* @param array $counterSignUserIds 加签人ID数组 [C_id, D_id]
* @return bool
* @throws \Exception
*/
public function createCounterSignNode($parentNodeInstanceId, $currentUserId, $counterSignUserIds)
{
// 使用事务保证数据一致性
return DB::transaction(function () use ($parentNodeInstanceId, $currentUserId, $counterSignUserIds) {
// 1. 获取当前节点实例
$parentNode = ProcessNode::findOrFail($parentNodeInstanceId);
// 2. 权限校验:只有当前节点的处理人(且该节点状态为“待处理”)才能加签
$isAssignee = NodeAssignee::where('node_instance_id', $parentNodeInstanceId)
->where('user_id', $currentUserId)
->where('type', 'normal')
->exists();
if (!$isAssignee || $parentNode->status != 0) {
throw new \Exception('无操作权限或节点状态不正确');
}
// 3. 创建子节点(加签节点)
$counterSignNode = ProcessNode::create([
'process_instance_id' => $parentNode->process_instance_id,
'node_id' => $parentNode->node_id . '_countersign', // 节点ID加后缀区分
'node_name' => $parentNode->node_name . '(加签)',
'status' => 0, // 待处理
'parent_id' => $parentNode->id, // 指向父节点
]);
// 4. 将加签人添加到子节点的指派表
$assignees = [];
foreach ($counterSignUserIds as $userId) {
$assignees[] = [
'node_instance_id' => $counterSignNode->id,
'user_id' => $userId,
'status' => 0,
'type' => 'countersign', // 标记为加签人
];
}
NodeAssignee::insert($assignees);
// 5. 修改父节点状态为“加签中”
$parentNode->status = 3; // 3 = 加签中
$parentNode->save();
return true;
});
}
/**
* 处理加签节点的审批(加签人审批)
* @param int $nodeInstanceId 加签节点实例ID
* @param int $userId 加签人ID
* @param string $action approve / reject
* @return bool
*/
public function handleCounterSignApproval($nodeInstanceId, $userId, $action)
{
return DB::transaction(function () use ($nodeInstanceId, $userId, $action) {
// 1. 查询当前节点
$counterSignNode = ProcessNode::with('parent')->findOrFail($nodeInstanceId);
if ($counterSignNode->status != 0) {
throw new \Exception('节点已处理');
}
// 2. 更新该加签人的处理状态
$assignee = NodeAssignee::where('node_instance_id', $nodeInstanceId)
->where('user_id', $userId)
->where('type', 'countersign')
->firstOrFail();
$assignee->status = ($action == 'approve') ? 1 : 2; // 1通过,2拒绝
$assignee->save();
// 3. 检查该加签节点是否所有加签人都处理完毕
$pendingCount = NodeAssignee::where('node_instance_id', $nodeInstanceId)
->where('status', 0)
->count();
if ($pendingCount == 0) {
// 4. 所有加签人处理完毕
// 如果有任何一人拒绝,则整个节点视为拒绝(可根据业务调整:比如全部通过才算通过)
$rejectedCount = NodeAssignee::where('node_instance_id', $nodeInstanceId)
->where('status', 2)
->count();
if ($rejectedCount > 0) {
// 加签节点拒绝 -> 父节点也拒绝
$counterSignNode->status = 2; // 拒绝
$counterSignNode->parent->status = 2; // 父节点拒绝
} else {
// 加签节点通过 -> 恢复父节点为待处理
$counterSignNode->status = 1; // 通过
$counterSignNode->parent->status = 0; // 父节点恢复待处理
}
$counterSignNode->save();
$counterSignNode->parent->save();
}
return true;
});
}
}
查询待办列表(支持加签)
// 查询当前用户的待办
$userId = auth()->id();
$todos = NodeAssignee::where('user_id', $userId)
->where('status', 0)
->with(['node.processInstance']) // 关联节点和流程实例
->get();
// 注意:这里需要区分 normal 和 countersign
foreach ($todos as $todo) {
if ($todo->type == 'countersign') {
echo "这是一个加签待办,来自流程:{$todo->node->processInstance->name}";
} else {
echo "这是一个正常待办,来自流程:{$todo->node->processInstance->name}";
}
}
前端交互设计(关键用户体验)
加签操作在 UI 上需要有明确且简洁的指引:
- 显示加签按钮:
- 仅在当前节点状态为“待处理”时可见。
- 建议放在“审批通过/拒绝”旁边,作为“更多操作”下的选项(避免误点)。
- 选择加签人:
- 弹窗或下拉菜单选择用户(注意排除当前审批人,避免逻辑循环)。
- 状态提示:
- 在“我的申请”和“待办列表”中,清晰显示当前节点是“等待A审批”,还是“等待A(加签中,等待B和C审批)”。
- 可以在节点名称旁加一个徽章
[加签]。
需要注意的复杂场景与经验
- 节点的“或签”与“会签”:
- 上述代码默认是“会签”(所有加签人都处理完才算完成),如果需要“或签”(一人通过即可),修改
NodeService中第3步的检查逻辑即可。
- 上述代码默认是“会签”(所有加签人都处理完才算完成),如果需要“或签”(一人通过即可),修改
- 加签层级限制:
- 业务上通常不允许无限加签(比如A加签B,B加签C,C加签D...),否则流程会非常混乱,建议在
createCounterSignNode中增加层级判断:parent->parent_id != null(已经有加签记录),则禁止再加签。
- 业务上通常不允许无限加签(比如A加签B,B加签C,C加签D...),否则流程会非常混乱,建议在
- 审批历史视图:
- 你的审批记录查询SQL需要能正确处理
parent_id的关系,最好使用JOIN或递归查询将父子节点合并展示,让用户能看清单:“经理B审批”,随后“经理B加签了C”,C通过,B继续审批通过”。
- 你的审批记录查询SQL需要能正确处理
- 性能考虑:
- 上述代码使用单表
ProcessNode和NodeAssignee适用大多数场景,如果流程实例数量非常大(百万级),建议考虑将节点状态缓存到 Redis 或用更成熟的流程引擎(如Camunda BPM的PHP客户端)。
- 上述代码使用单表
流程加签在PHP中的核心实现就是 “创建子节点 + 改变父节点状态 + 子节点审批完恢复父节点”,关键在于处理好:
- 数据库的
parent_id和type字段。 - 清晰的权限校验(仅当前处理人可加签)。
- 节点状态切换的原子性(使用事务)。
- 前端清晰的交互提示。
按照上述方案,你可以在不依赖复杂外部组件的情况下,快速实现一个稳定、可维护的加签功能。
版权声明:足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
足球直播
足球直播
足球直播
足球直播
足球直播
中超直播
中超直播
中超直播
中超直播
中超直播
英超直播
英超直播
英超直播
英超直播
英超直播
德甲直播
德甲直播
德甲直播
德甲直播
德甲直播
西甲直播
西甲直播
西甲直播
西甲直播
西甲直播
意甲直播
意甲直播
意甲直播
意甲直播
意甲直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播