From 6bb08a13ceb2b2862081a5bad130e0765c5775f6 Mon Sep 17 00:00:00 2001 From: terrylee Date: Mon, 29 Jun 2026 14:00:57 +0800 Subject: [PATCH 1/4] =?UTF-8?q?feat(sales):=20=E5=8F=96=E8=B2=A8=E7=A2=BC/?= =?UTF-8?q?=E9=80=9A=E8=A1=8C=E7=A2=BC=E6=89=B9=E6=AC=A1=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=EF=BC=88=E6=9C=80=E5=A4=9A1000=E7=AD=86=EF=BC=89+=20=E6=89=B9?= =?UTF-8?q?=E6=AC=A1=E6=B8=85=E5=96=AE=20CSV=20=E4=B8=8B=E8=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 取貨碼沿用「商品版」綁定(product_id,相容舊 slot_no)批次產碼 - 通行碼批次產碼;名稱自動加 #1..#N 流水 - 新增 batch_no 欄位標記同一批,提供批次下載路由與 UTF-8 BOM CSV - 數量>1 時忽略自訂碼、系統產生不重複隨機碼;單筆行為不變 - 產生後顯示可重複下載清單橫幅;新增 zh_TW/en/ja 翻譯鍵 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Admin/SalesController.php | 242 +++++++++++++++++- app/Models/Transaction/PassCode.php | 1 + app/Models/Transaction/PickupCode.php | 1 + ...29_140000_add_batch_no_to_codes_tables.php | 35 +++ lang/en.json | 7 + lang/ja.json | 7 + lang/zh_TW.json | 7 + .../partials/batch-result-banner.blade.php | 27 ++ .../admin/sales/pass-codes/index.blade.php | 45 +++- .../admin/sales/pickup-codes/index.blade.php | 42 ++- routes/web.php | 2 + 11 files changed, 410 insertions(+), 6 deletions(-) create mode 100644 database/migrations/2026_06_29_140000_add_batch_no_to_codes_tables.php create mode 100644 resources/views/admin/sales/partials/batch-result-banner.blade.php diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 01f8f24..4e5eeeb 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -19,6 +19,7 @@ use App\Models\Machine\Machine; use Carbon\Carbon; use Illuminate\Support\Facades\Auth; use Illuminate\Support\Facades\DB; +use Illuminate\Support\Str; use App\Models\System\SystemOperationLog; @@ -483,9 +484,11 @@ class SalesController extends Controller 'expires_hours' => 'nullable|integer|min:1|max:720', 'expires_at' => 'nullable|date|after:now', 'custom_code' => 'nullable|string|min:4|max:12', + 'quantity' => 'nullable|integer|min:1|max:1000', ]); $machine = Machine::findOrFail($validated['machine_id']); + $quantity = (int) ($validated['quantity'] ?? 1); // 綁商品時驗證該商品屬於此機台所屬公司(不要求機台當下有貨道;機台未上架仍可產碼, // 由前端提示、刷碼時若無貨道再由 B660 擋下)。 @@ -502,6 +505,66 @@ class SalesController extends Controller $expiresAt = $request->filled('expires_at') ? Carbon::parse($validated['expires_at']) : now()->addHours((int) ($request->expires_hours ?? 24)); + $usageLimit = $validated['usage_limit'] ?? 1; + + // 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。 + // 沿用單筆的綁定方式(優先 product_id,相容舊的 slot_no)。 + if ($quantity > 1) { + $batchNo = 'PUB' . now()->format('YmdHis') . strtoupper(Str::random(4)); + $codes = $this->generateUniqueCodeBatch(PickupCode::class, $machine->id, $quantity); + $nowTs = now(); + + $rows = []; + foreach ($codes as $code) { + $rows[] = [ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'product_id' => $validated['product_id'] ?? null, + 'slot_no' => $validated['slot_no'] ?? null, + 'code' => $code, + 'slug' => Str::random(16), + 'batch_no' => $batchNo, + 'status' => 'active', + 'usage_limit' => $usageLimit, + 'usage_count' => 0, + 'expires_at' => $expiresAt, + 'created_by' => Auth::id(), + 'created_at' => $nowTs, + 'updated_at' => $nowTs, + ]; + } + + DB::transaction(function () use ($rows) { + foreach (array_chunk($rows, 500) as $chunk) { + PickupCode::insert($chunk); + } + }); + + SystemOperationLog::create([ + 'company_id' => $machine->company_id, + 'user_id' => Auth::id(), + 'module' => 'pickup_code', + 'action' => 'batch_create', + 'target_id' => null, + 'target_type' => PickupCode::class, + 'new_values' => [ + 'batch_no' => $batchNo, + 'count' => count($rows), + 'machine_id' => $machine->id, + 'product_id' => $validated['product_id'] ?? null, + 'slot_no' => $validated['slot_no'] ?? null, + ], + ]); + + return back() + ->with('success', __(':count pickup codes generated', ['count' => count($rows)])) + ->with('code_batch', [ + 'type' => 'pickup', + 'batch_no' => $batchNo, + 'count' => count($rows), + 'download_url' => route('admin.sales.pickup-codes.batch-download', $batchNo), + ]); + } $pickupCode = PickupCode::create([ 'machine_id' => $validated['machine_id'], @@ -754,16 +817,72 @@ class SalesController extends Controller 'machine_id' => 'required|exists:machines,id', 'name' => 'required|string|max:50', 'expires_days' => 'nullable|integer|min:0', - 'custom_code' => 'required|string|min:4|max:12', + 'custom_code' => 'nullable|string|min:4|max:12', + 'quantity' => 'nullable|integer|min:1|max:1000', ]); $machine = Machine::findOrFail($validated['machine_id']); + $quantity = (int) ($validated['quantity'] ?? 1); $expiresAt = $request->expires_days ? now()->addDays((int) $request->expires_days) : null; + // 批次產生(>1 筆):忽略自訂碼,系統自動產生隨機唯一碼,並以 batch_no 標記同一批。 + if ($quantity > 1) { + $batchNo = 'PSB' . now()->format('YmdHis') . strtoupper(Str::random(4)); + $codes = $this->generateUniqueCodeBatch(PassCode::class, $machine->id, $quantity); + $nowTs = now(); + + $rows = []; + foreach ($codes as $i => $code) { + $rows[] = [ + 'company_id' => $machine->company_id, + 'machine_id' => $machine->id, + 'name' => $validated['name'] . ' #' . ($i + 1), + 'code' => $code, + 'slug' => Str::random(16), + 'batch_no' => $batchNo, + 'expires_at' => $expiresAt, + 'status' => 'active', + 'created_by' => Auth::id(), + 'created_at' => $nowTs, + 'updated_at' => $nowTs, + ]; + } + + DB::transaction(function () use ($rows) { + foreach (array_chunk($rows, 500) as $chunk) { + PassCode::insert($chunk); + } + }); + + SystemOperationLog::create([ + 'company_id' => $machine->company_id, + 'user_id' => Auth::id(), + 'module' => 'pass_code', + 'action' => 'batch_create', + 'target_id' => null, + 'target_type' => PassCode::class, + 'new_values' => [ + 'batch_no' => $batchNo, + 'count' => count($rows), + 'machine_id' => $machine->id, + 'name' => $validated['name'], + ], + ]); + + return back() + ->with('success', __(':count pass codes created', ['count' => count($rows)])) + ->with('code_batch', [ + 'type' => 'pass', + 'batch_no' => $batchNo, + 'count' => count($rows), + 'download_url' => route('admin.sales.pass-codes.batch-download', $batchNo), + ]); + } + $passCode = PassCode::create([ 'machine_id' => $validated['machine_id'], 'name' => $validated['name'] ?? 'Manual Generate', - 'code' => $validated['custom_code'] ?? PassCode::generateUniqueCode($validated['machine_id']), + 'code' => $request->custom_code ?: PassCode::generateUniqueCode($validated['machine_id']), 'expires_at' => $expiresAt, 'status' => 'active', 'company_id' => $machine->company_id, @@ -783,6 +902,125 @@ class SalesController extends Controller return back()->with('success', __('Pass code created: :code', ['code' => $passCode->code])); } + /** + * 批次產生指定數量的唯一 8 位數字碼(避開該機台目前有效的碼,並去除同批重複) + * + * @param class-string<\Illuminate\Database\Eloquent\Model> $modelClass PickupCode::class 或 PassCode::class + * @return string[] + */ + private function generateUniqueCodeBatch(string $modelClass, int $machineId, int $quantity): array + { + // 載入該機台目前仍有效的碼,避免與既有碼衝突 + $existing = array_flip( + $modelClass::where('machine_id', $machineId) + ->where('status', 'active') + ->where(function ($q) { + $q->whereNull('expires_at')->orWhere('expires_at', '>', now()); + }) + ->pluck('code') + ->all() + ); + + $codes = []; + $seen = []; + while (count($codes) < $quantity) { + $code = str_pad((string) random_int(0, 99999999), 8, '0', STR_PAD_LEFT); + if (isset($existing[$code]) || isset($seen[$code])) { + continue; + } + $seen[$code] = true; + $codes[] = $code; + } + + return $codes; + } + + /** + * 下載某一批取貨碼清單 (CSV) + */ + public function downloadPickupCodeBatch(string $batchNo) + { + $codes = PickupCode::with(['machine:id,name,serial_no', 'product:id,name']) + ->where('batch_no', $batchNo) + ->orderBy('id') + ->get(); + + abort_if($codes->isEmpty(), 404); + + $header = [ + __('Pickup Code'), + __('Target Machine'), + __('Select Product'), + __('Select Slot'), + __('Usage Limit'), + __('Expires At'), + __('Link'), + __('Created At'), + ]; + + return response()->streamDownload(function () use ($codes, $header) { + $out = fopen('php://output', 'w'); + fwrite($out, "\xEF\xBB\xBF"); // UTF-8 BOM,Excel 開啟才不會亂碼 + fputcsv($out, $header); + foreach ($codes as $c) { + fputcsv($out, [ + $c->code, + optional($c->machine)->name . ' (' . optional($c->machine)->serial_no . ')', + optional($c->product)->name ?? '', + $c->slot_no ?? '', + $c->usage_limit, + optional($c->expires_at)->format('Y-m-d H:i') ?? __('Permanent'), + $c->ticket_url, + optional($c->created_at)->format('Y-m-d H:i'), + ]); + } + fclose($out); + }, 'pickup-codes-' . $batchNo . '.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + + /** + * 下載某一批通行碼清單 (CSV) + */ + public function downloadPassCodeBatch(string $batchNo) + { + $codes = PassCode::with('machine:id,name,serial_no') + ->where('batch_no', $batchNo) + ->orderBy('id') + ->get(); + + abort_if($codes->isEmpty(), 404); + + $header = [ + __('Pass Code'), + __('Description / Name'), + __('Target Machine'), + __('Expires At'), + __('Link'), + __('Created At'), + ]; + + return response()->streamDownload(function () use ($codes, $header) { + $out = fopen('php://output', 'w'); + fwrite($out, "\xEF\xBB\xBF"); + fputcsv($out, $header); + foreach ($codes as $c) { + fputcsv($out, [ + $c->code, + $c->name, + optional($c->machine)->name . ' (' . optional($c->machine)->serial_no . ')', + optional($c->expires_at)->format('Y-m-d H:i') ?? __('Permanent'), + $c->ticket_url, + optional($c->created_at)->format('Y-m-d H:i'), + ]); + } + fclose($out); + }, 'pass-codes-' . $batchNo . '.csv', [ + 'Content-Type' => 'text/csv; charset=UTF-8', + ]); + } + /** * 更新通行碼 */ diff --git a/app/Models/Transaction/PassCode.php b/app/Models/Transaction/PassCode.php index c5bd7ff..8803eae 100644 --- a/app/Models/Transaction/PassCode.php +++ b/app/Models/Transaction/PassCode.php @@ -18,6 +18,7 @@ class PassCode extends Model 'name', 'code', 'slug', + 'batch_no', 'expires_at', 'status', 'created_by', diff --git a/app/Models/Transaction/PickupCode.php b/app/Models/Transaction/PickupCode.php index 1e84a75..bdd54b1 100644 --- a/app/Models/Transaction/PickupCode.php +++ b/app/Models/Transaction/PickupCode.php @@ -19,6 +19,7 @@ class PickupCode extends Model 'slot_no', 'code', 'slug', + 'batch_no', 'status', 'expires_at', 'used_at', diff --git a/database/migrations/2026_06_29_140000_add_batch_no_to_codes_tables.php b/database/migrations/2026_06_29_140000_add_batch_no_to_codes_tables.php new file mode 100644 index 0000000..7376c6f --- /dev/null +++ b/database/migrations/2026_06_29_140000_add_batch_no_to_codes_tables.php @@ -0,0 +1,35 @@ +string('batch_no', 32)->nullable()->after('slug')->index(); + }); + + Schema::table('pass_codes', function (Blueprint $table) { + $table->string('batch_no', 32)->nullable()->after('slug')->index(); + }); + } + + public function down(): void + { + Schema::table('pickup_codes', function (Blueprint $table) { + $table->dropIndex(['batch_no']); + $table->dropColumn('batch_no'); + }); + + Schema::table('pass_codes', function (Blueprint $table) { + $table->dropIndex(['batch_no']); + $table->dropColumn('batch_no'); + }); + } +}; diff --git a/lang/en.json b/lang/en.json index 079f8ba..097ce81 100644 --- a/lang/en.json +++ b/lang/en.json @@ -1,4 +1,11 @@ { + "Generate Quantity": "Generate Quantity", + "Batch mode: codes are auto-generated and the custom code is ignored.": "Batch mode: codes are auto-generated and the custom code is ignored.", + "Batch generated successfully": "Batch generated successfully", + "codes": "codes", + "Download List (CSV)": "Download List (CSV)", + ":count pickup codes generated": ":count pickup codes generated", + ":count pass codes created": ":count pass codes created", "(deducted by issued pickup orders)": "(deducted by issued pickup orders)", "-- Select Machine --": "-- Select Machine --", "10s": "10s", diff --git a/lang/ja.json b/lang/ja.json index 6fc3c9c..24b5fcb 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -1,4 +1,11 @@ { + "Generate Quantity": "生成数量", + "Batch mode: codes are auto-generated and the custom code is ignored.": "バッチモード:コードは自動生成され、カスタムコードは無視されます。", + "Batch generated successfully": "バッチ生成が完了しました", + "codes": "件", + "Download List (CSV)": "リストをダウンロード (CSV)", + ":count pickup codes generated": ":count 件の受取コードを生成しました", + ":count pass codes created": ":count 件の通行コードを作成しました", "(deducted by issued pickup orders)": "(発行済み受取注文の予約分を差し引き済み)", "-- Select Machine --": "-- 機器を選択 --", "10s": "10秒", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 93d16c8..c7c4945 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -1,4 +1,11 @@ { + "Generate Quantity": "產生數量", + "Batch mode: codes are auto-generated and the custom code is ignored.": "批次模式:系統自動產生隨機碼,將忽略自訂碼。", + "Batch generated successfully": "批次產生成功", + "codes": "筆", + "Download List (CSV)": "下載清單 (CSV)", + ":count pickup codes generated": "已產生 :count 筆取貨碼", + ":count pass codes created": "已建立 :count 筆通行碼", "(deducted by issued pickup orders)": "(已扣除已生成領藥單的預留量)", "-- Select Machine --": "-- 選擇機台 --", "10s": "10秒", diff --git a/resources/views/admin/sales/partials/batch-result-banner.blade.php b/resources/views/admin/sales/partials/batch-result-banner.blade.php new file mode 100644 index 0000000..43aa6d7 --- /dev/null +++ b/resources/views/admin/sales/partials/batch-result-banner.blade.php @@ -0,0 +1,27 @@ +@if(session('code_batch')) + @php($batch = session('code_batch')) +
+
+
+ + + +
+
+

