You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何为超市结账系统设计跨商品捆绑折扣功能?

超市结账系统捆绑折扣实现方案

问题描述

我正在开发一个支持多种单品折扣的超市结账系统,目前已实现两类单品折扣:

  • 买x赠y(Buy x amount of an item to get y free)
  • 买x件付y元(Buy x amount of an item for $y)

现在需要新增**「购买商品X和商品Y共付Z元」**的捆绑折扣,但当前系统的折扣计算仅针对单个BasketItem,无法适配多商品场景。试过新建名为“X和Y”的捆绑商品,但不符合单品单独扫码的要求,求合理实现建议。


当前系统代码实现

Item类

import java.math.BigDecimal;

public class Item {

    private final String sku;
    private final BigDecimal unitPrice;

    public Item(String sku, BigDecimal unitPrice) {
        this.sku = sku;
        this.unitPrice = unitPrice;
    }

    public BigDecimal getUnitPrice() {
        return unitPrice;
    }
}

BasketItem类

import java.math.BigDecimal;

public class BasketItem {

    private final Item item;
    private int quantity;
    private final Discount discount;

    public BasketItem(Item item, Discount discount) {
        this.item = item;
        this.discount = discount;
        this.quantity = 1;
    }

    public BasketItem(Item item, Discount discount, int quantity) {
        this.item = item;
        this.discount = discount;
        this.quantity = quantity;
    }

    public void increment() {
        this.quantity++;
    }

    public BigDecimal calculateSubTotal() {
        return item.getUnitPrice().multiply(new BigDecimal(quantity));
    }

    public BigDecimal calculateDiscount() {
        return discount.apply(this);
    }

    public Item getItem() {
        return item;
    }

    public int getQuantity() {
        return quantity;
    }
}

Discount接口

import java.math.BigDecimal;

public interface Discount {
    BigDecimal apply(BasketItem item);
}

买x赠y折扣类

import java.math.BigDecimal;
import java.security.InvalidParameterException;

public class BuyXGetYFree implements Discount {

    private final int buy;
    private final int free;

    public BuyXGetYFree(int buy, int free) {
        if (buy < 1) {
            throw new InvalidParameterException("Can't buy less than 1 item");
        }

        if (free < 1) {
            throw new InvalidParameterException("Can't free less than 1 item for free");
        }

        if (free > buy) {
            throw new InvalidParameterException("The free amount must not exceed the purchased amount");
        }

        this.buy = buy;
        this.free = free;
    }

    @Override
    public BigDecimal apply(BasketItem basketItem) {
        int quantity = basketItem.getQuantity();
        BigDecimal unitPrice = basketItem.getItem().getUnitPrice();

        if (buy + free > quantity && quantity > buy) {
            return unitPrice.multiply(BigDecimal.valueOf(quantity - buy));
        }

        int multiplier = quantity / (buy + free);
        int forFree = multiplier * free;
        return unitPrice.multiply(BigDecimal.valueOf(forFree));
    }
}

无折扣类

import java.math.BigDecimal;

public class NoDiscount implements Discount {
    @Override
    public BigDecimal apply(BasketItem item) {
        return BigDecimal.ZERO;
    }
}

买x件付y元折扣类

import java.math.BigDecimal;
import java.security.InvalidParameterException;

public class BuyXForY implements Discount{

    int buy;
    BigDecimal pay;

    public BuyXForY(int buy, BigDecimal pay) {
        if (buy < 1) {
            throw new InvalidParameterException("Buy must be at least 1");
        }
        this.buy = buy;
        this.pay = pay;
    }

