chengwl пре 3 дана
родитељ
комит
403da23e07

+ 2 - 7
packages/Longyi/Core/src/Contracts/ProductVariant.php

@@ -25,12 +25,7 @@ interface ProductVariant
     public function getEffectivePrice(?int $customerGroupId = null, ?int $channelId = null): float;
 
     /**
-     * Get images as array
+     * Get images for this variant (many-to-many with product_images).
      */
-    public function getImagesAttribute($value);
-
-    /**
-     * Set images from array
-     */
-    public function setImagesAttribute($value);
+    public function images();
 }

+ 36 - 0
packages/Longyi/Core/src/Database/Migrations/2026_03_07_000001_create_product_variant_images_table.php

@@ -0,0 +1,36 @@
+<?php
+
+use Illuminate\Database\Migrations\Migration;
+use Illuminate\Database\Schema\Blueprint;
+use Illuminate\Support\Facades\Schema;
+
+return new class extends Migration
+{
+    public function up(): void
+    {
+        Schema::create('product_variant_images', function (Blueprint $table) {
+            $table->increments('id');
+            $table->unsignedInteger('product_variant_id');
+            $table->unsignedInteger('product_image_id');
+            $table->integer('position')->default(0);
+
+            $table->foreign('product_variant_id')
+                ->references('id')
+                ->on('product_variants')
+                ->onDelete('cascade');
+
+            $table->foreign('product_image_id')
+                ->references('id')
+                ->on('product_images')
+                ->onDelete('cascade');
+
+            $table->unique(['product_variant_id', 'product_image_id'], 'variant_image_unique');
+            $table->index('product_image_id', 'idx_image_id');
+        });
+    }
+
+    public function down(): void
+    {
+        Schema::dropIfExists('product_variant_images');
+    }
+};

+ 135 - 23
packages/Longyi/Core/src/Http/Controllers/Admin/FlexibleVariantController.php

@@ -4,19 +4,22 @@ namespace Longyi\Core\Http\Controllers\Admin;
 
 use Illuminate\Http\JsonResponse;
 use Illuminate\Http\Request;
+use Illuminate\Http\UploadedFile;
 use Illuminate\Routing\Controller;
 use Illuminate\Foundation\Validation\ValidatesRequests;
+use Illuminate\Support\Facades\DB;
+use Illuminate\Support\Facades\Storage;
+use Illuminate\Support\Str;
+use Intervention\Image\ImageManager;
+use Longyi\Core\Helpers\FlexibleVariantOption;
+use Longyi\Core\Models\ProductOption;
+use Longyi\Core\Models\ProductOptionValue;
+use Longyi\Core\Models\ProductVariant;
 use Longyi\Core\Repositories\ProductOptionRepository;
 use Longyi\Core\Repositories\ProductOptionValueRepository;
 use Longyi\Core\Repositories\ProductVariantRepository;
-use Longyi\Core\Helpers\FlexibleVariantOption;
 use Webkul\Product\Helpers\Indexers\Price as PriceIndexer;
 use Webkul\Product\Repositories\ProductRepository;
-use Illuminate\Support\Facades\DB;
-use Longyi\Core\Models\ProductVariant;
-use Webkul\Product\Models\ProductVariantProxy;
-use Longyi\Core\Models\ProductOption;
-use Longyi\Core\Models\ProductOptionValue;
 
 class FlexibleVariantController extends Controller
 {
@@ -347,6 +350,8 @@ class FlexibleVariantController extends Controller
             'variants.*.quantity' => 'required|integer',
             'variants.*.values' => 'array',
             'variants.*.values.*.code' => 'required|string',
+            'variants.*.image_ids' => 'array',
+            'variants.*.image_ids.*' => 'integer|exists:product_images,id',
         ]);
 
 
@@ -364,8 +369,8 @@ class FlexibleVariantController extends Controller
             $variant->values()->detach();
 
             //删除产品选项
