一、前言
最近项目中连续遇到两次业务都需要用到工厂+策略模式来实现。具体业务如下,可以参考参考和自己的业务是不是相似。
1.系统预警功能
这个功能刚开始就包含三十多个指标,后续可能还会增加。为了以后增加指标的时候代码的扩展性强,而不是在一个方法里面if-else,就使用到这种工厂+策略的方式来实现。
2.三重福利功能
这个功能就是判断当前用户是否完成了具体任务,然后领取优惠券。虽然当时接到任务的时候需求就只有三个福利类型,但是为了预防以后增加福利的时候,能快速实现,也就采用了工厂+策略的方式来实现。
二、实现思路
下面通过三重福利来讲一讲具体的思路。
1.业务逻辑
首先需要明白这个功能的实现步骤。三重福利的具体实现思路如下:
我们会发现其实无论你福利类型怎么变化,有多少个,核心的逻辑就是这几点,这样的话,我们就可以使用模板的方式来实现。
2.工厂搭建
工厂主要是通过福利类型来生产具体的Strategy Handler,然后具体的Strategy Handler根据自己的逻辑去处理具体的业务。
(1)工厂接口:
/**
* @description: 福利工厂
* @author: Felix.Du
* @date: 2021/10/13 4:15 下午
*/
public interface BenefitsFactory {
/**
* 根据福利类型创建福利handler
*
* @param benefitsType
* @return 福利handler
*/
BenefitsHandler createBenefitsHandler(BenefitsTypeEnum benefitsType);
}
(2)工厂实现类:
/**
* @description:
* @author: Felix.Du
* @date: 2021/10/13 4:28 下午
*/
@Component
@Slf4j
public class BenefitsFactoryImpl implements BenefitsFactory, ApplicationContextAware {
private final Map<Integer, Object> typeToHandlerMap = new HashMap<>();
private ApplicationContext applicationContext;
@Override
public BenefitsHandler createBenefitsHandler(BenefitsTypeEnum benefitsType) {
try {
return getHandler(benefitsType);
} catch (Exception e) {
log.error("福利类型为{}的handler获取失败,请检查", benefitsType, e);
throw new RuntimeException();
}
}
@Override
public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
private BenefitsHandler getHandler(BenefitsTypeEnum benefitsType) {
BenefitsTypeEnum[] handlers = BenefitsTypeEnum.values();
if (CollUtil.isEmpty(typeToHandlerMap)
|| !Integer.valueOf(typeToHandlerMap.size()).equals(handlers.length)) {
initTypeToStrategyMap(handlers);
}
Object handler = typeToHandlerMap.get(benefitsType.getCode());
if (Objects.isNull(handler)) {
throw new RuntimeException("获取到的福利类为null");
}
return (BenefitsHandler) handler;
}
private void initTypeToStrategyMap(BenefitsTypeEnum[] handlers) {
typeToHandlerMap.clear();
for (BenefitsTypeEnum handler : handlers) {
try {
Class<?> aClass = Class.forName(handler.getClazzName());
Object bean = applicationContext.getBean(aClass);
typeToHandlerMap.put(handler.getCode(), bean);
} catch (ClassNotFoundException e) {
log.error("福利Handler{}无法加载", handler.getCode(), e);
}
}
}
}
(3)工厂涉及到的枚举类BenefitsTypeEnum:
package net.spo.enumerate;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
* @description: 福利类型枚举
* @author: Felix.Du
* @date: 2021/10/13 3:51 下午
*/
@AllArgsConstructor
public enum BenefitsTypeEnum {
/**
* 注册成功
*/
REGISTRATION_SUCCESS(1, "net.spo.handler.impl.RegistrationBenefitsHandler"),
/**
* 邀请好友
*/
INVITE_FRIENDS(2,"net.spo.handler.impl.InviteFriendsBenefitsHandler"),
/**
* 添加官方客服
*/
ADD_CUSTOMER_SERVICE_STAFF(3, "net.spo.handler.impl.BindCusServiceBenefitsHandler");
@Getter
private Integer code;
/**
* 全类名
*/
@Getter
private final String clazzName;
public static BenefitsTypeEnum getBenefitsTypeByCode(Integer code){
return Arrays
.stream(BenefitsTypeEnum.values())
.filter(benefitsTypeEnum -> code.equals(benefitsTypeEnum.getCode()))
.findFirst()
.get();
}
}
3.模板搭建
我们通过福利类型传入工厂,能够拿到具体的策略处理器实体类,接下来就可以着手写模板方法了。
(1)福利处理器 BenefitsHandler 接口:
/**
* @description: 福利处理器
* @author: Felix.Du
* @date: 2021/10/13 4:40 下午
*/
public interface BenefitsHandler {
/**
* 整个领奖流程,子类不需要实现
*
* @param userId 用户id
* @param benefitsType 福利类型
* @return 算力券/算力模板详情,用于领取后弹框
*/
List<HashrateCouponAndTemplateVO> entireBenefitsProcess(Long userId, BenefitsTypeEnum benefitsType);
/**
* 判断当前用户是否满足福利所需条件(具体地判断逻辑方法需要子类实现)
*
* @param userId 用户id
* @return true:完成; false:未完成
*/
boolean checkWhetherMissionAccomplished(Long userId);
/**
* 领取福利,子类不需要实现
*
* @param userId 用户id
* @param benefitsType 福利类型
* @return 算力券/算力模板详情,用于领取后弹框
*/
List<HashrateCouponAndTemplateVO> receiveBenefits(Long userId, BenefitsTypeEnum benefitsType);
/**
* 查询活动完成进度
* key->进度编号, value->进度情况
* @param userId 用户id
* @return 活动完成进度,key->进度编号, value->进度情况
*/
Map<String, String> getAccomplishedProgress(Long userId);
}
(2)抽象模板 AbstractBenefitsHandler :
/**
* @description: 福利抽象类
* @author: Felix.Du
* @date: 2021/10/13 4:54 下午
*/
public abstract class AbstractBenefitsHandler implements BenefitsHandler {
@Resource
public MineUserBenefitsMarkMapper mineUserBenefitsMarkMapper;
@Resource
public RocketMQTemplate rocketMqTemplate;
@Resource
public MineHashrateCouponCustomerService mineHashrateCouponCustomerService;
@Resource
public MineHashrateTemplateCustomerService mineHashrateTemplateCustomerService;
@Resource
public CustomerClient customerClient;
@Resource
public MineHashrateTemplateService mineHashrateTemplateService;
@Resource
public MineHashrateTemplateCustomerMapper mineHashrateTemplateCustomerMapper;
@Resource
public MineHashrateCouponCustomerMapper mineHashrateCouponCustomerMapper;
@Override
public List<HashrateCouponAndTemplateVO> entireBenefitsProcess(Long userId, BenefitsTypeEnum benefitsType) {
//查询是否已经领取该福利
boolean whetherReceive = getBenefitsMark(userId, benefitsType);
//已领取,返回错误提示
AssertsUtil.isTrue(whetherReceive, ResultCode.BusinessCode.REPEAT_RECEIVE);
//判断是否完成领取任务
boolean whetherAccomplish = checkWhetherMissionAccomplished(userId);
AssertsUtil.isFalse(whetherAccomplish, ResultCode.BusinessCode.UNFINISHED_TASK);
//未领取且完成任务,直接领奖
List<HashrateCouponAndTemplateVO> hashrateCouponAndTemplateVos = receiveBenefits(userId, benefitsType);
AssertsUtil.isTrue(CollectionUtils.isEmpty(hashrateCouponAndTemplateVos), ResultCode.BusinessCode.RECEIVE_FAILED);
//修改用户福利领取标识表
updateUserMark(userId, benefitsType);
return hashrateCouponAndTemplateVos;
}
/**
* 领取福利,子类不需要实现
*
* @param userId 用户id
* @param benefitsType 福利类型
* @return 算力券/算力模板详情,用于领取后弹框
*/
@Override
public List<HashrateCouponAndTemplateVO> receiveBenefits(Long userId, BenefitsTypeEnum benefitsType) {
CustomerWeChatInfoDTO cusInfo = customerClient.getWeChatInfo(userId).getData();
Enumerate.DistributionsStatus distributionsStatus = getDistributionsStatusByBenefitsType(benefitsType);
//获取可分配的算力模板和券
var list = mineHashrateTemplateService.lambdaQuery()
.eq(MineHashrateTemplate::getEnableStatus, Enumerate.IsEnable.ENABLE.getValue())
.eq(MineHashrateTemplate::getDistributionsStatus, distributionsStatus.getCode())
.list();
//待分配模板
var templateList = list
.stream()
.filter(item -> Objects.equals(item.getType(), HashrateTemplateEnum.TEMPLATE.getCode()))
.map(item -> {
var templateCouponDTO = new TopTemplateCouponDTO();
templateCouponDTO.setId(item.getId());
templateCouponDTO.setReason(distributionsStatus.getReason());
var dto = new TopRewardDTO();
dto.setUserId(cusInfo.getId());
dto.setRegisterAccount(cusInfo.getRegisterAccount());
templateCouponDTO.setList(List.of(dto));
return templateCouponDTO;
}).collect(Collectors.toList());
//待分配劵
var couponList = list
.stream()
.filter(item -> Objects.equals(item.getType(), HashrateTemplateEnum.COUPON.getCode()))
.map(item -> {
var templateCouponDTO = new TopTemplateCouponDTO();
templateCouponDTO.setId(item.getId());
templateCouponDTO.setReason(distributionsStatus.getReason());
var dto = new TopRewardDTO();
dto.setUserId(cusInfo.getId());
dto.setRegisterAccount(cusInfo.getRegisterAccount());
templateCouponDTO.setList(List.of(dto));
return templateCouponDTO;
}).collect(Collectors.toList());
//分配模板
var templateCustomerIdList = mineHashrateTemplateCustomerService.allocationHashrateTemplateByTop(templateList, new TopDurationDTO());
List<HashrateCouponAndTemplateVO> oneTemplateCount1 = new ArrayList<>();
List<HashrateCouponAndTemplateVO> oneTemplateCount2 = new ArrayList<>();
AtomicLong atomicLong = new AtomicLong();
if (!CollectionUtils.isEmpty(templateCustomerIdList)) {
var queryWrapper = new QueryWrapper<>();
queryWrapper.in("a.id", templateCustomerIdList);
oneTemplateCount1 = mineHashrateTemplateCustomerMapper.selectPageHashrateTemplateCustomer(null, queryWrapper)
.stream()
.map(item -> {
//弹出算力券详情
HashrateCouponAndTemplateVO hashrateCouponAndTemplateVO = new HashrateCouponAndTemplateVO();
hashrateCouponAndTemplateVO.setName(item.getName());
hashrateCouponAndTemplateVO.setUnit(item.getUnit());
hashrateCouponAndTemplateVO.setHashrateType(item.getHashrateType());
hashrateCouponAndTemplateVO.setEachAmount(item.getEachAmount());
hashrateCouponAndTemplateVO.setValidity(item.getValidity());
hashrateCouponAndTemplateVO.setHashrateTemplateId(item.getId());
hashrateCouponAndTemplateVO.setId(atomicLong.addAndGet(1));
hashrateCouponAndTemplateVO.setType(HashrateTemplateEnum.TEMPLATE.getCode());
return hashrateCouponAndTemplateVO;
}).collect(Collectors.toList());
}
//分配劵
var couponIdList = mineHashrateCouponCustomerService.allocationHashrateCouponByTop(couponList, new TopDurationDTO());
if (!CollectionUtils.isEmpty(couponIdList)) {
var queryWrapper = new QueryWrapper<>();
queryWrapper.in("a.id", couponIdList);
oneTemplateCount2 = mineHashrateCouponCustomerMapper.selectPageHashrateCouponCustomer(null, queryWrapper)
.stream()
.map(item -> {
//弹出算力券详情
HashrateCouponAndTemplateVO hashrateCouponAndTemplateVO = new HashrateCouponAndTemplateVO();
hashrateCouponAndTemplateVO.setName(item.getName());
hashrateCouponAndTemplateVO.setUnit(item.getUnit());
hashrateCouponAndTemplateVO.setHashrateType(item.getHashrateType());
hashrateCouponAndTemplateVO.setEachAmount(item.getEachAmount());
hashrateCouponAndTemplateVO.setValidity(item.getValidity());
hashrateCouponAndTemplateVO.setHashrateTemplateId(item.getId());
hashrateCouponAndTemplateVO.setId(atomicLong.addAndGet(1));
hashrateCouponAndTemplateVO.setType(HashrateTemplateEnum.COUPON.getCode());
return hashrateCouponAndTemplateVO;
}).collect(Collectors.toList());
}
oneTemplateCount1.addAll(oneTemplateCount2);
return oneTemplateCount1;
}
/**
* 根据用户id和福利类型查询是否已经领取该福利
*
* @param userId 用户id
* @param benefitsType 福利类型
* @return true:已领取; false:未领取
*/
public boolean getBenefitsMark(Long userId, BenefitsTypeEnum benefitsType) {
MineUserBenefitsMark mineUserBenefitsMark = mineUserBenefitsMarkMapper.selectOne(
Wrappers
.lambdaQuery(MineUserBenefitsMark.class)
.eq(MineUserBenefitsMark::getUserId, userId)
.eq(MineUserBenefitsMark::getBenefitsTypeId, benefitsType.getCode()));
//如果查询为空,说明还没领取,无创建记录
if (Objects.isNull(mineUserBenefitsMark)) {
return false;
}
return mineUserBenefitsMark.getMark() == UserMarkEnum.RECEIVED.getCode();
}
/**
* 根据福利类型获取算力模板自动分配类型
*
* @param benefitsType 福利类型
* @return 算力模板自动分配类型
*/
public Enumerate.DistributionsStatus getDistributionsStatusByBenefitsType(BenefitsTypeEnum benefitsType) {
Enumerate.DistributionsStatus distributionsStatus;
switch (benefitsType) {
case REGISTRATION_SUCCESS:
distributionsStatus = Enumerate.DistributionsStatus.C_TRIGGER_REGISTER;
break;
case INVITE_FRIENDS:
distributionsStatus = Enumerate.DistributionsStatus.C_TRIGGER_INVITE_FRIENDS;
break;
default:
distributionsStatus = Enumerate.DistributionsStatus.C_TRIGGER_ADD_CUSTOMER_SERVICE;
break;
}
return distributionsStatus;
}
/**
* 领取成功后,修改用户福利领取标识
*
* @param benefitsType 福利类型
*/
public void updateUserMark(Long userId, BenefitsTypeEnum benefitsType) {
MineUserBenefitsMark mineUserBenefitsMark = mineUserBenefitsMarkMapper.selectOne(
Wrappers
.lambdaQuery(MineUserBenefitsMark.class)
.eq(MineUserBenefitsMark::getUserId, userId)
.eq(MineUserBenefitsMark::getBenefitsTypeId, benefitsType.getCode())
);
//为空说明未领取,直接新增记录
if(Objects.isNull(mineUserBenefitsMark)){
MineUserBenefitsMark userBenefitsMark = new MineUserBenefitsMark();
userBenefitsMark.setUserId(userId);
userBenefitsMark.setBenefitsTypeId(Long.valueOf(benefitsType.getCode()));
userBenefitsMark.setMark(UserMarkEnum.RECEIVED.getCode());
mineUserBenefitsMarkMapper.insert(userBenefitsMark);
}else {
//不为空则修改状态
mineUserBenefitsMark.setMark(UserMarkEnum.RECEIVED.getCode());
mineUserBenefitsMarkMapper.updateById(mineUserBenefitsMark);
}
}
}
这里需要注意:AbstractBenefitsHandler福利模板需要去实现BenefitsHandler接口,抽象类可以选择实现接口的部分方法,这里就需要抽象类去实现抽象类的子类不实现的方法了。举个例子,比如说BenefitsHandler中有一个 判断当前用户是否满足福利所需条件 的方法checkWhetherMissionAccomplished(Long userId),很明显每福利的判断逻辑都不一样,这个需要子类自己去实现,写自己的逻辑,而模板父类不能关心子类的逻辑是如何的,这个时候模板类就不需要去实现这个方法了,而领取流程entireBenefitsProcess(Long userId, BenefitsTypeEnum benefitsType) 这个方法所有福利类型都是一样的逻辑,子类不会去关心,这个时候父类就需要去实现该方法,子类直接继承父类就行了。
(3)InviteFriendsBenefitsHandler 邀请好友福利处理器
@Component
@RequiredArgsConstructor
@Slf4j
public class InviteFriendsBenefitsHandler extends AbstractBenefitsHandler {
private final CustomerActivityClient customerActivityClient;
/**
* 邀请好友个数要求
*/
private static final Integer INVITE_LIMIT = 3;
/**
* 邀请好友实名认证个数限制
*/
private static final Integer INVITE_KYC_LIMIT = 2;
/**
* 邀请好友需要:1 邀请3人;2 邀请好友完成实名认证3人;
*
* @param userId 用户id
* @return 是否完成任务
*/
@Override
public boolean checkWhetherMissionAccomplished(Long userId) {
//获取已邀请好友信息
List<CustomerAccountDTO> invitedUsers = customerActivityClient.getInvitedUser(userId).getData();
if (invitedUsers.size() < INVITE_LIMIT) {
return Boolean.FALSE;
}
List<CustomerAccountDTO> kycUsers = invitedUsers
.stream()
.filter(u -> NumberUtil.equals(u.getKycStatus(), Enumerate.UserKycStatus.VERIFIED.getStatus()))
.collect(Collectors.toList());
if (kycUsers.size() < INVITE_KYC_LIMIT) {
return Boolean.FALSE;
}
return Boolean.TRUE;
}
/**
* 邀请好友需要:1 邀请3人;2 邀请好友完成实名认证3人;
* @param userId 用户id
* @return 活动完成进度,key->进度编号, value->进度情况
*/
@Override
public Map<String, String> getAccomplishedProgress(Long userId) {
UUIDContext.setUUID(UUID.randomUUID());
List<CustomerAccountDTO> invitedUsers = customerActivityClient.getInvitedUser(userId).getData();
int invitedAmount = 0;
int invitedKycAmount = 0;
if (!CollUtil.isEmpty(invitedUsers)){
invitedAmount = invitedUsers.size();
List<CustomerAccountDTO> kycUsers = invitedUsers
.stream()
.filter(u -> NumberUtil.equals(u.getKycStatus(), Enumerate.UserKycStatus.VERIFIED.getStatus()))
.collect(Collectors.toList());
invitedKycAmount = kycUsers.size();
}
String conditionInvite = invitedAmount + "/" + INVITE_LIMIT;
String conditionKyc = invitedKycAmount + "/" + INVITE_KYC_LIMIT;
Map<String, String> conditionMap = new HashMap<>(4);
conditionMap.put(BenefitsConstants.CONDITION_1, conditionInvite);
conditionMap.put(BenefitsConstants.CONDITION_2, conditionKyc);
return Map.of(BenefitsConstants.CONDITION_1, invitedAmount + "/" + INVITE_LIMIT,
BenefitsConstants.CONDITION_2, invitedKycAmount + "/" + INVITE_KYC_LIMIT);
}
我们可以看到子类只需要去实现判断当前用户是否完成指定任务checkWhetherMissionAccomplished方法,以及获取完成进度getAccomplishedProgress方法。其他方法子类不需要关心,由父类统一处理。这样我们在遇到在增加一个福利类型的时候就很简单了,只需要增加一个具体的福利类型Strategy Handler,然后去继承模板父类AbstractBenefitsHandler,同时再去实现上诉两个方法即可。其他的什么是否已经领取过,然后再去判断是否完成任务,最后再去领取奖励,这些逻辑完全不用关心。
还有一点需要注意,如果业务上增加了一个流程不同的福利,你也可以通过继承模板父类,然后覆写他的方法即可实现。
新公司短短工作两个月,就遇到了两次需要使用这种模式的业务,所以决定记录一下,方便自己温故而知新。
看没看懂都点个赞呗。