Files
fdesk/app/common/utils/approval/service/ApprovalFlowService.php
T
2025-09-08 20:00:12 +08:00

566 lines
19 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace app\common\utils\approval\service;
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("未找到可执行的审批步骤");
}
// 3. 计算截止时间
$deadlineAt = null;
if ($flow->timeout_hours > 0) {
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
}
// 4. 创建审批实例
$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,
'status' => 'processing', // processing 表示正在进行中
'current_step' => 0,
'deadline_at' => $deadlineAt,
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 5. 创建审批记录(待办)
$approverIds = $this->resolveApprovers($firstStep, (object)[
'initiator_id' => $initiatorId,
'form_data' => json_encode($formData)
]);
foreach ($approverIds as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'approver_id' => $approverId,
'step_index' => 0,
'action' => '', // 未操作时为空
'status' => 'pending',
'approved_at' => date('Y-m-d H:i:s', 0), // 未审批时设置为初始时间
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
// 6. 通知审批人
$instance = (object)[
'id' => $instanceId,
'flow_type' => $flowType,
'form_data' => json_encode($formData),
'initiator_id' => $initiatorId
];
$this->sendNotification($approverIds, $instance, 'pending', false);
// 7. 通知抄送人(当前步骤)
$this->notifyCCs($firstStep, $instance, 'pending');
return ['instance_id' => $instanceId];
}
/**
* 执行审批
*/
public function approve(int $instanceId, int $approverId, string $action, string $comment = '')
{
// 验证操作是否有效
if (!in_array($action, ['approve', 'reject'])) {
throw new BusinessException("无效的审批操作");
}
$instance = Db::table('approval_instances')->where('id', $instanceId)->first();
if (!$instance) {
throw new BusinessException("审批实例未找到");
}
// 获取流程定义
$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;
if (!$currentStep) {
throw new BusinessException("当前步骤配置错误");
}
// 检查用户是否有权限审批
if (!$this->canUserApproveStep($currentStep, $approverId, $instance)) {
throw new BusinessException("您无权审批此步骤");
}
// 检查用户是否已经审批过
$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("您已经审批过此步骤");
}
// 更新审批记录
Db::table('approval_records')
->where('instance_id', $instanceId)
->where('approver_id', $approverId)
->where('step_index', $currentStepIndex)
->where('status', 'pending')
->update([
'action' => $action,
'status' => $action, // 状态与操作一致
'comment' => $comment,
'approved_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
// 检查当前步骤是否完成(根据 type: any, all)
$stepCompleted = $this->isStepCompleted($instanceId, $currentStepIndex, $currentStep['type']);
if (!$stepCompleted) {
// 步骤未完成,不进入下一步
return ['status' => 'step_pending'];
}
// 步骤已完成,进入下一步或结束
$nextStepIndex = $this->getNextStepIndex($steps, json_decode($instance->form_data, true), $currentStepIndex);
if ($nextStepIndex === null) {
// 流程结束
Db::table('approval_instances')->where('id', $instanceId)->update([
'status' => $action, // 最终状态与最后一次操作一致
'finished_at' => date('Y-m-d H:i:s'),
'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'),
]);
// 获取下一步信息
$nextStep = $steps[$nextStepIndex];
// 创建下一步的审批记录
$nextApprovers = $this->resolveApprovers($nextStep, $instance);
foreach ($nextApprovers as $approverId) {
Db::table('approval_records')->insert([
'instance_id' => $instanceId,
'approver_id' => $approverId,
'step_index' => $nextStepIndex,
'action' => '',
'status' => 'pending',
'approved_at' => date('Y-m-d H:i:s', 0),
'created_at' => date('Y-m-d H:i:s'),
'updated_at' => date('Y-m-d H:i:s'),
]);
}
// 通知下一步审批人
$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)) {
// 记录抄送信息到数据库
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'),
]);
}
}
// 发送通知
$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;
case '<': return $actualValue < $value;
case '>=': return $actualValue >= $value;
case '<=': return $actualValue <= $value;
default: throw new BusinessException("不支持的操作符: {$op}");
}
}
/**
* 获取下一个有效步骤索引(考虑 condition)
*/
private function getNextStepIndex(array $steps, array $formData, int $currentStepIndex): ?int
{
for ($i = $currentStepIndex + 1; $i < count($steps); $i++) {
$step = $steps[$i];
if (!isset($step['condition']) || $this->evaluateCondition($step['condition'], $formData)) {
return $i;
}
}
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
{
// 获取当前步骤的所有审批记录
$records = Db::table('approval_records')
->where('instance_id', $instanceId)
->where('step_index', $stepIndex)
->get();
if ($records->isEmpty()) {
return false;
}
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;
}
}
/**
* 发送通知(你可以替换为你的通知系统)
*/
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
{
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.*')
->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 {
$status = 'approved';
}
} 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') {
$updateData['status'] = 'reject';
} elseif ($flow->timeout_action === 'auto_approve') {
$updateData['status'] = 'approve';
}
Db::table('approval_instances')->where('id', $instance->id)->update($updateData);
$this->notifyInitiator($instance, $flow->timeout_action);
}
}
}
// --- 辅助方法 ---
private function getUserIdsByRole(string $roleCode): array
{
return Db::table('user_roles as ur')
->join('roles as r', 'ur.role_id', '=', 'r.id')
->where('r.code', $roleCode)
->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";
}
}