-            $this->_product->productOptions()->delete();
-            $this->_product->variants()
+            $this->_product->options()->delete();
+            $this->_product->flexibleVariants()
                 ->where('id', '!=', $variant->id)
                 ->get()
                 ->each(
@@ -403,18 +408,23 @@ class FlexibleVariantController extends Controller
 
             $variant->sku = $variantData['sku'];
             $variant->quantity = $variantData['quantity'];
+            $variant->price = $variantData['price'];
             $variant->save();
 
-            // $basePrice->price = (int) bcmul($variantData['price'], $basePrice->currency->factor);
-            // $basePrice->save();
+            $this->syncVariantBasePrices($variant, (float) $variantData['price']);
 
             $optionsValues = $this->mapOptionValuesToIds($variantData['values']);
 
             $variant->values()->sync($optionsValues);
 
-            $variants[$variantIndex]['variant_id'] = $variant->id;
+            if (isset($variantData['image_ids'])) {
+                $imageSync = collect($variantData['image_ids'])
+                    ->values()
+                    ->mapWithKeys(fn ($imageId, $i) => [$imageId => ['position' => $i]]);
+                $variant->images()->sync($imageSync);
+            }
 
-            
+            $variants[$variantIndex]['variant_id'] = $variant->id;
         }
         
         $productOptions = collect($this->_selectedOptions)
@@ -432,6 +442,8 @@ class FlexibleVariantController extends Controller
         //删除无用的变体
         $this->_product->flexibleVariants()->whereNotIn('id', $variantIds)->delete();
         DB::commit();
+
+        $this->reindexProduct($productId);
         return response()->json([
             'success' => true,
             'message' => 'Variants saved successfully',
@@ -443,20 +455,52 @@ class FlexibleVariantController extends Controller
         
 
     }
-    //将选项值的code映射为id
+    /**
+     * Create or update base price indices (customer_group_id = NULL) for a variant across all channels.
+     */
+    protected function syncVariantBasePrices(ProductVariant $variant, float $price): void
+    {
+        foreach (core()->getAllChannels() as $channel) {
+            $basePrice = $variant->basePrices()
+                ->where('channel_id', $channel->id)
+                ->first();
+
+            $priceData = [
+                'min_price'         => $price,
+                'regular_min_price' => $price,
+                'max_price'         => $price,
+                'regular_max_price' => $price,
+            ];
+
+            if ($basePrice) {
+                $basePrice->update($priceData);
+            } else {
+                $variant->price_indices()->create(array_merge($priceData, [
+                    'channel_id'        => $channel->id,
+                    'customer_group_id' => null,
+                ]));
+            }
+        }
+    }
 
     public function mapOptionValuesToIds(array $values){
         $valueIds = [];
-        foreach ($values as $option => $value) {
-            $selectedOption = collect(
-                $this->_selectedOptions
-            )->first(
-                fn ($o) => $o['code'] == $option
-            );
-            $valueId = collect($selectedOption['option_values'])->first(
-                fn ($v) => $v['code'] == $value['code']
-            )['id'];
-            $valueIds[] = $valueId;
+        foreach ($values as $optionCode => $value) {
+            $selectedOption = collect($this->_selectedOptions)
+                ->first(fn ($o) => $o['code'] == $optionCode);
+
+            if (! $selectedOption) {
+                throw new \InvalidArgumentException("Option [{$optionCode}] not found in selected options.");
+            }
+
+            $optionValue = collect($selectedOption['option_values'])
+                ->first(fn ($v) => $v['code'] == $value['code']);
+
+            if (! $optionValue) {
+                throw new \InvalidArgumentException("Option value [{$value['code']}] not found in option [{$optionCode}].");
+            }
+
+            $valueIds[] = $optionValue['id'];
         }
         return $valueIds;
     }
@@ -613,6 +657,74 @@ class FlexibleVariantController extends Controller
         ]);
     }
 
+    /**
+     * Sync images for a variant.
+     *
+     * Accepts a mix of existing product_image IDs and new file uploads.
+     * New files are stored into the product gallery first, then all IDs
+     * are synced to the variant via the pivot table.
+     *
+     * POST /variants/{id}/images
+     * Body (multipart/form-data):
+     *   image_ids[]   - existing product_image IDs to keep/attach
+     *   uploads[]     - new image files to upload
+     */
+    public function syncVariantImages(int $id, Request $request): JsonResponse
+    {
+        $this->validate($request, [
+            'image_ids'   => 'array',
+            'image_ids.*' => 'integer|exists:product_images,id',
+            'uploads'     => 'array',
+            'uploads.*'   => 'image|max:5120',
+        ]);
+
+        $variant = ProductVariant::findOrFail($id);
+        $product = $variant->product;
+
+        $imageIds = collect($request->input('image_ids', []));
+
+        if ($request->hasFile('uploads')) {
+            foreach ($request->file('uploads') as $file) {
+                $newImage = $this->storeProductImage($file, $product);
+                $imageIds->push($newImage->id);
+            }
+        }
+
+        $syncData = $imageIds->values()->mapWithKeys(
+            fn ($imageId, $i) => [$imageId => ['position' => $i]]
+        );
+
+        $variant->images()->sync($syncData);
+
+        return response()->json([
+            'success' => true,
+            'data'    => $variant->load('images')->images,
+        ]);
+    }
+
+    /**
+     * Store an uploaded file into the product gallery (product_images table).
+     */
+    protected function storeProductImage(UploadedFile $file, $product): \Webkul\Product\Models\ProductImage
+    {
+        $directory = 'product/' . $product->id;
+
+        if (Str::contains($file->getMimeType(), 'image')) {
+            $manager = new ImageManager;
+            $image = $manager->make($file)->encode('webp');
+            $path = $directory . '/' . Str::random(40) . '.webp';
+            Storage::put($path, $image);
+        } else {
+            $path = $file->store($directory);
+        }
+
+        return $product->images()->create([
+            'type'     => 'images',
+            'path'     => $path,
+            'position' => $product->images()->count(),
+        ]);
+    }
+
     /**
      * Trigger price reindex for a product and its flexible variants.
      */

