1.营销活动需求

2.责任链模式介绍

3.用责任链模式编写营销活动的构思

4.编码

5.回顾与总结

1.营销活动需求

在一个电商项目中,营销活动是一个绕不开的功能。

image.png

image.png

在最简单的情况下,一般都是由业务方提出一个活动,然后程序员开发一个活动。但是随着项目的迭代,单个活动内的业务逻辑会愈来愈复杂,可能会出现如下问题

  • 在一个活动中添加一个条件会就会导致巨大的改动,也不符合程序开发过程中的开闭原则
  • 来一种活动就要开发一种活动,工作量巨大。
  • 得注意活动与活动之间的影响,代码混乱不堪。
  • 活动比较不灵活,配置复杂,编码复杂,业务逻辑运行复杂

为了解决上述这些问题,我们可以使用责任链模式,将所有活动条件活动时间适用门店指定商品等)做成可配置项,使用拖拉拽的方式自定义每一种活动中的所有条件,而不用每次来一种活动就开发一种活动

2.责任链模式介绍

责任链模式,旨在将一系列的责任对象串联成一个处理链(如同链表一样),将请求沿着这条处理链从上游到下游传递直到处理完毕或者不能请求为止。责任链模式能够使用多个对象都有机会处理请求,避免请求的发送者和接受者之间的耦合关系。

image.png

  • Handler抽象责任角色:负责定义具体的处理方法,并且要在该角色中定义nextHandler(下一个处理节点),责任链就如同链表,链表就含有next属性。同时我们要根据用户的配置方式,将多个责任类定义到一个责任链处理链条当中。
  • ConcreteHandler具体责任角色:实现抽象责任角色的具体类,在这里就是所有活动的条件,比如按商品所选择的责任类,按门店所选择的责任类,按会员所选择的责任类等等。
  • Client责任链的装配、使用者:Client再进行责任链的调用前,需要对责任链进行装配,就是将上述的多个责任类,通过nextHandler属性进行链接,因此Client角色的作用有两个:装配责任链,调用责任链头结点。

