|
@@ -41,6 +41,7 @@ class GiftProcessor
|
|
|
protected FreeGiftItemRepository $giftItemRepository,
|
|
protected FreeGiftItemRepository $giftItemRepository,
|
|
|
protected CartItemRepository $cartItemRepository,
|
|
protected CartItemRepository $cartItemRepository,
|
|
|
protected ProductRepository $productRepository,
|
|
protected ProductRepository $productRepository,
|
|
|
|
|
+ protected GiftProductResolver $productResolver,
|
|
|
protected Validator $validator
|
|
protected Validator $validator
|
|
|
) {}
|
|
) {}
|
|
|
|
|
|
|
@@ -78,22 +79,29 @@ class GiftProcessor
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 3. 以购物车真实数据校准「已占用数量」
|
|
|
|
|
|
|
+ // 3. 解析赠品商品:flexible_variant 必须落到具体变体,
|
|
|
|
|
+ // 购物车行的 sku 是 variant sku,认领行要靠 variant_id
|
|
|
|
|
+ $this->resolveGiftProducts($group);
|
|
|
|
|
+
|
|
|
|
|
+ // 4. 以购物车真实数据校准「已占用数量」
|
|
|
$this->registry->syncReservedQty($cart, $group);
|
|
$this->registry->syncReservedQty($cart, $group);
|
|
|
|
|
|
|
|
- // 4. 阶段 A
|
|
|
|
|
|
|
+ // 5. 阶段 A
|
|
|
$this->registerGiftItems($cart, $group);
|
|
$this->registerGiftItems($cart, $group);
|
|
|
|
|
|
|
|
- // 5. 阶段 B1
|
|
|
|
|
|
|
+ // 阶段 A 会新增候选,补一次解析
|
|
|
|
|
+ $this->resolveGiftProducts($group);
|
|
|
|
|
+
|
|
|
|
|
+ // 6. 阶段 B1
|
|
|
$this->syncGiftItems($cart, $group);
|
|
$this->syncGiftItems($cart, $group);
|
|
|
|
|
|
|
|
- // 6. 阶段 B2
|
|
|
|
|
|
|
+ // 7. 阶段 B2
|
|
|
$this->autoAddGiftItems($cart, $group);
|
|
$this->autoAddGiftItems($cart, $group);
|
|
|
|
|
|
|
|
- // 7. 清理孤儿赠品行(历史残留的重复行、规则下线后的残留)
|
|
|
|
|
|
|
+ // 8. 清理孤儿赠品行(历史残留的重复行、规则下线后的残留)
|
|
|
$this->removeOrphanGiftRows($cart, $group);
|
|
$this->removeOrphanGiftRows($cart, $group);
|
|
|
|
|
|
|
|
- // 8. 落库
|
|
|
|
|
|
|
+ // 9. 落库
|
|
|
$this->registry->persist($cartId);
|
|
$this->registry->persist($cartId);
|
|
|
} finally {
|
|
} finally {
|
|
|
$this->running = false;
|
|
$this->running = false;
|
|
@@ -292,15 +300,18 @@ class GiftProcessor
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $product = $this->productRepository->findOneWhere(['sku' => $gift->getSku()]);
|
|
|
|
|
|
|
+ $resolved = $this->productResolver->resolve($gift->getSku());
|
|
|
|
|
+
|
|
|
|
|
+ $product = $resolved['product'] ?? null;
|
|
|
|
|
+ $variant = $resolved['variant'] ?? null;
|
|
|
|
|
|
|
|
- if (! $this->productValidator->isEligible($product, $qty)) {
|
|
|
|
|
|
|
+ if (! $this->productValidator->isEligible($product, $qty, $variant)) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $gift->setProductId((int) $product->id);
|
|
|
|
|
|
|
+ $gift->setProductId((int) $product->id)->setVariantId($variant?->id);
|
|
|
|
|
|
|
|
- $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $gift->getCartItemId());
|
|
|
|
|
|
|
+ $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $gift->getCartItemId(), $variant);
|
|
|
|
|
|
|
|
// reserved 必须以行上的真实总量为准:writeGiftCartItem 传进去的是差额,
|
|
// reserved 必须以行上的真实总量为准:writeGiftCartItem 传进去的是差额,
|
|
|
// 加完之后行数量才是「已经占用」的量
|
|
// 加完之后行数量才是「已经占用」的量
|
|
@@ -350,9 +361,12 @@ class GiftProcessor
|
|
|
|
|
|
|
|
$qty = min($qty, $remaining);
|
|
$qty = min($qty, $remaining);
|
|
|
|
|
|
|
|
- $product = $this->productRepository->findOneWhere(['sku' => $sku]);
|
|
|
|
|
|
|
+ $resolved = $this->productResolver->resolve($sku);
|
|
|
|
|
|
|
|
- if (! $this->productValidator->isEligible($product, $qty)) {
|
|
|
|
|
|
|
+ $product = $resolved['product'] ?? null;
|
|
|
|
|
+ $variant = $resolved['variant'] ?? null;
|
|
|
|
|
+
|
|
|
|
|
+ if (! $this->productValidator->isEligible($product, $qty, $variant)) {
|
|
|
return ['success' => false, 'message' => trans('free_gift::app.cart.gift-unavailable'), 'cart_item_id' => null];
|
|
return ['success' => false, 'message' => trans('free_gift::app.cart.gift-unavailable'), 'cart_item_id' => null];
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -367,11 +381,12 @@ class GiftProcessor
|
|
|
$giftRule = $this->giftRuleRepository->findByCartRuleId($ruleId);
|
|
$giftRule = $this->giftRuleRepository->findByCartRuleId($ruleId);
|
|
|
|
|
|
|
|
$gift->setProductId((int) $product->id)
|
|
$gift->setProductId((int) $product->id)
|
|
|
|
|
+ ->setVariantId($variant?->id)
|
|
|
->setMinimalPrice($giftRule?->minimal_items_price);
|
|
->setMinimalPrice($giftRule?->minimal_items_price);
|
|
|
|
|
|
|
|
$existingId = $this->findGiftCartItem($cart, $gift)?->id ?: $row->cart_item_id;
|
|
$existingId = $this->findGiftCartItem($cart, $gift)?->id ?: $row->cart_item_id;
|
|
|
|
|
|
|
|
- $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $existingId);
|
|
|
|
|
|
|
+ $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $existingId, $variant);
|
|
|
|
|
|
|
|
$this->giftItemRepository->update([
|
|
$this->giftItemRepository->update([
|
|
|
'cart_item_id' => $cartItem->id,
|
|
'cart_item_id' => $cartItem->id,
|
|
@@ -395,7 +410,7 @@ class GiftProcessor
|
|
|
* @param \Webkul\Checkout\Contracts\Cart $cart
|
|
* @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
* @param \Webkul\Product\Contracts\Product $product
|
|
* @param \Webkul\Product\Contracts\Product $product
|
|
|
*/
|
|
*/
|
|
|
- protected function writeGiftCartItem($cart, $product, GiftItem $gift, float $qty, $cartItemId = null)
|
|
|
|
|
|
|
+ protected function writeGiftCartItem($cart, $product, GiftItem $gift, float $qty, $cartItemId = null, $variant = null)
|
|
|
{
|
|
{
|
|
|
/*
|
|
/*
|
|
|
* $qty 的语义是「本次新增的数量」,不是「这一行应有的总数量」。
|
|
* $qty 的语义是「本次新增的数量」,不是「这一行应有的总数量」。
|
|
@@ -415,12 +430,12 @@ class GiftProcessor
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $originalPrice = $this->getOriginalPrice($product, $qty);
|
|
|
|
|
|
|
+ $originalPrice = $this->getOriginalPrice($product, $qty, $variant);
|
|
|
|
|
|
|
|
// 赠品实付单价:原价 - 折扣,并以 minimal_items_price 兜底
|
|
// 赠品实付单价:原价 - 折扣,并以 minimal_items_price 兜底
|
|
|
$price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
$price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
|
|
|
|
|
|
- $weight = (float) ($product->weight ?? 0);
|
|
|
|
|
|
|
+ $weight = (float) ($variant->weight ?? $product->weight ?? 0);
|
|
|
|
|
|
|
|
$payload = [
|
|
$payload = [
|
|
|
'quantity' => $qty,
|
|
'quantity' => $qty,
|
|
@@ -441,12 +456,7 @@ class GiftProcessor
|
|
|
'weight' => $weight,
|
|
'weight' => $weight,
|
|
|
'total_weight' => $weight * $qty,
|
|
'total_weight' => $weight * $qty,
|
|
|
'base_total_weight' => $weight * $qty,
|
|
'base_total_weight' => $weight * $qty,
|
|
|
- 'additional' => [
|
|
|
|
|
- Gift::CART_ITEM_FLAG => $gift->getRuleId(),
|
|
|
|
|
- 'free_gift' => 1,
|
|
|
|
|
- 'free_gift_original_price' => $originalPrice,
|
|
|
|
|
- 'quantity' => $qty,
|
|
|
|
|
- ],
|
|
|
|
|
|
|
+ 'additional' => $this->giftAdditional($product, $variant, $gift, $originalPrice, $qty),
|
|
|
];
|
|
];
|
|
|
|
|
|
|
|
if ($cartItemId) {
|
|
if ($cartItemId) {
|
|
@@ -456,12 +466,40 @@ class GiftProcessor
|
|
|
return $this->cartItemRepository->create(array_merge($payload, [
|
|
return $this->cartItemRepository->create(array_merge($payload, [
|
|
|
'cart_id' => $cart->id,
|
|
'cart_id' => $cart->id,
|
|
|
'product_id' => $product->id,
|
|
'product_id' => $product->id,
|
|
|
- 'sku' => $product->sku,
|
|
|
|
|
|
|
+ 'sku' => $variant->sku ?? $product->sku,
|
|
|
'type' => $product->type,
|
|
'type' => $product->type,
|
|
|
- 'name' => $product->name,
|
|
|
|
|
|
|
+ 'name' => $variant->name ?? $product->name,
|
|
|
]));
|
|
]));
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 赠品行的 additional 载荷。
|
|
|
|
|
+ *
|
|
|
|
|
+ * flexible_variant 必须带 product_id / variant_id / variant_sku / variant_name / attributes:
|
|
|
|
|
+ * FlexibleVariant::validateCartItem() 靠 additional.variant_id 重新解析变体,
|
|
|
|
|
+ * 库存扣减(VariantInventoryListener)也按它找变体 —— 缺了这两个键,赠品行会被判失效。
|
|
|
|
|
+ * 变体信息直接复用类型自己的 getAdditionalOptions(),与前台加购保持同一份结构。
|
|
|
|
|
+ */
|
|
|
|
|
+ protected function giftAdditional($product, $variant, GiftItem $gift, float $originalPrice, float $qty): array
|
|
|
|
|
+ {
|
|
|
|
|
+ $additional = [
|
|
|
|
|
+ Gift::CART_ITEM_FLAG => $gift->getRuleId(),
|
|
|
|
|
+ 'free_gift' => 1,
|
|
|
|
|
+ 'free_gift_original_price' => $originalPrice,
|
|
|
|
|
+ 'quantity' => $qty,
|
|
|
|
|
+ ];
|
|
|
|
|
+
|
|
|
|
|
+ if (! $variant) {
|
|
|
|
|
+ return $additional;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $variantOptions = $product->getTypeInstance()->getAdditionalOptions(['variant_id' => (int) $variant->id]);
|
|
|
|
|
+
|
|
|
|
|
+ unset($variantOptions['variant_id']);
|
|
|
|
|
+
|
|
|
|
|
+ return array_merge($additional, $variantOptions, ['variant_id' => (int) $variant->id]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 在购物车里找到某条赠品候选对应的购物车行。
|
|
* 在购物车里找到某条赠品候选对应的购物车行。
|
|
|
*
|
|
*
|
|
@@ -477,11 +515,13 @@ class GiftProcessor
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if ($item->sku !== $gift->getSku()) {
|
|
|
|
|
- continue;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // flexible_variant 的行 sku 是 variant sku,而规则里配的可能是父商品 sku,
|
|
|
|
|
+ // 两者对不上,统一按「ruleId + variant_id」认领(无变体时退回 sku)
|
|
|
|
|
+ $variantId = (int) ($item->additional['variant_id'] ?? 0) ?: null;
|
|
|
|
|
|
|
|
- return $item;
|
|
|
|
|
|
|
+ if (GiftItem::cartKey($gift->getRuleId(), $variantId, (string) $item->sku) === $gift->getCartKey()) {
|
|
|
|
|
+ return $item;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
return null;
|
|
@@ -493,8 +533,17 @@ class GiftProcessor
|
|
|
* 与 Webkul 的 validateCartItem 用同一口径:取含特价、目录促销的最终价,
|
|
* 与 Webkul 的 validateCartItem 用同一口径:取含特价、目录促销的最终价,
|
|
|
* 而不是 catalog 里的标价,保证「赠品价 = 现价 × 折扣」。
|
|
* 而不是 catalog 里的标价,保证「赠品价 = 现价 × 折扣」。
|
|
|
*/
|
|
*/
|
|
|
- protected function getOriginalPrice($product, float $qty): float
|
|
|
|
|
|
|
+ protected function getOriginalPrice($product, float $qty, $variant = null): float
|
|
|
{
|
|
{
|
|
|
|
|
+ // flexible_variant 的成交价挂在变体上,先取变体价
|
|
|
|
|
+ if ($variant) {
|
|
|
|
|
+ $variantPrice = $this->productResolver->variantPrice($variant);
|
|
|
|
|
+
|
|
|
|
|
+ if ($variantPrice > 0) {
|
|
|
|
|
+ return $variantPrice;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
$finalPrice = (float) $product->getTypeInstance()->getFinalPrice($qty);
|
|
$finalPrice = (float) $product->getTypeInstance()->getFinalPrice($qty);
|
|
|
} catch (\Throwable) {
|
|
} catch (\Throwable) {
|
|
@@ -514,13 +563,13 @@ class GiftProcessor
|
|
|
*/
|
|
*/
|
|
|
protected function applyGiftPrice($cartItem, GiftItem $gift): void
|
|
protected function applyGiftPrice($cartItem, GiftItem $gift): void
|
|
|
{
|
|
{
|
|
|
- $product = $this->productRepository->findOneWhere(['sku' => $gift->getSku()]);
|
|
|
|
|
|
|
+ $resolved = $this->productResolver->resolve($gift->getSku());
|
|
|
|
|
|
|
|
- if (! $product) {
|
|
|
|
|
|
|
+ if (! $resolved) {
|
|
|
return;
|
|
return;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- $originalPrice = $this->getOriginalPrice($product, (float) $cartItem->quantity);
|
|
|
|
|
|
|
+ $originalPrice = $this->getOriginalPrice($resolved['product'], (float) $cartItem->quantity, $resolved['variant']);
|
|
|
|
|
|
|
|
$price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
$price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
|
|
|
|
|
@@ -555,18 +604,18 @@ class GiftProcessor
|
|
|
|
|
|
|
|
foreach ($group->getItems() as $gift) {
|
|
foreach ($group->getItems() as $gift) {
|
|
|
if ($gift->getCartItemId()) {
|
|
if ($gift->getCartItemId()) {
|
|
|
- $claimed[$gift->getKey()] = (int) $gift->getCartItemId();
|
|
|
|
|
|
|
+ $claimed[(int) $gift->getCartItemId()] = true;
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
foreach ($cart->all_items as $cartItem) {
|
|
foreach ($cart->all_items as $cartItem) {
|
|
|
- $ruleId = $cartItem->additional[Gift::CART_ITEM_FLAG] ?? null;
|
|
|
|
|
-
|
|
|
|
|
- if (! $ruleId) {
|
|
|
|
|
|
|
+ if (! ($cartItem->additional[Gift::CART_ITEM_FLAG] ?? null)) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (($claimed[((int) $ruleId).':'.$cartItem->sku] ?? null) === (int) $cartItem->id) {
|
|
|
|
|
|
|
+ // 按 cart_item_id 认领:flexible_variant 配置的是父商品 sku、
|
|
|
|
|
+ // 行上是 variant sku,用 sku 拼 key 会把好行误判成孤儿
|
|
|
|
|
+ if (isset($claimed[(int) $cartItem->id])) {
|
|
|
continue;
|
|
continue;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -574,6 +623,27 @@ class GiftProcessor
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 给分组里的每个候选解析「商品 + 变体」。
|
|
|
|
|
+ *
|
|
|
|
|
+ * resolver 内部按 SKU 缓存,重复调用安全。必须在 syncGiftItems /
|
|
|
|
|
+ * autoAddGiftItems 之前跑:否则 findGiftCartItem 拿不到 variant_id,
|
|
|
|
|
+ * flexible_variant 的赠品行会被当成"不存在"而被重复加购。
|
|
|
|
|
+ */
|
|
|
|
|
+ protected function resolveGiftProducts(GiftItemsGroup $group): void
|
|
|
|
|
+ {
|
|
|
|
|
+ foreach ($group->getItems() as $gift) {
|
|
|
|
|
+ $resolved = $this->productResolver->resolve($gift->getSku());
|
|
|
|
|
+
|
|
|
|
|
+ if (! $resolved) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ $gift->setProductId((int) $resolved['product']->id)
|
|
|
|
|
+ ->setVariantId($resolved['variant']?->id);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 自动加购总开关。
|
|
* 自动加购总开关。
|
|
|
*
|
|
*
|