connectTimeout(3)->timeout(8) ->post(self::BASE_URL . '/oauth2/accessToken', [ 'appKey' => $settings['client_id'], 'appSecret' => $settings['client_secret'], ]); } catch (ConnectionException) { throw new CourierOnboardingDingTalkException('获取钉钉应用凭证超时,尚未发起审批。'); } $token = $response->json('accessToken'); if (! $response->successful() || ! is_string($token) || trim($token) === '') { throw new CourierOnboardingDingTalkException('获取钉钉应用凭证失败,请检查应用凭据和权限。'); } return $token; } public function start(string $token, array $payload): string { try { $response = Http::acceptJson()->connectTimeout(3)->timeout(12) ->withHeaders(['x-acs-dingtalk-access-token' => $token]) ->post(self::BASE_URL . '/workflow/processInstances', $payload); } catch (ConnectionException) { throw new CourierOnboardingDingTalkException('发起请求超时,请先在钉钉核查是否已生成审批,勿重复发起。', true); } if (! $response->successful()) { // 不保存第三方原始响应,防止请求内容或凭据被带入入驻记录。 $code = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string) $response->json('code', '')); $uncertain = $response->serverError() || $response->status() === 408; throw new CourierOnboardingDingTalkException( '钉钉返回 HTTP ' . $response->status() . ($code !== '' ? '(' . substr($code, 0, 100) . ')' : '') . '。' . ($uncertain ? '请先核查是否已生成审批。' : '请检查流程配置和发起权限。'), $uncertain, ); } $id = $response->json('instanceId'); if (! is_string($id) || trim($id) === '') { throw new CourierOnboardingDingTalkException('钉钉响应缺少审批编号,请先核查是否已生成审批。', true); } return trim($id); } public function instance(string $token, string $instanceId): array { try { $response = Http::acceptJson()->connectTimeout(3)->timeout(8) ->withHeaders(['x-acs-dingtalk-access-token' => $token]) ->get(self::BASE_URL . '/workflow/processInstances', ['processInstanceId' => $instanceId]); } catch (ConnectionException) { throw new CourierOnboardingDingTalkException('查询钉钉审批超时。'); } if (! $response->successful() || ! is_array($response->json('result'))) { throw new CourierOnboardingDingTalkException('无法读取指定的钉钉审批。'); } return $response->json('result'); } public function belongsToProcess(string $token, string $instanceId, string $createdAt): bool { $created = Carbon::parse($createdAt); $nextToken = 0; // 仅查询创建时刻前后一分钟的指定流程与发起人,限制核查范围。 for ($page = 0; $page < 5; $page++) { $response = Http::acceptJson()->connectTimeout(3)->timeout(8) ->withHeaders(['x-acs-dingtalk-access-token' => $token]) ->post(self::BASE_URL . '/workflow/processes/instanceIds/query', [ 'processCode' => config('courier_onboarding.dingtalk.process_code'), 'userIds' => [config('courier_onboarding.dingtalk.originator_user_id')], 'startTime' => $created->copy()->subMinute()->getTimestampMs(), 'endTime' => $created->copy()->addMinute()->getTimestampMs(), 'maxResults' => 100, 'nextToken' => $nextToken, ]); if (! $response->successful() || ! is_array($response->json('result.list'))) { throw new CourierOnboardingDingTalkException('无法核对审批所属流程,未关联。'); } if (in_array($instanceId, $response->json('result.list'), true)) { return true; } $next = $response->json('result.nextToken'); if ($next === null || $next === '' || (int) $next === $nextToken) { return false; } $nextToken = (int) $next; } return false; } }