fix: 精确隔离OA审批接口故障 (#778)
This commit is contained in:
parent
9ae1c05862
commit
d5c55a8dbb
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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] ?? [];
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user