signature; if ($signatureDate === null && $existing && $this->sourcePdfIsIntact($existing)) { return $existing; } if ($existing && ! $existing->canSend()) { throw ValidationException::withMessages([ 'signature' => '该报价已经进入签署流程,签署原文不可重新生成。', ]); } $bytes = $this->pdf->render($version, $signatureDate); $path = $this->path($version, 'source.pdf'); $sha256 = hash('sha256', $bytes); if (! Storage::disk('uploads')->put($path, $bytes)) { throw new RuntimeException('报价 PDF 无法写入私有业务存储。'); } $signature = CourierQuoteSignature::updateOrCreate( ['company_id' => $version->company_id, 'quote_version_id' => $version->id], [ 'provider' => 'esign', 'environment' => (string) config('services.esign.environment', 'sandbox'), 'status' => CourierQuoteSignature::STATUS_PREPARED, 'organization_name' => $version->carrier_name, 'source_pdf_path' => $path, 'source_pdf_sha256' => $sha256, 'source_pdf_generated_at' => now(), 'last_error' => null, 'created_by' => $existing?->created_by ?? $userId, 'updated_by' => $userId, ], ); $this->updateVersionStatus($signature, $signature->statusLabel()); return $signature->fresh('version'); } public function send(CourierQuoteSignature $signature, array $data, int $userId): CourierQuoteSignature { $signature = DB::transaction(function () use ($signature, $data, $userId): CourierQuoteSignature { $locked = CourierQuoteSignature::query()->lockForUpdate()->findOrFail($signature->id); if (! $locked->canSend()) { throw ValidationException::withMessages(['signature' => '当前报价版本已经进入签署流程,不能重复推送。']); } if (! $this->sourcePdfIsIntact($locked)) { throw ValidationException::withMessages(['signature' => '待签报价 PDF 缺失或校验不一致,请重新生成后再推送。']); } $locked->update([ 'status' => CourierQuoteSignature::STATUS_SENDING, 'organization_name' => $data['organization_name'], 'operator_name' => $data['operator_name'], 'operator_account' => $data['operator_account'], 'notice_types' => (string) $data['notice_type'], 'expires_at' => Carbon::parse($data['expires_at']), 'last_error' => null, 'updated_by' => $userId, ]); return $locked->fresh('version'); }); $this->updateVersionStatus($signature, '推送中'); try { $client = app(EsignClient::class); $bytes = Storage::disk('uploads')->get($signature->source_pdf_path); $fileName = 'quote-' . $signature->version->version_code . '.pdf'; $fileId = $client->uploadPdf($bytes, $fileName); $signFlowId = $client->createSignFlow($fileId, $fileName, [ 'flow_title' => $signature->version->carrier_name . ' ' . $signature->version->version_code . ' 报价确认', 'organization_name' => $signature->organization_name, 'operator_name' => $signature->operator_name, 'operator_account' => $signature->operator_account, 'notice_types' => $signature->notice_types, ], $signature->expires_at->getTimestampMs()); $signature->update([ 'status' => CourierQuoteSignature::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, ]); $this->updateVersionStatus($signature, '待签署'); return $signature->fresh('version'); } catch (Throwable $exception) { $signature->update([ 'status' => CourierQuoteSignature::STATUS_FAILED, 'last_error' => $this->safeError($exception), 'updated_by' => $userId, ]); $this->updateVersionStatus($signature, '推送失败'); throw ValidationException::withMessages(['signature' => $signature->last_error]); } } public function synchronize(CourierQuoteSignature $signature, int $userId): CourierQuoteSignature { if (blank($signature->esign_sign_flow_id)) { throw ValidationException::withMessages(['signature' => '该报价版本尚未创建 E 签宝签署流程。']); } if ($signature->status === CourierQuoteSignature::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); return $remoteStatus === '2' ? $this->archive($signature, $client, $userId) : $signature; } catch (Throwable $exception) { $signature->update([ 'last_error' => $this->safeError($exception), 'last_synced_at' => now(), 'updated_by' => $userId, ]); throw ValidationException::withMessages(['signature' => $signature->last_error]); } } public function synchronizeById(int $signatureId): void { $signature = CourierQuoteSignature::allCompanies()->find($signatureId); if (! $signature) { return; } $this->synchronize($signature, $signature->updated_by); } public function acceptCallback(string $signFlowId, string $remoteStatus, string $payloadHash): ?CourierQuoteSignature { return DB::transaction(function () use ($signFlowId, $remoteStatus, $payloadHash): ?CourierQuoteSignature { $signature = CourierQuoteSignature::allCompanies() ->where('esign_sign_flow_id', $signFlowId) ->lockForUpdate() ->first(); if (! $signature) { return null; } if ($signature->callback_payload_hash === $payloadHash) { return null; } $signature->callback_payload_hash = $payloadHash; return $this->applyRemoteStatus($signature, $remoteStatus, $signature->updated_by); }); } public function sourcePdfIsIntact(CourierQuoteSignature $signature): bool { if (blank($signature->source_pdf_path) || blank($signature->source_pdf_sha256)) { return false; } $disk = Storage::disk('uploads'); return $disk->exists($signature->source_pdf_path) && hash_equals($signature->source_pdf_sha256, hash('sha256', $disk->get($signature->source_pdf_path))); } private function applyRemoteStatus(CourierQuoteSignature $signature, string $remoteStatus, int $userId): CourierQuoteSignature { [$status, $label] = match ($remoteStatus) { '1' => [CourierQuoteSignature::STATUS_SIGNING, '待签署'], '2' => [CourierQuoteSignature::STATUS_COMPLETED, '签署完成,待归档'], '3' => [CourierQuoteSignature::STATUS_REVOKED, '已撤销'], '5' => [CourierQuoteSignature::STATUS_EXPIRED, '已过期'], '7' => [CourierQuoteSignature::STATUS_REJECTED, '已拒签'], default => [$signature->status, $signature->statusLabel()], }; $signature->forceFill([ 'status' => $status, 'remote_status' => $remoteStatus, 'completed_at' => $remoteStatus === '2' ? ($signature->completed_at ?? now()) : $signature->completed_at, 'last_synced_at' => now(), 'last_error' => null, 'updated_by' => $userId, ])->save(); $this->updateVersionStatus($signature, $label); $signature->refresh(); return $signature; } private function archive(CourierQuoteSignature $signature, EsignClient $client, int $userId): CourierQuoteSignature { if ($signature->status === CourierQuoteSignature::STATUS_ARCHIVED) { return $signature; } $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。'); } $version = CourierQuoteVersion::allCompanies()->findOrFail($signature->quote_version_id); $signedPath = $this->path($version, 'signed.pdf'); $certificatePath = $this->path($version, 'completion-certificate.pdf'); $disk = Storage::disk('uploads'); if (! $disk->put($signedPath, $signedBytes) || ($certificateBytes !== null && ! $disk->put($certificatePath, $certificateBytes))) { throw new RuntimeException('签署归档文件无法写入私有业务存储。'); } $archive = [ 'status' => CourierQuoteSignature::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, ]; if ($certificateBytes !== null) { $archive['certificate_path'] = $certificatePath; $archive['certificate_sha256'] = hash('sha256', $certificateBytes); } $signature->update($archive); $this->updateVersionStatus($signature, '已签署并归档'); return $signature->fresh('version'); } private function path(CourierQuoteVersion $version, string $fileName): string { $versionCode = preg_replace('/[^A-Za-z0-9_-]/', '-', $version->version_code) ?: (string) $version->id; return "courier-quotes/{$version->company_id}/{$versionCode}/{$fileName}"; } private function updateVersionStatus(CourierQuoteSignature $signature, string $label): void { CourierQuoteVersion::allCompanies() ->whereKey($signature->quote_version_id) ->update(['signature_status' => $label]); } private function safeError(Throwable $exception): string { $message = preg_replace('/[[:cntrl:]]+/u', ' ', $exception->getMessage()) ?: 'E 签宝处理失败。'; return mb_substr($message, 0, 1000); } }