+ 15 - 13
packages/Longyi/Core/src/Models/ProductVariant.php

@@ -10,6 +10,7 @@ use Illuminate\Database\Eloquent\SoftDeletes;
 use Longyi\Core\Contracts\ProductVariant as ProductVariantContract;
 use Webkul\Product\Models\Product;
 use Webkul\Product\Models\ProductCustomerGroupPriceProxy;
+use Webkul\Product\Models\ProductImageProxy;
 use Webkul\Product\Models\ProductPriceIndexProxy;
 
 /**
@@ -54,7 +55,6 @@ class ProductVariant extends Model implements ProductVariantContract
         'weight',
         'quantity',
         'status',
-        'images',
         'sort_order',
     ];
 
@@ -73,7 +73,6 @@ class ProductVariant extends Model implements ProductVariantContract
         'quantity' => 'integer',
         'status' => 'boolean',
         'sort_order' => 'integer',
-        'images' => 'array',
         'special_price_from' => 'date',
         'special_price_to' => 'date',
         'deleted_at' => 'datetime',
@@ -89,6 +88,7 @@ class ProductVariant extends Model implements ProductVariantContract
         // When deleting a variant, detach all option values
         static::deleting(function ($variant) {
             $variant->values()->detach();
+            $variant->images()->detach();
         });
     }
 
@@ -107,6 +107,10 @@ class ProductVariant extends Model implements ProductVariantContract
     {
         return $this->morphMany(ProductPriceIndexProxy::modelClass(), 'priceable');
     }
+    public function basePrices(): MorphMany
+    {
+        return $this->price_indices()->whereNull('customer_group_id');
+    }
 
     /**
      * Get the customer group prices for this variant.
@@ -188,19 +192,17 @@ class ProductVariant extends Model implements ProductVariantContract
     }
 
     /**
-     * Get images as array
+     * Get the images for this variant (many-to-many with product_images).
      */
-    public function getImagesAttribute($value)
+    public function images(): BelongsToMany
     {
-        return $value ? json_decode($value, true) : [];
-    }
-
-    /**
-     * Set images from array
-     */
-    public function setImagesAttribute($value)
-    {
-        $this->attributes['images'] = $value ? json_encode($value) : null;
+        return $this->belongsToMany(
+            ProductImageProxy::modelClass(),
+            'product_variant_images',
+            'product_variant_id',
+            'product_image_id'
+        )->withPivot('position')
+         ->orderByPivot('position');
     }
 
     /**

+ 2 - 1
packages/Longyi/Core/src/Routes/admin-routes.php

@@ -32,7 +32,8 @@ Route::group(['middleware' => ['admin']], function () {
             Route::post('{id}/save-variants', [FlexibleVariantController::class, 'saveVariants'])->name('admin.flexible_variant.variants.save-variants');
             Route::put('/{id}', [FlexibleVariantController::class, 'updateVariant'])->name('admin.flexible_variant.variants.update');
             Route::delete('/{id}', [FlexibleVariantController::class, 'deleteVariant'])->name('admin.flexible_variant.variants.destroy');
-            
+            Route::post('/{id}/images', [FlexibleVariantController::class, 'syncVariantImages'])->name('admin.flexible_variant.variants.images.sync');
+
             // Bulk operations
             Route::post('/bulk/quantities', [FlexibleVariantController::class, 'bulkUpdateQuantities'])->name('admin.flexible_variant.variants.bulk.quantities');
             Route::post('/bulk/status', [FlexibleVariantController::class, 'bulkUpdateStatus'])->name('admin.flexible_variant.variants.bulk.status');