1 引言
任务通常有多种状态,通过特定条件触发状态流转,条件一般是用户完成某些行为,并且当用户完成任务时还会发放奖励。或许一开始简单的任务模块就能支撑,但是随着越来越多的运营活动出现的时候,代码就会开始不断堆积变得难以维护,任务状态的流转甚至变得不可控,开发成本直线上升,因此需要一个易于扩展可读性高的任务状态机。
目前有一些较为流行的状态机选择:Spring Statemachine、Squirrel statemachine和cola-component-statemachine等,但是引入状态机组件会使得系统变得更复杂并且扩展性不高,比如需要考虑调整业务逻辑去适配状态机,而不只是利用状态机简化业务逻辑。基于此考虑使用模板方法+事件驱动设计一个简单的任务状态机。
2 任务状态机设计
2.1 任务状态机核心模块:任务、任务模板和行为
任务
任务是最核心的模块,记录每个用户当前的参与状态。任务包含状态和事件,状态即用户任务当前所处的阶段,比如未绑定、待开启、进行中、待领奖和已完成这些阶段。事件即状态流转事件,每一个事件会关联一个或多个起始状态以及一个唯一的目标状态。由事件触发任务状态的流转,通过发布事件匹配事件处理器来完成相应的事件处理逻辑。
任务模板
任务模板是任务的配置中心,通过模板为用户自动生成任务。模板定义了任务的基本属性,包括任务名称、行为到事件的映射、完成任务的通知和完成任务的奖励等等。此外可以通过定义场景将多个模板组织起来,每个场景可以包含多个模板。
行为
即用户的行为的抽象,比如支付订单
2.2 模板方法实现事件处理器
事件处理器有五个要素:可处理事件、任务源状态、任务目标状态、条件检查和状态流转。事件处理器接口定义如下:
public interface StatusHandler {
boolean canHandleEvent(Byte eventCode);
Set<Byte> fromStatus();
Byte toStatus();
boolean checkCondition(TaskContext ctx);
void transitionStatus(Byte eventCode, TaskContext ctx);
}
可以灵活配置事件处理器,比如绑定任务处理器、开始任务处理器和完成任务处理器等等,类图如下:
2.3 事件驱动方式进行任务状态流转
用户一个行为可能触发多个任务状态的流转,当上报用户行为时,通过模板配置的行为和事件的映射关系触发对应的事件发布。然后匹配事件处理器找到可以处理该事件的事件处理器后执行相应的处理逻辑。
代码如下:
@Component
@RequiredArgsConstructor
public class StateMachine {
private final List<StatusHandler> StatusHandlers;
/**
* 发布事件
* @param eventCode 事件编码
* @param ctx 任务上下文信息
*/
public void fireEvent(Byte eventCode, TaskContext ctx) {
StatusHandler statusHandler = StatusHandlers.stream().filter(a -> a.canHandleEvent(eventCode)).findFirst().orElse(null);
Assert.notNull(statusHandler, "无法处理该事件");
boolean check = statusHandler.checkCondition(ctx);
if (!check) {
return;
}
statusHandler.transitionStatus(eventCode, ctx);
}
/**
* 上报行为
* @param actionDTO 行为数据
* @return 任务上下文信息
*/
public List<TaskContext> reportAction(ActionDTO actionDTO) {
List<TemplateEnum> templateList = TemplateEnum.listTemplateByActionId(actionDTO.getActionId());
if (CollectionUtils.isEmpty(templateList)) {
return null;
}
List<TaskContext> taskContexts = new ArrayList<>();
for (TemplateEnum template : templateList) {
TaskContext context = new TaskContext();
context.setTemplateId(template.getId());
context.setActionId(actionDTO.getActionId());
context.setName(template.getName());
context.setDoctorId(actionDTO.getDoctorId());
context.setRewardItemId(actionDTO.getRewardItemId());
fireEvent(template.getEventCodeByActionId(actionDTO.getActionId()), context);
taskContexts.add(context);
}
return taskContexts;
}
}
状态流转
由事件处理器完成任务状态的流转,即在未绑定、待开启、进行中、待领奖和已结束之间流转
事件可以扩展,不同任务可以有不同的状态和流转方式,比如对于签到任务,不需要待开启和进行中两个状态,那么可以增加事件E和相应的处理器来增加未绑定到待领奖的流转方式,即如下:
此外,还可以接入画像系统用于判断哪些用户可以参与,哪些用户不能参与。以及奖励系统,完成任务后给用户发放奖励。等等。