|
|
@@ -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.
|
|
|
*/
|