如何为超市结账系统设计跨商品捆绑折扣功能?
超市结账系统捆绑折扣实现方案
问题描述
我正在开发一个支持多种单品折扣的超市结账系统,目前已实现两类单品折扣:
- 买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 } }
捆绑折扣实现方案
核心思路
把折扣计算逻辑从单个商品级别升级到购物篮全局级别,新增「全局折扣」类型,让这类折扣能访问整个购物篮的商品数据,从而计算多商品捆绑的优惠金额。
具体改造步骤
- 拆分折扣接口:把原
Discount拆分为ItemDiscount(单品折扣)和GlobalDiscount(全局/捆绑折扣),明确职责。 - 扩展结账类:在
Checkout中新增全局折扣列表,计算总价时先算单品折扣,再叠加全局捆绑折扣。 - 实现捆绑折扣类:编写
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
相关产品推荐
相关产品推荐