    @Override
    public BigDecimal apply(BasketItem item) {
        int quantity = item.getQuantity();

        if (quantity < buy) {
            return BigDecimal.ZERO;
        }

        int multiplier = quantity / buy;
        int remainder = quantity % buy;

        BigDecimal buyAtDiscountedPrice = new BigDecimal(multiplier).multiply(pay);
        BigDecimal buyAtNormalPrice = new BigDecimal(multiplier).multiply(new BigDecimal(remainder));
        BigDecimal normalPrice = new BigDecimal(quantity).multiply(item.getItem().getUnitPrice());

        return normalPrice.subtract(buyAtDiscountedPrice.add(buyAtNormalPrice));
    }
}

折扣分配类

public class DiscountAssignment {

    private final Discount discount;
    private final Item item;

    public DiscountAssignment(Discount discount, Item item) {
        this.discount = discount;
        this.item = item;
    }

    public Discount getDiscount() {
        return discount;
    }

    public Item getItem() {
        return item;
    }
}

结账类

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Checkout {

    private final List<BasketItem> basketItems;
    private final List<DiscountAssignment> discountAssignments;

    public Checkout(List<DiscountAssignment> discountAssignments) {
        this.basketItems = new ArrayList<>();
        this.discountAssignments = discountAssignments;
    }

    public void scan(Item item) {
        Optional<BasketItem> basketItemOptional = basketItems.stream().filter(basketItem -> basketItem.getItem().equals(item)).findFirst();

        if (basketItemOptional.isPresent()) {
            basketItemOptional.get().increment();
            return;
        }

        Optional<DiscountAssignment> discountsToApply = discountAssignments.stream().filter(discountAssignment -> discountAssignment.getItem().equals(item)).findFirst();

        if (discountsToApply.isPresent()) {
            basketItems.add(new BasketItem(item, discountsToApply.get().getDiscount()));
        } else {
            basketItems.add(new BasketItem(item, new NoDiscount()));
        }
    }

    public BigDecimal total() {
        System.out.println(basketItems);

        return basketItems.stream()
                .map(BasketItem::calculateSubTotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add)
                .subtract(basketItems.stream()
                        .map(BasketItem::calculateDiscount)
                        .reduce(BigDecimal.ZERO, BigDecimal::add));
    }
}

测试代码

import java.math.BigDecimal;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        Item drink = new Item("Drink", new BigDecimal(1));
        Item candy = new Item("Candy", new BigDecimal(1));

        DiscountAssignment buyOneGetOneFreeDiscount = new DiscountAssignment(new BuyXGetYFree(1, 1), drink);
        DiscountAssignment buyTwoForOneDollar = new DiscountAssignment(new BuyXForY(2, BigDecimal.ONE), candy);

        Checkout checkout = new Checkout(List.of(buyOneGetOneFreeDiscount, buyTwoForOneDollar));
        checkout.scan(drink);
        checkout.scan(drink);
        checkout.scan(drink);
        checkout.scan(drink);
        checkout.scan(candy);
        checkout.scan(candy);

        System.out.println("Total cost: " + checkout.total()); // 输出4
    }
}

捆绑折扣实现方案

核心思路

把折扣计算逻辑从单个商品级别升级到购物篮全局级别,新增「全局折扣」类型,让这类折扣能访问整个购物篮的商品数据,从而计算多商品捆绑的优惠金额。

具体改造步骤

  1. 拆分折扣接口:把原Discount拆分为ItemDiscount(单品折扣)和GlobalDiscount(全局/捆绑折扣),明确职责。
  2. 扩展结账类:在Checkout中新增全局折扣列表,计算总价时先算单品折扣,再叠加全局捆绑折扣。
  3. 实现捆绑折扣类:编写BuyXYForZ类,根据购物篮中目标商品的数量计算可享受的捆绑次数,进而算出折扣金额。

改造后代码

拆分折扣接口

import java.math.BigDecimal;
import java.util.List;

// 单品折扣接口,保留原有单商品计算逻辑
public interface ItemDiscount {
    BigDecimal apply(BasketItem item);
}

// 全局折扣接口,用于多商品捆绑计算
public interface GlobalDiscount {
    BigDecimal apply(List<BasketItem> basketItems);
}

