|
|
@@ -0,0 +1,180 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace Longyi\FreeGift\Services;
|
|
|
+
|
|
|
+use Longyi\FreeGift\ApiResource\GiftBannerData;
|
|
|
+use Longyi\FreeGift\ApiResource\GiftGroupData;
|
|
|
+use Longyi\FreeGift\ApiResource\GiftProductData;
|
|
|
+use Longyi\FreeGift\Config\Gift;
|
|
|
+use Longyi\FreeGift\Repositories\FreeGiftItemRepository;
|
|
|
+use Longyi\FreeGift\Repositories\FreeGiftRuleRepository;
|
|
|
+use Webkul\Product\ProductImage;
|
|
|
+use Webkul\Product\Repositories\ProductRepository;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 购物车的赠品候选快照(规则名额 + 每个赠品是否已加购)。
|
|
|
+ *
|
|
|
+ * 只读 free_gift_items / cart_items 的现状,不触发重算 —— 调用前购物车
|
|
|
+ * 应已经过 collectTotals()(readCart / 加购 / 更新数量内部都会跑)。
|
|
|
+ *
|
|
|
+ * 被两处复用:
|
|
|
+ * - GraphQL 的 giftCandidates / giftProducts / giftBanners(GiftCartProvider)
|
|
|
+ * - 购物车接口本身的 CartData::freeGifts(直接内嵌,省一次请求)
|
|
|
+ */
|
|
|
+class GiftCartSummary
|
|
|
+{
|
|
|
+ public function __construct(
|
|
|
+ protected FreeGiftItemRepository $giftItemRepository,
|
|
|
+ protected FreeGiftRuleRepository $giftRuleRepository,
|
|
|
+ protected ProductRepository $productRepository,
|
|
|
+ protected ProductImage $productImage
|
|
|
+ ) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * @return array{0: GiftGroupData[], 1: GiftProductData[], 2: GiftBannerData[]}
|
|
|
+ */
|
|
|
+ public function forCart(int $cartId): array
|
|
|
+ {
|
|
|
+ [$groups, $gifts] = $this->buildGroups($cartId);
|
|
|
+
|
|
|
+ return [$groups, $gifts, $this->buildBanners($groups)];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 只取赠品候选列表(购物车接口内嵌用)。
|
|
|
+ *
|
|
|
+ * @return GiftProductData[]
|
|
|
+ */
|
|
|
+ public function gifts(int $cartId): array
|
|
|
+ {
|
|
|
+ return $this->buildGroups($cartId)[1];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 按规则聚合:groups 只放名额信息,gifts 是全部候选(带 rule_id / cart_item_id)。
|
|
|
+ *
|
|
|
+ * @return array{0: GiftGroupData[], 1: GiftProductData[]}
|
|
|
+ */
|
|
|
+ protected function buildGroups(int $cartId): array
|
|
|
+ {
|
|
|
+ $groups = [];
|
|
|
+
|
|
|
+ $gifts = [];
|
|
|
+
|
|
|
+ $allowedByRule = [];
|
|
|
+ $reservedByRule = [];
|
|
|
+
|
|
|
+ foreach ($this->giftItemRepository->getByCart($cartId) as $row) {
|
|
|
+ if ($row->allowed_qty < 1) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $giftRule = $this->giftRuleRepository->findByCartRuleId($row->rule_id);
|
|
|
+
|
|
|
+ if (! $giftRule) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $product = $row->product_id ? $this->productRepository->find($row->product_id) : null;
|
|
|
+
|
|
|
+ if (! $product) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $ruleId = (int) $row->rule_id;
|
|
|
+ $ruleName = $giftRule->cart_rule?->name;
|
|
|
+
|
|
|
+ if (! isset($groups[$ruleId])) {
|
|
|
+ $group = new GiftGroupData;
|
|
|
+
|
|
|
+ $group->id = (string) $ruleId;
|
|
|
+ $group->rule_id = $ruleId;
|
|
|
+ $group->rule_name = $ruleName;
|
|
|
+ $group->type = (int) $row->rule_type;
|
|
|
+ $group->auto_add = (bool) $row->auto_add;
|
|
|
+ $group->allowed_qty = 0;
|
|
|
+ $group->reserved_qty = 0;
|
|
|
+ $group->remaining_qty = 0;
|
|
|
+
|
|
|
+ $groups[$ruleId] = $group;
|
|
|
+
|
|
|
+ $allowedByRule[$ruleId] = [];
|
|
|
+ $reservedByRule[$ruleId] = 0.0;
|
|
|
+ }
|
|
|
+
|
|
|
+ $allowedByRule[$ruleId][] = (float) $row->allowed_qty;
|
|
|
+ $reservedByRule[$ruleId] += (float) $row->reserved_qty;
|
|
|
+
|
|
|
+ $image = $this->productImage->getProductBaseImage($product);
|
|
|
+
|
|
|
+ $gift = new GiftProductData;
|
|
|
+
|
|
|
+ $gift->id = $ruleId.':'.$row->sku;
|
|
|
+ $gift->sku = $row->sku;
|
|
|
+ $gift->rule_id = $ruleId;
|
|
|
+ $gift->rule_name = $ruleName;
|
|
|
+ $gift->product_id = (int) $product->id;
|
|
|
+ $gift->name = $product->name;
|
|
|
+ $gift->url = route('shop.product_or_category.index', $product->url_key);
|
|
|
+ $gift->image = $image['medium_image_url'] ?? null;
|
|
|
+ $gift->price = core()->formatPrice((float) $product->price);
|
|
|
+ $gift->allowed_qty = (float) $row->allowed_qty;
|
|
|
+ $gift->reserved_qty = (float) $row->reserved_qty;
|
|
|
+ $gift->remaining_qty = max(0, (float) $row->allowed_qty - (float) $row->reserved_qty);
|
|
|
+ $gift->cart_item_id = $row->cart_item_id ? (int) $row->cart_item_id : null;
|
|
|
+ $gift->is_deleted = (bool) $row->is_item_deleted;
|
|
|
+
|
|
|
+ $gifts[] = $gift;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($groups as $ruleId => $group) {
|
|
|
+ $allowed = $allowedByRule[$ruleId] ?: [0.0];
|
|
|
+
|
|
|
+ // 「一组合计送 N 个」:整组共用 N 个名额,N 取单条的允许量而不是求和
|
|
|
+ $group->allowed_qty = $group->type === Gift::TYPE_ONE
|
|
|
+ ? (float) max($allowed)
|
|
|
+ : (float) array_sum($allowed);
|
|
|
+
|
|
|
+ $group->reserved_qty = (float) ($reservedByRule[$ruleId] ?? 0);
|
|
|
+ $group->remaining_qty = max(0, $group->allowed_qty - $group->reserved_qty);
|
|
|
+ }
|
|
|
+
|
|
|
+ return [array_values($groups), $gifts];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 需要展示 Banner 的规则(受总开关 + 规则自身 show_banner 控制)。
|
|
|
+ *
|
|
|
+ * @param GiftGroupData[] $groups
|
|
|
+ * @return GiftBannerData[]
|
|
|
+ */
|
|
|
+ protected function buildBanners(array $groups): array
|
|
|
+ {
|
|
|
+ $banners = [];
|
|
|
+
|
|
|
+ if (! core()->getConfigData('sales.free_gift.general.show_banner')) {
|
|
|
+ return $banners;
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($groups as $group) {
|
|
|
+ $giftRule = $this->giftRuleRepository->findByCartRuleId($group->rule_id);
|
|
|
+
|
|
|
+ if (! $giftRule || ! $giftRule->show_banner) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+
|
|
|
+ $banner = new GiftBannerData;
|
|
|
+
|
|
|
+ $banner->id = (string) $group->rule_id;
|
|
|
+ $banner->rule_id = $group->rule_id;
|
|
|
+ $banner->text = $giftRule->banner_alt ?: trans('free_gift::app.cart.banner-default');
|
|
|
+ $banner->image = $giftRule->bannerImageUrl();
|
|
|
+ $banner->allowed_qty = $group->allowed_qty;
|
|
|
+ $banner->remaining_qty = $group->remaining_qty;
|
|
|
+
|
|
|
+ $banners[] = $banner;
|
|
|
+ }
|
|
|
+
|
|
|
+ return $banners;
|
|
|
+ }
|
|
|
+}
|