springboot使用策略模式实现一个基本的促销
- 策略模式
定义了算法族,分别封装起来,让它们之间可以互相替换,
此模式让算法的变化独立于使用算法的客户
源码:https://github.com/youxiu326/sb_promotion.git
- 实体层
一共三个实体,分别为商品实体,促销实体,促销结果实体
商品实体定义了商品的销售价 优惠金额 优惠后金额 数量。。。
促销实体定义了促销类型 名称 参与该促销的商品集合
package com.youxiu326.entity; import java.io.Serializable; import java.math.BigDecimal; /** * 商品: * * <br>优惠金额 <span color="red">discountAmount</span> * <br>优惠后价格 -1(默认等于销售金额) <span color="red">finalAmount</span> * <br>销售价 <span color="red">amount</span> * */ public class Product implements Serializable { private String code; private String name; /** * 销售价 */ private BigDecimal amount = BigDecimal.ZERO; /** * 优惠了多少金额 */ private BigDecimal discountAmount = BigDecimal.ZERO; /** * 优惠后金额 */ private BigDecimal finalAmount = new BigDecimal("-1"); private Integer quantity; public Product(){} public Product(String code, String name, BigDecimal amount, Integer quantity) { this.code = code; this.name = name; this.amount = amount; this.quantity = quantity; } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public String getName() { return name; } public void setName(String name) { this.name = name; } public BigDecimal getAmount() { return amount; } public double getAmountDouble(){ return amount.doubleValue(); } public void setAmount(BigDecimal amount) { this.amount = amount; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } public BigDecimal getDiscountAmount() { return discountAmount; } public void setDiscountAmount(BigDecimal discountAmount) { this.discountAmount = discountAmount; } /** * 优惠后金额(默认等于交易金额) * @return */ public BigDecimal getFinalAmount() { if(finalAmount.compareTo(new BigDecimal("-1"))==0) { finalAmount = amount; } return finalAmount; } public void setFinalAmount(BigDecimal finalAmount) { this.finalAmount = finalAmount; } }
package com.youxiu326.entity; import java.io.Serializable; import java.util.List; /** * 促销实体类 */ public class Promotion implements Serializable { /** * <span color="red">促销类型:</span> * <br>FREEONE 免最低一件 * <br>REBATE 八折 * <br>REDUCE 满100减20 */ public static enum Type{ FREEONE,REBATE,REDUCE } private Type type; private String name; /** * 哪些商品应用促销 */ private List<Product> products; public Promotion(){} public Promotion(Type type, String name,List<Product> products) { this.type = type; this.name = name; this.products = products; } public Type getType() { return type; } public void setType(Type type) { this.type = type; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List<Product> getProducts() { return products; } public void setProducts(List<Product> products) { this.products = products; } }
package com.youxiu326.entity; import java.io.Serializable; /** * 促销结果 */ public class PromotionResult implements Serializable { private String name; private Promotion.Type type; private Object result; public String getName() { return name; } public void setName(String name) { this.name = name; } public Promotion.Type getType() { return type; } public void setType(Promotion.Type type) { this.type = type; } public Object getResult() { return result; } public void setResult(Object result) { this.result = result; } }
package com.youxiu326.exception; /** * 自定义异常 */ public class ServiceException extends Exception { private Exception exception; public ServiceException(String message, Exception exception) { super(message); this.exception = exception; } public ServiceException(String message) { this(message, null); } public ServiceException(Exception exception) { this(null, exception); } public Exception getException() { return exception; } public Exception getRootCause() { if (exception instanceof ServiceException) { return ((ServiceException) exception).getRootCause(); } return exception == null ? this : exception; } @Override public String toString() { if (exception instanceof ServiceException) { return ((ServiceException) exception).toString(); } return exception == null ? super.toString() : exception.toString(); } }
- 促销实体类
定义了一个抽象类 PromotionStrategy.java
定义了三个促销策略:
/**
* 满足价格大于等于500
* <br>减免价格最低一件商品促销
*/
@Component
public class FreeOneStrategy extends PromotionStrategy/**
* 满足大于200
* <br>八折促销
*/
@Component
public class RebateStrategy extends PromotionStrategy/**
* 满足满100
* <br>减10促销
*/
@Component
public class ReduceStrategy extends PromotionStrategy
package com.youxiu326.abst; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import java.math.BigDecimal; import java.util.List; /** * 促销抽象类 * 定义公共方法,让子类继承 * 定义抽象方法,让子类实现 */ public abstract class PromotionStrategy { public abstract Promotion.Type getType(); /** * 定义执行促销方法 * @param promotion 促销 * @param products 参加促销的商品集合 * @return */ public abstract List<PromotionResult> execute(Promotion promotion, List<Product> products); /* //加法 BigDecimal result1 = num1.add(num2); //减法 BigDecimal result2 = num1.subtract(num2); //乘法 BigDecimal result3 = num1.multiply(num2); //除法 BigDecimal result5 = num2.divide(num1,20,BigDecimal.ROUND_HALF_UP); //绝对值 BigDecimal result4 = num3.abs(); 比较大小 结果是: -1:小于; 0 :等于; 1 :大于; BigDecimal b1 = new BigDecimal("-121454125453.145"); if(b1.compareTo(BigDecimal.ZERO)==-1) { System.out.println("金额为负数!"); } */ //优惠金额 discountAmount //优惠后价格 -1(默认等于销售金额) finalAmount //销售价 amount /** * <span color="red">平摊优惠金额</span> * @param products * @param disAmount */ protected void sharedAmount(List<Product> products,BigDecimal disAmount){ //计算总金额 double totalAmountTemp = products.stream().mapToDouble(it->( it.getFinalAmount().multiply(new BigDecimal(it.getQuantity().toString()))).doubleValue() ).sum(); //总金额 BigDecimal totalAmount = new BigDecimal(totalAmountTemp+""); //已分摊金额 BigDecimal sharedAmount = new BigDecimal("0");; //平摊金额到明细 for(int i=0;i<products.size();i++) { Product product = products.get(i); if(i == products.size() - 1) { //② 如果是最后一件商品 ,将剩余优惠金额计算到这个商品内 //例如: // 商品001 销售价10 数量1 商品002 销售价20 数量2 商品001,002 总共优惠了5元 // 商品001 已经确定可优惠1元 // 那么最后一个商品002 可以优惠 6-1 5元 product.setDiscountAmount(product.getDiscountAmount().add(disAmount).subtract(sharedAmount)); }else { //该商品总数量 BigDecimal quantity = new BigDecimal(product.getQuantity().toString()); //① 将总优惠金额 * (该商品销售价/总销售价) 得出该商品所占优惠金额 // 例如: // 商品001 销售价10 数量1 商品002 销售价20 数量2 商品001,002 总共优惠了5元 // 商品001可优惠金额= 5*(10*1/50) 1元 // 商品002可优惠金额= 5*(20*2/50) 4元 //得出该商品可优惠金额 BigDecimal itemDisAmount = disAmount.multiply( (product.getAmount().multiply(quantity).divide(totalAmount,2,BigDecimal.ROUND_HALF_UP)) ); product.setDiscountAmount(product.getDiscountAmount().add(itemDisAmount)); sharedAmount =sharedAmount.add(itemDisAmount); } } // ③计算出 商品优惠后的价格 finalAmount products.stream().forEach(it->{ BigDecimal quantity = new BigDecimal(it.getQuantity().toString()); it.setFinalAmount(it.getAmount().multiply(quantity).subtract(it.getDiscountAmount())); }); } }
package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.OptionalDouble; /** * 满足价格大于等于500 * <br>减免价格最低一件商品促销 */ @Component public class FreeOneStrategy extends PromotionStrategy { /** * 指定促销类型为:FREEONE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.FREEONE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<PromotionResult>(); //计算总金额 总数量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 这儿简单处理定死了规则 //不满足促销规则的返回空促销 if (totalAmount<500 || totalQuantity<=1){ return results; } //获得可优惠金额 double reduceAmount = products.stream().mapToDouble(Product::getAmountDouble).min().orElse(0); //平摊金额 sharedAmount(products, new BigDecimal(reduceAmount+"")); //创建减免促销信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.FREEONE); result.setResult(reduceAmount); results.add(result); return results; } }
package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * 满足大于200 * <br>八折促销 */ @Component public class RebateStrategy extends PromotionStrategy { /** * 指定促销类型为:REBATE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.REBATE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<PromotionResult>(); //计算总金额 总数量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 这儿简单处理定死了规则 //不满足促销规则的返回空促销 if (totalAmount<200){ return results; } //获得可优惠金额 double reduceAmount = totalAmount * 0.2; //平摊金额 sharedAmount(products, new BigDecimal(reduceAmount+"")); //创建减免促销信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.REBATE); result.setResult(reduceAmount); results.add(result); return results; } }
package com.youxiu326.strategy; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import org.springframework.stereotype.Component; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; /** * 满足满100 * <br>减10促销 */ @Component public class ReduceStrategy extends PromotionStrategy { /** * 指定促销类型为:REDUCE * @return */ @Override public Promotion.Type getType() { return Promotion.Type.REDUCE; } @Override public List<PromotionResult> execute(Promotion promotion, List<Product> products) { List<PromotionResult> results = new ArrayList<>(); //计算总金额 总数量 double totalAmount = products.stream().mapToDouble(it->( (it.getAmount().multiply(new BigDecimal(it.getQuantity().toString())))).subtract(it.getDiscountAmount()).doubleValue() ).sum(); int totalQuantity = products.stream().mapToInt(it->it.getQuantity()).sum(); //TODO 这儿简单处理定死了规则 //不满足促销规则的返回空促销 if (totalAmount<100){ return results; } //获得可优惠金额 double reduceAmount = 10; //平摊金额 sharedAmount(products, new BigDecimal(reduceAmount+"")); //创建减免促销信息 PromotionResult result = new PromotionResult(); result.setName(promotion.getName()); result.setType(Promotion.Type.REDUCE); result.setResult(reduceAmount); results.add(result); return results; } }
package com.youxiu326.context; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import java.util.Collection; import java.util.List; /** * 促销上下文 */ public class PromotionContext { /** * 促销策略 */ private PromotionStrategy strategy; /** * 当前促销 */ private Promotion promotion; public static Collection<PromotionStrategy> strategys; public PromotionContext(){} public PromotionContext(Promotion promotion) throws ServiceException { this.promotion = promotion; //初始化促销列表 if(strategys == null)throw new ServiceException("无可用促销"); //根据传入的促销 找到对应的促销策略 【strategy】 strategy = strategys.stream().filter(it->it.getType() == promotion.getType()).findFirst().orElse(null); if(strategy == null) throw new ServiceException("找不到符合促销类型"); } public List<PromotionResult> execute(List<Product> products) throws ServiceException{ return strategy.execute(promotion, products); } public Collection<PromotionStrategy> getStrategys() { return strategys; } public void setStrategys(Collection<PromotionStrategy> strategys) { PromotionContext.strategys = strategys; } }
- 促销service
处理促销业务逻辑
package com.youxiu326.service; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; public interface PromotionService { /** * <span color="red">结算方法</span> * <br>结算对象包含: * <br>1.商品优惠价格 * <br>2.优惠后最终价格 * <br>3.优惠信息 * @param products 要结算的商品集合 * @return Object 返回结算后的对象 */ public Map<String,Object> settlement(List<Product> products) throws ServiceException; /** * 查询可用的促销 * @return */ public List<Promotion> findUsablePromotions(); /** * 过滤出可以参加指定促销的商品 * @param promotion 指定促销 * @param products 要过滤的商品集合 * @return 返回过滤后的商品集合 */ public List<Product> filterProduct(Promotion promotion, List<Product> products); /** * 执行促销 * @param promotions 促销集合 * @param products 商品集合 * @return */ public List<PromotionResult> execute(List<Promotion> promotions, List<Product> products) throws ServiceException; }
package com.youxiu326.service.impl; import com.youxiu326.abst.PromotionStrategy; import com.youxiu326.context.PromotionContext; import com.youxiu326.entity.Product; import com.youxiu326.entity.Promotion; import com.youxiu326.entity.PromotionResult; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.stereotype.Service; import javax.annotation.PostConstruct; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * 促销service */ @Service public class PromotionServiceImpl implements PromotionService { private static final Logger LOGGER = LoggerFactory.getLogger(PromotionServiceImpl.class); //@Autowired //private ContextStartup contextStartup; @Autowired private ApplicationContext application; /** * 被@PostConstruct修饰的方法会在服务器加载Servlet的时候运行,并且只会被服务器调用一次, * <br>类似于Serclet的inti()方法 * <br>被@PostConstruct修饰的方法会在构造函数之后,init()方法之前运行 */ @PostConstruct public void init() { PromotionContext.strategys = application.getBeansOfType(PromotionStrategy.class).values(); } @Override public Map<String,Object> settlement(List<Product> products) throws ServiceException{ Map<String,Object> resultMap = new HashMap<>(); //查询可用促销 List<Promotion> promotions = findUsablePromotions(); //执行促销 List<PromotionResult> result = execute(promotions,products); //返回促销结果 与商品 resultMap.put("promotionResult", result); resultMap.put("products", products); return resultMap; } /** * 执行促销 * @param promotions 促销集合 * @param products 商品集合 * @return */ @Override public List<PromotionResult> execute(List<Promotion> promotions, List<Product> products) throws ServiceException { LOGGER.info("促销开始执行 促销数量:{} 商品数量:{}",promotions.size(),products.size()); products.stream().forEach(it->LOGGER.info("执行促销商品信息->编号:{} 价格:{} 数量:{}",it.getCode(),it.getAmount(),it.getQuantity())); //返回促销结果 List<PromotionResult> promotionResults = new ArrayList<PromotionResult>(); //遍历执行促销 for (Promotion promotion : promotions) { //根据传入的促销 得到对应的促销上下文 PromotionContext context = new PromotionContext(promotion); //过滤出可以参加该促销的商品 List<Product> filterProductList = filterProduct(promotion, products); //根据策略模式 执行先对应的促销规则,返回促销结果 List<PromotionResult> result = context.execute(filterProductList); if (result!=null){ promotionResults.addAll(result); } } //TODO 如果有的促销参加多次,应该要进行一定处理,只取一个即可 LOGGER.info("促销执行结束"); return promotionResults; } /** * 查询可用的促销 * @return */ @Override public List<Promotion> findUsablePromotions(){ //TODO 这儿你可以直接查询数据库 List<Promotion> promotions = new ArrayList<>(); Promotion p1 = new Promotion(Promotion.Type.FREEONE,"价格大于等于500免最低一件",null); Promotion p2 = new Promotion(Promotion.Type.REBATE,"大于200八折",null); Promotion p3 = new Promotion(Promotion.Type.REDUCE,"满100减10",null); promotions.add(p1); promotions.add(p2); promotions.add(p3); LOGGER.info("查询到可用促销数量:{}",promotions.size()); return promotions; } /** * 过滤出可以参加指定促销的商品 * @param promotion 指定促销 * @param products 要过滤的商品集合 * @return 返回过滤后的商品集合 */ @Override public List<Product> filterProduct(Promotion promotion, List<Product> products){ List<Product> list = new ArrayList<Product>(); products.stream().forEach(it->{ if (isMatching(promotion, it)) { list.add(it); } }); return list; } /** * 判断该商品是否可以参加该促销 * @param promotion * @param product * @return */ private boolean isMatching(Promotion promotion, Product product) { //TODO 这里你应该查询数据库 1.看满足该促销的商品中是否包含该商品,2.如果该促销未设置商品默认所有商品都满足 List<Product> products = null; //没有 所以商品都满足参加该促销的要求 返回true if(products == null || products.size() == 0)return true; //如果该商品在该促销商品集合内 则返回true 否则返回false long count = products.stream().filter(it->it.getCode().equals(product.getCode())).count(); return count>0?true:false; } }
- Junit
package com.youxiu326; import com.youxiu326.entity.Product; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.context.junit4.SpringRunner; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Map; @RunWith(SpringRunner.class) @SpringBootTest public class SbPromotionApplicationTests { @Autowired PromotionService service; @Test public void contextLoads() throws ServiceException { List<Product> products = new ArrayList<>(); Product p1 = new Product("YX001", "牙刷", new BigDecimal("50"), 2); Product p2 = new Product("YX002", "电视", new BigDecimal("200"), 2); Product p3 = new Product("YX003", "圆珠笔", new BigDecimal("20"), 2); Product p4 = new Product("YX004", "水杯", new BigDecimal("60"), 1); Product p5 = new Product("YX005", "充电宝", new BigDecimal("400"), 1); /*products.add(p1); products.add(p2); products.add(p3); products.add(p4);*/ products.add(p5); Map<String, Object> result = service.settlement(products); System.out.println(result); } }
- 为了方便 写了一个简易页面
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <base th:href="${#httpServletRequest.getContextPath()+'/'}"> <meta charset="UTF-8"> <title>springboot+策略模式 实现简单促销</title> </head> <body> <h2>提交商品</h2> <body> <table> <tr> <td>商品名称</td> <td>商品价格</td> <td>商品数量</td> <td>操作</td> </tr> <tr> <td><input id="name" /></td> <td><input id="amount" type="number" /></td> <td><input id="quantity" type="number" /></td> <td><input type="button" value="添加该商品" onclick="data()"/></td> </tr> </table> <h6>购物车商品</h6> <div width="400px" height="400px" id="showShoop"></div> <br> <input type="button" onclick="sub()" value="将这些商品进行促销"/> <br> <h6>参加促销信息展示</h6> <div width="400px" height="400px" id="showPro"></div> <h6>已添加商品信息展示</h6> <table> <tr> <td>商品编号</td> <td>商品名称</td> <td>商品数量</td> <td>优惠后总价</td> <td>总优惠金额</td> </tr> <tbody id="showTab"></tbody> </table> </body> <script src="/jquery-1.11.3.min.js"></script> <script> var products = []; var num = 1; <!-- 准备数据 --> function data(){ var name = $("#name").val(); var amount = $("#amount").val(); var quantity = $("#quantity").val(); if(name=="" || amount=="" || quantity=="" || name==undefined || amount==undefined || quantity==undefined ){ alert("商品格式有误"); return ; } var code = "youxiu-"+ num; num=num+1; var product = {"code":code,"name":name,"amount":amount,"quantity":quantity}; products.push(product); console.log(products); var em = "<span>"+product.name+"--"+product.quantity+"--"+product.amount+"</span><br>"; $("#showShoop").append(em); } function sub(){ if(products.length<=0){ alert("请添加商品"); retur; } $.ajax({ type: 'POST', url: "/data", data: JSON.stringify(products), contentType: "application/json; charset=utf-8", dataType: "json", success: function(map){ if(map==null){ alert("出错了"); } var promotionResult = map.promotionResult; var result = map.products; console.log("促销结果:"); console.log(promotionResult); console.log(); console.log("促销后商品信息:"); console.log(result); //促销信息展示 $("#showPro").empty(); for(var i=0;i<promotionResult.length;i++){ var em = "<span>"+promotionResult[i].name+"------"+promotionResult[i].result+"</span><br>"; $("#showPro").append(em); } //促销商品展示 $("#showTab").empty(); for(var i=0;i<result.length;i++){ var em = "<tr><td>"+result[i].code+"</td><td>"+result[i].name+"</td><td>"+result[i].quantity+"</td><td>"+result[i].finalAmount+"</td><td>"+result[i].discountAmount+"</td></tr>"; $("#showTab").append(em); } }, error:function(map){ alert(map); console.log(map); } }); } </script> </html>
package com.youxiu326.controller; import com.youxiu326.entity.Product; import com.youxiu326.exception.ServiceException; import com.youxiu326.service.PromotionService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.*; import java.util.List; import java.util.Map; /** * 促销controller */ @Controller public class PromotionCtrl { @Autowired private PromotionService service; @GetMapping("/index") public String index(){ return "/index"; } @PostMapping("/data") @ResponseBody public Map data(@RequestBody List<Product> products){ Map map = null; try { map = service.settlement(products); } catch (ServiceException e) { return null; } return map; } }
演示地址:http://promotion.youxiu326.xin
源码:https://github.com/youxiu326/sb_promotion.git