CaiWuXiTong/modules/EcommerceFinance/Services/DingTalkWorklogClient.php

167 lines
6.0 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Carbon\CarbonInterface;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Modules\EcommerceFinance\Exceptions\DingTalkWorklogException;
class DingTalkWorklogClient
{
private const LIST_URL = 'https://oapi.dingtalk.com/topapi/report/list';
private const TOKEN_CACHE_KEY = 'dingtalk:app_access_token';
public function isConfigured(): bool
{
return trim((string) config('dingtalk.client_id', '')) !== ''
&& trim((string) config('dingtalk.client_secret', '')) !== '';
}
/**
* @return array{reports:list<array<string,mixed>>,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)));
}
}