From 125e9ea8aa0218761152b9aef558ef1ac7204efe Mon Sep 17 00:00:00 2001 From: limeng <1131177064@qq.com> Date: Sat, 17 Oct 2026 12:48:24 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AD=98=E9=87=8F=E6=8A=A5=E4=BB=B7?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=E7=94=9F=E6=95=88=E5=B9=B6=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E7=AD=BE=E7=BD=B2=E5=8E=9F=E4=BB=B6=E9=A2=84=E8=A7=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Business/CourierStockQuoteEntries.php | 2 +- app/Models/Business/CourierQuoteVersion.php | 14 ++++++++ app/Support/CourierQuoteChangeService.php | 4 +-- app/Support/CourierRequoteService.php | 2 +- app/Support/QuoteManagementSnapshot.php | 31 +++++++++++++---- .../partials/stock-quote-review.blade.php | 2 +- .../quote-change-show.blade.php | 2 +- .../quote-management.blade.php | 11 ++++++ .../stock-quote-entry.blade.php | 2 +- .../Business/CourierStockQuoteBatchTest.php | 2 +- .../Business/CourierStockQuoteEntryTest.php | 34 +++++++++++++++++-- 11 files changed, 90 insertions(+), 16 deletions(-) diff --git a/app/Http/Controllers/Business/CourierStockQuoteEntries.php b/app/Http/Controllers/Business/CourierStockQuoteEntries.php index beb94014..a2c58aca 100644 --- a/app/Http/Controllers/Business/CourierStockQuoteEntries.php +++ b/app/Http/Controllers/Business/CourierStockQuoteEntries.php @@ -166,7 +166,7 @@ class CourierStockQuoteEntries extends Controller $headers = ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']; if ($preview) { $mime = $attachment['mime_type'] ?? $disk->mimeType($attachment['path']); - abort_unless(in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true), 415, '此格式请下载原文件查看。'); + abort_unless(in_array($mime, ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true), 415, '此格式请下载原文件查看。'); return $disk->response($attachment['path'], $attachment['name'], [...$headers, 'Content-Type' => $mime]); } diff --git a/app/Models/Business/CourierQuoteVersion.php b/app/Models/Business/CourierQuoteVersion.php index 74916822..05bd7336 100644 --- a/app/Models/Business/CourierQuoteVersion.php +++ b/app/Models/Business/CourierQuoteVersion.php @@ -61,6 +61,20 @@ class CourierQuoteVersion extends Model return $this->status === self::STATUS_VOIDED; } + /** 存量报价审核后按执行日期生效,不以补签或历史签署资料为前置条件。 */ + public function isApprovedStockQuote(): bool + { + return $this->status === self::STATUS_APPROVED + && $this->change?->status === CourierQuoteChange::STATUS_APPROVED + && $this->change->isStockQuote(); + } + + public function signatureLabel(): string + { + return $this->isApprovedStockQuote() && !in_array($this->signature_status, ['历史签署资料已归档', '已签署并归档'], true) + ? '存量报价,审核生效(无需签章)' : (string) $this->signature_status; + } + public function change(): BelongsTo { return $this->belongsTo(CourierQuoteChange::class, 'source_change_id'); diff --git a/app/Support/CourierQuoteChangeService.php b/app/Support/CourierQuoteChangeService.php index cd9f578b..7f6d64e1 100644 --- a/app/Support/CourierQuoteChangeService.php +++ b/app/Support/CourierQuoteChangeService.php @@ -142,7 +142,7 @@ final class CourierQuoteChangeService $hasHistoricalFiles = $change->isStockQuote() && collect(CourierStockQuoteEntry::query()->findOrFail(data_get($change->quote_payload, 'stock_quote_entry_id'))->files)->contains('kind', 'signed'); if ($change->isStockQuote() && $historicalDocumentsConfirmed && ! $hasHistoricalFiles) { - throw ValidationException::withMessages(['stock_signature_verified' => '尚未上传历史签署资料,请补充资料或保留待确认/待补签状态。']); + throw ValidationException::withMessages(['stock_signature_verified' => '尚未上传历史签署资料,请补充资料或取消历史资料核验;不影响存量报价审核生效。']); } $historicalSigned = $hasHistoricalFiles && $historicalDocumentsConfirmed; $values = $source['values']; @@ -182,7 +182,7 @@ final class CourierQuoteChangeService 'values_json' => json_encode($values, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'formulas_json' => json_encode($formulas, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'status' => 'approved', - 'signature_status' => $change->isStockQuote() ? ($historicalSigned ? '历史签署资料已归档' : '待确认/待补签') : '未签署', + 'signature_status' => $change->isStockQuote() ? ($historicalSigned ? '历史签署资料已归档' : '存量报价,审核生效(无需签章)') : '未签署', 'created_by' => $change->created_by, 'approved_by' => $userId, ]); diff --git a/app/Support/CourierRequoteService.php b/app/Support/CourierRequoteService.php index c2ff6478..5bc9dbbb 100644 --- a/app/Support/CourierRequoteService.php +++ b/app/Support/CourierRequoteService.php @@ -370,7 +370,7 @@ final class CourierRequoteService { return $versions ->filter(static fn (CourierQuoteVersion $version): bool => ! $version->isVoided() - && self::isSignedVersion($version) + && ($version->isApprovedStockQuote() || self::isSignedVersion($version)) && ($includeTemporary || ! $version->isTemporary()) && $version->coversDate($date)) ->sortBy([ diff --git a/app/Support/QuoteManagementSnapshot.php b/app/Support/QuoteManagementSnapshot.php index 9a1090cb..cbd3353e 100644 --- a/app/Support/QuoteManagementSnapshot.php +++ b/app/Support/QuoteManagementSnapshot.php @@ -165,11 +165,12 @@ final class QuoteManagementSnapshot 'column_count' => $columnCount, 'formula_count' => $this->formulaCount($formulas), 'status' => $this->versionLifecycleStatus($version), - 'signature_status' => $version->signature_status, + 'signature_status' => $version->signatureLabel(), 'values' => $values, 'formulas' => $formulas, 'source_change_id' => $version->source_change_id, 'signed_files' => $this->signedFiles($version), + 'stock_originals' => $this->stockOriginals($version), 'signed_at' => $version->signature?->completed_at?->format('Y-m-d H:i') ?? $version->signature?->archived_at?->format('Y-m-d H:i'), ]; @@ -205,6 +206,24 @@ final class QuoteManagementSnapshot return $files; } + private function stockOriginals(CourierQuoteVersion $version): array + { + if (!$version->change?->isStockQuote() || !($entry = $version->change->stockEntry)) return []; + $files = []; + foreach ($entry->files as $index => $file) { + if (($file['kind'] ?? '') !== 'signed') continue; + // 早期手动上传未记录 MIME;入口按扩展名提供,响应仍按实际 MIME 白名单校验。 + $previewable = isset($file['mime_type']) + ? in_array($file['mime_type'], ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true) + : in_array(strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)), ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'], true); + $files[] = [ + 'name' => $file['name'], 'previewable' => $previewable, + 'url' => route('business.courier-stock-quotes.download', [$entry, $index, 'preview' => $previewable ? 1 : 0]), + ]; + } + return $files; + } + private function decodeMatrix(?string $json): array { if ($json === null || trim($json) === '') { @@ -224,7 +243,7 @@ final class QuoteManagementSnapshot return '已作废'; } - if (! $this->isSigned($version)) { + if (! $this->isExecutable($version)) { return '待生效(待签章)'; } @@ -235,10 +254,10 @@ final class QuoteManagementSnapshot return $version->isTemporary() ? '生效中(临时)' : '生效中'; } - /** 回签单已归档(以签署记录为准,版本上的文字状态可能滞后)。 */ - private function isSigned(CourierQuoteVersion $version): bool + /** 存量报价审核生效;其他报价仍要求回签归档。 */ + private function isExecutable(CourierQuoteVersion $version): bool { - return in_array($version->signature_status, ['已签署并归档', '历史签署资料已归档'], true) + return $version->isApprovedStockQuote() || in_array($version->signature_status, ['已签署并归档', '历史签署资料已归档'], true) || $version->signature?->status === \App\Models\Business\CourierQuoteSignature::STATUS_ARCHIVED; } @@ -261,7 +280,7 @@ final class QuoteManagementSnapshot : 'name:' . $version->company_name)) as $group) { $current = CourierRequoteService::resolveAt($group, $today); $base = CourierRequoteService::resolveAt($group, $today, false); - $started = $group->filter(fn (CourierQuoteVersion $version): bool => ! $version->isVoided() && $this->isSigned($version) + $started = $group->filter(fn (CourierQuoteVersion $version): bool => ! $version->isVoided() && $this->isExecutable($version) && $version->effective_date !== null && $version->effective_date->lte($today)); foreach ($started as $version) { if ($current !== null && $version->id === $current->id) { diff --git a/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php b/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php index 9cb8e9db..7ae666d2 100644 --- a/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php +++ b/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php @@ -22,7 +22,7 @@ @if(collect($stockEntry->files)->contains('kind', 'signed'))

@endif -

未核验签署依据时,审核后仍为“待确认/待补签”。历史资料归档与本系统电子签署分别记录。

+

存量报价审核通过后按实际执行日期生效,无需签章。历史签署资料可另行核验归档,不影响报价生效。

@endif diff --git a/resources/views/business/pages/courier-payables/quote-change-show.blade.php b/resources/views/business/pages/courier-payables/quote-change-show.blade.php index 3cfe18ba..5230df4e 100644 --- a/resources/views/business/pages/courier-payables/quote-change-show.blade.php +++ b/resources/views/business/pages/courier-payables/quote-change-show.blade.php @@ -88,7 +88,7 @@
追溯核算
{{ $change->recalculationStatusLabel() }}
{{ $stockEntry ? '录入范围' : '变价明细' }}
{{ $stockEntry ? $stockEntry->source_range : ($requoteDiff ? ($requoteDiff['same_structure'] ? count($requoteDiff['cells']) . ' 处价格变动' : '计费方式调整') : $change->items->count() . ' 项') }}
新报价版本
{{ $change->version?->version_code ?? '尚未生成' }}
-
签署状态
{{ $change->version?->signature_status ?? '未进入签署' }}
+
签署状态
{{ $change->version?->signatureLabel() ?? '未进入签署' }}
变更依据与说明
{{ $change->summary }}
diff --git a/resources/views/business/pages/courier-payables/quote-management.blade.php b/resources/views/business/pages/courier-payables/quote-management.blade.php index a40cea8c..1f4bd7d2 100644 --- a/resources/views/business/pages/courier-payables/quote-management.blade.php +++ b/resources/views/business/pages/courier-payables/quote-management.blade.php @@ -849,6 +849,7 @@
原工作表内容 · 只读
+
@@ -1114,6 +1115,16 @@ setDetail('range', text(quote.source_range, '-')); setDetail('date', text(quote.period_label || quote.effective_date, 'Excel 未标注')); setDetail('formulas', String(quote.formula_count)); + var originals = root.querySelector('[data-stock-original-links]'); + originals.replaceChildren(); + (quote.stock_originals || []).forEach(function (file, index, files) { + var link = create('a', 'quote-button', (file.previewable ? '预览签署原件' : '下载签署原件') + (files.length > 1 ? ' ' + (index + 1) : '')); + link.href = file.url; + link.title = file.name; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + originals.appendChild(link); + }); var signedBox = root.querySelector('[data-quote-signed]'); var files = quote.signed_files || {}; signedBox.hidden = !files.signed; diff --git a/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php b/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php index f36cb4d2..4c74ba7b 100644 --- a/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php +++ b/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php @@ -69,7 +69,7 @@

支持 xlsx、xls,单文件不超过 20 MB。原始内容和公式会保留。

多工作表文件请填写本次报价所在的准确工作表名称。

最多 10 个,每个不超过 20 MB。

-

上传资料后由审核人核验;缺少有效签署依据时标记为待确认/待补签。

+

上传资料后由审核人核验归档;存量报价审核后按执行日期生效,无需签章。

diff --git a/tests/Feature/Business/CourierStockQuoteBatchTest.php b/tests/Feature/Business/CourierStockQuoteBatchTest.php index 36bb81bd..de14a2e9 100644 --- a/tests/Feature/Business/CourierStockQuoteBatchTest.php +++ b/tests/Feature/Business/CourierStockQuoteBatchTest.php @@ -364,7 +364,7 @@ class CourierStockQuoteBatchTest extends FeatureTestCase $reviewer->companies()->attach($this->company->id); $changes = app(CourierQuoteChangeService::class); $approved = $changes->approve($entry->change, (int) $reviewer->id); - $this->assertSame('待确认/待补签', $approved->version->signature_status); + $this->assertSame('存量报价,审核生效(无需签章)', $approved->version->signature_status); $change = $changes->create([ 'source_quote_id' => $approved->version->version_code, 'change_type' => '费率调整', 'effective_date' => '2026-10-16', 'summary' => '基于存量版本调价,原件继续保留', diff --git a/tests/Feature/Business/CourierStockQuoteEntryTest.php b/tests/Feature/Business/CourierStockQuoteEntryTest.php index bfae5f2c..24947b53 100644 --- a/tests/Feature/Business/CourierStockQuoteEntryTest.php +++ b/tests/Feature/Business/CourierStockQuoteEntryTest.php @@ -75,12 +75,18 @@ class CourierStockQuoteEntryTest extends FeatureTestCase $this->assertDatabaseCount('business_courier_quote_versions', 0); $this->loginAs($reviewer, $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); $version = CourierQuoteVersion::firstOrFail(); - $this->assertSame('待确认/待补签', $version->signature_status); + $this->assertSame('存量报价,审核生效(无需签章)', $version->signature_status); $this->assertSame('EXP-001', $version->courier_expcode); $this->assertSame(CourierQuoteChange::RECALCULATION_NOT_REQUIRED, $entry->change->fresh()->recalculation_status); $this->assertSame('2025-12-01', $version->effective_date->toDateString()); $this->assertDatabaseCount('business_courier_quote_signatures', 0); - $this->assertSame('待生效(待签章)', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); + $this->assertSame('生效中', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); + $this->assertSame($version->id, \App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())->id); + // 已上线旧记录无需改库或重新审核,也按存量审核规则生效。 + $version->update(['signature_status' => '待确认/待补签']); + $snapshot = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; + $this->assertSame('生效中', $snapshot['status']); + $this->assertSame('存量报价,审核生效(无需签章)', $snapshot['signature_status']); } public function test_historical_documents_require_reviewer_verification_and_remain_distinct_from_esign(): void @@ -110,6 +116,30 @@ class CourierStockQuoteEntryTest extends FeatureTestCase $this->assertDatabaseCount('business_courier_quote_versions', 0); } + public function test_approved_stock_quote_exposes_all_originals_without_requiring_signature_verification(): void + { + $entry = $this->createEntry(['signed_files' => [ + UploadedFile::fake()->createWithContent('signed-original.pdf', "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n%%EOF"), + UploadedFile::fake()->image('签署原件二.png'), + ]]); + $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change)); + $this->loginAs($this->reviewer(), $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); + $quote = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; + $this->assertSame('生效中', $quote['status']); + $this->assertCount(2, $quote['stock_originals']); + $this->assertSame([], $quote['signed_files']); + $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management'])) + ->assertOk()->assertSee('data-stock-original-links', false)->assertSee('预览签署原件'); + foreach ($quote['stock_originals'] as $original) { + $this->assertTrue($original['previewable']); + $response = $this->loginAs()->get($original['url'])->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff'); + $this->assertStringStartsWith('inline;', $response->headers->get('Content-Disposition')); + } + $version = CourierQuoteVersion::firstOrFail(); + $version->update(['status' => CourierQuoteVersion::STATUS_VOIDED]); + $this->assertNull(\App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())); + } + public function test_same_code_and_type_cannot_be_imported_twice_but_same_names_with_different_codes_can(): void { $this->createEntry();