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', ]; // 解析首步审批人(支持 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, ]); // 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)); } 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; $msg = "{$prefix}您有一条审批 {$text}:流程ID {$instance->id},类型 {$instance->flow_type}"; // MessageService::send($userIds, $msg); $pushData = [ 'sender_id' => 0, 'sender_type' => 'system', 'title' => $msg, 'content' => $msg, 'scene' => 1, 'recipient' => $userIds, 'push_method' => 1 ]; $msgId = (new MessageNotificationsService())->pushNotice($pushData); var_dump($msgId); } 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, * records: array, * 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 */ 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; } }