3.用责任链模式编写营销活动的构思
要用责任链模式编写营销活动,我们需要准备的工作如下:

  • 编写每一个营销活动条件的责任类,里面有每一个责任类的具体业务处理逻辑。(活动时间责任类,活动适用门店责任类,活动范围责任类。)
  • 活动表,里面包含这个营销活动的id,活动名称折扣值状态等基本信息。

    CREATE TABLE `activity` (
    `id` int NOT NULL AUTO_INCREMENT,
    `activity_name` varchar(30) COLLATE utf8_bin DEFAULT NULL COMMENT '活动名称',
    `discount` decimal(10,4) DEFAULT NULL COMMENT '折扣',
    `status` int DEFAULT NULL COMMENT '状态0未开始 1进行中 2已结束',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动表';
  • 活动明细类,返回这个活动对应的商品id,指定日期,指定门店等等

    CREATE TABLE `activity_time_detail` (
    `id` int NOT NULL AUTO_INCREMENT,
    `activity_id` int DEFAULT NULL COMMENT '活动id',
    `start_time` datetime DEFAULT NULL COMMENT '活动开始时间',
    `end_time` datetime DEFAULT NULL COMMENT '活动结束时间',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动时间表';
    CREATE TABLE `activity_product_detail` (
    `id` int NOT NULL AUTO_INCREMENT,
    `activity_id` int DEFAULT NULL COMMENT '活动id',
    `product_id` int DEFAULT NULL COMMENT '商品id',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动商品表';
    CREATE TABLE `activity_store_detail` (
    `id` int NOT NULL AUTO_INCREMENT,
    `activity_id` int DEFAULT NULL COMMENT '活动id',
    `store_id` int DEFAULT NULL COMMENT '活动匹配门店',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动门店表';
  • 活动责任类明细表,包含这个活动拥有哪几个责任类。

    CREATE TABLE `activity_condition_detail` (
    `id` int NOT NULL AUTO_INCREMENT,
    `activity_id` int DEFAULT NULL COMMENT '活动id',
    `activity_condition_id` int DEFAULT NULL COMMENT '活动责任类id',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动条件明细表';
  • 责任类表,通过活动明细查询责任类,再根据反射创建好责任类,方便调用。

    CREATE TABLE `activity_condition_class` (
    `id` int NOT NULL AUTO_INCREMENT,
    `condition_class` varchar(255) COLLATE utf8_bin DEFAULT NULL COMMENT '条件class全类名,反射用',
    `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_time` datetime DEFAULT NULL COMMENT '更新时间',
    PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin COMMENT='活动责任类表';

以上全都用代码生成器生成对应service。

4.编码
抽象责任类:

package com.example.activity.service;
import com.example.activity.vo.HandlerConditionVo;
import java.math.BigDecimal;

public abstract class IActivityHandler {
    public IActivityHandler nextHandler;
    public boolean hasNext() {
        return this.nextHandler != null;
    }
    /**
     * 查询这个商品符不符合这个活动并且返回折扣率
     */
    protected abstract BigDecimal handle(HandlerConditionVo handlerConditionVo);

}

创建具体责任类:

  • ActivityProductHandler按商品id的筛选责任类:

    
    @Service
    public class ActivityProductHandler extends IActivityHandler {
    
      @Autowired
      private ActivityProductDetailService activityProductDetailService;
    
      @Autowired
      private ActivityService activityService;
    
      @Override
      public BigDecimal handle(HandlerConditionVo handlerConditionVo) {
          List<ActivityProductDetail> productDetails = activityProductDetailService.list(Wrappers.<ActivityProductDetail>lambdaQuery()
                  .eq(ActivityProductDetail::getActivityId, handlerConditionVo.getActivityId())
                  .eq(ActivityProductDetail::getProductId, handlerConditionVo.getProductId()));
    
          //如果符合活动条件,则继续向下搜索
          if (CollectionUtil.isNotEmpty(productDetails)) {
              if (hasNext()) {
                  return nextHandler.handle(handlerConditionVo);
              } else {
                  return activityService.getById(handlerConditionVo.getActivityId()).getDiscount();
              }
          }
          //如果不符合条件,返回该商品折扣率为100%
          return new BigDecimal("100");
      }
    }
  • ActivityStoreHandler按门店id的筛选责任类:

    
    @Service
    public class ActivityStoreHandler extends IActivityHandler {
    
      @Autowired
      private ActivityStoreDetailService storeDetailService;
    
      @Autowired
      private ActivityService activityService;
    
      @Override
      public BigDecimal handle(HandlerConditionVo handlerConditionVo) {
          ActivityStoreDetail storeDetails = storeDetailService.getOne(Wrappers.<ActivityStoreDetail>lambdaQuery()
                  .eq(ActivityStoreDetail::getStoreId, handlerConditionVo.getStoreId())
                  .eq(ActivityStoreDetail::getActivityId, handlerConditionVo.getActivityId()).last("limit 0,1"));
    
          //如果符合活动条件,则继续向下搜索
          if (storeDetails != null) {
              if (hasNext()) {
                  return nextHandler.handle(handlerConditionVo);
              } else {
                  return activityService.getById(handlerConditionVo.getActivityId()).getDiscount();
              }
          }
          //如果不符合条件,返回该商品折扣率为100%
          return new BigDecimal("100");
      }
    }
  • ActivityTimeHandler按活动时间的筛选责任类:

    @Service
    public class ActivityTimeHandler extends IActivityHandler {
    
      @Autowired
      private ActivityTimeDetailService timeDetailService;
    
      @Autowired
      private ActivityService activityService;
    
      @Override
      public BigDecimal handle(HandlerConditionVo handlerConditionVo) {
    
          List<ActivityTimeDetail> timeDetails = timeDetailService.list(Wrappers.<ActivityTimeDetail>lambdaQuery()
                  .eq(ActivityTimeDetail::getActivityId, handlerConditionVo.getActivityId())
                  .ge(ActivityTimeDetail::getStartTime, LocalDateTime.now())
                  .le(ActivityTimeDetail::getEndTime, LocalDateTime.now()));
    
          //如果符合活动条件,则继续向下搜索
          if (CollectionUtil.isNotEmpty(timeDetails)) {
              if (hasNext()) {
                  return nextHandler.handle(handlerConditionVo);
              } else {
                  return activityService.getById(handlerConditionVo.getActivityId()).getDiscount();
              }
          }
          //如果不符合条件,返回该商品折扣率为100%
          return new BigDecimal("100");
      }
    }
    
  • client的具体初始化流程和统一调用入口

我们往数据库插入这三条数据:

INSERT INTO `test`.`activity_condition_class`(`id`, `condition_class`, `create_time`, `update_time`) VALUES (1, 'com.example.activity.service.impl.ActivityTimeHandler', '2024-07-10 19:48:03', NULL);
INSERT INTO `test`.`activity_condition_class`(`id`, `condition_class`, `create_time`, `update_time`) VALUES (2, 'com.example.activity.service.impl.ActivityStoreHandler', '2024-07-10 19:48:22', NULL);
INSERT INTO `test`.`activity_condition_class`(`id`, `condition_class`, `create_time`, `update_time`) VALUES (3, 'com.example.activity.service.impl.ActivityProductHandler', '2024-07-10 19:49:08', NULL);
@Service
public class ActivityClient {


    /**
     * 活动缓存
     */
    Cache<Integer, IActivityHandler> activityCache = Caffeine.newBuilder()
            .maximumSize(100)
            .expireAfterAccess(600L, TimeUnit.SECONDS)
            .build();


    @Autowired
    private ActivityConditionClassService conditionClassService;

    @Autowired
    private ActivityConditionDetailService conditionDetailService;

    public BigDecimal clientHandler(HandlerConditionVo handlerConditionVo) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
        IActivityHandler activityHandler = activityCache.getIfPresent(handlerConditionVo.getActivityId());
        //查询缓存,如果没有就初始化
        if (activityHandler == null) {
            //查询活动条件
            List<ActivityConditionDetail> conditionDetails = conditionDetailService.list(Wrappers.<ActivityConditionDetail>lambdaQuery()
                    .eq(ActivityConditionDetail::getActivityId, handlerConditionVo.getActivityId()));

            List<Integer> conditionIds = conditionDetails.stream().map(ActivityConditionDetail::getActivityConditionId).collect(Collectors.toList());
            //查询活动条件对应的类
            List<ActivityConditionClass> activityConditionClasses = conditionClassService.listByIds(conditionIds);

            //装配这些活动
            //创建哑结点,找第0个节点创建
            IActivityHandler headHandler = (IActivityHandler) Class.forName(activityConditionClasses.get(0).getConditionClass()).newInstance();

            //创建前置节点,轮询用
            IActivityHandler preHandler = headHandler;
            for (int i = 1; i < activityConditionClasses.size(); i++) {
                IActivityHandler handler = (IActivityHandler) Class.forName(activityConditionClasses.get(i).getConditionClass()).newInstance();
                preHandler.nextHandler = handler;
                preHandler = preHandler.nextHandler;
                //重新赋值
                activityCache.put(handlerConditionVo.getActivityId(), activityHandler);
                activityHandler = headHandler;
            }
        }
        return activityHandler.handle(handlerConditionVo);
    }

}

5.回顾与总结

通过使用责任链设计模式,完成了营销活动的配置,包括责任链表的组装方式,简化开发流程,使代码的设计更加抽象,扩展性更强。


苏凌峰
73 声望38 粉丝

你的迷惑在于想得太多而书读的太少。