改造原有折扣类

将所有单品折扣类改为实现ItemDiscount,以BuyXGetYFree为例:

import java.math.BigDecimal;
import java.security.InvalidParameterException;

public class BuyXGetYFree implements ItemDiscount {

    private final int buy;
    private final int free;

    public BuyXGetYFree(int buy, int free) {
        if (buy < 1) {
            throw new InvalidParameterException("Can't buy less than 1 item");
        }

        if (free < 1) {
            throw new InvalidParameterException("Can't free less than 1 item for free");
        }

        if (free > buy) {
            throw new InvalidParameterException("The free amount must not exceed the purchased amount");
        }

        this.buy = buy;
        this.free = free;
    }

    @Override
    public BigDecimal apply(BasketItem basketItem) {
        int quantity = basketItem.getQuantity();
        BigDecimal unitPrice = basketItem.getItem().getUnitPrice();

        if (buy + free > quantity && quantity > buy) {
            return unitPrice.multiply(BigDecimal.valueOf(quantity - buy));
        }

        int multiplier = quantity / (buy + free);
        int forFree = multiplier * free;
        return unitPrice.multiply(BigDecimal.valueOf(forFree));
    }
}

BuyXForY、NoDiscount类同理修改为实现ItemDiscount。

改造折扣分配类

public class ItemDiscountAssignment {

    private final ItemDiscount discount;
    private final Item item;

    public ItemDiscountAssignment(ItemDiscount discount, Item item) {
        this.discount = discount;
        this.item = item;
    }

    public ItemDiscount getDiscount() {
        return discount;
    }

    public Item getItem() {
        return item;
    }
}

实现捆绑折扣类

import java.math.BigDecimal;
import java.util.List;
import java.util.Optional;

public class BuyXYForZ implements GlobalDiscount {
    private final Item itemX;
    private final Item itemY;
    private final BigDecimal bundlePrice;

    public BuyXYForZ(Item itemX, Item itemY, BigDecimal bundlePrice) {
        this.itemX = itemX;
        this.itemY = itemY;
        this.bundlePrice = bundlePrice;
    }

    @Override
    public BigDecimal apply(List<BasketItem> basketItems) {
        // 获取购物篮中两个商品的数量
        int countX = getQuantityOfItem(basketItems, itemX);
        int countY = getQuantityOfItem(basketItems, itemY);

        // 可组成的捆绑组数取两者最小值
        int bundleCount = Math.min(countX, countY);
        if (bundleCount == 0) {
            return BigDecimal.ZERO;
        }

        // 计算原价总和与捆绑价总和的差值,即为折扣金额
        BigDecimal originalTotal = itemX.getUnitPrice().add(itemY.getUnitPrice()).multiply(BigDecimal.valueOf(bundleCount));
        BigDecimal bundleTotal = bundlePrice.multiply(BigDecimal.valueOf(bundleCount));

        return originalTotal.subtract(bundleTotal);
    }

    private int getQuantityOfItem(List<BasketItem> basketItems, Item targetItem) {
        Optional<BasketItem> itemOpt = basketItems.stream()
                .filter(bi -> bi.getItem().equals(targetItem))
                .findFirst();
        return itemOpt.map(BasketItem::getQuantity).orElse(0);
    }
}

