fix: 精确隔离OA审批接口故障 (#778)

This commit is contained in:
yueyang 2026-10-29 22:41:53 +08:00
parent 9ae1c05862
commit d5c55a8dbb
4 changed files with 58 additions and 26 deletions

View File

@ -0,0 +1,22 @@
<?php
namespace Modules\EcommerceFinance\Exceptions;
use RuntimeException;
use Throwable;
class DingTalkProcessInstanceException extends RuntimeException
{
public function __construct(
string $message,
private readonly bool $stopsRun = false,
?Throwable $previous = null,
) {
parent::__construct($message, 0, $previous);
}
public function stopsRun(): bool
{
return $this->stopsRun;
}
}

View File

@ -8,6 +8,7 @@ use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Facades\Schema;
use Illuminate\Support\Str;
use Modules\EcommerceFinance\Exceptions\DingTalkProcessInstanceException;
use RuntimeException;
use Throwable;
@ -27,8 +28,6 @@ class DingProcessDetailMirrorSynchronizer
private const APPROVAL_API_BACKLOG_BATCH_LIMIT = 750;
private const APPROVAL_API_MAX_CONSECUTIVE_FAILURES = 5;
private const BUSINESS_ID_SYNC_LIMIT = 50;
private readonly ConnectionInterface $connection;
@ -37,8 +36,6 @@ class DingProcessDetailMirrorSynchronizer
private bool $approvalApiAvailable = true;
private int $approvalApiFailureStreak = 0;
/** @var array<string,array<string,mixed>> */
private array $approvalSnapshots = [];
@ -67,7 +64,6 @@ class DingProcessDetailMirrorSynchronizer
{
$this->approvalApiAttempts = 0;
$this->approvalApiAvailable = true;
$this->approvalApiFailureStreak = 0;
$this->approvalSnapshots = [];
$scope = $this->scope();
$state = $this->state();
@ -177,7 +173,6 @@ class DingProcessDetailMirrorSynchronizer
$this->approvalApiAttempts = 0;
$this->approvalApiAvailable = true;
$this->approvalApiFailureStreak = 0;
$this->approvalSnapshots = [];
$scope = $this->scope();
$state = $this->state();
@ -1310,20 +1305,16 @@ class DingProcessDetailMirrorSynchronizer
$this->approvalApiAttempts++;
try {
$snapshot = $this->approvalClient->processSnapshot($processInstanceId);
$this->approvalApiFailureStreak = 0;
return $this->approvalSnapshots[$processInstanceId] = $snapshot;
return $this->approvalSnapshots[$processInstanceId] =
$this->approvalClient->processSnapshot($processInstanceId);
} catch (Throwable $exception) {
$this->approvalApiFailureStreak++;
if ($this->approvalApiFailureStreak
>= self::APPROVAL_API_MAX_CONSECUTIVE_FAILURES) {
if ($exception instanceof DingTalkProcessInstanceException
&& $exception->stopsRun()) {
$this->approvalApiAvailable = false;
}
Log::warning('钉钉官方审批接口读取流程状态失败。', [
'process_instance_id' => $processInstanceId,
'exception' => $exception::class,
'consecutive_failures' => $this->approvalApiFailureStreak,
]);
return null;

View File

@ -2,9 +2,10 @@
namespace Modules\EcommerceFinance\Services;
use Illuminate\Http\Client\ConnectionException;
use Illuminate\Http\Client\RequestException;
use Illuminate\Support\Facades\Http;
use RuntimeException;
use Modules\EcommerceFinance\Exceptions\DingTalkProcessInstanceException;
class DingTalkProcessInstanceClient
{
@ -48,7 +49,9 @@ class DingTalkProcessInstanceClient
{
$processInstanceId = trim($processInstanceId);
if ($processInstanceId === '') {
throw new RuntimeException('DingTalk process instance ID is required.');
throw new DingTalkProcessInstanceException(
'DingTalk process instance ID is required.',
);
}
if (isset($this->snapshots[$processInstanceId])) {
return $this->snapshots[$processInstanceId];
@ -63,23 +66,28 @@ class DingTalkProcessInstanceClient
->get(self::PROCESS_INSTANCE_URL, [
'processInstanceId' => $processInstanceId,
]);
} catch (RequestException $exception) {
throw new RuntimeException(
} catch (ConnectionException|RequestException $exception) {
throw new DingTalkProcessInstanceException(
'DingTalk process instance lookup failed.',
0,
true,
$exception,
);
}
if (! $response->successful()) {
throw new RuntimeException(
'DingTalk process instance lookup returned HTTP ' . $response->status() . '.',
$status = $response->status();
throw new DingTalkProcessInstanceException(
'DingTalk process instance lookup returned HTTP ' . $status . '.',
in_array($status, [401, 403, 429], true) || $status >= 500,
);
}
$result = $response->json('result');
if (! is_array($result)) {
throw new RuntimeException('DingTalk process instance response was incomplete.');
throw new DingTalkProcessInstanceException(
'DingTalk process instance response was incomplete.',
);
}
$values = [];
@ -114,7 +122,10 @@ class DingTalkProcessInstanceClient
return $this->accessToken;
}
if (! $this->isConfigured()) {
throw new RuntimeException('DingTalk process API credentials are not configured.');
throw new DingTalkProcessInstanceException(
'DingTalk process API credentials are not configured.',
true,
);
}
try {
@ -126,12 +137,19 @@ class DingTalkProcessInstanceClient
])
->throw();
} catch (RequestException $exception) {
throw new RuntimeException('DingTalk application token lookup failed.', 0, $exception);
throw new DingTalkProcessInstanceException(
'DingTalk application token lookup failed.',
true,
$exception,
);
}
$token = trim((string) $response->json('access_token'));
if ((int) $response->json('errcode', -1) !== 0 || $token === '') {
throw new RuntimeException('DingTalk application token was unavailable.');
throw new DingTalkProcessInstanceException(
'DingTalk application token was unavailable.',
true,
);
}
$expiresIn = max(300, (int) $response->json('expires_in', 7200));

View File

@ -5,6 +5,7 @@ namespace Tests\Unit\EcommerceFinance;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Modules\EcommerceFinance\Exceptions\DingTalkProcessInstanceException;
use Modules\EcommerceFinance\Repositories\DingProcessDetailMirrorRepository;
use Modules\EcommerceFinance\Services\DingProcessDetailMirrorSynchronizer;
use Modules\EcommerceFinance\Services\DingTalkProcessInstanceClient;
@ -1174,7 +1175,7 @@ class FakeDingTalkProcessInstanceClient extends DingTalkProcessInstanceClient
{
$this->calls[] = $processInstanceId;
if (in_array($processInstanceId, $this->failingInstances, true)) {
throw new RuntimeException('审批接口模拟失败');
throw new DingTalkProcessInstanceException('审批接口模拟失败');
}
$cashierStatus = $this->cashierStatusByInstance[$processInstanceId] ?? 'not_started';
$tasks = $this->tasksByInstance[$processInstanceId] ?? [];