{{ __('Batch generated successfully') }}

+

+ {{ __('Batch No') }}: {{ $batch['batch_no'] }} + · {{ $batch['count'] }} {{ __('codes') }} +

+
+
+ + + + + {{ __('Download List (CSV)') }} + +
+@endif diff --git a/resources/views/admin/sales/pass-codes/index.blade.php b/resources/views/admin/sales/pass-codes/index.blade.php index 37f00e8..c4e77c9 100644 --- a/resources/views/admin/sales/pass-codes/index.blade.php +++ b/resources/views/admin/sales/pass-codes/index.blade.php @@ -12,6 +12,7 @@ passCodeName: '', expiresDays: 7, customCode: '', + quantity: 1, status: '{{ request('status', '') }}', showQrModal: false, activeQrCode: '', @@ -145,7 +146,7 @@ window.showToast('{{ __('Please enter a description') }}', 'error'); return; } - if (!this.customCode.trim()) { + if (parseInt(this.quantity) <= 1 && !this.customCode.trim()) { window.showToast('{{ __('Please enter or generate a pass code') }}', 'error'); return; } @@ -194,12 +195,15 @@ this.selectedMachine = ''; this.passCodeName = ''; this.expiresDays = 7; + this.quantity = 1; this.generateRandomCode(); this.syncSelect('modal-pass-machine', ''); } }); } } " @ajax:navigate.window.prevent="fetchTabData(activeTab, $event.detail.url)"> + @include('admin.sales.partials.batch-result-banner') + {{-- Page Header --}} @@ -302,6 +306,41 @@ + {{-- Quantity (Batch Generate) --}} +
+ +
+
+ +
+ +
+ +
+
+ +
+
+

