566 lines
19 KiB
PHP
566 lines
19 KiB
PHP
<?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";
|
||
}
|
||
} |