1160 lines
48 KiB
PHP
1160 lines
48 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
|
||
{
|
||
return Db::transaction(function () use ($flowType, $bizType, $bizId, $formData, $initiatorId) {
|
||
$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("流程步骤配置错误");
|
||
|
||
// 找首个有效步骤索引(考虑条件)
|
||
$firstIndex = $this->getNextStepIndex($steps, $formData, -1);
|
||
if ($firstIndex === null) throw new BusinessException("未找到可执行的审批步骤");
|
||
$firstStep = $steps[$firstIndex];
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
$deadlineAt = null;
|
||
if (($flow->timeout_hours ?? 0) > 0) {
|
||
$deadlineAt = date('Y-m-d H:i:s', time() + $flow->timeout_hours * 3600);
|
||
}
|
||
|
||
$instanceId = Db::table('approval_instances')->insertGetId([
|
||
'flow_type' => $flowType,
|
||
'biz_type' => $bizType,
|
||
'biz_id' => $bizId,
|
||
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||
'initiator_id' => $initiatorId,
|
||
'status' => 'processing',
|
||
'current_step' => $firstIndex,
|
||
'deadline_at' => $deadlineAt,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
$instance = (object)[
|
||
'id' => $instanceId,
|
||
'flow_type' => $flowType,
|
||
'form_data' => json_encode($formData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES),
|
||
'initiator_id' => $initiatorId,
|
||
'current_step' => $firstIndex,
|
||
'status' => 'processing',
|
||
'flow_type' => $flowType,
|
||
'biz_type' => $bizType,
|
||
'biz_id' => $bizId,
|
||
];
|
||
|
||
// 解析首步审批人(支持 role_id 或 roles.name)
|
||
$approverIds = $this->resolveApprovers($firstStep, $instance, true);
|
||
foreach ($approverIds as $uid) {
|
||
$this->insertApprovalRecord($instanceId, $firstIndex, $uid);
|
||
}
|
||
|
||
$this->sendNotification($approverIds, $instance, 'pending', false);
|
||
$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'], true)) {
|
||
throw new BusinessException("无效的审批操作");
|
||
}
|
||
|
||
return Db::transaction(function () use ($instanceId, $approverId, $action, $comment) {
|
||
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
|
||
if (!$instance) throw new BusinessException("审批实例未找到");
|
||
if ($instance->status !== 'processing') {
|
||
// return ['status' => 'noop', 'reason' => 'instance_not_processing', 'instance_status' => $instance->status];
|
||
// throw new BusinessException("实例未处理");
|
||
return ['status' => 'noop', 'msg' => '实例未处理'];
|
||
}
|
||
|
||
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
|
||
if (!$flow) throw new BusinessException("审批流程定义未找到");
|
||
|
||
$steps = json_decode($flow->steps, true);
|
||
$idx = (int)$instance->current_step;
|
||
$step = $steps[$idx] ?? null;
|
||
if (!$step) throw new BusinessException("当前步骤配置错误");
|
||
|
||
// 找到该用户在本步的 pending 记录(加锁)
|
||
$pendingRecord = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('approver_id', $approverId)
|
||
->where('step_index', $idx)
|
||
->where('status', 'pending')
|
||
->lockForUpdate()
|
||
->first();
|
||
|
||
if (!$pendingRecord) {
|
||
// 已处理过?
|
||
$anyRecord = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('approver_id', $approverId)
|
||
->where('step_index', $idx)
|
||
->first();
|
||
if ($anyRecord && $anyRecord->status !== 'pending') {
|
||
// return ['status' => 'noop', 'reason' => 'already_handled', 'record_status' => $anyRecord->status];
|
||
// throw new BusinessException("已处理");
|
||
return ['status' => 'noop', 'msg' => '已处理'];
|
||
}
|
||
// 步骤已推进
|
||
if ((int)$instance->current_step !== $idx) {
|
||
// return ['status' => 'noop', 'reason' => 'not_current_step', 'current_step' => (int)$instance->current_step];
|
||
// throw new BusinessException("非当前步骤");
|
||
return ['status' => 'noop', 'msg' => '非当前审批'];
|
||
}
|
||
// return ['status' => 'noop', 'reason' => 'no_pending_record'];
|
||
return ['status' => 'noop', 'msg' => '无待处理记录'];
|
||
}
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
|
||
// 1) 更新当前人的审批记录
|
||
Db::table('approval_records')->where('id', $pendingRecord->id)->update([
|
||
'action' => $action,
|
||
'status' => ($action === 'approve') ? 'approved' : 'rejected',
|
||
'comment' => $comment,
|
||
'approved_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
// 异步生成pdf文件 9.11 add
|
||
if ($action === 'approve') {
|
||
\Webman\RedisQueue\Redis::send('stock-order-pdf', ['id'=>$instance->biz_id]);
|
||
}
|
||
|
||
// 2) 聚合本步结论(any / all)
|
||
$typeRaw = strtolower($step['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
$outcome = $this->getStepOutcome($instanceId, $idx, $type); // 'pending' | 'approve' | 'reject'
|
||
|
||
if ($outcome === 'pending') {
|
||
// return ['status' => 'step_pending'];
|
||
return ['status' => 'step_pending', 'msg' => '审批待定'];
|
||
}
|
||
|
||
// 3) 有结论:关闭本步其余 pending
|
||
$this->closeOtherPending($instanceId, $idx, [$approverId]);
|
||
|
||
// 4) 拒绝 => 结束
|
||
if ($outcome === 'reject') {
|
||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||
'status' => 'rejected',
|
||
'finished_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
$this->sendNotification([$instance->initiator_id], $instance, 'reject', false);
|
||
// return ['status' => 'completed', 'result' => 'reject'];
|
||
return ['status' => 'completed', 'msg' => '已拒绝'];
|
||
}
|
||
|
||
// 5) 通过 => 进入下一步或结束
|
||
$formData = json_decode($instance->form_data, true) ?: [];
|
||
$nextIndex = $this->getNextStepIndex($steps, $formData, $idx);
|
||
|
||
if ($nextIndex === null) {
|
||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||
'status' => 'approved',
|
||
'finished_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
$this->sendNotification([$instance->initiator_id], $instance, 'approve', false);
|
||
// return ['status' => 'completed', 'result' => 'approve'];
|
||
return ['status' => 'completed', 'msg' => '已同意'];
|
||
}
|
||
|
||
// 乐观推进 current_step
|
||
$affected = Db::table('approval_instances')
|
||
->where('id', $instanceId)
|
||
->where('current_step', $idx)
|
||
->where('status', 'processing')
|
||
->update(['current_step' => $nextIndex, 'updated_at' => $now]);
|
||
if (!$affected) {
|
||
// return ['status' => 'noop', 'reason' => 'concurrent_progressed'];
|
||
return ['status' => 'noop', 'msg' => '并发进展'];
|
||
}
|
||
|
||
// 生成下一步待办 + 通知
|
||
$nextStep = $steps[$nextIndex];
|
||
$nextApprovers = $this->resolveApprovers($nextStep, $instance, true);
|
||
foreach ($nextApprovers as $uid) {
|
||
$this->insertApprovalRecord($instanceId, $nextIndex, $uid);
|
||
}
|
||
$this->sendNotification($nextApprovers, $instance, 'pending', false);
|
||
$this->notifyCCs($nextStep, $instance, 'pending');
|
||
|
||
// return ['status' => 'next_step', 'step_name' => $nextStep['name'] ?? ('Step '.$nextIndex)];
|
||
return ['status' => 'next_step', 'msg' => '进行下一步:'.$nextStep['name'] ?? ('Step '.$nextIndex)];
|
||
});
|
||
}
|
||
|
||
/** 撤回(仅发起人在无已处理记录前可撤回) */
|
||
public function revoke(int $instanceId, int $initiatorId, string $reason = '')
|
||
{
|
||
return Db::transaction(function () use ($instanceId, $initiatorId, $reason) {
|
||
$instance = Db::table('approval_instances')->where('id', $instanceId)->lockForUpdate()->first();
|
||
if (!$instance) throw new BusinessException('审批实例未找到');
|
||
if ((int)$instance->initiator_id !== (int)$initiatorId) throw new BusinessException('无权撤回');
|
||
if ($instance->status !== 'processing') throw new BusinessException('流程已结束或不可撤回');
|
||
|
||
$handled = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->whereIn('status', ['approved','rejected','timeout','skipped'])
|
||
->count() > 0;
|
||
if ($handled) throw new BusinessException('当前进度不可撤回');
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
Db::table('approval_instances')->where('id', $instanceId)->update([
|
||
'status' => 'revoked',
|
||
'finished_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('status', 'pending')
|
||
->update(['status' => 'skipped', 'approved_at' => $now, 'updated_at' => $now]);
|
||
|
||
$this->sendNotification([$initiatorId], $instance, 'revoked', false);
|
||
return ['status' => 'completed', 'result' => 'revoked'];
|
||
});
|
||
}
|
||
|
||
/** 我的待办 */
|
||
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.*')
|
||
->distinct()
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
/** 审批历史 */
|
||
public function getApprovalHistory(int $instanceId): array
|
||
{
|
||
return Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->orderBy('step_index')
|
||
->orderByRaw('COALESCE(approved_at, created_at) asc')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
/** 流程图数据(把 skipped/timeout 视作已完结的中性状态) */
|
||
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')->orderByRaw('COALESCE(approved_at, created_at) asc')
|
||
->get()
|
||
->groupBy('step_index');
|
||
|
||
$formData = json_decode($instance->form_data, true) ?: [];
|
||
$data = [];
|
||
foreach ($steps as $i => $step) {
|
||
$actions = ($records->get($i) ?? collect())->toArray();
|
||
|
||
if (!empty($actions)) {
|
||
$cntApproved = 0; $cntRejected = 0; $cntPending = 0;
|
||
foreach ($actions as $a) {
|
||
if ($a->status === 'approved') $cntApproved++;
|
||
elseif ($a->status === 'rejected') $cntRejected++;
|
||
elseif ($a->status === 'pending') $cntPending++;
|
||
// skipped/timeout 不阻断完结显示
|
||
}
|
||
if ($cntRejected > 0) $status = 'rejected';
|
||
elseif ($cntPending > 0) $status = 'processing';
|
||
elseif ($cntApproved > 0) $status = 'approved';
|
||
else $status = 'approved'; // 全是 skipped/timeout 也视为已过
|
||
} else {
|
||
if ($i < (int)$instance->current_step) {
|
||
if (isset($step['condition']) && !$this->evaluateCondition($step['condition'], $formData)) {
|
||
$status = 'skipped';
|
||
} else {
|
||
$status = 'approved';
|
||
}
|
||
} elseif ($i === (int)$instance->current_step && $instance->status === 'processing') {
|
||
$status = 'processing';
|
||
} else {
|
||
$status = 'pending';
|
||
}
|
||
}
|
||
|
||
$data[] = [
|
||
'index' => $i,
|
||
'name' => $step['name'] ?? ('Step '.$i),
|
||
'type' => $step['type'] ?? 'all',
|
||
'status' => $status,
|
||
'actions' => $actions,
|
||
];
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/** 定时任务:处理超时(实例级) */
|
||
public function handleTimeouts()
|
||
{
|
||
$now = date('Y-m-d H:i:s');
|
||
$expired = Db::table('approval_instances')
|
||
->where('status', 'processing')
|
||
->whereNotNull('deadline_at')
|
||
->where('deadline_at', '<=', $now)
|
||
->get();
|
||
|
||
foreach ($expired as $instance) {
|
||
Db::transaction(function () use ($instance, $now) {
|
||
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
|
||
if (!$flow) return;
|
||
|
||
$final = ($flow->timeout_action === 'auto_approve') ? 'approved' : 'rejected';
|
||
$finalAction = ($flow->timeout_action === 'auto_approve') ? 'approve' : 'reject';
|
||
|
||
Db::table('approval_instances')->where('id', $instance->id)->update([
|
||
'status' => $final,
|
||
'finished_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
|
||
Db::table('approval_records')
|
||
->where('instance_id', $instance->id)
|
||
->where('step_index', $instance->current_step)
|
||
->where('status', 'pending')
|
||
->update(['status' => 'timeout', 'approved_at' => $now, 'updated_at' => $now]);
|
||
|
||
$this->notifyInitiator($instance, $finalAction);
|
||
});
|
||
}
|
||
}
|
||
|
||
// ---------- 内部工具 ----------
|
||
|
||
/** 解析审批人(role/user/initiator_leader/field_user/dept_head) */
|
||
private function resolveApprovers(array $step, object $instance, bool $avoidSelf = true): array
|
||
{
|
||
$resolved = [];
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
|
||
$confs = $step['approvers'] ?? [];
|
||
foreach ($confs as $cfg) {
|
||
$typ = $cfg['type'] ?? '';
|
||
$val = $cfg['value'] ?? null;
|
||
$ids = [];
|
||
|
||
switch ($typ) {
|
||
case 'role':
|
||
$sys = isset($cfg['system_id']) ? (int)$cfg['system_id'] : null;
|
||
$includeGlobal = array_key_exists('include_global', $cfg) ? (bool)$cfg['include_global'] : true;
|
||
$ids = $this->getUserIdsByRole($val, $sys, true, $includeGlobal);
|
||
break;
|
||
case 'user':
|
||
$ids = [(int)$val];
|
||
break;
|
||
case 'initiator_leader':
|
||
$leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id);
|
||
if ($leaderId) $ids = [(int)$leaderId];
|
||
break;
|
||
case 'field_user':
|
||
if (isset($formData[$val])) $ids = [(int)$formData[$val]];
|
||
break;
|
||
case 'dept_head':
|
||
$head = $this->getDeptHeadId((int)$val);
|
||
if ($head) $ids = [(int)$head];
|
||
break;
|
||
default:
|
||
throw new BusinessException("不支持的审批人类型: {$typ}");
|
||
}
|
||
$resolved = array_merge($resolved, $ids);
|
||
}
|
||
|
||
$resolved = array_unique(array_filter(array_map('intval', $resolved)));
|
||
if ($avoidSelf) {
|
||
// $resolved = array_values(array_filter($resolved, fn($id) => $id !== (int)$instance->initiator_id));
|
||
$resolved = array_values($resolved);
|
||
}
|
||
if (empty($resolved)) {
|
||
throw new BusinessException("未解析到任何审批人");
|
||
}
|
||
return $resolved;
|
||
}
|
||
|
||
/** 通知抄送人 */
|
||
private function notifyCCs(array $step, object $instance, string $action = 'pending'): void
|
||
{
|
||
$ccConfig = $step['cc'] ?? [];
|
||
if (empty($ccConfig)) return;
|
||
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
$uids = [];
|
||
|
||
foreach ($ccConfig as $cc) {
|
||
$typ = $cc['type'] ?? '';
|
||
$val = $cc['value'] ?? null;
|
||
|
||
switch ($typ) {
|
||
case 'role':
|
||
$uids = array_merge($uids, $this->getUserIdsByRole($val, null, true, true));
|
||
break;
|
||
case 'user': $uids[] = (int)$val; break;
|
||
case 'initiator_leader':
|
||
$leaderId = $this->getLeaderIdByUserId((int)$instance->initiator_id);
|
||
if ($leaderId) $uids[] = (int)$leaderId;
|
||
break;
|
||
case 'field_user':
|
||
if (isset($formData[$val])) $uids[] = (int)$formData[$val];
|
||
break;
|
||
case 'dept_head':
|
||
$head = $this->getDeptHeadId((int)$val);
|
||
if ($head) $uids[] = (int)$head;
|
||
break;
|
||
default: break;
|
||
}
|
||
}
|
||
|
||
$uids = array_unique(array_filter(array_map('intval', $uids)));
|
||
if (empty($uids)) return;
|
||
|
||
$now = date('Y-m-d H:i:s');
|
||
foreach ($uids as $uid) {
|
||
$exists = Db::table('approval_ccs')
|
||
->where('instance_id', $instance->id)
|
||
->where('user_id', $uid)
|
||
->count() > 0;
|
||
if (!$exists) {
|
||
Db::table('approval_ccs')->insert([
|
||
'instance_id' => $instance->id,
|
||
'user_id' => $uid,
|
||
'notified_at' => $now,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
}
|
||
|
||
$this->sendNotification($uids, $instance, $action, true);
|
||
}
|
||
|
||
/** 条件求值(简单表达式) */
|
||
private function evaluateCondition(?string $condition, array $formData): bool
|
||
{
|
||
if (!$condition) return true;
|
||
$pattern = '/^(\w+)\s*(==|!=|>|<|>=|<=)\s*(.+)$/';
|
||
if (!preg_match($pattern, $condition, $m)) {
|
||
throw new BusinessException("条件格式错误: {$condition}");
|
||
}
|
||
$field = $m[1]; $op = $m[2]; $value = trim($m[3], "\"'");
|
||
$actual = $formData[$field] ?? null;
|
||
if (is_numeric($value) && is_numeric($actual)) { $value=(float)$value; $actual=(float)$actual; }
|
||
return match ($op) {
|
||
'==' => $actual == $value,
|
||
'!=' => $actual != $value,
|
||
'>' => $actual > $value,
|
||
'<' => $actual < $value,
|
||
'>=' => $actual >= $value,
|
||
'<=' => $actual <= $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++) {
|
||
$s = $steps[$i];
|
||
if (!isset($s['condition']) || $this->evaluateCondition($s['condition'], $formData)) {
|
||
return $i;
|
||
}
|
||
}
|
||
return null;
|
||
}
|
||
|
||
/**
|
||
* 聚合本步骤结论(按类型)
|
||
* 返回:'pending' | 'approve' | 'reject'
|
||
*
|
||
* any:有 reject => reject;否则有 approve => approve;否则 pending
|
||
* all:有 reject => reject;有 pending => pending;否则(全 approved 或 approved+skipped/timeout)=> approve
|
||
*/
|
||
private function getStepOutcome(int $instanceId, int $stepIndex, string $type): string
|
||
{
|
||
$records = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('step_index', $stepIndex)
|
||
->get();
|
||
|
||
$pending = 0; $approved = 0; $rejected = 0;
|
||
foreach ($records as $r) {
|
||
if ($r->status === 'pending') { $pending++; }
|
||
elseif ($r->status === 'approved'){ $approved++; }
|
||
elseif ($r->status === 'rejected'){ $rejected++; }
|
||
// skipped/timeout 不影响聚合结论
|
||
}
|
||
|
||
$type = strtolower($type);
|
||
if ($type === 'any') {
|
||
if ($rejected > 0) return 'reject';
|
||
if ($approved > 0) return 'approve';
|
||
return 'pending';
|
||
}
|
||
// all
|
||
if ($rejected > 0) return 'reject';
|
||
if ($pending > 0) return 'pending';
|
||
return $approved > 0 ? 'approve' : 'pending';
|
||
}
|
||
|
||
/** 关闭本步其余待办为 skipped */
|
||
private function closeOtherPending(int $instanceId, int $stepIndex, array $keepApproverIds = []): void
|
||
{
|
||
$now = date('Y-m-d H:i:s');
|
||
$q = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('step_index', $stepIndex)
|
||
->where('status', 'pending');
|
||
|
||
if (!empty($keepApproverIds)) $q->whereNotIn('approver_id', $keepApproverIds);
|
||
|
||
$q->update(['status'=>'skipped','approved_at'=>$now,'updated_at'=>$now]);
|
||
}
|
||
|
||
/** 创建待办(去重) */
|
||
private function insertApprovalRecord(int $instanceId, int $stepIndex, int $approverId): void
|
||
{
|
||
$now = date('Y-m-d H:i:s');
|
||
$exists = Db::table('approval_records')
|
||
->where('instance_id', $instanceId)
|
||
->where('step_index', $stepIndex)
|
||
->where('approver_id', $approverId)
|
||
->count() > 0;
|
||
if (!$exists) {
|
||
Db::table('approval_records')->insert([
|
||
'instance_id' => $instanceId,
|
||
'approver_id' => $approverId,
|
||
'step_index' => $stepIndex,
|
||
'action' => null,
|
||
'status' => 'pending',
|
||
'comment' => null,
|
||
'approved_at' => null,
|
||
'created_at' => $now,
|
||
'updated_at' => $now,
|
||
]);
|
||
}
|
||
}
|
||
|
||
/** 发送通知(接你自己的消息系统即可) */
|
||
private function sendNotification(array $userIds, object $instance, string $action, bool $isCC): void
|
||
{
|
||
$map = [
|
||
'pending' => '待审批',
|
||
'approve' => '已通过',
|
||
'reject' => '已拒绝',
|
||
'revoked' => '已撤回'
|
||
];
|
||
$prefix = $isCC ? '【抄送】' : '【审批】';
|
||
$text = $map[$action] ?? $action;
|
||
|
||
$approvalType = array_flip(\app\common\service\SysDictService::pluckKeyValueByCode('approval_type'));
|
||
|
||
switch ($instance->flow_type) {
|
||
case 'stock_orders': // 备货单
|
||
$stockOrder = \app\common\model\StockOrders::find($instance->biz_id);
|
||
$title = $prefix."备货单:{$stockOrder->order_no}";
|
||
$flowType = $approvalType[$instance->flow_type];
|
||
$content = "{$prefix}你有一条审批 {$text}:类型 {$flowType},业务单号 {$stockOrder->order_no}";
|
||
break;
|
||
case 'stock_orders_nullify': // 备货单作废
|
||
break;
|
||
}
|
||
|
||
noticePush([
|
||
'title' => $title,
|
||
'content' => $content,
|
||
'recipient' => $userIds,
|
||
'email' => 1
|
||
]);
|
||
}
|
||
|
||
private function notifyInitiator(object $instance, string $result): void
|
||
{
|
||
$this->sendNotification([(int)$instance->initiator_id], $instance, $result, false);
|
||
}
|
||
|
||
// ---------- 数据访问(roles / user_roles 结构) ----------
|
||
|
||
/**
|
||
* 从角色取用户ID
|
||
* @param mixed $roleIdOrName 数字 => role_id;字符串 => roles.name
|
||
* @param int|null $systemId 限定 roles.system_id;null 不限定
|
||
* @param bool $onlyActive 仅取 status=1 角色,默认 true
|
||
* @param bool $includeGlobal 是否包含全局角色(system_id=0),默认 true
|
||
*/
|
||
private function getUserIdsByRole($roleIdOrName, ?int $systemId = null, bool $onlyActive = true, bool $includeGlobal = true): array
|
||
{
|
||
if (is_int($roleIdOrName) || (is_string($roleIdOrName) && ctype_digit($roleIdOrName))) {
|
||
$roleId = (int)$roleIdOrName;
|
||
$needJoin = $onlyActive || !is_null($systemId) || $includeGlobal === false;
|
||
|
||
if (!$needJoin) {
|
||
$rows = Db::table('user_roles')->where('role_id', $roleId)->select('user_id')->get();
|
||
} else {
|
||
$q = Db::table('user_roles as ur')
|
||
->join('roles as r', 'ur.role_id', '=', 'r.id')
|
||
->where('ur.role_id', $roleId)
|
||
->select('ur.user_id');
|
||
if ($onlyActive) $q->where('r.status', 1);
|
||
if (!is_null($systemId)) {
|
||
$includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId);
|
||
}
|
||
$rows = $q->get();
|
||
}
|
||
} else {
|
||
$name = (string)$roleIdOrName;
|
||
$q = Db::table('user_roles as ur')
|
||
->join('roles as r', 'ur.role_id', '=', 'r.id')
|
||
->where('r.name', $name)
|
||
->select('ur.user_id');
|
||
if ($onlyActive) $q->where('r.status', 1);
|
||
if (!is_null($systemId)) {
|
||
$includeGlobal ? $q->whereIn('r.system_id', [0, $systemId]) : $q->where('r.system_id', $systemId);
|
||
}
|
||
$rows = $q->get();
|
||
}
|
||
|
||
$ids = [];
|
||
foreach ($rows as $row) $ids[] = (int)$row->user_id;
|
||
return array_values(array_unique($ids));
|
||
}
|
||
|
||
private function getLeaderIdByUserId(int $userId): ?int
|
||
{
|
||
$id = Db::table('user')->where('id', $userId)->value('report_to');
|
||
return $id ? (int)$id : null;
|
||
}
|
||
|
||
private function getDeptHeadId(int $deptId): ?int
|
||
{
|
||
$id = Db::table('departments')->where('id', $deptId)->value('head_user_id');
|
||
return $id ? (int)$id : null;
|
||
}
|
||
|
||
|
||
/**
|
||
* 查询某个审批实例的审批记录(附带步骤名/类型,可选带出审批人姓名)
|
||
*
|
||
* @param int $instanceId
|
||
* @param string|null $userNameColumn 可选:如 'name' 或 'nickname'。传 null 则不 join 用户表
|
||
* @return array{
|
||
* instance: array,
|
||
* steps: array<int, array{name:string,type:string,condition?:string}>,
|
||
* records: array<int, array{
|
||
* id:int, instance_id:int, approver_id:int, step_index:int,
|
||
* action:?string, status:string, comment:?string,
|
||
* approved_at:?string, created_at:?string, updated_at:?string,
|
||
* approver_name?:?string,
|
||
* step_name:string, step_type:string,
|
||
* }>,
|
||
* summary: array{approved:int,rejected:int,pending:int,skipped:int,timeout:int,total:int}
|
||
* }
|
||
*/
|
||
public function getInstanceRecords(int $instanceId, ?string $userNameColumn = null): array
|
||
{
|
||
// 1) 实例 + 流程步骤
|
||
$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) ?: [];
|
||
|
||
// 2) 记录查询(可选带出审批人姓名)
|
||
if ($userNameColumn) {
|
||
// 注意:确保 user 表存在该列
|
||
$records = Db::table('approval_records as ar')
|
||
->leftJoin('user as u', 'ar.approver_id', '=', 'u.id')
|
||
->where('ar.instance_id', $instanceId)
|
||
->orderBy('ar.step_index')
|
||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||
->select(
|
||
'ar.*',
|
||
Db::raw("u.`{$userNameColumn}` as approver_name")
|
||
)
|
||
->get()
|
||
->toArray();
|
||
} else {
|
||
$records = Db::table('approval_records as ar')
|
||
->where('ar.instance_id', $instanceId)
|
||
->orderBy('ar.step_index')
|
||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||
->select('ar.*')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
// 3) 附加步骤名/类型;统计汇总
|
||
$summary = ['approved'=>0,'rejected'=>0,'pending'=>0,'skipped'=>0,'timeout'=>0,'total'=>0];
|
||
foreach ($records as &$r) {
|
||
$idx = (int)$r->step_index;
|
||
$meta = $steps[$idx] ?? [];
|
||
$r->step_name = $meta['name'] ?? ('Step '.$idx);
|
||
$r->step_type = $meta['type'] ?? 'all';
|
||
|
||
if (isset($summary[$r->status])) {
|
||
$summary[$r->status]++;
|
||
}
|
||
$summary['total']++;
|
||
}
|
||
unset($r);
|
||
|
||
return [
|
||
'instance' => [
|
||
'id' => (int)$instance->id,
|
||
'flow_type' => $instance->flow_type,
|
||
'biz_type' => $instance->biz_type,
|
||
'biz_id' => $instance->biz_id,
|
||
'status' => $instance->status,
|
||
'current_step' => (int)$instance->current_step,
|
||
'created_at' => $instance->created_at,
|
||
'updated_at' => $instance->updated_at,
|
||
'finished_at' => $instance->finished_at,
|
||
],
|
||
'steps' => array_map(function($s){
|
||
return [
|
||
'name' => $s['name'] ?? '',
|
||
'type' => $s['type'] ?? 'all',
|
||
'condition' => $s['condition'] ?? null,
|
||
];
|
||
}, $steps),
|
||
'records' => array_map(function($r){
|
||
// 转成数组(Illuminate 返回的是 stdClass)
|
||
return [
|
||
'id' => (int)$r->id,
|
||
'instance_id' => (int)$r->instance_id,
|
||
'approver_id' => (int)$r->approver_id,
|
||
'step_index' => (int)$r->step_index,
|
||
'action' => $r->action,
|
||
'status' => $r->status,
|
||
'comment' => $r->comment,
|
||
'approved_at' => $r->approved_at,
|
||
'created_at' => $r->created_at,
|
||
'updated_at' => $r->updated_at,
|
||
'approver_name'=> property_exists($r, 'approver_name') ? $r->approver_name : null,
|
||
'step_name' => $r->step_name,
|
||
'step_type' => $r->step_type,
|
||
];
|
||
}, $records),
|
||
'summary' => $summary,
|
||
];
|
||
}
|
||
|
||
/**
|
||
* 极简:审批实例的时间轴(只返回每一步的总状态)
|
||
*
|
||
* @param int $instanceId
|
||
* @return array<int, array{index:int,name:string,type:string,status:string}>
|
||
*/
|
||
public function getSimpleTimeline1(int $instanceId): array
|
||
{
|
||
// 基础数据
|
||
$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) ?: [];
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
$currentStep = (int)$instance->current_step;
|
||
|
||
// 读出所有记录并分组
|
||
$records = Db::table('approval_records as ar')
|
||
->where('ar.instance_id', $instanceId)
|
||
->orderBy('ar.step_index')
|
||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||
->select('ar.*')
|
||
->get()
|
||
->toArray();
|
||
|
||
$byStep = [];
|
||
foreach ($records as $r) {
|
||
$idx = (int)$r->step_index;
|
||
$byStep[$idx][] = $r;
|
||
}
|
||
|
||
$timeline = [];
|
||
|
||
foreach ($steps as $i => $meta) {
|
||
$name = $meta['name'] ?? ('Step '.$i);
|
||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
$condStr = $meta['condition'] ?? null;
|
||
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
|
||
|
||
$recs = $byStep[$i] ?? [];
|
||
$hasRecs = !empty($recs);
|
||
|
||
// 默认状态
|
||
$status = 'pending';
|
||
|
||
if ($hasRecs) {
|
||
// 统计是否全部 timeout(且无 approve/reject/pending)
|
||
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
|
||
foreach ($recs as $rr) {
|
||
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
|
||
}
|
||
|
||
// 用已有的聚合规则拿到结论
|
||
$outcome = $this->getStepOutcome($instanceId, $i, $type); // 'pending'|'approve'|'reject'
|
||
|
||
if ($outcome === 'reject') {
|
||
$status = 'rejected';
|
||
} elseif ($outcome === 'approve') {
|
||
$status = 'approved';
|
||
} else {
|
||
// 没有结论,再看是不是纯超时
|
||
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
|
||
$status = 'timeout';
|
||
} else {
|
||
$status = 'processing';
|
||
}
|
||
}
|
||
} else {
|
||
// 无记录:根据位置与条件判断展示
|
||
if ($i < $currentStep) {
|
||
$status = $condOk ? 'approved' : 'skipped';
|
||
} elseif ($i === $currentStep && $instance->status === 'processing') {
|
||
// 理论上当前步应有记录;防御性展示为 processing
|
||
$status = 'processing';
|
||
} else {
|
||
$status = $condOk ? 'pending' : 'skipped';
|
||
}
|
||
}
|
||
|
||
$timeline[] = [
|
||
'index' => (int)$i,
|
||
'name' => $name,
|
||
'type' => $type, // any | all
|
||
'status' => $status, // approved | rejected | processing | pending | skipped | timeout
|
||
];
|
||
}
|
||
|
||
return $timeline;
|
||
}
|
||
|
||
|
||
/**
|
||
* 极简时间轴(含每步时间 & 申请人信息)
|
||
*
|
||
* @param int $instanceId
|
||
* @param string|null $userNameColumn 申请人姓名列(如 'name' / 'nickname')。不需要就传 null
|
||
* @param string $initiatorPlacement 'prepend' | 'attach'
|
||
* - 'prepend':在最前插入一条“发起申请”块(推荐)
|
||
* - 'attach' :把申请人信息并到第一步的返回项里(不新增块)
|
||
*
|
||
* 返回:按显示顺序的数组,每项:
|
||
* {
|
||
* index:int // 步骤索引;若为“发起申请”块则为 -1
|
||
* name:string // 步骤名或“发起申请”
|
||
* type:string // any|all 或 '-'(发起块)
|
||
* status:string // approved|rejected|processing|pending|skipped|timeout|submitted(发起块)
|
||
* started_at:?string
|
||
* finished_at:?string
|
||
* duration_seconds:?int
|
||
* // 仅发起块或 attach 时的第一步会包含:
|
||
* initiator_id?:int
|
||
* initiator_name?:?string
|
||
* }
|
||
*/
|
||
public function getSimpleTimeline(int $instanceId, ?string $userNameColumn = null, string $initiatorPlacement = 'prepend'): array
|
||
{
|
||
// 基础数据
|
||
$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) ?: [];
|
||
$formData = json_decode($instance->form_data ?? '{}', true) ?: [];
|
||
$curStep = (int)$instance->current_step;
|
||
|
||
// 申请人
|
||
$initiatorId = (int)$instance->initiator_id;
|
||
$initiatorName = null;
|
||
if ($userNameColumn) {
|
||
$initiatorName = Db::table('user')->where('id', $initiatorId)->value($userNameColumn);
|
||
}
|
||
|
||
// 审批记录
|
||
$records = Db::table('approval_records as ar')
|
||
->where('ar.instance_id', $instanceId)
|
||
->orderBy('ar.step_index')
|
||
->orderByRaw('COALESCE(ar.approved_at, ar.created_at) asc')
|
||
->select('ar.*')
|
||
->get()
|
||
->toArray();
|
||
|
||
$byStep = [];
|
||
foreach ($records as $r) {
|
||
$idx = (int)$r->step_index;
|
||
$byStep[$idx][] = $r;
|
||
}
|
||
|
||
$timeline = [];
|
||
|
||
// 可选:先插入“发起申请”块(index = -1)
|
||
if ($initiatorPlacement === 'prepend') {
|
||
$appliedAt = $instance->created_at ?: null;
|
||
$timeline[] = [
|
||
'index' => -1,
|
||
'name' => '发起申请',
|
||
'type' => '-',
|
||
'status' => 'submitted',
|
||
'started_at' => $appliedAt,
|
||
'finished_at' => $appliedAt,
|
||
'duration_seconds' => 0,
|
||
'initiator_id' => $initiatorId,
|
||
'initiator_name' => $initiatorName,
|
||
];
|
||
}
|
||
|
||
// 每一步
|
||
foreach ($steps as $i => $meta) {
|
||
$name = $meta['name'] ?? ('Step '.$i);
|
||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
$condStr = $meta['condition'] ?? null;
|
||
$condOk = $condStr ? $this->evaluateCondition($condStr, $formData) : true;
|
||
|
||
$recs = $byStep[$i] ?? [];
|
||
$hasRecs = !empty($recs);
|
||
|
||
$status = 'pending';
|
||
$startedAt = null;
|
||
$finishedAt = null;
|
||
$durationSec = null;
|
||
|
||
if ($hasRecs) {
|
||
// 统计时间
|
||
$minCreated = null;
|
||
$minApproveAt = null; $maxApproveAt = null;
|
||
$minRejectAt = null;
|
||
$maxTimeoutAt = null;
|
||
$cnt = ['pending'=>0,'approved'=>0,'rejected'=>0,'timeout'=>0];
|
||
|
||
foreach ($recs as $rr) {
|
||
if ($rr->created_at && (!$minCreated || $rr->created_at < $minCreated)) $minCreated = $rr->created_at;
|
||
if ($rr->approved_at) {
|
||
if ($rr->status === 'approved') {
|
||
if (!$minApproveAt || $rr->approved_at < $minApproveAt) $minApproveAt = $rr->approved_at;
|
||
if (!$maxApproveAt || $rr->approved_at > $maxApproveAt) $maxApproveAt = $rr->approved_at;
|
||
} elseif ($rr->status === 'rejected') {
|
||
if (!$minRejectAt || $rr->approved_at < $minRejectAt) $minRejectAt = $rr->approved_at;
|
||
} elseif ($rr->status === 'timeout') {
|
||
if (!$maxTimeoutAt || $rr->approved_at > $maxTimeoutAt) $maxTimeoutAt = $rr->approved_at;
|
||
}
|
||
}
|
||
if (isset($cnt[$rr->status])) $cnt[$rr->status]++;
|
||
}
|
||
|
||
$startedAt = $minCreated;
|
||
|
||
// 总结果
|
||
$outcome = $this->getStepOutcome($instanceId, $i, $type); // 'pending'|'approve'|'reject'
|
||
if ($outcome === 'reject') {
|
||
$status = 'rejected';
|
||
$finishedAt = $minRejectAt;
|
||
} elseif ($outcome === 'approve') {
|
||
$status = 'approved';
|
||
$finishedAt = ($type === 'any') ? $minApproveAt : $maxApproveAt;
|
||
} else {
|
||
if ($cnt['timeout'] > 0 && $cnt['approved'] === 0 && $cnt['rejected'] === 0 && $cnt['pending'] === 0) {
|
||
$status = 'timeout';
|
||
$finishedAt = $maxTimeoutAt;
|
||
} else {
|
||
$status = 'processing';
|
||
}
|
||
}
|
||
|
||
if ($startedAt && $finishedAt) {
|
||
$durationSec = max(0, strtotime($finishedAt) - strtotime($startedAt));
|
||
}
|
||
} else {
|
||
// 无记录:根据位置与条件展示
|
||
if ($i < $curStep) {
|
||
$status = $condOk ? 'approved' : 'skipped';
|
||
} elseif ($i === $curStep && $instance->status === 'processing') {
|
||
$status = 'processing';
|
||
} else {
|
||
$status = $condOk ? 'pending' : 'skipped';
|
||
}
|
||
}
|
||
|
||
$row = [
|
||
'index' => (int)$i,
|
||
'name' => $name,
|
||
'type' => $type,
|
||
'status' => $status,
|
||
'started_at' => $startedAt,
|
||
'finished_at' => $finishedAt,
|
||
'duration_seconds' => $durationSec,
|
||
];
|
||
|
||
// 如果需要把申请人信息“并到第一步”而不是独立一块
|
||
if ($initiatorPlacement === 'attach' && empty($timeline)) {
|
||
$row['initiator_id'] = $initiatorId;
|
||
$row['initiator_name'] = $initiatorName;
|
||
}
|
||
|
||
$timeline[] = $row;
|
||
}
|
||
|
||
return $timeline;
|
||
}
|
||
|
||
|
||
/**
|
||
* 按业务ID获取“最新进展”的极简状态
|
||
* - 已完结:直接返回实例最终状态
|
||
* - 进行中:返回当前步骤(索引、名称、类型)与当前待处理审批者列表(pending)
|
||
*
|
||
* @param string $bizId
|
||
* @param string|null $bizType 可选限定业务类型
|
||
* @param string|null $userNameColumn 可选:审批人姓名字段(如 user.name / nickname)
|
||
* @return array{
|
||
* instance_id:int, flow_type:string, biz_type:string, biz_id:string,
|
||
* status:string, // 实例状态:approved|rejected|revoked|processing
|
||
* finished_at:?string, updated_at:?string,
|
||
* step?: array{
|
||
* index:int, name:string, type:string, // any|all
|
||
* status:string, // processing|timeout(进行中场景)
|
||
* pending_approvers: array<int, array{approver_id:int, approver_name?:?string}>
|
||
* }
|
||
* }
|
||
*/
|
||
public function getBriefStatusByBiz(string $bizId, ?string $bizType = null, ?string $userNameColumn = null): array
|
||
{
|
||
// 1) 最新实例
|
||
$q = Db::table('approval_instances')->where('biz_id', $bizId);
|
||
if ($bizType !== null && $bizType !== '') {
|
||
$q->where('biz_type', $bizType);
|
||
}
|
||
$instance = $q->orderByDesc('id')->first();
|
||
if (!$instance) {
|
||
// throw new BusinessException('未找到对应的审批实例');
|
||
return [];
|
||
}
|
||
|
||
$base = [
|
||
'instance_id' => (int)$instance->id,
|
||
'flow_type' => $instance->flow_type,
|
||
'biz_type' => $instance->biz_type,
|
||
'biz_id' => $instance->biz_id,
|
||
'status' => $instance->status,
|
||
'finished_at' => $instance->finished_at,
|
||
'updated_at' => $instance->updated_at,
|
||
];
|
||
|
||
// 2) 已完结:直接返回
|
||
if ($instance->status !== 'processing') {
|
||
return $base;
|
||
}
|
||
|
||
// 3) 进行中:取当前步骤 + 当前 pending 审批者
|
||
$flow = Db::table('approval_flows')->where('type', $instance->flow_type)->first();
|
||
if (!$flow) {
|
||
// 容错:没有流程定义
|
||
return $base;
|
||
}
|
||
$steps = json_decode($flow->steps, true) ?: [];
|
||
$idx = (int)$instance->current_step;
|
||
$meta = $steps[$idx] ?? [];
|
||
|
||
$typeRaw = strtolower($meta['type'] ?? 'all');
|
||
$type = in_array($typeRaw, ['any','all']) ? $typeRaw : 'all';
|
||
|
||
// 当前步的所有记录
|
||
$recsQ = Db::table('approval_records')->where('instance_id', $instance->id)->where('step_index', $idx);
|
||
|
||
// 待处理人(只列出 pending)
|
||
if ($userNameColumn) {
|
||
$pendingRows = Db::table('approval_records as ar')
|
||
->leftJoin('user as u', 'ar.approver_id', '=', 'u.id')
|
||
->where('ar.instance_id', $instance->id)
|
||
->where('ar.step_index', $idx)
|
||
->where('ar.status', 'pending')
|
||
->select('ar.approver_id', Db::raw("u.`{$userNameColumn}` as approver_name"))
|
||
->get()
|
||
->toArray();
|
||
} else {
|
||
$pendingRows = Db::table('approval_records')
|
||
->where('instance_id', $instance->id)
|
||
->where('step_index', $idx)
|
||
->where('status', 'pending')
|
||
->select('approver_id')
|
||
->get()
|
||
->toArray();
|
||
}
|
||
|
||
$pendingApprovers = [];
|
||
foreach ($pendingRows as $r) {
|
||
$row = ['approver_id' => (int)$r->approver_id];
|
||
if ($userNameColumn && property_exists($r, 'approver_name')) {
|
||
$row['approver_name'] = $r->approver_name;
|
||
}
|
||
$pendingApprovers[] = $row;
|
||
}
|
||
|
||
// 当前步的聚合状态(进行中通常返回 processing;若全被超时任务置为 timeout,则返回 timeout)
|
||
// 先快速统计是否纯 timeout
|
||
$stats = Db::table('approval_records')
|
||
->where('instance_id', $instance->id)
|
||
->where('step_index', $idx)
|
||
->selectRaw("SUM(CASE WHEN status='pending' THEN 1 ELSE 0 END) as c_pending")
|
||
->selectRaw("SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as c_approved")
|
||
->selectRaw("SUM(CASE WHEN status='rejected' THEN 1 ELSE 0 END) as c_rejected")
|
||
->selectRaw("SUM(CASE WHEN status='timeout' THEN 1 ELSE 0 END) as c_timeout")
|
||
->first();
|
||
|
||
$stepStatus = 'processing';
|
||
if ($stats && (int)$stats->c_timeout > 0 && (int)$stats->c_pending === 0 && (int)$stats->c_approved === 0 && (int)$stats->c_rejected === 0) {
|
||
$stepStatus = 'timeout';
|
||
}
|
||
|
||
$base['step'] = [
|
||
'index' => $idx,
|
||
'name' => $meta['name'] ?? ('Step '.$idx),
|
||
'type' => $type, // any|all
|
||
'status' => $stepStatus, // processing|timeout
|
||
'pending_approvers' => $pendingApprovers, // 当前正在等待的审批者
|
||
];
|
||
|
||
return $base;
|
||
}
|
||
|
||
|
||
} |