CaiWuXiTong/modules/EcommerceFinance/Services/DingTalkProcessInstanceClient.php
yueyang 2cf75ac10b feat: 按出纳节点重做流程付款状态 (#685)
付款状态改由出纳任务节点判断,金额差额独立展示。
2026-10-27 14:01:23 +08:00

152 lines
4.9 KiB
PHP

<?php
namespace Modules\EcommerceFinance\Services;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use RuntimeException;
class DingTalkProcessInstanceClient
{
private const PROCESS_INSTANCE_URL = 'https://api.dingtalk.com/v1.0/workflow/processInstances';
private const TOKEN_SAFETY_SECONDS = 60;
private ?string $accessToken = null;
private int $accessTokenExpiresAt = 0;
/** @var array<string,array<string,mixed>> */
private array $snapshots = [];
public function isConfigured(): bool
{
return trim((string) config('dingtalk.client_id', '')) !== ''
&& trim((string) config('dingtalk.client_secret', '')) !== '';
}
/**
* Return DingTalk's latest form component values by component name.
*
* @return array<string,mixed>
*/
public function formValues(string $processInstanceId): array
{
return $this->processSnapshot($processInstanceId)['form_values'];
}
/**
* @return array{
* form_values:array<string,mixed>,
* tasks:list<array<string,mixed>>,
* operation_records:list<array<string,mixed>>,
* status:?string,
* result:?string
* }
*/
public function processSnapshot(string $processInstanceId): array
{
$processInstanceId = trim($processInstanceId);
if ($processInstanceId === '') {
throw new RuntimeException('DingTalk process instance ID is required.');
}
if (isset($this->snapshots[$processInstanceId])) {
return $this->snapshots[$processInstanceId];
}
try {
$response = Http::acceptJson()
->timeout($this->timeout())
->withHeaders([
'x-acs-dingtalk-access-token' => $this->appAccessToken(),
])
->get(self::PROCESS_INSTANCE_URL, [
'processInstanceId' => $processInstanceId,
]);
} catch (RequestException $exception) {
throw new RuntimeException(
'DingTalk process instance lookup failed.',
0,
$exception,
);
}
if (! $response->successful()) {
throw new RuntimeException(
'DingTalk process instance lookup returned HTTP ' . $response->status() . '.',
);
}
$result = $response->json('result');
if (! is_array($result)) {
throw new RuntimeException('DingTalk process instance response was incomplete.');
}
$values = [];
foreach ((array) ($result['formComponentValues'] ?? []) as $component) {
if (! is_array($component)) {
continue;
}
$name = trim((string) ($component['name'] ?? ''));
if ($name !== '') {
$values[$name] = $component['value'] ?? null;
}
}
return $this->snapshots[$processInstanceId] = [
'form_values' => $values,
'tasks' => array_values(array_filter(
(array) ($result['tasks'] ?? []),
'is_array',
)),
'operation_records' => array_values(array_filter(
(array) ($result['operationRecords'] ?? []),
'is_array',
)),
'status' => isset($result['status']) ? (string) $result['status'] : null,
'result' => isset($result['result']) ? (string) $result['result'] : null,
];
}
private function appAccessToken(): string
{
if ($this->accessToken !== null && time() < $this->accessTokenExpiresAt) {
return $this->accessToken;
}
if (! $this->isConfigured()) {
throw new RuntimeException('DingTalk process API credentials are not configured.');
}
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 (RequestException $exception) {
throw new RuntimeException('DingTalk application token lookup failed.', 0, $exception);
}
$token = trim((string) $response->json('access_token'));
if ((int) $response->json('errcode', -1) !== 0 || $token === '') {
throw new RuntimeException('DingTalk application token was unavailable.');
}
$expiresIn = max(300, (int) $response->json('expires_in', 7200));
$this->accessToken = $token;
$this->accessTokenExpiresAt = time() + max(
120,
$expiresIn - self::TOKEN_SAFETY_SECONDS,
);
return $token;
}
private function timeout(): int
{
return max(3, min(30, (int) config('dingtalk.timeout', 10)));
}
}