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; case 'group_members': { // $gid = (int)($cfg['group_id'] ?? 0); $gid = (int)($cfg['value'] ?? 0); if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); $ids = $this->getGroupMemberIds($gid); // A 方案 break; } case 'scenario_leader_of_initiator': { $formData = json_decode($instance->form_data ?? '{}', true) ?: []; // $scenario = $cfg['scenario'] ?? ($formData[$cfg['scenario_field']] ?? null); $scenario = $val ?? ($formData[$cfg['scenario_field']] ?? null); if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); $mustBelong = array_key_exists('must_belong', $cfg) ? (bool)$cfg['must_belong'] : true; $preferPrimary = array_key_exists('prefer_primary', $cfg) ? (bool)$cfg['prefer_primary'] : true; $ids = $this->getLeadersByScenarioForInitiator( (int)$instance->initiator_id, (string)$scenario, $mustBelong, $preferPrimary ); // B 方案(按场景) 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; case 'group_members': { $gid = (int)($val ?? 0); if ($gid <= 0) throw new BusinessException('group_members 需要 group_id'); $uids = $this->getGroupMemberIds($gid); // A 方案 break; } case 'scenario_leader_of_initiator': { $formData = json_decode($instance->form_data ?? '{}', true) ?: []; $scenario = $val ?? ($formData[$cc['scenario_field']] ?? null); if (!$scenario) throw new BusinessException('缺少业务场景(scenario 或 scenario_field)'); $mustBelong = array_key_exists('must_belong', $cc) ? (bool)$cc['must_belong'] : true; $preferPrimary = array_key_exists('prefer_primary', $cc) ? (bool)$cc['prefer_primary'] : true; $uids = $this->getLeadersByScenarioForInitiator( (int)$instance->initiator_id, (string)$scenario, $mustBelong, $preferPrimary ); // B 方案(按场景) 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}"), }; } /** 获取下一个有效步骤索引 */ 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; } /** 聚合本步骤结论 */ private function getStepOutcome(int $instanceId, int $round, int $stepIndex, string $type): string { $records = Db::table('approval_records') ->where('instance_id', $instanceId) ->where('round', $round) // ->where('step_index', $stepIndex) ->get(); $pending = $approved = $rejected = $timeout = $skipped = 0; foreach ($records as $r) { switch ($r->status) { case 'pending': $pending++; break; case 'approved': $approved++; break; case 'rejected': $rejected++; break; case 'timeout': $timeout++; break; case 'skipped': $skipped++; break; } } $type = strtolower($type); if ($type === 'any') { if ($rejected > 0) return 'reject'; if ($approved > 0) return 'approve'; // 全为 skipped/timeout 且无 pending/rejected ⇒ 通过 if ($pending === 0 && $rejected === 0 && $approved === 0) return 'approve'; return 'pending'; } if ($rejected > 0) return 'reject'; if ($pending > 0) return 'pending'; return 'approve'; } /** 关闭本步其余待办为 skipped */ private function closeOtherPending(int $instanceId, int $round, int $stepIndex, array $keepApproverIds = []): void { $now = date('Y-m-d H:i:s'); $q = Db::table('approval_records') ->where('instance_id', $instanceId) ->where('round', $round) // ->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(object $instance, int $stepIndex, int $approverId): void { $round = (int)($instance->round ?? 1); $now = date('Y-m-d H:i:s'); $exists = Db::table('approval_records') ->where('instance_id', (int)$instance->id) ->where('round', $round) // 限当前轮 ->where('step_index', $stepIndex) ->where('approver_id', $approverId) ->where('status', 'pending') ->exists(); if ($exists) return; Db::table('approval_records')->insert([ 'instance_id' => (int)$instance->id, 'approver_id' => $approverId, 'step_index' => $stepIndex, 'round' => $round, // 'action' => null, 'status' => 'pending', 'comment' => null, 'approved_at' => null, 'created_at' => $now, 'updated_at' => $now, ]); } /** 取“某实例、某轮次、某步骤”里当前 pending 的审批人 */ private function getPendingApproverIds(int $instanceId, int $round, int $stepIndex): array { $ids = Db::table('approval_records') ->where('instance_id', $instanceId) ->where('round', $round) ->where('step_index', $stepIndex) ->where('status', 'pending') ->pluck('approver_id')->toArray(); return array_values(array_unique(array_map('intval', $ids))); } /** 从角色取用户ID */ 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; } /** A) 取组内全部成员 */ private function getGroupMemberIds(int $groupId): array { $rows = Db::table('user_group_members') ->where('group_id', $groupId) ->where('status', 1) ->pluck('user_id')->toArray(); return array_values(array_unique(array_map('intval', $rows))); } /** B) 按“场景”在发起人所在的组里找负责人 */ private function getLeadersByScenarioForInitiator( int $initiatorId, string $scenarioCode, bool $mustBelong = true, bool $preferPrimary = true ): array { // 找出“该场景下发起人所在的所有组” $groups = Db::table('user_group as g') ->join('user_group_members as m', 'm.group_id', '=', 'g.id') ->where('g.scenario_code', $scenarioCode) ->where('g.status', 1) ->where('m.user_id', $initiatorId) ->where('m.status', 1) ->orderByDesc('m.is_primary') // 主归属优先 ->orderByDesc('g.sort') // 其次按组排序 ->select('g.id', 'm.is_primary') ->get()->toArray(); if (empty($groups)) { if ($mustBelong) { throw new BusinessException('发起人在该业务场景下未加入任何分组'); } // 不强制属于:允许只按“该场景下任一组”的负责人(不推荐) $groups = Db::table('user_group as g') ->where('g.scenario_code', $scenarioCode) ->where('g.status', 1) ->orderByDesc('g.sort') ->select('g.id', Db::raw('0 as is_primary')) ->get()->toArray(); if (empty($groups)) { throw new BusinessException('该业务场景下未配置任何分组'); } } // 选择一个“目标组” $targetGroupId = null; if ($preferPrimary) { foreach ($groups as $g) { if ((int)$g->is_primary === 1) { $targetGroupId = (int)$g->id; break; } } } if (!$targetGroupId) { $targetGroupId = (int)$groups[0]->id; // 用排序第一的组 } // 取负责人(可支持多负责人) $leaders = Db::table('user_group_members') ->where('group_id', $targetGroupId) ->where('is_leader', 1) ->where('status', 1) ->pluck('user_id')->toArray(); $ids = array_values(array_unique(array_map('intval', $leaders))); if (empty($ids)) throw new BusinessException('目标分组未配置负责人'); return $ids; } public function StetApprovers(array $step, object $instance, bool $avoidSelf = true){ return$this->resolveApprovers($step, $instance, $avoidSelf); } }