背景
大家好,我是顾,最近 面试鸭 需要开发一个经验值体系的能力,需要对经验值进行增加、减少,有很多的触发情况,比如用户发布题解,用户浏览题目,用户邀请新用户、用户转化为会员、签到等等,为了复用经验系统的代码,以及和业务代码解耦,决定使用Spring Boot 的事件处理机制来处理各种经验值增减的逻辑。
介绍
在 Spring Boot 中,事件监听是一种 Spring 的机制,允许开发者定义和触发自定义事件,并通过注册监听器来响应这些事件。这种机制为应用程序中的事件处理提供了更高的解耦性,使代码更加清晰和灵活。Spring的事件监听机制是在JDK事件监听的基础上进行的扩展,也是使用了观察者模式并在其基础上进行进一步抽象和改进。
流程
创建自定义的事件类
把经验体系的属性封装在自定义的事件类中,通过实现 ApplicationEvent 表示这是经验值事件类
/**
* 经验值事件类
*
* @author gulihua
* @date 2024-11-25 14:27
*/
@Getter
public class ExperienceEvent extends ApplicationEvent {
/**
* 经验类型枚举
*/
private UserExperienceEnum userExperience;
/**
* 用户ID
*/
private Long userId;
/**
* 业务ID(操作的对象ID)
*/
private Long bizId;
/**
* 描述
*/
private String desc;
/**
* 参数
*/
private UserExperienceRecordParamsDTO param;
/**
* 登录用户
*/
private User loginUser;
/**
* 自定义原因
*/
private UserExperienceRecordReasonEnum reason;
/**
* 是否是取消经验值事件
*/
private boolean isCancel;
public ExperienceEvent(Object source, UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser, boolean isCancel) {
super(source);
this.userExperience = userExperience;
this.userId = userId;
this.bizId = bizId;
this.desc = desc;
this.param = param;
this.loginUser = loginUser;
this.isCancel = isCancel;
}
public ExperienceEvent(Object source, UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser, UserExperienceRecordReasonEnum reason, boolean isCancel) {
super(source);
this.userExperience = userExperience;
this.userId = userId;
this.bizId = bizId;
this.desc = desc;
this.param = param;
this.loginUser = loginUser;
this.reason = reason;
this.isCancel = isCancel;
}
}
创建事件监听器
创建一个自定义的事件监听器,用来处理经验增加、扣除事件。如果是取消事件,需要把经验值改成负的。
/**
* 经验值事件监听器
*
* @author gulihua
* @date 2024-11-25 14:28
*/
@Slf4j
@Service
public class ExperienceEventListener implements ApplicationListener<ExperienceEvent> {
@Resource
private UserExperienceRecordService userExperienceRecordService;
@Resource
private RedissonClient redissonClient;
@Override
public void onApplicationEvent(ExperienceEvent experienceEvent) {
UserExperienceRecord userExperienceRecord = new UserExperienceRecord();
userExperienceRecord.setExperience(experienceEvent.getUserExperience().getValue());
userExperienceRecord.setDescription(experienceEvent.getDesc());
userExperienceRecord.setReason(experienceEvent.getUserExperience().getAddReason());
userExperienceRecord.setUserId(experienceEvent.getUserId());
userExperienceRecord.setBizId(experienceEvent.getBizId());
userExperienceRecord.setExperienceType(experienceEvent.getUserExperience().getType());
String params = JSONUtil.toJsonStr(experienceEvent.getParam());
userExperienceRecord.setParams(params);
// 取消操作
if (experienceEvent.isCancel()) {
userExperienceRecord.setExperience(userExperienceRecord.getExperience() * -1);
userExperienceRecord.setReason(experienceEvent.getUserExperience().getCancelReason());
}
// 自定义原因
if (null != experienceEvent.getReason()) {
userExperienceRecord.setReason(experienceEvent.getReason().getValue());
}
userExperienceRecordService.addUserExperienceRecord(userExperienceRecord, experienceEvent.getLoginUser());
}
}
经验值记录具体数据库的操作,包括添加记录、添加经验值数据
@Override
public int addUserExperienceRecord(UserExperienceRecord userExperienceRecord, User loginUser) {
// 参数校验:用户要存在,经验值要合理
Integer experience = userExperienceRecord.getExperience();
Integer reason = userExperienceRecord.getReason();
Long userId = userExperienceRecord.getUserId();
log.info("userExperienceRecord params = {}", userExperienceRecord);
if (experience > MAX_ADD_EXPERIENCE || experience == 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (UserExperienceRecordReasonEnum.getEnumByValue(reason) == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
if (userId <= 0) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
User user = userService.getById(userId);
if (user == null) {
throw new BusinessException(ErrorCode.PARAMS_ERROR);
}
// 往用户经验值记录表插入记录
int insertRows = this.baseMapper.insert(userExperienceRecord);
if (insertRows <= 0) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据插入失败");
}
// 修改用户表的经验值
UpdateWrapper<User> updateWrapper = new UpdateWrapper<>();
updateWrapper.eq("id", userId);
if (experience > 0) {
updateWrapper.setSql(String.format("experience = experience + %d", experience));
} else {
updateWrapper.setSql(String.format("experience = experience - %d", Math.abs(experience)));
}
boolean updateResult = userService.update(updateWrapper);
if (!updateResult) {
throw new BusinessException(ErrorCode.SYSTEM_ERROR, "数据更新失败");
}
return experience;
}
因为有些经验值一天是有获取上限的,不然用户可能会重复刷,所以,我们需要对有些经验值的类型去做判断,比如刷题,用户每天获取的上限是1个积分,发布题解,上限是6个积分,所以就需要在获取积分时去做判断今天是否达到了上限值。
/**
* 核实是否要增加记录
*
* @return 是否执行后续的操作
*/
private boolean check(ExperienceEvent experienceEvent) {
// 发布
if (!experienceEvent.isCancel()) {
// 只能执行一次
if (ExperienceLimitTypeEnum.ONLY_ONE.equals(experienceEvent.getUserExperience().getLimitType())) {
// 判断之前有没对目标做过操作
QueryWrapper<UserExperienceRecord> oldRecord = new QueryWrapper<>();
oldRecord.eq("userId", experienceEvent.getUserId());
oldRecord.eq("bizId", experienceEvent.getBizId());
oldRecord.eq("experienceType", experienceEvent.getUserExperience().getType());
if (userExperienceRecordService.count(oldRecord) > 0) {
return false;
}
} else if (ExperienceLimitTypeEnum.DAY.equals(experienceEvent.getUserExperience().getLimitType())) {
// 每天经验上限
Date now = new Date();
Date startDate = DateUtil.beginOfDay(now);
Date endDate = DateUtil.endOfDay(now);
Long sum = userExperienceRecordService.sumUserTodayExperienceRecord(startDate, endDate, experienceEvent.getUserExperience().getType(), experienceEvent.getUserId());
if (sum != null && sum >= experienceEvent.getUserExperience().getDayMaxExperience()) {
return false;
}
}
} else {
Long sum = userExperienceRecordService.sumUserExperienceRecord(experienceEvent.getBizId(), experienceEvent.getUserExperience().getType(), experienceEvent.getUserId());
if (sum == null || sum <= 0) {
return false;
}
}
return true;
}
创建事件发布者
这个类主要用来发布具体的事件,包括发布增加经验值事件,扣除经验值事件
/**
* 经验值事件发布者
*
* @author gulihua
* @date 2024-11-25 14:54
*/
@Slf4j
@Component
@AllArgsConstructor
public class ExperienceEventPublisher {
private ApplicationEventPublisher publisher;
/**
* 发布增加经验值事件
*
* @param userExperience 经验值枚举
* @param userId 增加经验值的用户ID
* @param bizId 业务ID
* @param desc 描述
* @param param 参数
* @param loginUser 登录用户
**/
public void publishAddEvent(UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser) {
ExperienceEvent customEvent = new ExperienceEvent(this, userExperience, userId, bizId, desc, param, loginUser, false);
publisher.publishEvent(customEvent);
}
/**
* 发布扣除经验值事件
*
* @param userExperience 经验值枚举
* @param userId 扣除经验值的用户ID
* @param bizId 业务ID
* @param desc 描述
* @param param 参数
* @param loginUser 登录用户
**/
public void publishCancelEvent(UserExperienceEnum userExperience, Long userId, Long bizId, String desc, UserExperienceRecordParamsDTO param, User loginUser) {
ExperienceEvent customEvent = new ExperienceEvent(this, userExperience, userId, bizId, desc, param, loginUser, true);
publisher.publishEvent(customEvent);
}
经验值类型枚举
在经验值事件处理中,有一个很重要的字段,那就是经验类型的枚举,很多的经验事件的处理都是围绕这个枚举值来判断的,与其说是枚举类,不如说是经验类型的配置类。里面包含了经验值类型,经验值原因,经验值的大小,每天经验值的获取上限等等。
/**
* 用户经验值枚举
*
* @author gulihua
*/
public enum UserExperienceEnum {
/**
* 题解被设置为推荐答案
*/
BEST_ANSWER(2, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getBestAnswer(),
UserExperienceRecordReasonEnum.BEST_ANSWER.getValue(),
UserExperienceRecordReasonEnum.CANCEL_BEST_ANSWER.getValue(),
ExperienceLimitTypeEnum.NOT_LIMIT
),
/**
* 题解被精选
*/
GOOD_ANSWER(3, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getGoodAnswer(),
UserExperienceRecordReasonEnum.GOOD_ANSWER.getValue(),
UserExperienceRecordReasonEnum.CANCEL_GOOD_ANSWER.getValue(),
ExperienceLimitTypeEnum.NOT_LIMIT
),
/**
* 发布一篇回答讨论
*/
ADD_ANSWER(4, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getAddAnswer(),
UserExperienceRecordReasonEnum.ADD_ANSWER.getValue(),
UserExperienceRecordReasonEnum.CANCEL_ADD_ANSWER.getValue(),
ExperienceLimitTypeEnum.DAY,
6,
false
),
/**
* 题解被置顶
*/
TOP_ANSWER(8, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getTopAnswer(),
UserExperienceRecordReasonEnum.TOP_ANSWER.getValue(),
UserExperienceRecordReasonEnum.CANCEL_TOP_ANSWER.getValue(),
ExperienceLimitTypeEnum.NOT_LIMIT
),
/**
* 邀请新用户注册
*/
INVITE_USER(5, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getInviteUser(),
UserExperienceRecordReasonEnum.INVITE_USER.getValue(),
UserExperienceRecordReasonEnum.CANCEL_INVITE_USER.getValue(),
ExperienceLimitTypeEnum.NOT_LIMIT
),
/**
* 邀请用户成为会员
*/
INVITE_USER_VIP(6, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getInviteUserVip(),
UserExperienceRecordReasonEnum.INVITE_USER_VIP.getValue(),
UserExperienceRecordReasonEnum.CANCEL_INVITE_USER_VIP.getValue(),
ExperienceLimitTypeEnum.NOT_LIMIT
),
/**
* 签到
*/
SIAN_IN(15, UserExperienceRecordServiceImpl.USER_EXPERIENCE_RULE.getSignin(),
UserExperienceRecordReasonEnum.SIGN_IN.getValue(),
UserExperienceRecordReasonEnum.CANCEL_SYSTEM.getValue(),
ExperienceLimitTypeEnum.DAY,
true,
1
),
// ... 其他类型的枚举
;
/**
* 经验类型
*/
private int type;
/**
* 经验值
*/
private int value;
/**
* 添加经验值原因
*/
private int addReason;
/**
* 扣除经验值原因
*/
private int cancelReason;
/**
* 经验值获取上限类型
*/
private ExperienceLimitTypeEnum limitType;
/**
* 是否限制操作次数(优先级比经验值上限高)
*/
private boolean isLimitOperation;
/**
* 限制操作的次数
*/
private int operationLimit;
/**
* 每天经验值上限
*/
private int dayMaxExperience;
public int getOperationLimit() {
return this.operationLimit;
}
public ExperienceLimitTypeEnum getLimitType() {
return this.limitType;
}
public int getValue() {
return this.value;
}
public int getAddReason() {
return this.addReason;
}
public int getCancelReason() {
return this.cancelReason;
}
public boolean getIsLimitOperation() {
return this.isLimitOperation;
}
public int getType() {
return this.type;
}
public int getDayMaxExperience() {
return this.dayMaxExperience;
}
UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType) {
this.type = type;
this.value = value;
this.addReason = addReason;
this.cancelReason = cancelReason;
this.limitType = limitType;
}
UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType, int dayMaxExperience, boolean isLimitOperation) {
this.type = type;
this.value = value;
this.addReason = addReason;
this.cancelReason = cancelReason;
this.dayMaxExperience = dayMaxExperience;
this.limitType = limitType;
this.isLimitOperation = isLimitOperation;
}
UserExperienceEnum(int type, int value, int addReason, int cancelReason, ExperienceLimitTypeEnum limitType, boolean isLimitOperation, int operationLimit) {
this.type = type;
this.value = value;
this.addReason = addReason;
this.cancelReason = cancelReason;
this.limitType = limitType;
this.isLimitOperation = isLimitOperation;
this.operationLimit = operationLimit;
}
调用
比如在发布题解时,需要增加经验值,直接调用发布事件的方法即可,指定类型枚举
/**
* 每日首次发布题解,增加经验值
*
* @param questionAnswer 答案
* @param loginUser 登录用户
**/
private void addAnswerExperience(QuestionAnswer questionAnswer, User loginUser) {
String url = String.format("/question/%s?comment=%s", questionAnswer.getQuestionId(), questionAnswer.getId());
String descriptionTemplate = "发布一篇回答讨论,<a href=\"%s\" target=\"_blank\">查看该讨论</a>";
String description = String.format(descriptionTemplate, url);
UserExperienceRecordParamsDTO paramsDTO = new UserExperienceRecordParamsDTO();
paramsDTO.setQuestionId(questionAnswer.getQuestionId());
paramsDTO.setAnswerId(questionAnswer.getId());
experienceEventPublisher.publishAddEvent(
UserExperienceEnum.ADD_ANSWER,
questionAnswer.getUserId(),
questionAnswer.getId(),
description,
paramsDTO,
loginUser
);
}
在需要扣除经验值的时候,比如题解被删除时,调用扣除经验值的事件,可以指定自定义的原因
/**
* 题解被删除,扣除经验值
*
* @param oldQuestionAnswer 答案
* @param loginUser 登录用户
**/
private void deleteAnswerCancelExperience(QuestionAnswer oldQuestionAnswer, User loginUser) {
String url = String.format("/question/%s", oldQuestionAnswer.getQuestionId());
UserExperienceRecordParamsDTO paramsDTO = new UserExperienceRecordParamsDTO();
paramsDTO.setQuestionId(oldQuestionAnswer.getQuestionId());
paramsDTO.setAnswerId(oldQuestionAnswer.getId());
UserExperienceEnum userAddAnswerExperienceEnum = UserExperienceEnum.ADD_ANSWER;
String descriptionTemplate = "回答讨论被删除,扣除经验值,<a href=\"%s\" target=\"_blank\">查看题目</a>";
String description = String.format(descriptionTemplate, url);
experienceEventPublisher.publishCancelEvent(
userAddAnswerExperienceEnum,
oldQuestionAnswer.getUserId(),
oldQuestionAnswer.getId(),
description,
paramsDTO,
UserExperienceRecordReasonEnum.DELETE_ANSWER,
loginUser
);
}
异步处理
经验值的增加减少并不是用户操作时立马关心的,如果经验值处理的时间过长也会影响请求的响应时间,会阻塞主线程,所以可以把触发事件处理成异步的操作,首先可以创建一个异步的线程池配置,专门用来处理经验值的相关事件,使用@EnableAsync来开启异步支持,它会由Spring创建一个代理,在单独的线程中去执行任务。
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
// 经验值线程池
@Bean("experienceThreadPool")
public TaskExecutor experienceThreadPool() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(4);
executor.setMaxPoolSize(10);
executor.setKeepAliveSeconds(100);
executor.setQueueCapacity(1000);
executor.setThreadNamePrefix("experiencePool-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
然后再需要异步处理的方法上加上@Async("experienceThreadPool")注解,这样这个方法会在刚刚自定义的线程池中去执行。
@Override
@Async("experienceThreadPool")
public void onApplicationEvent(ExperienceEvent experienceEvent) {
UserExperienceRecord userExperienceRecord = new UserExperienceRecord();
userExperienceRecord.setExperience(experienceEvent.getUserExperience().getValue());
userExperienceRecord.setDescription(experienceEvent.getDesc());
userExperienceRecord.setReason(experienceEvent.getUserExperience().getAddReason());
userExperienceRecord.setUserId(experienceEvent.getUserId());
userExperienceRecord.setBizId(experienceEvent.getBizId());
userExperienceRecord.setExperienceType(experienceEvent.getUserExperience().getType());
String params = JSONUtil.toJsonStr(experienceEvent.getParam());
userExperienceRecord.setParams(params);
// 取消操作
if (experienceEvent.isCancel()) {
userExperienceRecord.setExperience(userExperienceRecord.getExperience() * -1);
userExperienceRecord.setReason(experienceEvent.getUserExperience().getCancelReason());
}
// 自定义原因
if (null != experienceEvent.getReason()) {
userExperienceRecord.setReason(experienceEvent.getReason().getValue());
}
userExperienceRecordService.addUserExperienceRecord(userExperienceRecord, experienceEvent.getLoginUser());
}
总结
使用 Spring 的异步事件处理将一些耗时的操作从主线程中分离出来,避免阻塞主线程,从而提高系统的吞吐量。对于不需要立即完成的任务,比如说经验值的处理,操作日志的记录等,可以进行异步处理,从而提高用户体验。它最重要的功能就是解耦,Spring Event 允许模块之间通过事件通信,减少模块之间的直接依赖,提高代码的可维护性和可扩展性,发布者只需要发布事件即可,并不需要关心具体的业务实现。
苏ICP备16040035号-5