Files
fdesk/app/common/utils/approval/service/ApprovalFlowService.php
T

566 lines
19 KiB
PHP
Raw Normal View History

2025-09-04 10:26:28 +08:00
<?php
2025-09-08 11:34:19 +08:00
namespace app\common\utils\approval\service;
2025-09-04 10:26:28 +08:00
use support\exception\BusinessException;
use support\Db;
class ApprovalFlowService
{
// --- 公共方法 ---
/**
* 启动审批流程
*/
public function start(string $flowType, string $bizType, string $bizId, array $formData, int $initiatorId): array
{
// 1. 获取流程定义
$flow = Db::table('approval_flows')
->where('type', $flowType)
->where('is_active', 1)
->first();
if (!$flow) {
throw new BusinessException("审批流程未找到或已停用: {$flowType}");
}
$steps = json_decode($flow->steps, true);
if (!is_array($steps) || empty($steps)) {
throw new BusinessException("流程步骤配置错误");
}
// 2. 查找第一个有效步骤(考虑 condition)
$firstStep = $this->getNextValidStep($steps, $formData);
if (!$firstStep) {
throw new BusinessException("未找到可执行的审批步骤");
}
2025-09-08 20:00:12 +08:00
// 3. 计算截止时间
$deadlineAt = null;
if ($flow->timeout_hours > 0) {
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
}
// 4. 创建审批实例
2025-09-04 10:26:28 +08:00
$instanceId = Db::table('approval_instances')->insertGetId([
'flow_type' => $flowType,
'biz_type' => $bizType,
'biz_id' => $bizId,
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE),
'initiator_id' => $initiatorId,
2025-09-08 20:00:12 +08:00
'status' => 'processing', // processing 表示正在进行中
2025-09-04 10:26:28 +08:00
'current_step' => 0,
2025-09-08 20:00:12 +08:00
'deadline_at' => $deadlineAt,
2025-09-04 10:26:28 +08:00
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
2025-09-08 20:00:12 +08:00
// 5. 创建审批记录(待办)
$approverIds = $this->resolveApprovers($firstStep, (object)[
'initiator_id' => $initiatorId,
'form_data' => json_encode($formData)
]);
2025-09-04 10:26:28 +08:00
foreach ($approverIds as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'approver_id' => $approverId,
2025-09-08 20:00:12 +08:00
'step_index' => 0,
'action' => '', // 未操作时为空
2025-09-04 10:26:28 +08:00
'status' => 'pending',
2025-09-08 20:00:12 +08:00
'approved_at' => date('Y-m-d H:i:s', 0), // 未审批时设置为初始时间
2025-09-04 10:26:28 +08:00
'created_at' => date('Y-m-d H:i:s'),
2025-09-08 20:00:12 +08:00
'updated_at' => date('Y-m-d H:i:s'),
2025-09-04 10:26:28 +08:00
]);
}
2025-09-08 20:00:12 +08:00
// 6. 通知审批人
$instance = (object)[
'id' => $instanceId,
'flow_type' => $flowType,
'form_data' => json_encode($formData),
'initiator_id' => $initiatorId
];
$this->sendNotification($approverIds, $instance, 'pending', false);
2025-09-04 10:26:28 +08:00
2025-09-08 20:00:12 +08:00
// 7. 通知抄送人(当前步骤)
$this->notifyCCs($firstStep, $instance, 'pending');
2025-09-04 10:26:28 +08:00
return ['instance_id' => $instanceId];
}
/**
* 执行审批
*/
public function approve(int $instanceId, int $approverId, string $action, string $comment = '')
{
2025-09-08 20:00:12 +08:00
// 验证操作是否有效
if (!in_array($action, ['approve', 'reject'])) {
throw new BusinessException("无效的审批操作");
}
2025-09-04 10:26:28 +08:00
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$instance) {
throw new BusinessException("审批实例未找到");
}
2025-09-08 20:00:12 +08:00
// 获取流程定义
$flow = Db::table('approval_flows')
->where('type', $instance->flow_type)
->first();
if (!$flow) {
throw new BusinessException("审批流程定义未找到");
}
$steps = json_decode($flow->steps, true);
$currentStepIndex = $instance->current_step;
$currentStep = $steps[$currentStepIndex] ?? null;
2025-09-04 10:26:28 +08:00
if (!$currentStep) {
throw new BusinessException("当前步骤配置错误");
}
// 检查用户是否有权限审批
if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) {
throw new BusinessException("您无权审批此步骤");
}
2025-09-08 20:00:12 +08:00
// 检查用户是否已经审批过
$existingRecord = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $approverId)
->where('step_index', $currentStepIndex)
->first();
if ($existingRecord && $existingRecord->status !== 'pending') {
throw new BusinessException("您已经审批过此步骤");
}
2025-09-04 10:26:28 +08:00
// 更新审批记录
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $approverId)
2025-09-08 20:00:12 +08:00
->where('step_index', $currentStepIndex)
2025-09-04 10:26:28 +08:00
->where('status', 'pending')
->update([
2025-09-08 20:00:12 +08:00
'action' => $action,
'status' => $action, // 状态与操作一致
2025-09-04 10:26:28 +08:00
'comment' => $comment,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
2025-09-08 20:00:12 +08:00
// 检查当前步骤是否完成(根据 type: any, all)
$stepCompleted = $this->isStepCompleted($instanceId, $currentStepIndex, $currentStep['type']);
2025-09-04 10:26:28 +08:00
if (!$stepCompleted) {
// 步骤未完成,不进入下一步
return ['status' => 'step_pending'];
}
// 步骤已完成,进入下一步或结束
2025-09-08 20:00:12 +08:00
$nextStepIndex = $this->getNextStepIndex($steps, json_decode($instance->form_data, true), $currentStepIndex);
2025-09-04 10:26:28 +08:00
2025-09-08 20:00:12 +08:00
if ($nextStepIndex === null) {
2025-09-04 10:26:28 +08:00
// 流程结束
Db::table('approval_instances')->where('id', $instanceId)->update([
2025-09-08 20:00:12 +08:00
'status' => $action, // 最终状态与最后一次操作一致
'finished_at' => date('Y-m-d H:i:s'),
2025-09-04 10:26:28 +08:00
'updated_at' => date('Y-m-d H:i:s'),
]);
// 通知发起人结果
$this->sendNotification([$instance->initiator_id], $instance, $action, false);
return ['status' => 'completed', 'result' => $action];
}
// 进入下一步
Db::table('approval_instances')->where('id', $instanceId)->update([
'current_step' => $nextStepIndex,
'updated_at' => date('Y-m-d H:i:s'),
]);
2025-09-08 20:00:12 +08:00
// 获取下一步信息
$nextStep = $steps[$nextStepIndex];
2025-09-04 10:26:28 +08:00
// 创建下一步的审批记录
$nextApprovers = $this->resolveApprovers($nextStep, $instance);
foreach ($nextApprovers as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'approver_id' => $approverId,
2025-09-08 20:00:12 +08:00
'step_index' => $nextStepIndex,
'action' => '',
2025-09-04 10:26:28 +08:00
'status' => 'pending',
2025-09-08 20:00:12 +08:00
'approved_at' => date('Y-m-d H:i:s', 0),
2025-09-04 10:26:28 +08:00
'created_at' => date('Y-m-d H:i:s'),
2025-09-08 20:00:12 +08:00
'updated_at' => date('Y-m-d H:i:s'),
2025-09-04 10:26:28 +08:00
]);
}
// 通知下一步审批人
$this->sendNotification($nextApprovers, $instance, 'pending', false);
// 通知下一步抄送人
$this->notifyCCs($nextStep, $instance, 'pending');
return ['status' => 'next_step', 'step_name' => $nextStep['name']];
}
/**
* 解析审批人
*/
private function resolveApprovers(array $step, object $instance): array
{
$resolved = [];
$formData = json_decode($instance->form_data ?? '{}', true);
foreach ($step['approvers'] as $approverConfig) {
$approverIds = [];
switch ($approverConfig['type']) {
case 'role':
$approverIds = $this->getUserIdsByRole($approverConfig['value']);
break;
case 'user':
$approverIds = [(int)$approverConfig['value']];
break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId($instance->initiator_id);
if ($leaderId) $approverIds = [$leaderId];
break;
case 'field_user':
if (isset($formData[$approverConfig['value']])) {
$approverIds = [(int)$formData[$approverConfig['value']]];
}
break;
case 'dept_head':
$approverIds = $this->getDeptHeadId($approverConfig['value']);
if ($approverIds) $approverIds = [$approverIds];
break;
default:
throw new BusinessException("不支持的审批人类型: {$approverConfig['type']}");
}
$resolved = array_merge($resolved, $approverIds);
}
return array_unique(array_filter($resolved));
}
/**
* 通知抄送人
*/
private function notifyCCs(array $step, object $instance, string $action = 'pending')
{
$ccConfig = $step['cc'] ?? [];
$ccUserIds = [];
$formData = json_decode($instance->form_data ?? '{}', true);
foreach ($ccConfig as $cc) {
switch ($cc['type']) {
case 'role':
$ccUserIds = array_merge($ccUserIds, $this->getUserIdsByRole($cc['value']));
break;
case 'user':
$ccUserIds[] = (int)$cc['value'];
break;
case 'initiator_leader':
$leaderId = $this->getLeaderIdByUserId($instance->initiator_id);
if ($leaderId) $ccUserIds[] = $leaderId;
break;
case 'field_user':
if (isset($formData[$cc['value']])) {
$ccUserIds[] = (int)$formData[$cc['value']];
}
break;
case 'dept_head':
$deptHeadId = $this->getDeptHeadId($cc['value']);
if ($deptHeadId) $ccUserIds[] = $deptHeadId;
break;
default:
break;
}
}
$ccUserIds = array_unique(array_filter($ccUserIds));
if (!empty($ccUserIds)) {
2025-09-08 20:00:12 +08:00
// 记录抄送信息到数据库
foreach ($ccUserIds as $userId) {
// 检查是否已存在抄送记录
$exists = Db::table('approval_ccs')
->where('instance_id', $instance->id)
->where('user_id', $userId)
->exists();
if (!$exists) {
Db::table('approval_ccs')->insert([
'instance_id' => $instance->id,
'user_id' => $userId,
'notified_at' => date('Y-m-d H:i:s'),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
}
// 发送通知
2025-09-04 10:26:28 +08:00
$this->sendNotification($ccUserIds, $instance, $action, true);
}
}
/**
* 评估条件
*/
private function evaluateCondition(?string $condition, array $formData): bool
{
if (!$condition) return true;
$pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/';
if (!preg_match($pattern, $condition, $matches)) {
throw new BusinessException("条件格式错误: {$condition}");
}
$field = $matches[1];
$op = $matches[2];
$value = trim($matches[3], "\"'");
$actualValue = $formData[$field] ?? null;
if (is_numeric($value) && is_numeric($actualValue)) {
$value = (float)$value;
$actualValue = (float)$actualValue;
}
switch ($op) {
case '==': return $actualValue == $value;
case '!=': return $actualValue != $value;
case '>': return $actualValue > $value;
2025-09-08 20:00:12 +08:00
case '<': return $actualValue < $value;
2025-09-04 10:26:28 +08:00
case '>=': return $actualValue >= $value;
case '<=': return $actualValue <= $value;
default: throw new BusinessException("不支持的操作符: {$op}");
}
}
/**
2025-09-08 20:00:12 +08:00
* 获取下一个有效步骤索引(考虑 condition)
2025-09-04 10:26:28 +08:00
*/
2025-09-08 20:00:12 +08:00
private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int
2025-09-04 10:26:28 +08:00
{
2025-09-08 20:00:12 +08:00
for ($i = $currentStepIndex + 1; $i < count($steps); $i++) {
2025-09-04 10:26:28 +08:00
$step = $steps[$i];
if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) {
2025-09-08 20:00:12 +08:00
return $i;
2025-09-04 10:26:28 +08:00
}
}
return null;
}
/**
* 检查用户是否有权限审批
*/
private function canUserApproveStep(array $step, int $userId, object $instance): bool
{
$approvers = $this->resolveApprovers($step, $instance);
return in_array($userId, $approvers);
}
/**
* 检查当前步骤是否完成
*/
private function isStepCompleted(int $instanceId, int $stepIndex, string $type): bool
{
2025-09-08 20:00:12 +08:00
// 获取当前步骤的所有审批记录
$records = Db::table('approval_records')
2025-09-04 10:26:28 +08:00
->where('instance_id', $instanceId)
2025-09-08 20:00:12 +08:00
->where('step_index', $stepIndex)
->get();
2025-09-04 10:26:28 +08:00
2025-09-08 20:00:12 +08:00
if ($records->isEmpty()) {
return false;
2025-09-04 10:26:28 +08:00
}
2025-09-08 20:00:12 +08:00
switch ($type) {
case 'any':
// 任意一人审批即可(approve或reject都算完成)
foreach ($records as $record) {
if ($record->status !== 'pending') {
return true;
}
}
return false;
case 'all':
// 所有人都必须审批
foreach ($records as $record) {
if ($record->status === 'pending') {
return false;
}
}
return true;
default:
return false;
2025-09-04 10:26:28 +08:00
}
}
/**
* 发送通知(你可以替换为你的通知系统)
*/
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC)
{
$actionText = [
'pending' => '待审批',
'approve' => '已通过',
'reject' => '已拒绝'
][$action] ?? $action;
$prefix = $isCC ? '[抄送]' : '';
$message = "{$prefix}您有一条审批 {$actionText}:流程ID {$instance->id},类型 {$instance->flow_type}";
// TODO: 调用你的消息系统,如邮件、站内信、企业微信等
// 示例:MessageService::send($userIds, $message);
echo "通知用户 " . implode(',', $userIds) . ":$message\n";
}
/**
* 获取用户待审批列表
* @param int $userId 用户ID
* @return array
*/
public function getPendingApprovals(int $userId): array
{
2025-09-08 20:00:12 +08:00
return Db::table('approval_records as ar')
->join('approval_instances as ai', 'ar.instance_id', '=', 'ai.id')
->where('ar.approver_id', $userId)
->where('ar.status', 'pending')
->where('ai.status', 'processing')
->select('ai.*')
2025-09-04 10:26:28 +08:00
->get()
->toArray();
}
/**
* 获取审批历史记录
* @param int $instanceId
* @return array
*/
public function getApprovalHistory(int $instanceId): array
{
return Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->toArray();
}
/**
* 获取审批流程图数据 (简化版)
* @param int $instanceId
* @return array
*/
public function getFlowDiagramData(int $instanceId): array
{
$instance = Db::table('approval_instances')->find($instanceId);
if (!$instance) return [];
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if (!$flow) return [];
$steps = json_decode($flow->steps, true);
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->orderBy('step_index')
->orderBy('approved_at')
->get()
->groupBy('step_index');
$diagramData = [];
foreach ($steps as $index => $step) {
$status = 'pending';
$actions = $records->get($index, collect())->toArray();
if (!empty($actions)) {
$hasReject = collect($actions)->contains('action', 'reject');
if ($hasReject) {
$status = 'rejected';
} else {
2025-09-08 20:00:12 +08:00
$status = 'approved';
2025-09-04 10:26:28 +08:00
}
} elseif ($index < $instance->current_step) {
$status = 'approved';
} elseif ($index == $instance->current_step) {
$status = 'processing';
}
$diagramData[] = [
'index' => $index,
'name' => $step['name'],
'type' => $step['type'],
'status' => $status,
'actions' => $actions
];
}
return $diagramData;
}
/**
* 处理超时的审批实例 (定时任务调用)
*/
public function handleTimeouts()
{
$now = date('Y-m-d H:i:s');
$expiredInstances = Db::table('approval_instances')
->where('status', 'processing')
->where('deadline_at', '<=', $now)
->get();
foreach ($expiredInstances as $instance) {
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
if ($flow && $flow->timeout_action) {
$updateData = [
'finished_at' => $now,
'updated_at' => $now
];
if ($flow->timeout_action === 'auto_reject') {
2025-09-08 20:00:12 +08:00
$updateData['status'] = 'reject';
2025-09-04 10:26:28 +08:00
} elseif ($flow->timeout_action === 'auto_approve') {
2025-09-08 20:00:12 +08:00
$updateData['status'] = 'approve';
2025-09-04 10:26:28 +08:00
}
2025-09-08 20:00:12 +08:00
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, $flow->timeout_action);
2025-09-04 10:26:28 +08:00
}
}
}
// --- 辅助方法 ---
private function getUserIdsByRole(string $roleCode): array
{
return Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
2025-09-08 20:00:12 +08:00
->where('r.code', $roleCode)
2025-09-04 10:26:28 +08:00
->pluck('ur.user_id')
->toArray();
}
private function getLeaderIdByUserId(int $userId): ?int
{
return Db::table('user')->where('id', $userId)->value('report_to');
}
private function getDeptHeadId(int $deptId): ?int
{
return Db::table('departments')->where('id', $deptId)->value('head_user_id');
}
private function notifyInitiator(object $instance, string $result)
{
echo "通知发起人 {$instance->initiator_id} 审批结果: {$result}\n";
}
}