| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235 |
- <?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,
- protected GiftProductResolver $productResolver
- ) {}
- /**
- * @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 = [];
- $giftsByRule = [];
- foreach ($this->giftItemRepository->getByCart($cartId) as $row) {
- if ($row->allowed_qty < 1) {
- continue;
- }
- $giftRule = $this->giftRuleRepository->findByCartRuleId($row->rule_id);
- if (! $giftRule) {
- continue;
- }
- $resolved = $this->resolveRowProduct($row);
- if (! $resolved) {
- continue;
- }
- $product = $resolved['product'];
- $variant = $resolved['variant'];
- $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 = $variant->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(
- $variant ? $this->productResolver->variantPrice($variant) : (float) $product->price
- );
- $gift->allowed_qty = (float) $row->allowed_qty;
- $gift->reserved_qty = (float) $row->reserved_qty;
- // 「一组合计送 N 个」的规则下面会按整组名额修正,这里先放逐行差值
- $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;
- $giftsByRule[$ruleId][] = $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);
- /*
- * 「一组合计送 N 个」:名额是整组共享的,逐候选的 remaining_qty 也必须
- * 按组修正。否则名额已经被某个 SKU 用掉的组里,其余候选还会各自显示
- * "可领 N 件",接口就变成「已送 1 件 + 还能领 1 件」,
- * 前端展示出来就像送了 2 件。
- *
- * 已加购 / 被用户删掉的候选一律 0;剩下的名额按顺序分配给未加购的候选。
- */
- if ($group->type === Gift::TYPE_ONE) {
- $remaining = $group->remaining_qty;
- foreach ($giftsByRule[$ruleId] ?? [] as $gift) {
- if ($gift->reserved_qty > 0 || $gift->cart_item_id || $gift->is_deleted) {
- $gift->remaining_qty = 0;
- continue;
- }
- $take = min((float) $gift->allowed_qty, $remaining);
- $gift->remaining_qty = max(0, $take);
- $remaining -= $take;
- }
- }
- }
- return [array_values($groups), $gifts];
- }
- /**
- * 把 free_gift_items 的一行解析成「商品 + 变体」。
- *
- * 优先按 SKU 解析(flexible_variant 能拿到具体变体与变体价),
- * 解析不到再退回 product_id(历史数据 / 规则里 SKU 写错的情况)。
- *
- * @return array{product: object, variant: ?\Longyi\Core\Models\ProductVariant}|null
- */
- protected function resolveRowProduct($row): ?array
- {
- $resolved = $this->productResolver->resolve((string) $row->sku);
- if ($resolved) {
- return $resolved;
- }
- $product = $row->product_id ? $this->productRepository->find((int) $row->product_id) : null;
- return $product ? ['product' => $product, 'variant' => null] : null;
- }
- /**
- * 需要展示 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;
- }
- }
|