+ {{ __('Batch mode: codes are auto-generated and the custom code is ignored.') }} +

+
+
+ maxlength="12">
diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php index 2328787..f0b84a0 100644 --- a/resources/views/admin/sales/pickup-codes/index.blade.php +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -37,7 +37,7 @@ transition-all duration-300",
- + @include('admin.sales.partials.batch-result-banner') {{-- Page Header --}}
+ + {{-- Quantity (Batch Generate) --}} +
+ +
+
+ +
+ +
+ +
+
+ +
+
+

+ {{ __('Batch mode: codes are auto-generated and the custom code is ignored.') }} +

+
{{-- Pickup Code Preview Section --}} @@ -311,8 +345,10 @@ transition-all duration-300", + maxlength="12"> {{-- Validity Period Section (Flexible) --}} @@ -504,6 +540,7 @@ transition-all duration-300", expiryMode: 'hours', customExpiry: '', usageLimit: 1, + quantity: 1, showQrModal: false, activeQrCode: '', activeTicketUrl: '', @@ -801,6 +838,7 @@ transition-all duration-300", this.selectedProduct = ''; this.productNotOnMachine = false; this.usageLimit = 1; + this.quantity = 1; this.expiryMode = 'hours'; this.expiresHours = 24; const tomorrow = new Date(); diff --git a/routes/web.php b/routes/web.php index e963a60..28e9aa7 100644 --- a/routes/web.php +++ b/routes/web.php @@ -167,12 +167,14 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::post('/pickup-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePickupCode'])->name('pickup-codes.store'); Route::patch('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePickupCode'])->name('pickup-codes.update'); Route::delete('/pickup-codes/{pickupCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPickupCode'])->name('pickup-codes.destroy'); + Route::get('/pickup-codes/batch/{batchNo}/download', [App\Http\Controllers\Admin\SalesController::class, 'downloadPickupCodeBatch'])->name('pickup-codes.batch-download'); // 通行碼 Route::get('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'passCodes'])->name('pass-codes'); Route::post('/pass-codes', [App\Http\Controllers\Admin\SalesController::class, 'storePassCode'])->name('pass-codes.store'); Route::patch('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'updatePassCode'])->name('pass-codes.update'); Route::delete('/pass-codes/{passCode}', [App\Http\Controllers\Admin\SalesController::class, 'destroyPassCode'])->name('pass-codes.destroy'); + Route::get('/pass-codes/batch/{batchNo}/download', [App\Http\Controllers\Admin\SalesController::class, 'downloadPassCodeBatch'])->name('pass-codes.batch-download'); Route::get('/orders', [App\Http\Controllers\Admin\SalesController::class, 'orders'])->name('orders'); Route::get('/promotions', [App\Http\Controllers\Admin\SalesController::class, 'promotions'])->name('promotions'); From 9dbde50f560530536c205388589b659de39c9326 Mon Sep 17 00:00:00 2001 From: terrylee Date: Mon, 29 Jun 2026 14:10:13 +0800 Subject: [PATCH 2/4] =?UTF-8?q?fix(sales):=20=E5=8F=96=E8=B2=A8=E7=A2=BC?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=BD=88=E7=AA=97=E3=80=8C=E7=94=A2=E7=94=9F?= =?UTF-8?q?=E6=95=B8=E9=87=8F=E3=80=8D=E6=94=B9=E7=82=BA=E7=8D=A8=E7=AB=8B?= =?UTF-8?q?=E6=95=B4=E5=88=97=EF=BC=8C=E9=81=BF=E5=85=8D=E8=88=87=E4=BD=BF?= =?UTF-8?q?=E7=94=A8=E6=AC=A1=E6=95=B8=E4=B8=8A=E9=99=90=E9=87=8D=E7=96=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../views/admin/sales/pickup-codes/index.blade.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/resources/views/admin/sales/pickup-codes/index.blade.php b/resources/views/admin/sales/pickup-codes/index.blade.php index f0b84a0..2239728 100644 --- a/resources/views/admin/sales/pickup-codes/index.blade.php +++ b/resources/views/admin/sales/pickup-codes/index.blade.php @@ -293,11 +293,12 @@ transition-all duration-300", + - {{-- Quantity (Batch Generate) --}} -
- + {{-- Quantity (Batch Generate) — 獨立整列,避免與使用次數上限擠在同一列重疊 --}} +
+
-
{{-- Pickup Code Preview Section --}}
From 179348ad662ca4f4fa030afd862f43e7d5712560 Mon Sep 17 00:00:00 2001 From: terrylee Date: Mon, 29 Jun 2026 14:24:47 +0800 Subject: [PATCH 3/4] =?UTF-8?q?feat(sales):=20=E6=89=B9=E6=AC=A1=E7=94=A2?= =?UTF-8?q?=E7=A2=BC=E6=95=B8=E9=87=8F=E4=B8=8A=E9=99=90=E4=BE=9D=E8=A7=92?= =?UTF-8?q?=E8=89=B2=E5=88=86=E7=B4=9A=EF=BC=88=E5=AE=A2=E6=88=B6=E7=AB=AF?= =?UTF-8?q?=E6=9C=80=E5=A4=9A20=E3=80=81=E7=AE=A1=E7=90=86=E5=93=A1?= =?UTF-8?q?=E6=9C=80=E5=A4=9A1000=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 平台管理員(isSystemAdmin):數量上限 1000,快捷 10/50/100/1000 - 客戶端(租戶):數量上限 20,快捷 10/20,欄位輸入超過自動夾回 20 - 後端 store 以角色動態 max 驗證,防止前端被繞過送出大量產碼 - 取貨碼/通行碼兩表單同步;前端 max 與快捷選項由後端依角色下發 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Admin/SalesController.php | 21 +++++++++++++++++-- .../admin/sales/pass-codes/index.blade.php | 11 ++++++---- .../admin/sales/pickup-codes/index.blade.php | 13 +++++++----- 3 files changed, 34 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index 4e5eeeb..9a735be 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -374,6 +374,11 @@ class SalesController extends Controller 'tab' => $tab, ]; + // 批次產生數量上限/快捷選項依角色分級:平台管理員 1000(10/50/100/1000)、客戶端 20(10/20) + $isSystemAdmin = Auth::user()->isSystemAdmin(); + $data['maxBatchQuantity'] = $isSystemAdmin ? 1000 : 20; + $data['batchQuickOptions'] = $isSystemAdmin ? [10, 50, 100, 1000] : [10, 20]; + // 1. 取貨碼列表 (list) if (!$isAjax || $tab === 'list') { // 僅顯示目前帳號可存取機台的取貨碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope) @@ -475,6 +480,10 @@ class SalesController extends Controller */ public function storePickupCode(Request $request) { + // 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆。 + // 後端強制把關,避免前端被繞過送出大量產碼請求。 + $maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20; + $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', // 取貨碼改綁「商品」:新流程傳 product_id;舊的綁「貨道」流程仍可傳 slot_no(向後相容)。 @@ -484,7 +493,7 @@ class SalesController extends Controller 'expires_hours' => 'nullable|integer|min:1|max:720', 'expires_at' => 'nullable|date|after:now', 'custom_code' => 'nullable|string|min:4|max:12', - 'quantity' => 'nullable|integer|min:1|max:1000', + 'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity, ]); $machine = Machine::findOrFail($validated['machine_id']); @@ -704,6 +713,11 @@ class SalesController extends Controller 'tab' => $tab, ]; + // 批次產生數量上限/快捷選項依角色分級:平台管理員 1000(10/50/100/1000)、客戶端 20(10/20) + $isSystemAdmin = Auth::user()->isSystemAdmin(); + $data['maxBatchQuantity'] = $isSystemAdmin ? 1000 : 20; + $data['batchQuickOptions'] = $isSystemAdmin ? [10, 50, 100, 1000] : [10, 20]; + // 1. 通行碼列表 (list) if (!$isAjax || $tab === 'list') { // 僅顯示目前帳號可存取機台的通行碼(機台授權以 machine_user 為準,whereHas 會套用 Machine 的 machine_access 全域 scope) @@ -813,12 +827,15 @@ class SalesController extends Controller */ public function storePassCode(Request $request) { + // 批次數量上限依角色分級:平台管理員最多 1000 筆;客戶端(租戶)最多 20 筆(後端強制把關)。 + $maxQuantity = Auth::user()->isSystemAdmin() ? 1000 : 20; + $validated = $request->validate([ 'machine_id' => 'required|exists:machines,id', 'name' => 'required|string|max:50', 'expires_days' => 'nullable|integer|min:0', 'custom_code' => 'nullable|string|min:4|max:12', - 'quantity' => 'nullable|integer|min:1|max:1000', + 'quantity' => 'nullable|integer|min:1|max:' . $maxQuantity, ]); $machine = Machine::findOrFail($validated['machine_id']); diff --git a/resources/views/admin/sales/pass-codes/index.blade.php b/resources/views/admin/sales/pass-codes/index.blade.php index c4e77c9..41c38dc 100644 --- a/resources/views/admin/sales/pass-codes/index.blade.php +++ b/resources/views/admin/sales/pass-codes/index.blade.php @@ -13,6 +13,8 @@ expiresDays: 7, customCode: '', quantity: 1, + maxBatchQty: {{ (int) ($maxBatchQuantity ?? 20) }}, + batchQuickOptions: @js($batchQuickOptions ?? [10, 20]), status: '{{ request('status', '') }}', showQrModal: false, activeQrCode: '', @@ -309,7 +311,7 @@ {{-- Quantity (Batch Generate) --}}
@@ -318,16 +320,17 @@
-
-
-