改造结账类

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class Checkout {

    private final List<BasketItem> basketItems;
    private final List<ItemDiscountAssignment> itemDiscountAssignments;
    private final List<GlobalDiscount> globalDiscounts;

    // 支持同时传入单品折扣和全局捆绑折扣
    public Checkout(List<ItemDiscountAssignment> itemDiscountAssignments, List<GlobalDiscount> globalDiscounts) {
        this.basketItems = new ArrayList<>();
        this.itemDiscountAssignments = itemDiscountAssignments;
        this.globalDiscounts = globalDiscounts;
    }

    // 兼容原有仅单品折扣的场景
    public Checkout(List<ItemDiscountAssignment> itemDiscountAssignments) {
        this(itemDiscountAssignments, new ArrayList<>());
    }

    public void scan(Item item) {
        Optional<BasketItem> basketItemOptional = basketItems.stream().filter(basketItem -> basketItem.getItem().equals(item)).findFirst();

        if (basketItemOptional.isPresent()) {
            basketItemOptional.get().increment();
            return;
        }

        Optional<ItemDiscountAssignment> discountsToApply = itemDiscountAssignments.stream().filter(discountAssignment -> discountAssignment.getItem().equals(item)).findFirst();

        if (discountsToApply.isPresent()) {
            basketItems.add(new BasketItem(item, discountsToApply.get().getDiscount()));
        } else {
            basketItems.add(new BasketItem(item, new NoDiscount()));
        }
    }

    public BigDecimal total() {
        // 计算单品总价
        BigDecimal subtotal = basketItems.stream()
                .map(BasketItem::calculateSubTotal)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 计算单品折扣总和
        BigDecimal itemDiscountTotal = basketItems.stream()
                .map(BasketItem::calculateDiscount)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 计算全局捆绑折扣总和
        BigDecimal globalDiscountTotal = globalDiscounts.stream()
                .map(discount -> discount.apply(basketItems))
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        // 最终总价 = 单品总价 - 单品折扣 - 全局捆绑折扣
        return subtotal.subtract(itemDiscountTotal).subtract(globalDiscountTotal);
    }
}

改造BasketItem类

import java.math.BigDecimal;

public class BasketItem {

    private final Item item;
    private int quantity;
    private final ItemDiscount discount;

    public BasketItem(Item item, ItemDiscount discount) {
        this.item = item;
        this.discount = discount;
        this.quantity = 1;
    }

    public BasketItem(Item item, ItemDiscount discount, int quantity) {
        this.item = item;
        this.discount = discount;
        this.quantity = quantity;
    }

    public void increment() {
        this.quantity++;
    }

    public BigDecimal calculateSubTotal() {
        return item.getUnitPrice().multiply(new BigDecimal(quantity));
    }

    public BigDecimal calculateDiscount() {
        return discount.apply(this);
    }

    public Item getItem() {
        return item;
    }

    public int getQuantity() {
        return quantity;
    }
}

捆绑折扣测试示例

import java.math.BigDecimal;
import java.util.List;

public class Test {

    public static void main(String[] args) {
        Item drink = new Item("Drink", new BigDecimal(1));
        Item candy = new Item("Candy", new BigDecimal(1));

        // 单品折扣:饮料买1赠1
        ItemDiscountAssignment buyOneGetOneFree = new ItemDiscountAssignment(new BuyXGetYFree(1, 1), drink);
        // 单品折扣:糖果买2付1元
        ItemDiscountAssignment buyTwoForOne = new ItemDiscountAssignment(new BuyXForY(2, BigDecimal.ONE), candy);
        // 捆绑折扣:饮料+糖果各1件共付1.5元
        GlobalDiscount bundleDiscount = new BuyXYForZ(drink, candy, new BigDecimal("1.5"));

        Checkout checkout = new Checkout(List.of(buyOneGetOneFree, buyTwoForOne), List.of(bundleDiscount));
        checkout.scan(drink);
        checkout.scan(drink);
        checkout.scan(candy);
        checkout.scan(candy);

        // 计算逻辑:
        // 单品总价:2+2=4元
        // 单品折扣:饮料减1元,糖果减1元,合计减2元
        // 捆绑折扣:可组成2组,每组原价2元,捆绑价1.5元,合计减1元
        // 最终总价:4-2-1=1元
        System.out.println("Total cost: " + checkout.total()); // 输出1
    }
}

内容的提问来源于stack exchange,提问作者hfg124g76g1

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.05 22:05:24