299 lines
12 KiB
PHP
299 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use App\Models\Business\CourierQuoteSignature;
|
|
use App\Models\Business\CourierQuoteVersion;
|
|
use App\Services\Esign\EsignClient;
|
|
use Illuminate\Support\Carbon;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Storage;
|
|
use Illuminate\Validation\ValidationException;
|
|
use RuntimeException;
|
|
use Throwable;
|
|
|
|
final class CourierQuoteSignatureService
|
|
{
|
|
public function __construct(private readonly CourierQuotePdf $pdf)
|
|
{
|
|
}
|
|
|
|
public function prepareVersion(CourierQuoteVersion $version, int $userId): CourierQuoteSignature
|
|
{
|
|
$existing = $version->signature;
|
|
|
|
if ($existing && $this->sourcePdfIsIntact($existing)) {
|
|
return $existing;
|
|
}
|
|
|
|
if ($existing && ! $existing->canSend()) {
|
|
throw ValidationException::withMessages([
|
|
'signature' => '该报价已经进入签署流程,签署原文不可重新生成。',
|
|
]);
|
|
}
|
|
|
|
$bytes = $this->pdf->render($version);
|
|
$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 === '' || $certificateUrl === '') {
|
|
throw new RuntimeException('E 签宝未返回签署文件或完成证书下载地址。');
|
|
}
|
|
|
|
$signedBytes = $client->download($signedUrl);
|
|
$certificateBytes = $client->download($certificateUrl);
|
|
|
|
if (! str_starts_with($signedBytes, '%PDF-') || ! 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) || ! $disk->put($certificatePath, $certificateBytes)) {
|
|
throw new RuntimeException('签署归档文件无法写入私有业务存储。');
|
|
}
|
|
|
|
$signature->update([
|
|
'status' => CourierQuoteSignature::STATUS_ARCHIVED,
|
|
'signed_pdf_path' => $signedPath,
|
|
'signed_pdf_sha256' => hash('sha256', $signedBytes),
|
|
'certificate_path' => $certificatePath,
|
|
'certificate_sha256' => hash('sha256', $certificateBytes),
|
|
'archived_at' => now(),
|
|
'last_synced_at' => now(),
|
|
'last_error' => null,
|
|
'updated_by' => $userId,
|
|
]);
|
|
$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);
|
|
}
|
|
}
|