From 21f13772a422d741c0c19b39b080713a3e39c2b8 Mon Sep 17 00:00:00 2001 From: zhoushengchen <3057083327@qq.com> Date: Wed, 2 Sep 2026 18:05:32 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E9=99=84?= =?UTF-8?q?=E5=8A=A0=E8=B4=B9=E7=B1=BB=E5=9E=8B=E8=87=AA=E7=BB=B4=E6=8A=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CourierSurchargeQuoteRepository.php | 36 +++- .../CourierSurchargeTypeRepository.php | 154 ++++++++++++++++++ .../Business/CourierSurchargeQuotes.php | 4 +- .../Business/CourierSurchargeTypes.php | 58 +++++++ app/Http/Controllers/Business/Pages.php | 3 + app/Models/Business/CourierSurchargeType.php | 35 ++++ ...0_create_courier_surcharge_types_table.php | 62 +++++++ .../courier-surcharge.blade.php | 34 +++- routes/admin.php | 3 + .../Business/CourierSurchargePageTest.php | 76 +++++++++ 10 files changed, 457 insertions(+), 8 deletions(-) create mode 100644 app/Business/CourierSurchargeTypeRepository.php create mode 100644 app/Http/Controllers/Business/CourierSurchargeTypes.php create mode 100644 app/Models/Business/CourierSurchargeType.php create mode 100644 database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php diff --git a/app/Business/CourierSurchargeQuoteRepository.php b/app/Business/CourierSurchargeQuoteRepository.php index 1d0af5a7..8cc35a10 100644 --- a/app/Business/CourierSurchargeQuoteRepository.php +++ b/app/Business/CourierSurchargeQuoteRepository.php @@ -3,6 +3,7 @@ namespace App\Business; use App\Models\Business\CourierSurchargeQuote; +use App\Models\Business\CourierSurchargeType; use Illuminate\Database\QueryException; use Illuminate\Support\Collection; use Illuminate\Support\Facades\Schema; @@ -27,10 +28,14 @@ class CourierSurchargeQuoteRepository public const SCOPES = ['自有快递', '代发仓']; + public function __construct(private readonly CourierSurchargeTypeRepository $types) + { + } + public function ready(): bool { try { - return Schema::hasTable((new CourierSurchargeQuote())->getTable()); + return Schema::hasTable((new CourierSurchargeQuote())->getTable()) && $this->types->ready(); } catch (QueryException) { return false; } @@ -101,6 +106,12 @@ class CourierSurchargeQuoteRepository return true; })->values(); + $typeRecords = $this->types->all(); + $activeTypeNames = $typeRecords + ->where('status', CourierSurchargeType::STATUS_ACTIVE) + ->pluck('name') + ->values(); + return [ 'ready' => true, 'rows' => $filtered->map(fn (CourierSurchargeQuote $quote): array => $this->present($quote)), @@ -112,7 +123,14 @@ class CourierSurchargeQuoteRepository ], 'options' => [ 'couriers' => $all->pluck('courier')->filter()->unique()->sort()->values(), - 'types' => collect(self::TYPES), + 'types' => $activeTypeNames, + 'filter_types' => $typeRecords->pluck('name')->values(), + 'type_records' => $typeRecords->map(fn ($type): array => [ + 'id' => $type->id, + 'name' => $type->name, + 'status' => $type->status, + 'status_label' => $type->statusLabel(), + ])->values(), 'modes' => collect(self::MODES), 'scopes' => self::SCOPES, 'reconcilers' => $all->pluck('reconciler')->filter()->unique()->sort()->values(), @@ -121,6 +139,18 @@ class CourierSurchargeQuoteRepository ]; } + /** @return list */ + public function validTypeNames(): array + { + return $this->types->validNames(); + } + + /** @return list */ + public function activeTypeNames(): array + { + return $this->types->activeNames(); + } + /** @param array $data */ public function create(array $data, int $userId): CourierSurchargeQuote { @@ -192,6 +222,8 @@ class CourierSurchargeQuoteRepository return [ 'couriers' => collect(), 'types' => collect(self::TYPES), + 'filter_types' => collect(self::TYPES), + 'type_records' => collect(), 'modes' => collect(self::MODES), 'scopes' => self::SCOPES, 'reconcilers' => collect(), diff --git a/app/Business/CourierSurchargeTypeRepository.php b/app/Business/CourierSurchargeTypeRepository.php new file mode 100644 index 00000000..677db037 --- /dev/null +++ b/app/Business/CourierSurchargeTypeRepository.php @@ -0,0 +1,154 @@ + */ + public const DEFAULT_NAMES = [ + '退回费', '拦截费', '改地址', '异形件', '超长费', + '上楼费', '送货费', '超区费', '安装费', '保费', '发出运费附加', + ]; + + public function ready(): bool + { + try { + return Schema::hasTable((new CourierSurchargeType())->getTable()); + } catch (QueryException) { + return false; + } + } + + /** @return Collection */ + public function all(): Collection + { + if (! $this->ready()) { + return collect(); + } + + try { + $types = CourierSurchargeType::query() + ->orderBy('sort_order') + ->orderBy('id') + ->get(); + + if ($types->isEmpty() && company_id()) { + $this->ensureDefaults((int) company_id()); + $types = CourierSurchargeType::query() + ->orderBy('sort_order') + ->orderBy('id') + ->get(); + } + + return $types; + } catch (QueryException) { + return collect(); + } + } + + /** @return list */ + public function validNames(): array + { + return $this->all()->pluck('name')->values()->all(); + } + + /** @return list */ + public function activeNames(): array + { + return $this->all() + ->where('status', CourierSurchargeType::STATUS_ACTIVE) + ->pluck('name') + ->values() + ->all(); + } + + public function create(string $name, int $userId): CourierSurchargeType + { + $name = trim($name); + if ($name === '') { + throw ValidationException::withMessages(['name' => '附加费类型不能为空。']); + } + + try { + if (CourierSurchargeType::query()->where('name', $name)->exists()) { + throw ValidationException::withMessages(['name' => '当前公司已有同名附加费类型。']); + } + + $sortOrder = ((int) CourierSurchargeType::query()->max('sort_order')) + 1; + + return CourierSurchargeType::query()->create([ + 'company_id' => company_id(), + 'name' => $name, + 'status' => CourierSurchargeType::STATUS_ACTIVE, + 'sort_order' => $sortOrder, + 'created_by' => $userId, + 'updated_by' => $userId, + ]); + } catch (QueryException $exception) { + if ((int) $exception->getCode() === 23000) { + throw ValidationException::withMessages(['name' => '当前公司已有同名附加费类型。']); + } + + throw $exception; + } + } + + public function setStatus(CourierSurchargeType $type, string $status, int $userId): CourierSurchargeType + { + abort_unless(in_array($status, [CourierSurchargeType::STATUS_ACTIVE, CourierSurchargeType::STATUS_INACTIVE], true), 422, '附加费类型状态不合法。'); + + $type->forceFill([ + 'status' => $status, + 'updated_by' => $userId, + ])->save(); + + return $type->fresh(); + } + + private function ensureDefaults(int $companyId): void + { + foreach (self::DEFAULT_NAMES as $sortOrder => $name) { + CourierSurchargeType::query()->firstOrCreate( + ['company_id' => $companyId, 'name' => $name], + [ + 'status' => CourierSurchargeType::STATUS_ACTIVE, + 'sort_order' => $sortOrder, + 'created_by' => null, + 'updated_by' => null, + ], + ); + } + } + + /** @return array */ + public function usageCounts(Collection $types): array + { + if ($types->isEmpty() || ! Schema::hasTable((new CourierSurchargeQuote())->getTable())) { + return []; + } + + $counts = array_fill_keys($types->pluck('name')->all(), 0); + CourierSurchargeQuote::query()->get(['types'])->each(function (CourierSurchargeQuote $quote) use (&$counts): void { + foreach (array_unique((array) $quote->types) as $name) { + if (array_key_exists($name, $counts)) { + $counts[$name]++; + } + } + }); + + return $counts; + } +} diff --git a/app/Http/Controllers/Business/CourierSurchargeQuotes.php b/app/Http/Controllers/Business/CourierSurchargeQuotes.php index 62633b9f..43893dc5 100644 --- a/app/Http/Controllers/Business/CourierSurchargeQuotes.php +++ b/app/Http/Controllers/Business/CourierSurchargeQuotes.php @@ -56,8 +56,8 @@ class CourierSurchargeQuotes extends Controller 'courier' => ['required', 'string', 'max:191'], 'scope' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::SCOPES)], 'reconciler' => ['required', 'string', 'max:100'], - 'types' => ['required', 'array', 'min:1', 'max:11'], - 'types.*' => ['string', 'in:' . implode(',', CourierSurchargeQuoteRepository::TYPES)], + 'types' => ['required', 'array', 'min:1', 'max:50'], + 'types.*' => ['string', 'in:' . implode(',', $this->quotes->activeTypeNames())], 'mode' => ['required', 'in:' . implode(',', CourierSurchargeQuoteRepository::MODES)], 'standard' => ['required', 'string', 'max:2000'], 'condition' => ['required', 'string', 'max:2000'], diff --git a/app/Http/Controllers/Business/CourierSurchargeTypes.php b/app/Http/Controllers/Business/CourierSurchargeTypes.php new file mode 100644 index 00000000..020bf9ab --- /dev/null +++ b/app/Http/Controllers/Business/CourierSurchargeTypes.php @@ -0,0 +1,58 @@ +middleware('auth'); + $this->middleware('company.identify'); + $this->middleware('business.page:courier-payables,courier-surcharge'); + } + + public function store(Request $request): RedirectResponse + { + abort_unless($this->types->ready(), 503, '附加费类型数据表尚未完成迁移,请联系管理员。'); + + $data = $request->validate([ + 'name' => ['required', 'string', 'max:100'], + ]); + $this->types->create($data['name'], (int) user_id()); + + return $this->redirect()->with('success', '附加费类型已新增。'); + } + + public function setStatus(Request $request, CourierSurchargeType $type): RedirectResponse + { + $this->ensureCompany($type); + abort_unless($this->types->ready(), 503, '附加费类型数据表尚未完成迁移,请联系管理员。'); + + $status = $request->validate([ + 'status' => ['required', 'in:' . CourierSurchargeType::STATUS_ACTIVE . ',' . CourierSurchargeType::STATUS_INACTIVE], + ])['status']; + $this->types->setStatus($type, $status, (int) user_id()); + + return $this->redirect()->with('success', '附加费类型状态已更新。'); + } + + private function ensureCompany(CourierSurchargeType $type): void + { + abort_unless((int) $type->company_id === (int) company_id(), 404, '附加费类型不存在。'); + } + + private function redirect(): RedirectResponse + { + return redirect()->route('business.pages.show', [ + 'company_id' => company_id() ?: 1, + 'section' => 'courier-payables', + 'page' => 'courier-surcharge', + ]); + } +} diff --git a/app/Http/Controllers/Business/Pages.php b/app/Http/Controllers/Business/Pages.php index 628dc698..f0f775aa 100644 --- a/app/Http/Controllers/Business/Pages.php +++ b/app/Http/Controllers/Business/Pages.php @@ -12,6 +12,7 @@ use App\Models\Business\CourierAgreementSignature; use App\Models\Business\CourierSigningSubject; use App\Models\Business\InstallationDetail; use App\Business\CourierSurchargeQuoteRepository; +use App\Business\CourierSurchargeTypeRepository; use App\Models\StoreReceivables\StoreZhongcaiReconciliation; use App\Utilities\SidebarPermissions; use Illuminate\Http\Request; @@ -73,6 +74,7 @@ class Pages extends Controller private readonly SupplierPayableReportRepository $supplierPayableReports, private readonly UpstreamQueryCacheFactory $upstreamCaches, private readonly CourierSurchargeQuoteRepository $courierSurchargeQuotes, + private readonly CourierSurchargeTypeRepository $courierSurchargeTypes, ) { $this->middleware('auth'); $this->middleware('company.identify'); @@ -482,6 +484,7 @@ class Pages extends Controller 'courierSurchargeSummary' => $data['summary'], 'courierSurchargeOptions' => $data['options'], 'courierSurchargeReady' => $data['ready'], + 'courierSurchargeTypeUsage' => $this->courierSurchargeTypes->usageCounts($data['options']['type_records'] ?? collect()), ]; } diff --git a/app/Models/Business/CourierSurchargeType.php b/app/Models/Business/CourierSurchargeType.php new file mode 100644 index 00000000..b69f83cf --- /dev/null +++ b/app/Models/Business/CourierSurchargeType.php @@ -0,0 +1,35 @@ + 'integer', + 'sort_order' => 'integer', + 'created_by' => 'integer', + 'updated_by' => 'integer', + ]; + + public function statusLabel(): string + { + return $this->status === self::STATUS_ACTIVE ? '已启用' : '已停用'; + } +} diff --git a/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php b/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php new file mode 100644 index 00000000..8d89b062 --- /dev/null +++ b/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php @@ -0,0 +1,62 @@ +increments('id'); + $table->unsignedInteger('company_id'); + $table->string('name', 100); + $table->string('status', 16)->default('active'); + $table->unsignedInteger('sort_order')->default(0); + $table->unsignedInteger('created_by')->nullable(); + $table->unsignedInteger('updated_by')->nullable(); + $table->timestamps(); + $table->softDeletes(); + + $table->unique(['company_id', 'name', 'deleted_at'], 'courier_surcharge_types_name_unique'); + $table->index(['company_id', 'status', 'sort_order'], 'courier_surcharge_types_status_index'); + }); + } + + $companyIds = DB::table('companies')->pluck('id'); + foreach ($companyIds as $companyId) { + foreach (self::DEFAULT_TYPES as $sortOrder => $name) { + $exists = DB::table('business_courier_surcharge_types') + ->where('company_id', $companyId) + ->where('name', $name) + ->exists(); + + if (! $exists) { + DB::table('business_courier_surcharge_types')->insert([ + 'company_id' => $companyId, + 'name' => $name, + 'status' => 'active', + 'sort_order' => $sortOrder, + 'created_by' => null, + 'updated_by' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + } + } + + public function down(): void + { + Schema::dropIfExists('business_courier_surcharge_types'); + } +}; diff --git a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php index a4690207..5825aa11 100644 --- a/resources/views/business/pages/courier-payables/courier-surcharge.blade.php +++ b/resources/views/business/pages/courier-payables/courier-surcharge.blade.php @@ -4,6 +4,7 @@ $summary = $courierSurchargeSummary ?? []; $options = $courierSurchargeOptions ?? []; $ready = (bool) ($courierSurchargeReady ?? false); + $typeUsage = $courierSurchargeTypeUsage ?? []; $statusClass = static fn (string $status): string => $status === '已启用' ? 'is-active' : 'is-inactive'; @endphp @@ -30,9 +31,10 @@ .sm-table-wrap { overflow:auto; } .sm-table { width:100%; min-width:82rem; border-collapse:separate; border-spacing:0; font-size:.75rem; } .sm-table th,.sm-table td { padding:.62rem .7rem; border-right:1px solid var(--sm-border); border-bottom:1px solid var(--sm-border); text-align:left; vertical-align:top; white-space:nowrap; } .sm-table th { position:sticky; top:0; z-index:1; background:var(--sm-soft); font-weight:700; } .sm-table tr:hover td { background:#fcfaf4; } .sm-table .wrap { max-width:18rem; white-space:normal; line-height:1.5; } .sm-table .actions { display:flex; gap:.35rem; } .sm-table .actions button { height:1.8rem; padding:0 .5rem; font-size:.7rem; } .sm-badge { display:inline-flex; align-items:center; padding:.15rem .5rem; border-radius:999px; font-size:.7rem; font-weight:700; } .sm-badge.is-active { background:#edf5e8; color:#53793e; } .sm-badge.is-inactive { background:#fff4df; color:#935f07; } .sm-empty { padding:3rem 1rem; text-align:center; color:var(--sm-muted); } .sm-rule-note { padding:1rem 1.15rem; color:var(--sm-muted); font-size:.75rem; line-height:1.65; background:#fffdf8; } + .sm-type-manager { display:grid; gap:1rem; padding:1.15rem; } .sm-type-create { display:grid; grid-template-columns:minmax(0,1fr) auto; gap:.7rem; align-items:end; padding-bottom:1rem; border-bottom:1px solid var(--sm-border); } .sm-type-list { display:grid; gap:.55rem; } .sm-type-list__head { display:grid; gap:.2rem; color:var(--sm-muted); font-size:.72rem; line-height:1.5; } .sm-type-list__head strong { color:var(--sm-ink); font-size:.8rem; } .sm-type-row { display:flex; align-items:center; justify-content:space-between; gap:.75rem; min-height:2.6rem; padding:.5rem .65rem; border:1px solid var(--sm-border); border-radius:.45rem; background:#fffdf8; } .sm-type-row > div { display:flex; align-items:center; flex-wrap:wrap; gap:.45rem; min-width:0; } .sm-type-row small { color:var(--sm-muted); font-size:.68rem; } .sm-type-row form { flex:none; } .sm-modal { position:fixed; inset:0; z-index:70; display:none; align-items:flex-start; justify-content:center; padding:4rem 1rem; overflow-y:auto; background:rgba(23,23,23,.42); } .sm-modal.is-open { display:flex; } .sm-modal__panel { width:min(42rem,100%); overflow:hidden; border:1px solid var(--sm-border); border-radius:.75rem; background:#fff; box-shadow:0 1rem 3rem rgba(23,23,23,.2); } .sm-modal__head { display:flex; align-items:center; justify-content:space-between; padding:1rem 1.15rem; border-bottom:1px solid var(--sm-border); } .sm-modal__head h2 { margin:0; font-size:1rem; } .sm-modal__close { border:0; background:transparent; color:var(--sm-muted); cursor:pointer; } .sm-form { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:.8rem; padding:1.15rem; } .sm-form__field { display:grid; gap:.3rem; } .sm-form__field--full { grid-column:span 2; } .sm-form__field label,.sm-form__field legend { color:var(--sm-muted); font-size:.72rem; font-weight:600; } .sm-form__types { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:.45rem; margin:0; padding:0; border:0; } .sm-type-option { display:flex; align-items:center; gap:.35rem; min-height:2.1rem; padding:.35rem .45rem; border:1px solid var(--sm-border); border-radius:.4rem; background:#fffdf8; color:var(--sm-ink); font-size:.72rem; cursor:pointer; } .sm-type-option:has(input:checked) { border-color:var(--sm-accent); background:var(--sm-soft); color:var(--sm-accent); font-weight:700; } .sm-type-option input { width:1rem; height:1rem; accent-color:var(--sm-accent); } .sm-form input,.sm-form select { height:2.25rem; } .sm-form textarea { min-height:4rem; padding:.55rem .6rem; resize:vertical; } .sm-form__foot { grid-column:span 2; display:flex; justify-content:flex-end; gap:.5rem; margin-top:.2rem; } @media (max-width:1100px) { .sm-summary { grid-template-columns:repeat(3,1fr); } .sm-summary__intro { grid-column:span 3; border-bottom:1px solid var(--sm-border); } .sm-toolbar { grid-template-columns:repeat(3,minmax(0,1fr)); } .sm-actions { grid-column:span 3; } } - @media (max-width:620px) { .sm-summary { grid-template-columns:repeat(2,1fr); } .sm-summary__intro { grid-column:span 2; } .sm-metric { border-left:0; border-top:1px solid var(--sm-border); } .sm-toolbar,.sm-form { grid-template-columns:minmax(0,1fr); } .sm-form__types { grid-template-columns:repeat(2,minmax(0,1fr)); } .sm-actions,.sm-form__field--full,.sm-form__foot { grid-column:auto; } .sm-actions .sm-button { flex:1; } } + @media (max-width:620px) { .sm-summary { grid-template-columns:repeat(2,1fr); } .sm-summary__intro { grid-column:span 2; } .sm-metric { border-left:0; border-top:1px solid var(--sm-border); } .sm-toolbar,.sm-form,.sm-type-create { grid-template-columns:minmax(0,1fr); } .sm-form__types { grid-template-columns:repeat(2,minmax(0,1fr)); } .sm-actions,.sm-form__field--full,.sm-form__foot { grid-column:auto; } .sm-actions .sm-button { flex:1; } }
{{ $ready ? '正式维护数据' : '数据表尚未就绪' }} {{ $ready ? '报价规则已保存到当前公司数据表,刷新页面后仍会保留;后续结算页面将按启用中的规则计算。' : '附加费报价数据表尚未完成迁移,当前不能保存规则。请由管理员在目标数据库完成迁移后再使用本页。' }}
@@ -46,13 +48,13 @@
-
筛选报价规则
+
筛选报价规则
-
+
重置
@@ -90,19 +92,43 @@
+ + diff --git a/routes/admin.php b/routes/admin.php index d2ed3236..448df8f3 100644 --- a/routes/admin.php +++ b/routes/admin.php @@ -4,6 +4,7 @@ use App\Http\Controllers\Business\CourierQuoteChanges; use App\Http\Controllers\Business\CourierQuoteSignatures; use App\Http\Controllers\Business\CourierAgreementSignatures; use App\Http\Controllers\Business\CourierSurchargeQuotes; +use App\Http\Controllers\Business\CourierSurchargeTypes; use Illuminate\Support\Facades\Route; use Modules\EcommerceFinance\Http\Controllers\AccountBills; use Modules\EcommerceFinance\Http\Controllers\AlipayBills; @@ -135,6 +136,8 @@ Route::prefix('workspace/courier-payables/courier-surcharge') Route::post('/', [CourierSurchargeQuotes::class, 'store'])->name('store'); Route::patch('{quote}', [CourierSurchargeQuotes::class, 'update'])->name('update'); Route::post('{quote}/status', [CourierSurchargeQuotes::class, 'setStatus'])->name('status'); + Route::post('types', [CourierSurchargeTypes::class, 'store'])->name('types.store'); + Route::post('types/{type}/status', [CourierSurchargeTypes::class, 'setStatus'])->name('types.status'); }); Route::get('workspace/{section}/{page?}/{tab?}', 'Business\Pages@show') diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php index a4fd325d..020ed761 100644 --- a/tests/Feature/Business/CourierSurchargePageTest.php +++ b/tests/Feature/Business/CourierSurchargePageTest.php @@ -3,7 +3,9 @@ namespace Tests\Feature\Business; use App\Models\Auth\Permission; +use App\Models\Business\CourierSurchargeType; use App\Models\Business\CourierSurchargeQuote; +use App\Business\CourierSurchargeTypeRepository; use Tests\Feature\FeatureTestCase; class CourierSurchargePageTest extends FeatureTestCase @@ -22,6 +24,8 @@ class CourierSurchargePageTest extends FeatureTestCase ->assertSeeText('附加费报价维护') ->assertSeeText('正式维护数据') ->assertSeeText('报价规则列表') + ->assertSeeText('管理附加费类型') + ->assertSeeText('类型列表') ->assertSeeText('新增报价规则') ->assertSeeText('结算方式') ->assertSeeText('计价标准') @@ -42,6 +46,78 @@ class CourierSurchargePageTest extends FeatureTestCase ->assertSeeText('后续结算页面将按启用中的规则计算'); } + public function test_courier_surcharge_type_can_be_created_and_duplicate_is_rejected(): void + { + $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); + $this->loginAs(); + + $this->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费']) + ->assertRedirect(); + + $this->assertDatabaseHas('business_courier_surcharge_types', [ + 'company_id' => company_id(), + 'name' => '偏远地区费', + 'status' => CourierSurchargeType::STATUS_ACTIVE, + ]); + + $this->withExceptionHandling() + ->from(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge'])) + ->post(route('business.courier-surcharge.types.store'), ['name' => '偏远地区费']) + ->assertRedirect() + ->assertSessionHasErrors('name'); + } + + public function test_inactive_type_is_rejected_for_new_quotes_but_keeps_historical_value(): void + { + $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); + $this->loginAs(); + + $type = app(CourierSurchargeTypeRepository::class)->all()->firstWhere('name', '超长费'); + $this->assertNotNull($type); + $quote = $this->createQuote(); + + $this->post(route('business.courier-surcharge.types.status', ['type' => $type->id]), [ + 'status' => CourierSurchargeType::STATUS_INACTIVE, + ])->assertRedirect(); + + $this->withExceptionHandling()->post(route('business.courier-surcharge.store'), [ + 'courier' => '停用类型测试快递', + 'scope' => '自有快递', + 'reconciler' => '张会计', + 'types' => ['超长费'], + 'mode' => '固定单价', + 'standard' => '1 元/单', + 'condition' => '测试', + 'effective_at' => '2026-09-02', + ])->assertSessionHasErrors('types.0'); + + $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge'])) + ->assertOk() + ->assertSeeText($quote->courier) + ->assertSeeText('超长费') + ->assertSeeText('管理附加费类型'); + + $page = $this->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'courier-surcharge'])) + ->assertOk(); + $this->assertStringNotContainsString('value="超长费">超长费', $page->getContent()); + } + + public function test_type_status_endpoint_rejects_another_company_type(): void + { + $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); + $otherType = CourierSurchargeType::withoutGlobalScopes()->create([ + 'company_id' => company_id() + 999, + 'name' => '其他公司类型', + 'status' => CourierSurchargeType::STATUS_ACTIVE, + 'sort_order' => 99, + ]); + + $this->loginAs(); + $this->withExceptionHandling()->post(route('business.courier-surcharge.types.status', ['type' => $otherType->id]), [ + 'status' => CourierSurchargeType::STATUS_INACTIVE, + ])->assertNotFound(); + } + public function test_courier_surcharge_page_filters_persisted_rules(): void { $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail()); -- 2.47.2 From 8a7e47610cfb8787c9fdc13286c16d236233acb4 Mon Sep 17 00:00:00 2001 From: zhoushengchen <3057083327@qq.com> Date: Wed, 2 Sep 2026 18:13:59 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=BF=9B?= =?UTF-8?q?=E4=BB=93=E8=B4=B9=E9=99=84=E5=8A=A0=E8=B4=B9=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Business/CourierSurchargeTypeRepository.php | 2 +- .../2026_09_02_010000_create_courier_surcharge_types_table.php | 2 +- tests/Feature/Business/CourierSurchargePageTest.php | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/Business/CourierSurchargeTypeRepository.php b/app/Business/CourierSurchargeTypeRepository.php index 677db037..674cdee2 100644 --- a/app/Business/CourierSurchargeTypeRepository.php +++ b/app/Business/CourierSurchargeTypeRepository.php @@ -20,7 +20,7 @@ class CourierSurchargeTypeRepository /** @var list */ public const DEFAULT_NAMES = [ '退回费', '拦截费', '改地址', '异形件', '超长费', - '上楼费', '送货费', '超区费', '安装费', '保费', '发出运费附加', + '上楼费', '送货费', '超区费', '安装费', '保费', '发出运费附加', '进仓费', ]; public function ready(): bool diff --git a/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php b/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php index 8d89b062..97d9e02d 100644 --- a/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php +++ b/database/migrations/2026_09_02_010000_create_courier_surcharge_types_table.php @@ -9,7 +9,7 @@ return new class extends Migration { private const DEFAULT_TYPES = [ '退回费', '拦截费', '改地址', '异形件', '超长费', - '上楼费', '送货费', '超区费', '安装费', '保费', '发出运费附加', + '上楼费', '送货费', '超区费', '安装费', '保费', '发出运费附加', '进仓费', ]; public function up(): void diff --git a/tests/Feature/Business/CourierSurchargePageTest.php b/tests/Feature/Business/CourierSurchargePageTest.php index 020ed761..1ab3dea9 100644 --- a/tests/Feature/Business/CourierSurchargePageTest.php +++ b/tests/Feature/Business/CourierSurchargePageTest.php @@ -26,6 +26,7 @@ class CourierSurchargePageTest extends FeatureTestCase ->assertSeeText('报价规则列表') ->assertSeeText('管理附加费类型') ->assertSeeText('类型列表') + ->assertSeeText('进仓费') ->assertSeeText('新增报价规则') ->assertSeeText('结算方式') ->assertSeeText('计价标准') -- 2.47.2 From 3eefded75a0a6d1bed6ac806ed0f13ad4be33804 Mon Sep 17 00:00:00 2001 From: zhoushengchen <3057083327@qq.com> Date: Wed, 2 Sep 2026 18:19:07 +0800 Subject: [PATCH 3/3] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=BF=9B?= =?UTF-8?q?=E4=BB=93=E8=B4=B9=E5=A2=9E=E9=87=8F=E8=BF=81=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...d_inbound_warehouse_fee_surcharge_type.php | 45 +++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 database/migrations/2026_09_02_020000_add_inbound_warehouse_fee_surcharge_type.php diff --git a/database/migrations/2026_09_02_020000_add_inbound_warehouse_fee_surcharge_type.php b/database/migrations/2026_09_02_020000_add_inbound_warehouse_fee_surcharge_type.php new file mode 100644 index 00000000..58ba41c7 --- /dev/null +++ b/database/migrations/2026_09_02_020000_add_inbound_warehouse_fee_surcharge_type.php @@ -0,0 +1,45 @@ +pluck('id') as $companyId) { + $query = DB::table('business_courier_surcharge_types') + ->where('company_id', $companyId) + ->where('name', '进仓费'); + + if ($query->whereNull('deleted_at')->exists()) { + continue; + } + + $sortOrder = ((int) DB::table('business_courier_surcharge_types') + ->where('company_id', $companyId) + ->max('sort_order')) + 1; + + DB::table('business_courier_surcharge_types')->insert([ + 'company_id' => $companyId, + 'name' => '进仓费', + 'status' => 'active', + 'sort_order' => $sortOrder, + 'created_by' => null, + 'updated_by' => null, + 'created_at' => now(), + 'updated_at' => now(), + ]); + } + } + + public function down(): void + { + // 类型字典不做物理删除,避免回滚迁移时破坏已建立的业务引用。 + } +}; -- 2.47.2