|
|
@@ -185,7 +185,15 @@ class GiftProcessor
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
- $gift->setCartItemId((int) $cartItem->id);
|
|
|
+ /*
|
|
|
+ * 行才是真相:认领到的行必须立刻同步 reservedQty。
|
|
|
+ *
|
|
|
+ * syncReservedQty() 在本轮更早的位置已经跑过,那时这些候选可能还没
|
|
|
+ * 注册进来(规则刚改 SKU / 状态行缺失 / 上一轮 persist 没落库),
|
|
|
+ * reservedQty 还是 0。不同步的话 B2 会认为「还差 1 个」,又拿这个
|
|
|
+ * cartItemId 去累加,最终出现「后台设 1 件、购物车 2 件」。
|
|
|
+ */
|
|
|
+ $gift->setCartItemId((int) $cartItem->id)->setReservedQty((float) $cartItem->quantity);
|
|
|
|
|
|
// 规则已不再成立 → 把赠品从购物车里摘掉
|
|
|
if ($gift->getAllowedQty() < 1) {
|
|
|
@@ -391,7 +399,12 @@ class GiftProcessor
|
|
|
|
|
|
$gift->setProductId((int) $product->id)
|
|
|
->setVariantId($variant?->id)
|
|
|
- ->setMinimalPrice($giftRule?->minimal_items_price);
|
|
|
+ ->setMinimalPrice($giftRule?->minimal_items_price)
|
|
|
+ /*
|
|
|
+ * 带上名额:writeGiftCartItem() 的「单行数量不超过允许量」防线
|
|
|
+ * 依赖它(手动领取是另一个入口,同样不能超过名额)。
|
|
|
+ */
|
|
|
+ ->setAllowedQty((float) $row->allowed_qty);
|
|
|
|
|
|
$existingId = $this->findGiftCartItem($cart, $gift)?->id ?: $row->cart_item_id;
|
|
|
|
|
|
@@ -434,6 +447,20 @@ class GiftProcessor
|
|
|
// 指针悬空(赠品行被删过 / 购物车被重建)→ 退回新建
|
|
|
if ($existing && (int) $existing->cart_id === (int) $cart->id) {
|
|
|
$qty = (float) $existing->quantity + $qty;
|
|
|
+
|
|
|
+ /*
|
|
|
+ * 封顶:一行赠品的数量不允许超过该候选的允许量。
|
|
|
+ *
|
|
|
+ * 自动加购和手动领取是两个入口,若某一轮认领失败 / 两次请求并发
|
|
|
+ * 拿到了过期的 reserved_qty,就会在同一行上累加出「名额 1 却 2 件」。
|
|
|
+ * 这里按 allowed_qty 截断,作为最后一道防线(allowed < 1 时跳过,
|
|
|
+ * 例如手动领取场景候选没带名额)。
|
|
|
+ */
|
|
|
+ $allowed = (float) $gift->getAllowedQty();
|
|
|
+
|
|
|
+ if ($allowed >= 1) {
|
|
|
+ $qty = min($qty, $allowed);
|
|
|
+ }
|
|
|
} else {
|
|
|
$cartItemId = null;
|
|
|
}
|