本文目录导读:

在PHP项目中实现流程转办(即工作流中把任务从一个办理人转给另一个办理人),通常需要结合工作流引擎(如 Camunda、Flowable、Yii2-workflow 等)或自定义状态机来实现。
下面从核心设计思路、数据库设计、后端代码实现(PHP + MySQL 示例)以及注意事项四个方面来讲解。
核心设计思路
流程转办的核心是更新当前任务的处理人,同时记录转办历史以便审计。
关键操作:
- 权限校验:只有当前任务的处理人或管理员才能转办。
- 状态变更:将当前任务(或流程实例)的
assignee字段更新为新的处理人。 - 历史记录:记录转办人、接收人、转办时间、原因等。
- 通知触发(可选):通知新处理人有待办任务。
数据库设计示例
假设有以下两张核心表:
任务表 task
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| process_instance_id | int | 流程实例ID |
| task_name | varchar | 任务节点名称 |
| assignee | int | 当前处理人(用户ID) |
| status | tinyint | 0=待处理, 1=已完成, 2=转办中 |
| created_at | datetime | 创建时间 |
| updated_at | datetime | 更新时间 |
转办记录表 task_transfer_log
| 字段名 | 类型 | 说明 |
|---|---|---|
| id | int | 主键 |
| task_id | int | 任务ID |
| from_user_id | int | 原处理人 |
| to_user_id | int | 新处理人 |
| reason | varchar | 转办原因 |
| operator_id | int | 操作人(通常是原处理人) |
| created_at | datetime | 转办时间 |
PHP 代码实现示例
转办接口(Controller 层)
// TransferController.php
public function transferTask(Request $request, $taskId)
{
// 1. 校验用户身份(当前登录用户)
$currentUser = auth()->user();
// 2. 获取当前任务
$task = Task::findOrFail($taskId);
// 3. 权限判断:只有任务当前处理人或管理员可以转办
if ($task->assignee != $currentUser->id && !$currentUser->isAdmin()) {
return response()->json(['error' => '无转办权限'], 403);
}
// 4. 接收新处理人ID
$newAssigneeId = $request->input('to_user_id');
$reason = $request->input('reason', '');
// 5. 校验新处理人是否合法(不能转给自己,不能转给不存在的人)
if ($newAssigneeId == $currentUser->id) {
return response()->json(['error' => '不能转办给自己'], 400);
}
if (!User::find($newAssigneeId)) {
return response()->json(['error' => '新处理人不存在'], 400);
}
// 6. 开启数据库事务,保证原子性
DB::beginTransaction();
try {
// 6.1 记录转办历史
TaskTransferLog::create([
'task_id' => $task->id,
'from_user_id' => $task->assignee,
'to_user_id' => $newAssigneeId,
'reason' => $reason,
'operator_id' => $currentUser->id,
]);
// 6.2 更新任务的处理人
$task->assignee = $newAssigneeId;
$task->status = 0; // 重置为“待处理”
$task->save();
DB::commit();
// 7. 可选:发送通知给新处理人
notifyNewAssignee($task, $newAssigneeId);
return response()->json(['message' => '转办成功']);
} catch (\Exception $e) {
DB::rollback();
Log::error('转办失败:'.$e->getMessage());
return response()->json(['error' => '转办失败,请重试'], 500);
}
}
获取可转办的用户列表(辅助接口)
// UserController.php
public function getTransferableUsers()
{
// 通常返回所有活跃用户,但可做业务过滤(如相同部门、相同角色)
$users = User::where('status', 1)
->select('id', 'name', 'department')
->get();
return response()->json($users);
}
前端示例(Vue/React 简化版)
// 转办弹窗
function showTransferDialog(taskId) {
const newUserId = prompt('输入新处理人ID:');
const reason = prompt('转办原因:');
fetch(`/api/task/${taskId}/transfer`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ to_user_id: newUserId, reason: reason })
})
.then(res => res.json())
.then(data => {
if (data.message) alert(data.message);
else alert(data.error);
});
}
复杂流程引擎中的转办(如 Camunda + PHP)
如果你的项目使用外部工作流引擎(如 Camunda BPM),转办通常通过调用引擎 REST API 实现。
Camunda REST API 示例
POST /task/{taskId}/assignee
请求体:{ "userId": "newAssignee" }
PHP 调用示例(使用 Guzzle)
use GuzzleHttp\Client;
function camundaTransferTask($taskId, $newAssignee) {
$client = new Client(['base_uri' => 'http://camunda-server:8080/engine-rest']);
$response = $client->post("/task/{$taskId}/assignee", [
'json' => ['userId' => $newAssignee]
]);
if ($response->getStatusCode() == 204) {
// 成功,记录本地日志
Log::info("Camunda 任务 {$taskId} 已转办给 {$newAssignee}");
return true;
} else {
Log::error("Camunda 转办失败:".$response->getBody());
return false;
}
}
注意事项与最佳实践
-
权限控制严格
- 只允许当前处理人或管理员转办。
- 不能转给已离职或不存在的用户。
-
事务一致性
转办操作(更新任务 + 写入历史)应该放在一个数据库事务中,避免出现历史遗漏。 -
并发问题
如果任务可能被多人同时操作(极少见),建议使用乐观锁或行级锁(SELECT ... FOR UPDATE)防止重复转办。 -
历史追踪
记录完整转办链(原处理人 → 新处理人 → 时间 → 原因),便于审计。 -
通知机制
转办后最好通过站内信、邮件或微信模板消息通知新处理人。 -
转办后的流程状态
- 如果转办发生在“待审批”状态,应保持该状态不变。
- 如果原任务有超时或催办逻辑,转办后应重置计时器。
-
批量转办(可选)
如果管理员需要将某个用户的所有任务转给另一个人,可以扩展批量转办接口。
| 场景 | 实现方式 |
|---|---|
| 简单流程(自定义数据表) | 更新 task.assignee + 写入 task_transfer_log |
| 使用 Camunda 等引擎 | 调用引擎 REST API(POST /task/{id}/assignee) |
| 需要事务和审计 | 在 PHP 中开启 DB::transaction,同时写历史表 |
| 高并发场景 | 加锁(悲观锁或乐观锁) |
核心原则:权限校验 + 原子性更新 + 历史记录 + 通知触发。
如果你能提供更多项目细节(比如是否使用工作流引擎、数据库结构),我可以给出更针对性的代码示例。