PHP项目怎么实现流程加签?

wen java案例 7

本文目录导读:

PHP项目怎么实现流程加签?

  1. 核心思路与数据库设计
  2. PHP 核心代码实现示例 (ThinkPHP/Laravel 风格)
  3. 前端交互设计(关键用户体验)
  4. 需要注意的复杂场景与经验

在PHP项目中实现“流程加签”(即流程审批过程中,当前环节的处理人临时增加额外的审批人)是一个常见且复杂的需求,这需要结合工作流引擎symfony/workflowstate-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 风格)

假设我们有 ProcessInstanceControllerNodeService

加签操作 (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 上需要有明确且简洁的指引:

  1. 显示加签按钮
    • 仅在当前节点状态为“待处理”时可见。
    • 建议放在“审批通过/拒绝”旁边,作为“更多操作”下的选项(避免误点)。
  2. 选择加签人
    • 弹窗或下拉菜单选择用户(注意排除当前审批人,避免逻辑循环)。
  3. 状态提示
    • 在“我的申请”和“待办列表”中,清晰显示当前节点是“等待A审批”,还是“等待A(加签中,等待B和C审批)”。
    • 可以在节点名称旁加一个徽章 [加签]

需要注意的复杂场景与经验

  1. 节点的“或签”与“会签”
    • 上述代码默认是“会签”(所有加签人都处理完才算完成),如果需要“或签”(一人通过即可),修改 NodeService 中第3步的检查逻辑即可。
  2. 加签层级限制
    • 业务上通常不允许无限加签(比如A加签B,B加签C,C加签D...),否则流程会非常混乱,建议在 createCounterSignNode 中增加层级判断:parent->parent_id != null(已经有加签记录),则禁止再加签。
  3. 审批历史视图
    • 你的审批记录查询SQL需要能正确处理 parent_id 的关系,最好使用 JOIN 或递归查询将父子节点合并展示,让用户能看清单:“经理B审批”,随后“经理B加签了C”,C通过,B继续审批通过”。
  4. 性能考虑
    • 上述代码使用单表 ProcessNodeNodeAssignee 适用大多数场景,如果流程实例数量非常大(百万级),建议考虑将节点状态缓存到 Redis 或用更成熟的流程引擎(如 Camunda BPM 的PHP客户端)。

流程加签在PHP中的核心实现就是 “创建子节点 + 改变父节点状态 + 子节点审批完恢复父节点”,关键在于处理好:

  • 数据库的 parent_idtype 字段。
  • 清晰的权限校验(仅当前处理人可加签)。
  • 节点状态切换的原子性(使用事务)。
  • 前端清晰的交互提示。

按照上述方案,你可以在不依赖复杂外部组件的情况下,快速实现一个稳定、可维护的加签功能。

抱歉,评论功能暂时关闭!