>,pages:int} */ public function listReports( CarbonInterface $from, CarbonInterface $to, ?string $templateName = null, ): array { if (! $this->isConfigured()) { throw new DingTalkWorklogException( '钉钉应用凭据未配置', 'dingtalk_credentials_missing', ); } $templateName = trim((string) $templateName); $userId = trim((string) config('dingtalk.worklog.userid', '')); $size = (int) config('dingtalk.worklog.page_size', 20); $maxPages = (int) config('dingtalk.worklog.max_pages', 100); $cursor = 0; $pages = 0; $reports = []; do { $payload = [ 'start_time' => (int) $from->getTimestampMs(), 'end_time' => (int) $to->getTimestampMs(), 'cursor' => $cursor, 'size' => $size, ]; if ($templateName !== '') { $payload['template_name'] = $templateName; } if ($userId !== '') { $payload['userid'] = $userId; } try { $response = Http::asJson() ->acceptJson() ->timeout($this->timeout()) ->post( self::LIST_URL . '?access_token=' . rawurlencode($this->accessToken()), $payload, ); } catch (ConnectionException|RequestException $exception) { throw new DingTalkWorklogException( '钉钉日志列表请求失败', 'dingtalk_worklog_connection_failed', true, $exception, ); } $json = (array) $response->json(); $errorCode = (int) ($json['errcode'] ?? -1); $errorMessage = trim((string) ($json['errmsg'] ?? '')); if (! $response->successful() || $errorCode !== 0) { if ($errorCode === 88 || str_contains($errorMessage, 'qyapi_report_query') || str_contains($errorMessage, '60011')) { throw new DingTalkWorklogException( '钉钉日志接口缺少 qyapi_report_query 权限', 'dingtalk_worklog_permission_required', ); } throw new DingTalkWorklogException( '钉钉日志列表查询失败:' . ($errorMessage !== '' ? $errorMessage : 'HTTP ' . $response->status()), in_array($response->status(), [401, 403, 429], true) || $response->status() >= 500 ? 'dingtalk_worklog_request_retryable' : 'dingtalk_worklog_request_failed', in_array($response->status(), [401, 403, 429], true) || $response->status() >= 500, ); } $result = (array) ($json['result'] ?? []); $pageRows = (array) ($result['data_list'] ?? $result['list'] ?? $result['data'] ?? []); foreach ($pageRows as $row) { if (is_array($row)) { $reports[(string) ($row['report_id'] ?? $row['id'] ?? count($reports))] = $row; } } $pages++; $hasMore = filter_var($result['has_more'] ?? false, FILTER_VALIDATE_BOOL); $nextCursor = $result['next_cursor'] ?? null; if (! $hasMore || ! is_numeric($nextCursor) || $pages >= $maxPages) { break; } $cursor = (int) $nextCursor; } while (true); return ['reports' => array_values($reports), 'pages' => $pages]; } private function accessToken(): string { $token = Cache::remember( self::TOKEN_CACHE_KEY, now()->addMinutes(100), function (): ?string { try { $response = Http::acceptJson() ->timeout($this->timeout()) ->get((string) config('dingtalk.app_token_url'), [ 'appkey' => (string) config('dingtalk.client_id'), 'appsecret' => (string) config('dingtalk.client_secret'), ]) ->throw(); } catch (ConnectionException|RequestException $exception) { throw new DingTalkWorklogException( '钉钉应用令牌获取失败', 'dingtalk_token_request_failed', true, $exception, ); } $token = trim((string) $response->json('access_token')); return (int) $response->json('errcode', -1) === 0 && $token !== '' ? $token : null; }, ); if (! is_string($token) || $token === '') { throw new DingTalkWorklogException( '钉钉应用令牌不可用', 'dingtalk_token_unavailable', true, ); } return $token; } private function timeout(): int { return max(3, min(30, (int) config('dingtalk.timeout', 10))); } }