disableCache() ->whereNotNull('dingtalk_instance_id')->where('dingtalk_instance_id', '<>', '') ->whereNotIn('status', [CourierOnboarding::STATUS_CANCELLED, CourierOnboarding::STATUS_REJECTED]) ->whereDoesntHave('events', fn (Builder $query) => $query->withoutGlobalScope(\App\Scopes\Company::class) ->withTrashed()->whereIn('event_type', self::FINISHED_EVENTS)); } public function synchronize(int $companyId, int $id, string $token): string { $record = $this->candidates()->companyId($companyId)->find($id); if ($record === null) { return 'skipped'; } $instanceId = (string) $record->dingtalk_instance_id; $snapshot = $this->client->instance($token, $instanceId); $values = collect($snapshot['formComponentValues'] ?? [])->pluck('value', 'name')->all(); $marker = '[财务入驻:' . $record->company_id . ':' . $record->id . ':' . $record->number . ']'; if (($snapshot['originatorUserId'] ?? '') !== config('courier_onboarding.dingtalk.originator_user_id') || ! is_string($values['原因'] ?? null) || ! str_contains($values['原因'], $marker)) { throw new CourierOnboardingDingTalkException('钉钉审批与入驻申请不匹配,未同步资料。'); } $status = strtoupper((string) ($snapshot['status'] ?? '')); if (! in_array($status, ['COMPLETED', 'TERMINATED'], true)) { return 'pending'; } $approved = $status === 'COMPLETED' && strtolower((string) ($snapshot['result'] ?? '')) === 'agree'; $profile = []; if ($approved) { foreach (CourierOnboarding::PROFILE_FIELDS as $field => [$label]) { $value = $values[$label] ?? null; // 仅接受约定的普通文本值,不接收表格、文件或账号密码。 $profile[$field] = is_string($value) ? trim($value) : null; } $validator = Validator::make($profile, CourierOnboarding::profileRules(), [], CourierOnboarding::profileAttributes()); if ($validator->fails()) { $fields = array_map(fn (string $field): string => CourierOnboarding::PROFILE_FIELDS[$field][0], array_keys($validator->errors()->messages())); throw new CourierOnboardingDingTalkException('钉钉审批中的快递资料缺失或格式不正确:' . implode('、', $fields) . '。请补齐后等待重新同步。'); } } return DB::transaction(function () use ($companyId, $id, $instanceId, $approved, $profile): string { $record = $this->candidates()->companyId($companyId)->lockForUpdate()->find($id); if ($record === null || $record->dingtalk_instance_id !== $instanceId) { return 'skipped'; } if ($approved) { $record->update($profile); } $this->event($record, $approved ? 'dingtalk_profile_synced' : 'dingtalk_profile_closed', $approved ? '钉钉审批已通过,内部快递资料已同步到本入驻申请及快递资料。' : '钉钉审批未通过或已终止,保留财务系统原有资料及审核状态。'); return $approved ? 'synced' : 'closed'; }); } public function recordFailure(CourierOnboarding $record, string $message): void { $latest = CourierOnboardingEvent::allCompanies()->companyId($record->company_id) ->where('onboarding_id', $record->id)->where('event_type', 'dingtalk_profile_failed')->latest('id')->first(); if ($latest?->note !== $message) { $this->event($record, 'dingtalk_profile_failed', $message); } } 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, 'actor_type' => 'system', 'note' => $note, ]); } }