119 lines
5.3 KiB
PHP
119 lines
5.3 KiB
PHP
<?php
|
||
|
||
namespace App\Services\DingTalk;
|
||
|
||
use App\Exceptions\CourierOnboardingDingTalkException;
|
||
use Illuminate\Http\Client\ConnectionException;
|
||
use Illuminate\Support\Facades\Http;
|
||
use Illuminate\Support\Carbon;
|
||
|
||
/** 独立的「财务系统」应用,仅负责快递入驻审批。创建请求绝不自动重试。 */
|
||
class CourierOnboardingClient
|
||
{
|
||
private const BASE_URL = 'https://api.dingtalk.com/v1.0';
|
||
|
||
public function accessToken(): string
|
||
{
|
||
$settings = config('courier_onboarding.dingtalk');
|
||
foreach (['client_id', 'client_secret', 'process_code', 'originator_user_id', 'dept_id'] as $key) {
|
||
if (trim((string) ($settings[$key] ?? '')) === '') {
|
||
throw new CourierOnboardingDingTalkException('入驻钉钉应用配置不完整,请联系系统管理员。');
|
||
}
|
||
}
|
||
|
||
try {
|
||
$response = Http::acceptJson()->connectTimeout(3)->timeout(8)
|
||
->post(self::BASE_URL . '/oauth2/accessToken', [
|
||
'appKey' => $settings['client_id'],
|
||
'appSecret' => $settings['client_secret'],
|
||
]);
|
||
} catch (ConnectionException) {
|
||
throw new CourierOnboardingDingTalkException('获取钉钉应用凭证超时,尚未发起审批。');
|
||
}
|
||
|
||
$token = $response->json('accessToken');
|
||
if (! $response->successful() || ! is_string($token) || trim($token) === '') {
|
||
throw new CourierOnboardingDingTalkException('获取钉钉应用凭证失败,请检查应用凭据和权限。');
|
||
}
|
||
|
||
return $token;
|
||
}
|
||
|
||
public function start(string $token, array $payload): string
|
||
{
|
||
try {
|
||
$response = Http::acceptJson()->connectTimeout(3)->timeout(12)
|
||
->withHeaders(['x-acs-dingtalk-access-token' => $token])
|
||
->post(self::BASE_URL . '/workflow/processInstances', $payload);
|
||
} catch (ConnectionException) {
|
||
throw new CourierOnboardingDingTalkException('发起请求超时,请先在钉钉核查是否已生成审批,勿重复发起。', true);
|
||
}
|
||
|
||
if (! $response->successful()) {
|
||
// 不保存第三方原始响应,防止请求内容或凭据被带入入驻记录。
|
||
$code = preg_replace('/[^a-zA-Z0-9_.-]/', '', (string) $response->json('code', ''));
|
||
$uncertain = $response->serverError() || $response->status() === 408;
|
||
throw new CourierOnboardingDingTalkException(
|
||
'钉钉返回 HTTP ' . $response->status() . ($code !== '' ? '(' . substr($code, 0, 100) . ')' : '')
|
||
. '。' . ($uncertain ? '请先核查是否已生成审批。' : '请检查流程配置和发起权限。'),
|
||
$uncertain,
|
||
);
|
||
}
|
||
|
||
$id = $response->json('instanceId');
|
||
if (! is_string($id) || trim($id) === '') {
|
||
throw new CourierOnboardingDingTalkException('钉钉响应缺少审批编号,请先核查是否已生成审批。', true);
|
||
}
|
||
|
||
return trim($id);
|
||
}
|
||
|
||
public function instance(string $token, string $instanceId): array
|
||
{
|
||
try {
|
||
$response = Http::acceptJson()->connectTimeout(3)->timeout(8)
|
||
->withHeaders(['x-acs-dingtalk-access-token' => $token])
|
||
->get(self::BASE_URL . '/workflow/processInstances', ['processInstanceId' => $instanceId]);
|
||
} catch (ConnectionException) {
|
||
throw new CourierOnboardingDingTalkException('查询钉钉审批超时。');
|
||
}
|
||
|
||
if (! $response->successful() || ! is_array($response->json('result'))) {
|
||
throw new CourierOnboardingDingTalkException('无法读取指定的钉钉审批。');
|
||
}
|
||
|
||
return $response->json('result');
|
||
}
|
||
|
||
public function belongsToProcess(string $token, string $instanceId, string $createdAt): bool
|
||
{
|
||
$created = Carbon::parse($createdAt);
|
||
$nextToken = 0;
|
||
// 仅查询创建时刻前后一分钟的指定流程与发起人,限制核查范围。
|
||
for ($page = 0; $page < 5; $page++) {
|
||
$response = Http::acceptJson()->connectTimeout(3)->timeout(8)
|
||
->withHeaders(['x-acs-dingtalk-access-token' => $token])
|
||
->post(self::BASE_URL . '/workflow/processes/instanceIds/query', [
|
||
'processCode' => config('courier_onboarding.dingtalk.process_code'),
|
||
'userIds' => [config('courier_onboarding.dingtalk.originator_user_id')],
|
||
'startTime' => $created->copy()->subMinute()->getTimestampMs(),
|
||
'endTime' => $created->copy()->addMinute()->getTimestampMs(),
|
||
'maxResults' => 100, 'nextToken' => $nextToken,
|
||
]);
|
||
if (! $response->successful() || ! is_array($response->json('result.list'))) {
|
||
throw new CourierOnboardingDingTalkException('无法核对审批所属流程,未关联。');
|
||
}
|
||
if (in_array($instanceId, $response->json('result.list'), true)) {
|
||
return true;
|
||
}
|
||
$next = $response->json('result.nextToken');
|
||
if ($next === null || $next === '' || (int) $next === $nextToken) {
|
||
return false;
|
||
}
|
||
$nextToken = (int) $next;
|
||
}
|
||
|
||
return false;
|
||
}
|
||
}
|