query($companyId)->lockForUpdate()->findOrFail($id); $retryActivated = $retry && $record->status === CourierOnboarding::STATUS_ACTIVATED && $record->first_reviewed_at !== null && $record->final_reviewed_at !== null; if (filled($record->dingtalk_instance_id) || ($record->status !== CourierOnboarding::STATUS_APPROVED && ! $retryActivated)) { return null; } $latest = CourierOnboardingEvent::allCompanies()->withTrashed()->companyId($companyId) ->where('onboarding_id', $id)->whereIn('event_type', self::EVENTS)->latest('id')->first(); // 已开通的历史申请只允许补发明确失败的请求,不补建从未发起过的旧单。 if ($retryActivated && $latest?->event_type !== 'dingtalk_failed') { return null; } 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快递类型、合作方式等内部快递资料请由对接人员在首个补资料节点填写,后续审批全部通过后回传财务系统。" . "\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', ]); } }