|
|
@@ -0,0 +1,572 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Longyi\FreeGift\Services;
|
|
|
+
|
|
|
+use Longyi\FreeGift\Config\Gift;
|
|
|
+use Longyi\FreeGift\Repositories\FreeGiftItemRepository;
|
|
|
+use Longyi\FreeGift\Repositories\FreeGiftRuleRepository;
|
|
|
+use Longyi\FreeGift\Services\Actions\GiftActionPool;
|
|
|
+use Webkul\Checkout\Repositories\CartItemRepository;
|
|
|
+use Webkul\Product\Repositories\ProductRepository;
|
|
|
+use Webkul\Rule\Helpers\Validator;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 赠品主流程编排。
|
|
|
+ *
|
|
|
+ * 对应 Amasty 的 Plugin\Quote\TotalsCollectorPlugin,两阶段:
|
|
|
+ * 阶段 A registerGiftItems 按规则算出「该送什么、能送几个」
|
|
|
+ * 阶段 B1 syncGiftItems 校正已经在购物车里的赠品行(超额缩、失效删)
|
|
|
+ * 阶段 B2 autoAddGiftItems 把差额补进 cart_items
|
|
|
+ *
|
|
|
+ * 全程由 checkout.cart.collect.totals.before 触发,拿到的购物车对象是
|
|
|
+ * Webkul\Checkout\Contracts\Cart(模型实例)。
|
|
|
+ *
|
|
|
+ * 重要:这里绝不调用 cart()->addProduct(),因为 addProduct() 内部会再次
|
|
|
+ * collectTotals() 导致无限递归;赠品行一律用 CartItemRepository 直写。
|
|
|
+ */
|
|
|
+class GiftProcessor
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 重入保护(等效于 Amasty 的 amastyFreeGiftProcessed 标记)。
|
|
|
+ */
|
|
|
+ protected bool $running = false;
|
|
|
+
|
|
|
+ public function __construct(
|
|
|
+ protected GiftRegistry $registry,
|
|
|
+ protected GiftRuleResolver $ruleResolver,
|
|
|
+ protected GiftActionPool $actionPool,
|
|
|
+ protected GiftDiscountCalculator $calculator,
|
|
|
+ protected GiftProductValidator $productValidator,
|
|
|
+ protected FreeGiftRuleRepository $giftRuleRepository,
|
|
|
+ protected FreeGiftItemRepository $giftItemRepository,
|
|
|
+ protected CartItemRepository $cartItemRepository,
|
|
|
+ protected ProductRepository $productRepository,
|
|
|
+ protected Validator $validator
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 入口。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ public function process($cart): void
|
|
|
+ {
|
|
|
+ if ($this->running || ! $cart || ! $cart->id) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->running = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ $cartId = (int) $cart->id;
|
|
|
+
|
|
|
+ // cart_items 才是真相:调用方(如弹窗加购 GiftController::add)在
|
|
|
+ // 写入赠品行之前可能已经加载过 items / all_items 关联,本请求内
|
|
|
+ // 后续 collectTotals() 若用这份陈旧缓存判断「购物车里有没有赠品」,
|
|
|
+ // 就会把同一个赠品再加一次(重复行)。这里强制重新加载。
|
|
|
+ $cart->unsetRelation('items')->unsetRelation('all_items');
|
|
|
+
|
|
|
+ $group = $this->registry->group($cartId);
|
|
|
+
|
|
|
+ // 1. 清空「可送数量」,交给规则重算
|
|
|
+ $group->resetQtyAllowed();
|
|
|
+
|
|
|
+ // 2. 空车:恢复被用户手动删除的赠品的自动加购资格
|
|
|
+ if (! $cart->all_items->count()) {
|
|
|
+ $this->giftItemRepository->resetDeleted($cartId);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 以购物车真实数据校准「已占用数量」
|
|
|
+ $this->registry->syncReservedQty($cart, $group);
|
|
|
+
|
|
|
+ // 4. 阶段 A
|
|
|
+ $this->registerGiftItems($cart, $group);
|
|
|
+
|
|
|
+ // 5. 阶段 B1
|
|
|
+ $this->syncGiftItems($cart, $group);
|
|
|
+
|
|
|
+ // 6. 阶段 B2
|
|
|
+ $this->autoAddGiftItems($cart, $group);
|
|
|
+
|
|
|
+ // 7. 清理孤儿赠品行(历史残留的重复行、规则下线后的残留)
|
|
|
+ $this->removeOrphanGiftRows($cart, $group);
|
|
|
+
|
|
|
+ // 8. 落库
|
|
|
+ $this->registry->persist($cartId);
|
|
|
+ } finally {
|
|
|
+ $this->running = false;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 阶段 A:把命中的规则转成赠品候选。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ protected function registerGiftItems($cart, GiftItemsGroup $group): void
|
|
|
+ {
|
|
|
+ foreach ($this->ruleResolver->getApplicableRules($cart) as $rule) {
|
|
|
+ $giftRule = $this->giftRuleRepository->findByCartRuleId($rule->id);
|
|
|
+
|
|
|
+ if (! $giftRule) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $action = $this->actionPool->get($rule->action_type);
|
|
|
+
|
|
|
+ if (! $action) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 收集「命中条件」且「本身不是赠品」的购物车行
|
|
|
+ $matchedItems = [];
|
|
|
+
|
|
|
+ foreach ($cart->all_items as $item) {
|
|
|
+ if ($item->additional[Gift::CART_ITEM_FLAG] ?? null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (! $this->validator->validate($rule, $item)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $matchedItems[] = $item;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (empty($matchedItems)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $map = $action->getGiftQtyMap($rule, $matchedItems, $giftRule, $cart);
|
|
|
+
|
|
|
+ if (empty($map)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $group->registerMap(
|
|
|
+ $map,
|
|
|
+ (int) $rule->id,
|
|
|
+ $giftRule->items_discount,
|
|
|
+ (int) $giftRule->type,
|
|
|
+ (bool) $giftRule->auto_add,
|
|
|
+ (int) $rule->sort_order,
|
|
|
+ $giftRule->minimal_items_price
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 阶段 B1:校正已经在购物车里的赠品行。
|
|
|
+ *
|
|
|
+ * 只做「减少」和「移除」,不做「增加」——增加统一交给 B2,
|
|
|
+ * 这样才能正确处理「用户手动删过就不再自动加回」。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ protected function syncGiftItems($cart, GiftItemsGroup $group): void
|
|
|
+ {
|
|
|
+ foreach ($group->getItems() as $gift) {
|
|
|
+ $cartItem = $this->findGiftCartItem($cart, $gift);
|
|
|
+
|
|
|
+ if (! $cartItem) {
|
|
|
+ $gift->setCartItemId(null)->setReservedQty(0);
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $gift->setCartItemId((int) $cartItem->id);
|
|
|
+
|
|
|
+ // 规则已不再成立 → 把赠品从购物车里摘掉
|
|
|
+ if ($gift->getAllowedQty() < 1) {
|
|
|
+ $this->cartItemRepository->delete($cartItem->id);
|
|
|
+
|
|
|
+ $gift->setCartItemId(null)->setReservedQty(0);
|
|
|
+
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 规则允许量变小 → 缩到允许量
|
|
|
+ if ($gift->getAllowedQty() < $gift->getReservedQty()) {
|
|
|
+ $qty = $gift->getAllowedQty();
|
|
|
+
|
|
|
+ $cartItem = $this->cartItemRepository->update([
|
|
|
+ 'quantity' => $qty,
|
|
|
+ 'total' => (float) $cartItem->base_price * $qty,
|
|
|
+ 'base_total' => (float) $cartItem->base_price * $qty,
|
|
|
+ 'additional' => array_merge($cartItem->additional ?? [], ['quantity' => $qty]),
|
|
|
+ ], $cartItem->id);
|
|
|
+
|
|
|
+ $gift->setReservedQty($qty);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 校正价格:collectTotals() 开头的 validateItems() 会按商品现价
|
|
|
+ // 重写行价格,赠品的折后价会被冲成原价,必须在同一轮写回
|
|
|
+ $this->applyGiftPrice($cartItem, $gift);
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->shrinkOneOfGroups($cart, $group);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 「一组合计送 N 个」整组超发收缩。
|
|
|
+ *
|
|
|
+ * 手动加购等路径曾绕过整组名额(如名额 1 个却躺了两行),这里按
|
|
|
+ * sortOrder 优先级收缩:保住排序靠前的行,裁掉多余的行,实现自愈。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ protected function shrinkOneOfGroups($cart, GiftItemsGroup $group): void
|
|
|
+ {
|
|
|
+ foreach ($group->getOneOfRuleIds() as $ruleId) {
|
|
|
+ $cap = $group->getCapForOneOf($ruleId);
|
|
|
+
|
|
|
+ $items = $group->getItemsByRule($ruleId);
|
|
|
+
|
|
|
+ $used = 0;
|
|
|
+
|
|
|
+ foreach ($items as $gift) {
|
|
|
+ $used += $gift->getReservedQty();
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($used <= max(0, $cap)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序靠后的先缩
|
|
|
+ usort($items, fn (GiftItem $a, GiftItem $b) => $b->getSortOrder() <=> $a->getSortOrder());
|
|
|
+
|
|
|
+ foreach ($items as $gift) {
|
|
|
+ if ($used <= $cap) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+
|
|
|
+ $excess = min($gift->getReservedQty(), $used - $cap);
|
|
|
+
|
|
|
+ if ($excess < 1) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $cartItem = $this->findGiftCartItem($cart, $gift);
|
|
|
+
|
|
|
+ if (! $cartItem) {
|
|
|
+ $gift->setReservedQty(max(0, $gift->getReservedQty() - $excess));
|
|
|
+ } elseif ((float) $cartItem->quantity - $excess < 1) {
|
|
|
+ $this->cartItemRepository->delete($cartItem->id);
|
|
|
+
|
|
|
+ $gift->setCartItemId(null)->setReservedQty(0);
|
|
|
+ } else {
|
|
|
+ $qty = (float) $cartItem->quantity - $excess;
|
|
|
+
|
|
|
+ $this->cartItemRepository->update([
|
|
|
+ 'quantity' => $qty,
|
|
|
+ 'total' => (float) $cartItem->base_price * $qty,
|
|
|
+ 'base_total' => (float) $cartItem->base_price * $qty,
|
|
|
+ 'additional' => array_merge($cartItem->additional ?? [], ['quantity' => $qty]),
|
|
|
+ ], $cartItem->id);
|
|
|
+
|
|
|
+ $gift->setReservedQty($qty);
|
|
|
+ }
|
|
|
+
|
|
|
+ $used -= $excess;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 阶段 B2:把短缺的赠品补进购物车。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ protected function autoAddGiftItems($cart, GiftItemsGroup $group): void
|
|
|
+ {
|
|
|
+ if (! $this->isAutoAddEnabled()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($group->getItemsForAutoAdd() as $gift) {
|
|
|
+ $qty = $gift->getProcessQty();
|
|
|
+
|
|
|
+ if ($qty < 1) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $product = $this->productRepository->findOneWhere(['sku' => $gift->getSku()]);
|
|
|
+
|
|
|
+ if (! $this->productValidator->isEligible($product, $qty)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $gift->setProductId((int) $product->id);
|
|
|
+
|
|
|
+ $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $gift->getCartItemId());
|
|
|
+
|
|
|
+ $gift->setCartItemId((int) $cartItem->id)
|
|
|
+ ->setReservedQty($qty);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 手动把某个赠品加进购物车(弹窗选择 / 手动模式)。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ * @param string $sku
|
|
|
+ * @param int $ruleId
|
|
|
+ * @param float $qty
|
|
|
+ * @return array{success: bool, message: string, cart_item_id: int|null}
|
|
|
+ */
|
|
|
+ public function addGiftToCart($cart, string $sku, int $ruleId, float $qty = 1): array
|
|
|
+ {
|
|
|
+ if (! $cart || ! $cart->id) {
|
|
|
+ return ['success' => false, 'message' => trans('free_gift::app.cart.gift-rule-inactive'), 'cart_item_id' => null];
|
|
|
+ }
|
|
|
+
|
|
|
+ $row = $this->giftItemRepository->findOne((int) $cart->id, $sku, $ruleId);
|
|
|
+
|
|
|
+ if (! $row) {
|
|
|
+ return ['success' => false, 'message' => trans('free_gift::app.cart.gift-rule-inactive'), 'cart_item_id' => null];
|
|
|
+ }
|
|
|
+
|
|
|
+ $remaining = (float) $row->allowed_qty - (float) $row->reserved_qty;
|
|
|
+
|
|
|
+ // 「一组合计送 N 个」:名额整组共享,必须按组重算剩余,
|
|
|
+ // 否则每个 SKU 各自"还剩 1 个"时能把整组超发
|
|
|
+ if ((int) $row->rule_type === Gift::TYPE_ONE) {
|
|
|
+ $rows = $this->giftItemRepository->getByCart((int) $cart->id)
|
|
|
+ ->where('rule_id', $ruleId);
|
|
|
+
|
|
|
+ $cap = (float) $rows->max('allowed_qty');
|
|
|
+ $used = (float) $rows->sum('reserved_qty');
|
|
|
+
|
|
|
+ $remaining = max(0, $cap - $used);
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($remaining < 1) {
|
|
|
+ return ['success' => false, 'message' => trans('free_gift::app.cart.gift-unavailable'), 'cart_item_id' => null];
|
|
|
+ }
|
|
|
+
|
|
|
+ $qty = min($qty, $remaining);
|
|
|
+
|
|
|
+ $product = $this->productRepository->findOneWhere(['sku' => $sku]);
|
|
|
+
|
|
|
+ if (! $this->productValidator->isEligible($product, $qty)) {
|
|
|
+ return ['success' => false, 'message' => trans('free_gift::app.cart.gift-unavailable'), 'cart_item_id' => null];
|
|
|
+ }
|
|
|
+
|
|
|
+ $gift = new GiftItem(
|
|
|
+ $sku,
|
|
|
+ $ruleId,
|
|
|
+ $row->discount_amount,
|
|
|
+ (int) $row->rule_type,
|
|
|
+ (bool) $row->auto_add
|
|
|
+ );
|
|
|
+
|
|
|
+ $giftRule = $this->giftRuleRepository->findByCartRuleId($ruleId);
|
|
|
+
|
|
|
+ $gift->setProductId((int) $product->id)
|
|
|
+ ->setMinimalPrice($giftRule?->minimal_items_price);
|
|
|
+
|
|
|
+ $existingId = $this->findGiftCartItem($cart, $gift)?->id ?: $row->cart_item_id;
|
|
|
+
|
|
|
+ $cartItem = $this->writeGiftCartItem($cart, $product, $gift, $qty, $existingId);
|
|
|
+
|
|
|
+ $this->giftItemRepository->update([
|
|
|
+ 'cart_item_id' => $cartItem->id,
|
|
|
+ 'reserved_qty' => $qty,
|
|
|
+ 'is_item_deleted' => 0,
|
|
|
+ ], $row->id);
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'success' => true,
|
|
|
+ 'message' => trans('free_gift::app.cart.gift-added'),
|
|
|
+ 'cart_item_id' => (int) $cartItem->id,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 写入/更新一条赠品购物车行。
|
|
|
+ *
|
|
|
+ * 这里绝不走 cart()->addProduct(),否则 addProduct() 内部的 collectTotals()
|
|
|
+ * 会再次触发本监听器,形成无限递归。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ * @param \Webkul\Product\Contracts\Product $product
|
|
|
+ */
|
|
|
+ protected function writeGiftCartItem($cart, $product, GiftItem $gift, float $qty, $cartItemId = null)
|
|
|
+ {
|
|
|
+ $originalPrice = $this->getOriginalPrice($product, $qty);
|
|
|
+
|
|
|
+ // 赠品实付单价:原价 - 折扣,并以 minimal_items_price 兜底
|
|
|
+ $price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
|
+
|
|
|
+ $weight = (float) ($product->weight ?? 0);
|
|
|
+
|
|
|
+ $payload = [
|
|
|
+ 'quantity' => $qty,
|
|
|
+ 'price' => $price,
|
|
|
+ 'base_price' => $price,
|
|
|
+ 'price_incl_tax' => $price,
|
|
|
+ 'base_price_incl_tax' => $price,
|
|
|
+ 'total' => $price * $qty,
|
|
|
+ 'base_total' => $price * $qty,
|
|
|
+ 'total_incl_tax' => $price * $qty,
|
|
|
+ 'base_total_incl_tax' => $price * $qty,
|
|
|
+ 'tax_percent' => 0,
|
|
|
+ 'tax_amount' => 0,
|
|
|
+ 'base_tax_amount' => 0,
|
|
|
+ 'discount_percent' => 0,
|
|
|
+ 'discount_amount' => 0,
|
|
|
+ 'base_discount_amount' => 0,
|
|
|
+ 'weight' => $weight,
|
|
|
+ '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,
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+
|
|
|
+ if ($cartItemId) {
|
|
|
+ $updated = $this->cartItemRepository->update($payload, $cartItemId);
|
|
|
+
|
|
|
+ // 指针可能悬空(赠品行被删过 / 购物车被重建),退回新建
|
|
|
+ if ($updated && (int) $updated->cart_id === (int) $cart->id) {
|
|
|
+ return $updated;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $this->cartItemRepository->create(array_merge($payload, [
|
|
|
+ 'cart_id' => $cart->id,
|
|
|
+ 'product_id' => $product->id,
|
|
|
+ 'sku' => $product->sku,
|
|
|
+ 'type' => $product->type,
|
|
|
+ 'name' => $product->name,
|
|
|
+ ]));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 在购物车里找到某条赠品候选对应的购物车行。
|
|
|
+ *
|
|
|
+ * 同一个 SKU 被两条规则赠送时会拆成两行,所以必须用
|
|
|
+ * 「规则 ID + SKU」两个条件一起找。
|
|
|
+ *
|
|
|
+ * @param \Webkul\Checkout\Contracts\Cart $cart
|
|
|
+ */
|
|
|
+ protected function findGiftCartItem($cart, GiftItem $gift)
|
|
|
+ {
|
|
|
+ foreach ($cart->all_items as $item) {
|
|
|
+ if ((int) ($item->additional[Gift::CART_ITEM_FLAG] ?? 0) !== $gift->getRuleId()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ($item->sku !== $gift->getSku()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $item;
|
|
|
+ }
|
|
|
+
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 赠品行的折后单价基准(商品现价)。
|
|
|
+ *
|
|
|
+ * 与 Webkul 的 validateCartItem 用同一口径:取含特价、目录促销的最终价,
|
|
|
+ * 而不是 catalog 里的标价,保证「赠品价 = 现价 × 折扣」。
|
|
|
+ */
|
|
|
+ protected function getOriginalPrice($product, float $qty): float
|
|
|
+ {
|
|
|
+ try {
|
|
|
+ $finalPrice = (float) $product->getTypeInstance()->getFinalPrice($qty);
|
|
|
+ } catch (\Throwable) {
|
|
|
+ $finalPrice = 0;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $finalPrice > 0 ? $finalPrice : (float) $product->price;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 把赠品行的价格校正回赠品价。
|
|
|
+ *
|
|
|
+ * Webkul\Checkout\Cart::collectTotals() 一进来就跑 validateItems(),里面的
|
|
|
+ * Simple::validateCartItem() 发现「商品现价 != 行价格」就会把行价格改回
|
|
|
+ * 商品现价 —— 赠品的折后价(0 元 / 最低价)每轮都被冲成原价,到结算页露馅。
|
|
|
+ * 本监听器在该重写之后触发,这里按规则把价格写回,购物车和结算页就一致了。
|
|
|
+ */
|
|
|
+ protected function applyGiftPrice($cartItem, GiftItem $gift): void
|
|
|
+ {
|
|
|
+ $product = $this->productRepository->findOneWhere(['sku' => $gift->getSku()]);
|
|
|
+
|
|
|
+ if (! $product) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $originalPrice = $this->getOriginalPrice($product, (float) $cartItem->quantity);
|
|
|
+
|
|
|
+ $price = $this->calculator->priceFor($gift->getDiscount(), $originalPrice, $gift->getMinimalPrice());
|
|
|
+
|
|
|
+ if ((float) $cartItem->base_price === (float) $price) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ $qty = (float) $cartItem->quantity;
|
|
|
+
|
|
|
+ $this->cartItemRepository->update([
|
|
|
+ 'price' => $price,
|
|
|
+ 'price_incl_tax' => $price,
|
|
|
+ 'base_price' => $price,
|
|
|
+ 'base_price_incl_tax' => $price,
|
|
|
+ 'total' => $price * $qty,
|
|
|
+ 'total_incl_tax' => $price * $qty,
|
|
|
+ 'base_total' => $price * $qty,
|
|
|
+ 'base_total_incl_tax' => $price * $qty,
|
|
|
+ ], $cartItem->id);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理「孤儿赠品行」。
|
|
|
+ *
|
|
|
+ * cart_items 里带赠品标记的行,必须能对上注册表里「规则 + SKU」的指针行;
|
|
|
+ * 对不上的都是残留(陈旧关联/并发导致的重复加购、规则下线等),直接删掉,
|
|
|
+ * 否则购物车里会出现两行一模一样的赠品。
|
|
|
+ */
|
|
|
+ protected function removeOrphanGiftRows($cart, GiftItemsGroup $group): void
|
|
|
+ {
|
|
|
+ $claimed = [];
|
|
|
+
|
|
|
+ foreach ($group->getItems() as $gift) {
|
|
|
+ if ($gift->getCartItemId()) {
|
|
|
+ $claimed[$gift->getKey()] = (int) $gift->getCartItemId();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($cart->all_items as $cartItem) {
|
|
|
+ $ruleId = $cartItem->additional[Gift::CART_ITEM_FLAG] ?? null;
|
|
|
+
|
|
|
+ if (! $ruleId) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (($claimed[((int) $ruleId).':'.$cartItem->sku] ?? null) === (int) $cartItem->id) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $this->cartItemRepository->delete($cartItem->id);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 自动加购总开关。
|
|
|
+ *
|
|
|
+ * 后台配置的是「禁用」开关,所以缺省(配置从未保存)时视为开启,
|
|
|
+ * 避免商家没进过后台设置就把自动加购关掉了。
|
|
|
+ */
|
|
|
+ protected function isAutoAddEnabled(): bool
|
|
|
+ {
|
|
|
+ return ! (bool) core()->getConfigData('sales.free_gift.general.disable_auto_add');
|
|
|
+ }
|
|
|
+}
|