'不支持的协议文件类型。']); } $payload = is_array($data['payload'] ?? null) ? $data['payload'] : $data; $payload['authorization_items'] = array_values(array_map('strval', $data['authorization_items'] ?? [])); $idempotencyKey = 'agreement-' . hash('sha256', $companyId . '|' . (string) ($data['idempotency_key'] ?? '')); $businessNo = 'AGR-' . strtoupper(substr(hash('sha256', $idempotencyKey), 0, 20)); $signature = DB::transaction(function () use ($companyId, $document, $data, $payload, $idempotencyKey, $businessNo, $userId): CourierAgreementSignature { $existing = CourierAgreementSignature::allCompanies() ->where('company_id', $companyId) ->where('idempotency_key', $idempotencyKey) ->lockForUpdate() ->first(); if ($existing) { if ($existing->esign_sign_flow_id || in_array($existing->status, [CourierAgreementSignature::STATUS_SENDING, CourierAgreementSignature::STATUS_SIGNING, CourierAgreementSignature::STATUS_COMPLETED, CourierAgreementSignature::STATUS_ARCHIVED], true)) { return $existing; } if (! $existing->canSend()) { throw ValidationException::withMessages(['signature' => '该协议已进入不可重复发起状态。']); } return $existing; } $signature = new CourierAgreementSignature([ 'company_id' => $companyId, 'business_no' => $businessNo, 'idempotency_key' => $idempotencyKey, 'document_type' => $data['document_type'], 'template_code' => $document['code'], 'template_version' => $document['version'], 'source_file_name' => $document['file'], 'status' => CourierAgreementSignature::STATUS_SENDING, 'courier_name' => $data['courier_name'], 'company_full_name' => $data['company_full_name'], 'organization_name' => $data['organization_name'], 'operator_name' => $data['operator_name'] ?? null, 'operator_account' => $data['operator_account'] ?? null, 'payload' => $payload, 'environment' => (string) config('services.esign.environment', 'sandbox'), 'expires_at' => Carbon::parse($data['signing_deadline'])->endOfDay(), 'source_pdf_path' => "courier-agreements/{$companyId}/{$businessNo}/source.pdf", 'source_pdf_sha256' => '', 'created_by' => $userId, 'updated_by' => $userId, ]); $signature->save(); $bytes = $this->pdf->render($signature); $signature->source_pdf_sha256 = hash('sha256', $bytes); if (! Storage::disk('uploads')->put($signature->source_pdf_path, $bytes)) { throw new RuntimeException('协议 PDF 无法写入私有业务存储。'); } $signature->save(); return $signature; }); if ($signature->esign_sign_flow_id || $signature->status === CourierAgreementSignature::STATUS_SIGNING) { return $signature->fresh(); } $flowCreationStarted = false; try { $client = app(EsignClient::class); $bytes = Storage::disk('uploads')->get($signature->source_pdf_path); $fileName = 'agreement-' . $signature->business_no . '.pdf'; $fileId = $client->uploadPdf($bytes, $fileName); $signature->update([ 'esign_file_id' => $fileId, 'updated_by' => $userId, ]); $signer = [ 'flow_title' => $this->safeTitle($signature->company_full_name . ' ' . $signature->documentLabel()), 'organization_name' => $signature->organization_name, 'operator_name' => $signature->operator_name ?: $signature->courier_name, 'operator_account' => $signature->operator_account, 'notice_types' => '1', ]; $signer['signers'] = $this->signersFor($signature); $flowCreationStarted = true; $signFlowId = $client->createSignFlow($fileId, $fileName, $signer, $signature->expires_at->getTimestampMs()); $signature->update([ 'status' => CourierAgreementSignature::STATUS_SIGNING, 'esign_file_id' => $fileId, 'esign_sign_flow_id' => $signFlowId, 'remote_status' => '1', 'sent_at' => now(), 'last_synced_at' => now(), 'last_error' => null, 'updated_by' => $userId, ]); return $signature->fresh(); } catch (Throwable $exception) { $error = EsignErrorMessage::sanitize($exception->getMessage(), 1000) ?? 'E 签宝处理失败。'; $signature->update([ 'status' => $flowCreationStarted && ! EsignErrorMessage::isDefinitiveFailure($exception->getMessage()) ? CourierAgreementSignature::STATUS_UNKNOWN : CourierAgreementSignature::STATUS_FAILED, 'last_error' => $error, 'updated_by' => $userId, ]); throw ValidationException::withMessages(['signature' => $signature->last_error]); } } public function synchronize(CourierAgreementSignature $signature, ?int $userId = null): CourierAgreementSignature { if (blank($signature->esign_sign_flow_id)) { throw ValidationException::withMessages(['signature' => '该协议尚未创建 E 签宝签署流程。']); } if ($signature->status === CourierAgreementSignature::STATUS_ARCHIVED) { return $signature; } try { $client = app(EsignClient::class); $detail = $client->signFlowDetail($signature->esign_sign_flow_id); $remoteStatus = (string) ($detail['signFlowStatus'] ?? $detail['status'] ?? ''); if ($remoteStatus === '') { throw new RuntimeException('E 签宝未返回签署流程状态。'); } $signature = $this->applyRemoteStatus($signature, $remoteStatus, $userId ?: (int) $signature->updated_by); return $remoteStatus === '2' && $this->autoArchive($signature) ? $this->archive($signature, $client, $userId ?: (int) $signature->updated_by) : $signature; } catch (Throwable $exception) { $signature->update(['last_error' => mb_substr($exception->getMessage(), 0, 1000), 'last_synced_at' => now()]); throw ValidationException::withMessages(['signature' => $signature->last_error]); } } public function acceptCallback(string $signFlowId, string $remoteStatus, string $payloadHash, array $event = []): ?CourierAgreementSignature { return DB::transaction(function () use ($signFlowId, $remoteStatus, $payloadHash, $event): ?CourierAgreementSignature { $signature = CourierAgreementSignature::allCompanies()->disableCache()->where('esign_sign_flow_id', $signFlowId)->lockForUpdate()->first(); if (! $signature) { return null; } $eventKey = (string) ($event['event_key'] ?? hash('sha256', $signFlowId . '|' . $payloadHash)); $audit = CourierAgreementSignatureEvent::query()->disableCache()->where('event_key', $eventKey)->lockForUpdate()->first(); if ($audit || $signature->callback_payload_hash === $payloadHash) { return null; } if (in_array($signature->status, [ CourierAgreementSignature::STATUS_COMPLETED, CourierAgreementSignature::STATUS_ARCHIVED, CourierAgreementSignature::STATUS_REVOKED, CourierAgreementSignature::STATUS_EXPIRED, CourierAgreementSignature::STATUS_REJECTED, ], true) && $remoteStatus === $signature->remote_status) { return null; } if ($event !== []) { $audit = CourierAgreementSignatureEvent::create([ 'company_id' => $signature->company_id, 'agreement_signature_id' => $signature->id, 'event_key' => $eventKey, 'action' => (string) ($event['action'] ?? 'SIGN_FLOW_COMPLETE'), 'esign_sign_flow_id' => $signFlowId, 'event_timestamp' => (int) ($event['timestamp'] ?? now()->getTimestampMs()), 'verified' => true, 'raw_body' => (string) ($event['raw_body'] ?? ''), 'received_at' => now(), ]); } $signature->callback_payload_hash = $payloadHash; $result = $this->applyRemoteStatus($signature, $remoteStatus, (int) $signature->updated_by); if ($audit) { $audit->update(['processed' => true, 'processed_at' => now()]); } return $result; }); } public function archive(CourierAgreementSignature $signature, ?EsignClient $client = null, ?int $userId = null): CourierAgreementSignature { if ($signature->status === CourierAgreementSignature::STATUS_ARCHIVED) { return $signature; } $client ??= app(EsignClient::class); $urls = $client->downloadUrls($signature->esign_sign_flow_id); $signedUrl = (string) data_get($urls, 'files.0.downloadUrl', ''); $certificateUrl = (string) ($urls['certificateDownloadUrl'] ?? ''); if ($signedUrl === '') { throw new RuntimeException('E 签宝未返回签署文件下载地址。'); } $signedBytes = $client->download($signedUrl); $certificateBytes = $certificateUrl === '' ? null : $client->download($certificateUrl); if (! str_starts_with($signedBytes, '%PDF-') || ($certificateBytes !== null && ! str_starts_with($certificateBytes, '%PDF-'))) { throw new RuntimeException('E 签宝返回的签署文件或完成证书不是有效 PDF。'); } $base = "courier-agreements/{$signature->company_id}/{$signature->business_no}"; $signedPath = $base . '/signed.pdf'; $certificatePath = $base . '/completion-certificate.pdf'; $disk = Storage::disk('uploads'); if (! $disk->put($signedPath, $signedBytes) || ($certificateBytes !== null && ! $disk->put($certificatePath, $certificateBytes))) { throw new RuntimeException('签署归档文件无法写入私有业务存储。'); } $archive = [ 'status' => CourierAgreementSignature::STATUS_ARCHIVED, 'signed_pdf_path' => $signedPath, 'signed_pdf_sha256' => hash('sha256', $signedBytes), 'archived_at' => now(), 'last_synced_at' => now(), 'last_error' => null, 'updated_by' => $userId ?: (int) $signature->updated_by, ]; if ($certificateBytes !== null) { $archive['certificate_path'] = $certificatePath; $archive['certificate_sha256'] = hash('sha256', $certificateBytes); } $signature->update($archive); return $signature->fresh(); } public function autoArchive(CourierAgreementSignature $signature): bool { return ($signature->payload['archive_mode'] ?? 'auto') === 'auto'; } private function applyRemoteStatus(CourierAgreementSignature $signature, string $remoteStatus, int $userId): CourierAgreementSignature { $status = match ($remoteStatus) { '1' => CourierAgreementSignature::STATUS_SIGNING, '2' => CourierAgreementSignature::STATUS_COMPLETED, '3' => CourierAgreementSignature::STATUS_REVOKED, '5' => CourierAgreementSignature::STATUS_EXPIRED, '7' => CourierAgreementSignature::STATUS_REJECTED, default => $signature->status, }; $terminal = [CourierAgreementSignature::STATUS_COMPLETED, CourierAgreementSignature::STATUS_REVOKED, CourierAgreementSignature::STATUS_EXPIRED, CourierAgreementSignature::STATUS_REJECTED, CourierAgreementSignature::STATUS_ARCHIVED]; if (in_array($signature->status, $terminal, true) && $status === CourierAgreementSignature::STATUS_SIGNING) { return $signature; } $signature->forceFill([ 'status' => $status, 'remote_status' => $remoteStatus, 'completed_at' => $remoteStatus === '2' ? ($signature->completed_at ?? now()) : $signature->completed_at, 'last_synced_at' => now(), 'updated_by' => $userId, ])->save(); return $signature->refresh(); } private function signersFor(CourierAgreementSignature $signature): array { $payload = $signature->payload ?? []; if (in_array($signature->document_type, ['account-proof', 'account-change'], true)) { $isPerson = ($payload['counterparty_signer_type'] ?? 'organization') === 'person'; return [[ 'signer_type' => $isPerson ? 0 : 1, 'organization_name' => $isPerson ? null : $signature->organization_name, 'operator_name' => $isPerson ? ($signature->operator_name ?: $signature->organization_name) : $signature->operator_name, 'operator_account' => $signature->operator_account, ]]; } $primary = [ 'signer_type' => 1, 'organization_name' => $signature->organization_name, 'operator_name' => $signature->operator_name ?: $signature->courier_name, 'operator_account' => $signature->operator_account, ]; if ($signature->document_type === 'cooperation-contract') { return [ [ 'signer_type' => 1, 'organization_name' => $payload['cooperation_party_a_name'] ?? $signature->company_full_name, 'operator_name' => $payload['cooperation_party_a_name'] ?? $signature->company_full_name, 'operator_account' => $payload['secondary_esign_account'] ?? $signature->operator_account, ], [ 'signer_type' => 1, 'organization_name' => $payload['cooperation_party_b_name'] ?? $signature->courier_name, 'operator_name' => $signature->operator_name ?: ($payload['cooperation_party_b_name'] ?? $signature->courier_name), 'operator_account' => $signature->operator_account, ], ]; } if ($signature->document_type === 'entity-change') { return [ array_merge($primary, ['organization_name' => $payload['former_company_name'] ?? $signature->organization_name]), [ 'signer_type' => 1, 'organization_name' => $payload['changed_company_name'] ?? $signature->organization_name, 'operator_name' => $payload['changed_company_name'] ?? $signature->courier_name, 'operator_account' => $payload['secondary_esign_account'] ?? $signature->operator_account, ], ]; } if ($signature->document_type === 'authorization') { return [ array_merge($primary, [ 'organization_name' => $payload['principal_name'] ?? $signature->organization_name, 'operator_name' => $payload['principal_name'] ?? $signature->operator_name, ]), [ 'signer_type' => 1, 'organization_name' => $payload['trustee_name'] ?? $signature->organization_name, 'operator_name' => $payload['trustee_name'] ?? $signature->courier_name, 'operator_account' => $payload['secondary_esign_account'] ?? $signature->operator_account, ], ]; } return [$primary]; } private function safeTitle(string $title): string { $title = preg_replace('/[^\p{Han}\p{L}\p{N}_ -]/u', '-', $title) ?: '协议签署'; return mb_substr(trim($title), 0, 180); } }