feat: 新增商品 Excel 匯入功能與修復 HTTPS 混合內容問題
1. 新增商品 Excel 匯入功能 (ProductImport, Export Template) 2. 調整商品代號驗證規則為 1-5 碼 (Controller & Import) 3. 修正 HTTPS Mixed Content 問題 (AppServiceProvider)
This commit is contained in:
@@ -10,6 +10,9 @@ use App\Modules\Inventory\Models\Category;
|
||||
use Illuminate\Http\Request;
|
||||
use Inertia\Inertia;
|
||||
use Inertia\Response;
|
||||
use Maatwebsite\Excel\Facades\Excel;
|
||||
use App\Modules\Inventory\Exports\ProductTemplateExport;
|
||||
use App\Modules\Inventory\Imports\ProductImport;
|
||||
|
||||
class ProductController extends Controller
|
||||
{
|
||||
@@ -111,7 +114,7 @@ class ProductController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:2|unique:products,code',
|
||||
'code' => 'required|string|min:1|max:5|unique:products,code',
|
||||
'barcode' => 'required|string|unique:products,barcode',
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
@@ -124,7 +127,8 @@ class ProductController extends Controller
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
], [
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 2 碼',
|
||||
'code.max' => '商品代號最多 5 碼',
|
||||
'code.min' => '商品代號最少 1 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
@@ -149,7 +153,7 @@ class ProductController extends Controller
|
||||
public function update(Request $request, Product $product)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'code' => 'required|string|max:2|unique:products,code,' . $product->id,
|
||||
'code' => 'required|string|min:1|max:5|unique:products,code,' . $product->id,
|
||||
'barcode' => 'required|string|unique:products,barcode,' . $product->id,
|
||||
'name' => 'required|string|max:255',
|
||||
'category_id' => 'required|exists:categories,id',
|
||||
@@ -161,7 +165,8 @@ class ProductController extends Controller
|
||||
'purchase_unit_id' => 'nullable|exists:units,id',
|
||||
], [
|
||||
'code.required' => '商品代號為必填',
|
||||
'code.max' => '商品代號最多 2 碼',
|
||||
'code.max' => '商品代號最多 5 碼',
|
||||
'code.min' => '商品代號最少 1 碼',
|
||||
'code.unique' => '商品代號已存在',
|
||||
'barcode.required' => '條碼編號為必填',
|
||||
'barcode.unique' => '條碼編號已存在',
|
||||
@@ -189,4 +194,36 @@ class ProductController extends Controller
|
||||
|
||||
return redirect()->back()->with('success', '商品已刪除');
|
||||
}
|
||||
|
||||
/**
|
||||
* 下載匯入範本
|
||||
*/
|
||||
public function template()
|
||||
{
|
||||
return Excel::download(new ProductTemplateExport, 'products_template.xlsx');
|
||||
}
|
||||
|
||||
/**
|
||||
* 匯入商品
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:xlsx,xls',
|
||||
]);
|
||||
|
||||
try {
|
||||
Excel::import(new ProductImport, $request->file('file'));
|
||||
return redirect()->back()->with('success', '商品匯入成功');
|
||||
} catch (\Maatwebsite\Excel\Validators\ValidationException $e) {
|
||||
$failures = $e->failures();
|
||||
$messages = [];
|
||||
foreach ($failures as $failure) {
|
||||
$messages[] = '第 ' . $failure->row() . ' 行: ' . implode(', ', $failure->errors());
|
||||
}
|
||||
return redirect()->back()->withErrors(['file' => implode("\n", $messages)]);
|
||||
} catch (\Exception $e) {
|
||||
return redirect()->back()->withErrors(['file' => '匯入失敗: ' . $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
36
app/Modules/Inventory/Exports/ProductTemplateExport.php
Normal file
36
app/Modules/Inventory/Exports/ProductTemplateExport.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Inventory\Exports;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
|
||||
use Maatwebsite\Excel\Concerns\WithColumnFormatting;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadings;
|
||||
use PhpOffice\PhpSpreadsheet\Style\NumberFormat;
|
||||
|
||||
class ProductTemplateExport implements WithHeadings, WithColumnFormatting
|
||||
{
|
||||
public function headings(): array
|
||||
{
|
||||
return [
|
||||
'商品代號',
|
||||
'條碼',
|
||||
'商品名稱',
|
||||
'類別名稱',
|
||||
'品牌',
|
||||
'規格',
|
||||
'基本單位',
|
||||
'大單位',
|
||||
'換算率',
|
||||
|
||||
];
|
||||
}
|
||||
|
||||
public function columnFormats(): array
|
||||
{
|
||||
return [
|
||||
'A' => NumberFormat::FORMAT_TEXT, // 商品代號
|
||||
'B' => NumberFormat::FORMAT_TEXT, // 條碼
|
||||
];
|
||||
}
|
||||
}
|
||||
105
app/Modules/Inventory/Imports/ProductImport.php
Normal file
105
app/Modules/Inventory/Imports/ProductImport.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Modules\Inventory\Imports;
|
||||
|
||||
use App\Modules\Inventory\Models\Category;
|
||||
use App\Modules\Inventory\Models\Product;
|
||||
use App\Modules\Inventory\Models\Unit;
|
||||
use Illuminate\Validation\Rule;
|
||||
use Maatwebsite\Excel\Concerns\ToModel;
|
||||
use Maatwebsite\Excel\Concerns\WithHeadingRow;
|
||||
use Maatwebsite\Excel\Concerns\WithMapping;
|
||||
use Maatwebsite\Excel\Concerns\WithValidation;
|
||||
use Maatwebsite\Excel\Imports\HeadingRowFormatter;
|
||||
|
||||
class ProductImport implements ToModel, WithHeadingRow, WithValidation, WithMapping
|
||||
{
|
||||
private $categories;
|
||||
private $units;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// 禁用標題格式化,保留中文標題
|
||||
HeadingRowFormatter::default('none');
|
||||
|
||||
// 快取所有類別與單位,避免 N+1 查詢
|
||||
$this->categories = Category::pluck('id', 'name');
|
||||
$this->units = Unit::pluck('id', 'name');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $row
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function map($row): array
|
||||
{
|
||||
// 強制將代號與條碼轉為字串,避免純數字被當作整數處理導致 max:5 驗證錯誤
|
||||
if (isset($row['商品代號'])) {
|
||||
$row['商品代號'] = (string) $row['商品代號'];
|
||||
}
|
||||
if (isset($row['條碼'])) {
|
||||
$row['條碼'] = (string) $row['條碼'];
|
||||
}
|
||||
|
||||
return $row;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Model|null
|
||||
*/
|
||||
public function model(array $row)
|
||||
{
|
||||
// 查找關聯 ID
|
||||
$categoryId = $this->categories[$row['類別名稱']] ?? null;
|
||||
$baseUnitId = $this->units[$row['基本單位']] ?? null;
|
||||
$largeUnitId = isset($row['大單位']) ? ($this->units[$row['大單位']] ?? null) : null;
|
||||
|
||||
|
||||
// 若必要關聯找不到,理論上 Validation 會攔截,但此處做防禦性編程
|
||||
if (!$categoryId || !$baseUnitId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new Product([
|
||||
'code' => $row['商品代號'],
|
||||
'barcode' => $row['條碼'],
|
||||
'name' => $row['商品名稱'],
|
||||
'category_id' => $categoryId,
|
||||
'brand' => $row['品牌'] ?? null,
|
||||
'specification' => $row['規格'] ?? null,
|
||||
'base_unit_id' => $baseUnitId,
|
||||
'large_unit_id' => $largeUnitId,
|
||||
'conversion_rate' => $row['換算率'] ?? null,
|
||||
'purchase_unit_id' => null,
|
||||
]);
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'商品代號' => ['required', 'string', 'min:1', 'max:5', 'unique:products,code'],
|
||||
'條碼' => ['required', 'string', 'unique:products,barcode'],
|
||||
'商品名稱' => ['required', 'string'],
|
||||
'類別名稱' => ['required', function($attribute, $value, $fail) {
|
||||
if (!isset($this->categories[$value])) {
|
||||
$fail("找不到類別: " . $value);
|
||||
}
|
||||
}],
|
||||
'基本單位' => ['required', function($attribute, $value, $fail) {
|
||||
if (!isset($this->units[$value])) {
|
||||
$fail("找不到單位: " . $value);
|
||||
}
|
||||
}],
|
||||
'大單位' => ['nullable', function($attribute, $value, $fail) {
|
||||
if ($value && !isset($this->units[$value])) {
|
||||
$fail("找不到單位: " . $value);
|
||||
}
|
||||
}],
|
||||
|
||||
'換算率' => ['nullable', 'numeric', 'min:0.0001', 'required_with:大單位'],
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -30,6 +30,8 @@ Route::middleware('auth')->group(function () {
|
||||
|
||||
// 商品管理
|
||||
Route::middleware('permission:products.view')->group(function () {
|
||||
Route::get('/products/template', [ProductController::class, 'template'])->name('products.template');
|
||||
Route::post('/products/import', [ProductController::class, 'import'])->middleware('permission:products.create')->name('products.import');
|
||||
Route::get('/products', [ProductController::class, 'index'])->name('products.index');
|
||||
Route::post('/products', [ProductController::class, 'store'])->middleware('permission:products.create')->name('products.store');
|
||||
Route::put('/products/{product}', [ProductController::class, 'update'])->middleware('permission:products.edit')->name('products.update');
|
||||
|
||||
Reference in New Issue
Block a user