diff --git a/innopacks/common/database/migrations/2024_04_01_161034_init_innoshop_table.php b/innopacks/common/database/migrations/2024_04_01_161034_init_innoshop_table.php index 42bf584a..e64a6385 100644 --- a/innopacks/common/database/migrations/2024_04_01_161034_init_innoshop_table.php +++ b/innopacks/common/database/migrations/2024_04_01_161034_init_innoshop_table.php @@ -700,6 +700,7 @@ public function up(): void $table->unsignedInteger('product_video_id')->default(0)->index('p_pv_id')->comment('Video ID'); $table->unsignedInteger('product_sku_id')->default(0)->index('p_ps_id')->comment('SKU ID'); $table->unsignedInteger('tax_class_id')->default(0)->index('p_tc_id')->comment('Tax Class ID'); + $table->string('spu_code', 128)->nullable()->unique()->comment('SPU Code'); $table->string('slug', 128)->nullable()->unique()->comment('URL Slug'); $table->json('variables')->nullable()->comment('Product variables for sku with variants'); $table->boolean('is_virtual')->default(false)->comment('Is Virtual'); diff --git a/innopacks/common/src/Models/Attribute/Translation.php b/innopacks/common/src/Models/Attribute/Translation.php index 54b14656..d60b94ae 100644 --- a/innopacks/common/src/Models/Attribute/Translation.php +++ b/innopacks/common/src/Models/Attribute/Translation.php @@ -9,6 +9,8 @@ namespace InnoShop\Common\Models\Attribute; +use Illuminate\Database\Eloquent\Relations\BelongsTo; +use InnoShop\Common\Models\Attribute; use InnoShop\Common\Models\BaseModel; class Translation extends BaseModel @@ -18,4 +20,12 @@ class Translation extends BaseModel protected $fillable = [ 'locale', 'name', ]; + + /** + * @return BelongsTo + */ + public function attribute(): BelongsTo + { + return $this->belongsTo(Attribute::class, 'attribute_id'); + } } diff --git a/innopacks/common/src/Models/Category/Translation.php b/innopacks/common/src/Models/Category/Translation.php index 6383a511..bfef7b37 100644 --- a/innopacks/common/src/Models/Category/Translation.php +++ b/innopacks/common/src/Models/Category/Translation.php @@ -9,7 +9,9 @@ namespace InnoShop\Common\Models\Category; +use Illuminate\Database\Eloquent\Relations\BelongsTo; use InnoShop\Common\Models\BaseModel; +use InnoShop\Common\Models\Category; class Translation extends BaseModel { @@ -18,4 +20,12 @@ class Translation extends BaseModel protected $fillable = [ 'category_id', 'locale', 'name', 'content', 'meta_title', 'meta_description', 'meta_keywords', ]; + + /** + * @return BelongsTo + */ + public function category(): BelongsTo + { + return $this->belongsTo(Category::class, 'category_id'); + } } diff --git a/innopacks/common/src/Models/Product.php b/innopacks/common/src/Models/Product.php index d22f93f7..af5dadcd 100644 --- a/innopacks/common/src/Models/Product.php +++ b/innopacks/common/src/Models/Product.php @@ -28,8 +28,8 @@ class Product extends BaseModel use HasPackageFactory, Replicate, Translatable; protected $fillable = [ - 'brand_id', 'product_image_id', 'product_video_id', 'product_sku_id', 'tax_class_id', 'slug', 'is_virtual', - 'variables', 'position', 'active', 'weight', 'weight_class', 'sales', 'viewed', + 'brand_id', 'product_image_id', 'product_video_id', 'product_sku_id', 'tax_class_id', 'spu_code', 'slug', + 'is_virtual', 'variables', 'position', 'active', 'weight', 'weight_class', 'sales', 'viewed', ]; protected $casts = [ diff --git a/innopacks/common/src/Repositories/Attribute/ValueRepo.php b/innopacks/common/src/Repositories/Attribute/ValueRepo.php index 86bc5f13..d116588c 100644 --- a/innopacks/common/src/Repositories/Attribute/ValueRepo.php +++ b/innopacks/common/src/Repositories/Attribute/ValueRepo.php @@ -9,6 +9,7 @@ namespace InnoShop\Common\Repositories\Attribute; +use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Collection; use InnoShop\Common\Models\Attribute\Value; @@ -21,15 +22,17 @@ class ValueRepo extends BaseRepo /** * @param $attributeID * @param $translations - * @return void + * @return mixed */ - public function createAttribute($attributeID, $translations): void + public function createAttribute($attributeID, $translations): mixed { if (empty($attributeID) || empty($translations)) { - return; + return null; } $attrValue = Value::query()->create(['attribute_id' => $attributeID]); $this->updateTranslations($attrValue, $translations); + + return $attrValue; } /** @@ -70,6 +73,48 @@ public function all(array $filters = []): Collection return $this->builder($filters)->get(); } + /** + * @param $name + * @param string $locale + * @return mixed + * @throws Exception + */ + public function findByName($name, string $locale = ''): mixed + { + if (empty($locale)) { + $locale = locale_code(); + } + + $translation = Value\Translation::query() + ->where('name', $name) + ->where('locale', $locale) + ->first(); + + return $translation->value ?? null; + } + + /** + * @param $attributeRow + * @param $name + * @param string $locale + * @return mixed + * @throws Exception + */ + public function findOrCreateByName($attributeRow, $name, string $locale = ''): mixed + { + $attributeValue = $this->findByName($name, $locale); + if ($attributeValue) { + return $attributeValue; + } + + $data = []; + foreach (locales() as $locale) { + $data[$locale->code] = $name; + } + + return $this->createAttribute($attributeRow->id, $data); + } + /** * @param array $filters * @return Builder diff --git a/innopacks/common/src/Repositories/AttributeRepo.php b/innopacks/common/src/Repositories/AttributeRepo.php index 9035c9fe..12e34365 100644 --- a/innopacks/common/src/Repositories/AttributeRepo.php +++ b/innopacks/common/src/Repositories/AttributeRepo.php @@ -41,6 +41,47 @@ public function all(array $filters = []): Collection return $this->builder($filters)->get(); } + /** + * @param $name + * @param string $locale + * @return mixed + * @throws Exception + */ + public function findByName($name, string $locale = ''): mixed + { + if (empty($locale)) { + $locale = locale_code(); + } + + $translation = Attribute\Translation::query()->where('name', $name)->where('locale', $locale)->first(); + + return $translation->attribute ?? null; + } + + /** + * @param $name + * @param string $locale + * @return mixed + * @throws Throwable + */ + public function findOrCreateByName($name, string $locale = ''): mixed + { + $attribute = $this->findByName($name, $locale); + if ($attribute) { + return $attribute; + } + + $data = []; + foreach (locales() as $locale) { + $data['translations'][] = [ + 'locale' => $locale->code, + 'name' => $name, + ]; + } + + return $this->create($data); + } + /** * @param array $filters * @return Builder diff --git a/innopacks/common/src/Repositories/CategoryRepo.php b/innopacks/common/src/Repositories/CategoryRepo.php index b15bcb05..e2e3bef0 100644 --- a/innopacks/common/src/Repositories/CategoryRepo.php +++ b/innopacks/common/src/Repositories/CategoryRepo.php @@ -9,6 +9,7 @@ namespace InnoShop\Common\Repositories; +use Exception; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\Facades\DB; use InnoShop\Common\Models\Category; @@ -161,7 +162,7 @@ private function createOrUpdate(Category $category, $data): void $category->translations()->createMany($translations); DB::commit(); - } catch (\Exception $e) { + } catch (Exception $e) { DB::rollBack(); throw $e; } @@ -175,10 +176,10 @@ private function handleCategoryData($data): array { return [ 'parent_id' => $data['parent_id'] ?? 0, - 'slug' => $data['slug'], - 'image' => $data['image'] ?? '', - 'position' => $data['position'] ?? 0, - 'active' => $data['active'] ?? true, + 'slug' => $data['slug'] ?? null, + 'image' => $data['image'] ?? '', + 'position' => $data['position'] ?? 0, + 'active' => $data['active'] ?? true, ]; } @@ -280,4 +281,45 @@ public function getNameByID($id): string { return Category::query()->find($id)->description->name ?? ''; } + + /** + * @param $name + * @param string $locale + * @return mixed + * @throws Exception + */ + public function findByName($name, string $locale = ''): mixed + { + if (empty($locale)) { + $locale = locale_code(); + } + + $translation = Category\Translation::query()->where('name', $name)->where('locale', $locale)->first(); + + return $translation->category ?? null; + } + + /** + * @param $name + * @param string $locale + * @return mixed + * @throws Throwable + */ + public function findOrCreateByName($name, string $locale = ''): mixed + { + $category = $this->findByName($name, $locale); + if ($category) { + return $category; + } + + $data = []; + foreach (locales() as $locale) { + $data['translations'][] = [ + 'locale' => $locale->code, + 'name' => $name, + ]; + } + + return $this->create($data); + } } diff --git a/innopacks/common/src/Repositories/ProductRepo.php b/innopacks/common/src/Repositories/ProductRepo.php index ff7c1ace..bd0566ef 100644 --- a/innopacks/common/src/Repositories/ProductRepo.php +++ b/innopacks/common/src/Repositories/ProductRepo.php @@ -231,6 +231,7 @@ public function handleProductData($data): array } return [ + 'spu_code' => $data['spu_code'], 'slug' => $data['slug'], 'brand_id' => $data['brand_id'] ?? 0, 'product_image_id' => $data['product_image_id'] ?? 0, @@ -556,6 +557,32 @@ public function getListByProductIDs(mixed $productIDs): mixed ->get(); } + /** + * @param $spuCode + * @return ?Product + */ + public function findBySpuCode($spuCode): ?Product + { + if (empty($spuCode)) { + return null; + } + + return Product::query()->where('spu_code', $spuCode)->first(); + } + + /** + * @param $slug + * @return ?Product + */ + public function findBySlug($slug): ?Product + { + if (empty($spuCode)) { + return null; + } + + return Product::query()->where('slug', $slug)->first(); + } + /** * @param $keyword * @param int $limit diff --git a/innopacks/common/src/Services/CheckoutService.php b/innopacks/common/src/Services/CheckoutService.php index e0ecb4fe..c72191f2 100644 --- a/innopacks/common/src/Services/CheckoutService.php +++ b/innopacks/common/src/Services/CheckoutService.php @@ -30,11 +30,11 @@ class CheckoutService extends BaseService { - private int $customerID; + protected int $customerID; - private string $guestID; + protected string $guestID; - private array $cartList = []; + protected array $cartList = []; private array $addressList = []; @@ -78,7 +78,7 @@ public function __construct(int $customerID = 0, string $guestID = '') */ public static function getInstance(int $customerID = 0, string $guestID = ''): static { - return new self($customerID, $guestID); + return new static($customerID, $guestID); } /** diff --git a/innopacks/front/resources/views/products/show.blade.php b/innopacks/front/resources/views/products/show.blade.php index f3b97c97..54766ca6 100644 --- a/innopacks/front/resources/views/products/show.blade.php +++ b/innopacks/front/resources/views/products/show.blade.php @@ -1,6 +1,19 @@ @extends('layouts.app') @section('body-class', 'page-product') +@if($product->translation->meta_title ?? '') + @section('title', $product->translation->meta_title ?? '') +@endif + +@if($product->translation->meta_description ?? '') + @section('description', $product->translation->meta_description ?? '') +@endif + +@if($product->translation->meta_keywords ?? '') + @section('keywords', $product->translation->meta_keywords ?? '') +@endif + + @push('header') diff --git a/innopacks/panel/resources/css/page-product-form.scss b/innopacks/panel/resources/css/page-product-form.scss index 51c7bb8a..bc860250 100644 --- a/innopacks/panel/resources/css/page-product-form.scss +++ b/innopacks/panel/resources/css/page-product-form.scss @@ -313,4 +313,51 @@ body.page-product-form { } } } -} \ No newline at end of file + .variant-skus-table th { + padding-top: 0; + vertical-align: bottom; + } + + .variant-skus-table .batch-input-item { + margin: 0; + padding: 10px 0; + } + + .variant-skus-table .batch-input-item .input-group { + width: 100%; + min-width: 120px; + } + + .variant-skus-table .batch-input-item .form-control { + height: 31px; + font-size: 13px; + border-right: 0; + } + + .variant-skus-table .batch-input-item .btn { + border-color: #ced4da; + background: #fff; + font-size: 13px; + padding: 4px 8px; + } + + .variant-skus-table .batch-input-item .btn:hover { + background: #8446df; + border-color: #8446df; + color: #fff; + } + + .variant-skus-table thead th { + background: #f8f9fa; + border-bottom: 2px solid #dee2e6; + } + + .variant-skus-table tbody td .form-control { + font-size: 13px; + } + + /* Remove old batch input styles */ + .batch-input-area { + display: none; + } +} diff --git a/innopacks/panel/resources/views/orders/printing.blade.php b/innopacks/panel/resources/views/orders/printing.blade.php index 431512e0..329cab47 100755 --- a/innopacks/panel/resources/views/orders/printing.blade.php +++ b/innopacks/panel/resources/views/orders/printing.blade.php @@ -34,7 +34,7 @@ class="printer">{{ __("panel/order.print") }} {{ __("panel/order.telephone") }}: {{ $order['shipping_telephone'] }}
{{ __("panel/order.email") }}: {{ $order['email'] }}
{{ __("panel/order.shipping_address") }}: - {{ $order['shipping_customer_name'] . "(" . $order['shipping_telephone'] . ")". ' ' . $order['shipping_address_1'] . ' ' . $order['shipping_address_2'] . ' ' . $order['shipping_city'] . ' ' . $order['shipping_zone'] . ' ' . $order['shipping_country'] }} + {{ $order['shipping_customer_name'] . "(" . $order['shipping_telephone'] . ")". ' ', $order['shipping_address_1'] . ' ' . $order['shipping_address_2'] . ' ' . $order['shipping_city'] . ' ' . $order['shipping_zone'] . ' ' . $order['shipping_country'] }}
diff --git a/innopacks/panel/resources/views/products/_form_variant.blade.php b/innopacks/panel/resources/views/products/_form_variant.blade.php index 11272789..d6c3b7e5 100644 --- a/innopacks/panel/resources/views/products/_form_variant.blade.php +++ b/innopacks/panel/resources/views/products/_form_variant.blade.php @@ -2,55 +2,6 @@ - @endpush
diff --git a/innopacks/restapi/routes/panel-api.php b/innopacks/restapi/routes/panel-api.php index d9ef1aed..774c21fe 100644 --- a/innopacks/restapi/routes/panel-api.php +++ b/innopacks/restapi/routes/panel-api.php @@ -23,6 +23,7 @@ Route::get('/products', [PanelApiControllers\ProductController::class, 'index'])->name('products.index'); Route::get('/products/names', [PanelApiControllers\ProductController::class, 'names'])->name('products.names'); Route::get('/products/autocomplete', [PanelApiControllers\ProductController::class, 'autocomplete'])->name('products.autocomplete'); + Route::post('/products/import', [PanelApiControllers\ProductController::class, 'import'])->name('products.import'); Route::get('/categories', [PanelApiControllers\CategoryController::class, 'index'])->name('categories.index'); Route::get('/categories/names', [PanelApiControllers\CategoryController::class, 'names'])->name('categories.names'); diff --git a/innopacks/restapi/src/PanelApiControllers/ProductController.php b/innopacks/restapi/src/PanelApiControllers/ProductController.php index 33ef93d2..659423e1 100644 --- a/innopacks/restapi/src/PanelApiControllers/ProductController.php +++ b/innopacks/restapi/src/PanelApiControllers/ProductController.php @@ -10,10 +10,13 @@ namespace InnoShop\RestAPI\PanelApiControllers; use Exception; +use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; use InnoShop\Common\Repositories\ProductRepo; use InnoShop\Common\Resources\ProductSimple; +use InnoShop\RestAPI\Services\ProductImportService; +use Throwable; class ProductController extends BaseController { @@ -52,4 +55,30 @@ public function autocomplete(Request $request): AnonymousResourceCollection return ProductSimple::collection($products); } + + /** + * @param Request $request + * @return JsonResponse + * @throws Throwable + */ + public function import(Request $request): JsonResponse + { + try { + $data = $request->all(); + foreach ($data['products'] as $productData) { + $product = null; + $spuCode = $productData['spu_code'] ?? ''; + if (empty($spuCode)) { + throw new \Exception('Empty SPU code!'); + } + + $product = ProductRepo::getInstance()->findBySpuCode($spuCode); + ProductImportService::getInstance()->import($productData, $product); + } + + return create_json_success(); + } catch (Exception $e) { + return json_fail($e->getMessage()); + } + } } diff --git a/innopacks/restapi/src/Services/ProductImportService.php b/innopacks/restapi/src/Services/ProductImportService.php new file mode 100644 index 00000000..1a4ccd75 --- /dev/null +++ b/innopacks/restapi/src/Services/ProductImportService.php @@ -0,0 +1,122 @@ + + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ + +namespace InnoShop\RestAPI\Services; + +use Exception; +use InnoShop\Common\Repositories\Attribute\ValueRepo; +use InnoShop\Common\Repositories\AttributeRepo; +use InnoShop\Common\Repositories\CategoryRepo; +use InnoShop\Common\Repositories\ProductRepo; +use Throwable; + +class ProductImportService +{ + /** + * @return self + */ + public static function getInstance() + { + return new self; + } + + /** + * @param $productData + * @param $product + * @return mixed + * @throws Throwable + */ + public function import($productData, $product = null): mixed + { + $productData = $this->handleData($productData); + if (empty($product)) { + return ProductRepo::getInstance()->create($productData); + } + + return ProductRepo::getInstance()->update($product, $productData); + } + + /** + * @param $productData + * @return mixed + * @throws Throwable + */ + private function handleData($productData): mixed + { + $productData['attributes'] = $this->handleAttributes($productData['attributes']); + $productData['categories'] = $this->handleCategories($productData['categories']); + + return $productData; + } + + /** + * 输入 "attributes": [{"attribute": "功能","attribute_value": "防水"},{"attribute": "功能","attribute_value": "保暖"}], + * 输出 "attributes": [{"attribute_id": "1", "attribute_value_id": "3"},{"attribute_id": "1", "attribute_value_id": "11"}], + * @param $attributes + * @return array + * @throws Exception|Throwable + */ + private function handleAttributes($attributes): array + { + $result = []; + + foreach ($attributes as $attribute) { + if (isset($attribute['attribute_id']) && isset($attribute['attribute_value_id'])) { + $result[] = $attribute; + + continue; + } + + if (! isset($attribute['attribute']) || ! isset($attribute['attribute_value'])) { + throw new Exception('请提供 attribute 和 attribute_value'); + } + + $attributeRow = AttributeRepo::getInstance()->findOrCreateByName($attribute['attribute']); + if (empty($attributeRow)) { + throw new Exception("无法创建属性 {$attribute['attribute']} "); + } + + $attributeValueRow = ValueRepo::getInstance()->findOrCreateByName($attributeRow, $attribute['attribute_value']); + if (empty($attributeValueRow)) { + throw new Exception("无法创建属性值 {$attribute['attribute_value']} "); + } + + $attribute['attribute_id'] = $attributeRow->id; + $attribute['attribute_value_id'] = $attributeValueRow->id; + + $result[] = $attribute; + } + + return $result; + } + + /** + * 输入 "categories": ["帽子", "鞋子"], + * 输出 "categories": [1, 2], + * @param $categories + * @return array + * @throws Throwable + */ + private function handleCategories($categories): array + { + $result = []; + + foreach ($categories as $name) { + if (is_int($name)) { + $result[] = $name; + + continue; + } + $category = CategoryRepo::getInstance()->findOrCreateByName($name); + $result[] = $category->id; + } + + return $result; + } +}