167 lines
7.8 KiB
PHP
167 lines
7.8 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Exceptions\CourierOnboardingDingTalkException;
|
|
use App\Models\Business\CourierOnboarding;
|
|
use App\Models\Business\CourierOnboardingEvent;
|
|
use App\Services\DingTalk\CourierOnboardingClient;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Log;
|
|
use Throwable;
|
|
|
|
class CourierOnboardingDingTalk
|
|
{
|
|
private const EVENTS = ['dingtalk_starting', 'dingtalk_started', 'dingtalk_failed', 'dingtalk_unknown'];
|
|
|
|
public function __construct(private readonly CourierOnboardingClient $client)
|
|
{
|
|
}
|
|
|
|
/** 资料提交后同步尝试一次。外部服务失败不影响入驻资料提交。 */
|
|
public function start(int $companyId, int $id, bool $retry = false): string
|
|
{
|
|
if (! config('courier_onboarding.dingtalk.enabled')) {
|
|
return 'disabled';
|
|
}
|
|
|
|
$onboarding = null;
|
|
$sending = false;
|
|
try {
|
|
$onboarding = DB::transaction(function () use ($companyId, $id, $retry): ?CourierOnboarding {
|
|
$record = $this->query($companyId)->lockForUpdate()->findOrFail($id);
|
|
if (filled($record->dingtalk_instance_id)
|
|
|| ! in_array($record->status, [CourierOnboarding::STATUS_SUBMITTED, CourierOnboarding::STATUS_FIRST_APPROVED], true)) {
|
|
return null;
|
|
}
|
|
$latest = CourierOnboardingEvent::allCompanies()->withTrashed()->companyId($companyId)
|
|
->where('onboarding_id', $id)->whereIn('event_type', self::EVENTS)->latest('id')->first();
|
|
if ($latest !== null && (! $retry || $latest->event_type !== 'dingtalk_failed')) {
|
|
return null;
|
|
}
|
|
// 先持久化占位,再发 HTTP。进程中断也不能盲目重发。
|
|
$this->event($record, 'dingtalk_starting', '财务系统自动发起入驻审批。');
|
|
|
|
return $record;
|
|
});
|
|
if ($onboarding === null) {
|
|
return 'skipped';
|
|
}
|
|
|
|
$payload = $this->payload($onboarding);
|
|
$token = $this->client->accessToken();
|
|
$sending = true;
|
|
$instanceId = $this->client->start($token, $payload);
|
|
$this->bind($onboarding, $instanceId);
|
|
|
|
return 'started';
|
|
} catch (Throwable $exception) {
|
|
$uncertain = $exception instanceof CourierOnboardingDingTalkException ? $exception->uncertain : $sending;
|
|
$message = $exception instanceof CourierOnboardingDingTalkException
|
|
? $exception->getMessage() : '钉钉审批对接异常,请联系系统管理员核查。';
|
|
if ($onboarding !== null) {
|
|
try {
|
|
$this->event($onboarding, $uncertain ? 'dingtalk_unknown' : 'dingtalk_failed', $message);
|
|
} catch (Throwable) {
|
|
// 保留之前的 starting 记录,阻止在数据库故障后重复发起。
|
|
}
|
|
}
|
|
Log::warning('Courier onboarding DingTalk start did not complete.', [
|
|
'company_id' => $companyId, 'onboarding_id' => $id,
|
|
'uncertain' => $uncertain, 'exception_class' => $exception::class,
|
|
]);
|
|
|
|
return $uncertain ? 'unknown' : 'failed';
|
|
}
|
|
}
|
|
|
|
/** 核查超时请求后只补关联,不再次创建审批。 */
|
|
public function reconcile(int $companyId, int $id, string $instanceId): void
|
|
{
|
|
$record = $this->query($companyId)->findOrFail($id);
|
|
$token = $this->client->accessToken();
|
|
$snapshot = $this->client->instance($token, $instanceId);
|
|
$reason = collect($snapshot['formComponentValues'] ?? [])->firstWhere('name', '原因')['value'] ?? '';
|
|
if (($snapshot['originatorUserId'] ?? '') !== config('courier_onboarding.dingtalk.originator_user_id')
|
|
|| ! str_contains((string) $reason, $this->marker($record))
|
|
|| empty($snapshot['createTime'])
|
|
|| ! $this->client->belongsToProcess($token, $instanceId, $snapshot['createTime'])) {
|
|
throw new CourierOnboardingDingTalkException('该钉钉审批与本入驻申请不匹配,未关联。');
|
|
}
|
|
$this->bind($record, $instanceId);
|
|
}
|
|
|
|
public function payload(CourierOnboarding $record): array
|
|
{
|
|
$values = ['快递名称' => $record->displayName()];
|
|
foreach (CourierOnboarding::PROFILE_FIELDS as $field => [$label]) {
|
|
if (filled($record->{$field})) {
|
|
$values[$label] = (string) $record->{$field};
|
|
}
|
|
}
|
|
// 业务类型与「是否需要核对物流点」含义不同,不能相互代用。
|
|
$logistics = match ($record->service_type) {
|
|
'快递' => '否',
|
|
'快运', '大件专线' => '是',
|
|
default => null,
|
|
};
|
|
if ($logistics !== null) {
|
|
$values['快递是否为物流'] = $logistics;
|
|
}
|
|
if ($record->workplatform_account === '已有') {
|
|
$values['绑定快递名称'] = $record->displayName();
|
|
}
|
|
// 与该流程现有实例的 PhoneField 值保持一致,使用手机号文本。
|
|
$values['电话'] = (string) $record->contact_phone;
|
|
$detailUrl = rtrim((string) (config('courier_onboarding.public_url') ?: config('app.url')), '/')
|
|
. route('business.courier-onboardings.show', ['company_id' => $record->company_id, 'onboarding' => $record->id], false);
|
|
$values['原因'] = '快递方已提交入驻资料,申请开通快递合作。' . "\n"
|
|
. $this->marker($record) . "\n主体:" . $record->company_full_name . "\n业务类型:" . $record->service_type
|
|
. "\n对接人:" . $record->contact_name . "\n资料详情(登录财务系统查看):" . $detailUrl
|
|
. "\n报价签署及回传沿用财务系统的易签宝流程,无需重复上传报价单原件。";
|
|
|
|
// 系统对接方式、账号密码由后续节点补充;不传原件、占位密码或公开扫码链接。
|
|
return [
|
|
'processCode' => config('courier_onboarding.dingtalk.process_code'),
|
|
'originatorUserId' => config('courier_onboarding.dingtalk.originator_user_id'),
|
|
'deptId' => (int) config('courier_onboarding.dingtalk.dept_id'),
|
|
'formComponentValues' => collect($values)->map(fn (string $value, string $name): array => compact('name', 'value'))->values()->all(),
|
|
];
|
|
}
|
|
|
|
private function marker(CourierOnboarding $record): string
|
|
{
|
|
return '[财务入驻:' . $record->company_id . ':' . $record->id . ':' . $record->number . ']';
|
|
}
|
|
|
|
private function query(int $companyId)
|
|
{
|
|
return CourierOnboarding::allCompanies()->companyId($companyId);
|
|
}
|
|
|
|
private function bind(CourierOnboarding $record, string $instanceId): void
|
|
{
|
|
DB::transaction(function () use ($record, $instanceId): void {
|
|
$record = $this->query($record->company_id)->lockForUpdate()->findOrFail($record->id);
|
|
if (filled($record->dingtalk_instance_id)) {
|
|
if ($record->dingtalk_instance_id !== $instanceId) {
|
|
throw new CourierOnboardingDingTalkException('入驻申请已关联其他钉钉审批,未覆盖。', true);
|
|
}
|
|
|
|
return;
|
|
}
|
|
$record->update(['dingtalk_instance_id' => $instanceId]);
|
|
$this->event($record, 'dingtalk_started', '已发起钉钉审批,审批编号:' . $instanceId);
|
|
});
|
|
}
|
|
|
|
private function event(CourierOnboarding $record, string $type, string $note): void
|
|
{
|
|
CourierOnboardingEvent::create([
|
|
'company_id' => $record->company_id, 'onboarding_id' => $record->id,
|
|
'event_type' => $type, 'from_status' => $record->status, 'to_status' => $record->status,
|
|
'note' => $note, 'actor_type' => 'system',
|
|
]);
|
|
}
|
|
}
|