一、前言
至此,活动领域就基本上完成了,后续还有一个用户返利和结算,这个在下一节实现,这一节先对前面写的活动领域和策略领域进行优化,调整一些细节问题,并且补充一部分内容。
最后对接前端,前后端联调。
二、细节优化
1.Redis编码问题
前面我们在Redis中存储的值都是序列化后的乱码,我们还是希望它可以用JSON格式存储,这样既方便我们调试,也便于运维。
在Redis配置类中解开解码器的注释,这样就可以变成JSON格式了。注意,必须是用我这个解码器,那个 RedisCodec() 有问题,不会自动把数组中的对象格式化,甚至连数组中的map对象都不行,我用这个出了很多问题,一直报NPE,AI都没找出来这个问题,最后是我亲自打了无数个断点才找出来的。
config.setCodec(new JsonJacksonCodec());
@Bean("redissonClient")
public RedissonClient redissonClient(ConfigurableApplicationContext applicationContext, RedisClientConfigProperties properties) {
Config config = new Config();
// 根据需要可以设定编解码器;https://github.com/redisson/redisson/wiki/4.-%E6%95%B0%E6%8D%AE%E5%BA%8F%E5%88%97%E5%8C%96
config.setCodec(new new JsonJacksonCodec());
config.useSingleServer()
.setAddress("redis://" + properties.getHost() + ":" + properties.getPort())
.setPassword(properties.getPassword())
.setConnectionPoolSize(properties.getPoolSize())
.setConnectionMinimumIdleSize(properties.getMinIdleSize())
.setIdleConnectionTimeout(properties.getIdleTimeout())
.setConnectTimeout(properties.getConnectTimeout())
.setRetryAttempts(properties.getRetryAttempts())
.setRetryInterval(properties.getRetryInterval())
.setPingConnectionInterval(properties.getPingInterval())
.setKeepAlive(properties.isKeepAlive())
.setDatabase(properties.getDatabase())
;
return Redisson.create(config);
}
后续测试会发现可能会出现这个问题:
java.lang.ClassCastException: com.alibaba.fastjson.JSONObject cannot be cast to com.bigmarket.domain.strategy.model.entity.StrategyAwardEntity
这个问题是由于fastjson默认不会将存入Redis的数组中的元素按照@type标记的类来解析,而是会直接解析为一个普通的JSONObject,这就导致解析出来的对象无法转换为我们需要的实体类。
解决方法是修改配置类下面的解码器,增加参数Feature.SupportAutoType,这样就强制让fastjson去自动解析数组中的元素了。
private final Decoder<Object> decoder = (buf, state) ->
JSON.parseObject(new ByteBufInputStream(buf), Object.class, Feature.SupportAutoType);
2.活动结束后清理缓存
目前我们的缓存全部是持久化的,没有设置过期时间,这就导致即使活动都已经结束了,但是这个过期活动的缓存还存在Redis里面占地方,所以我们应当将策略部分的键值对(sku库存和奖品库存)设置过期时间至活动结束,基于安全性考虑,我们会多延长一天,也就是在活动结束后一天清理。
既然是在活动结束后一天清理,那必然要先知道活动多久结束,前面我们在写策略时没有考虑到这一点,因此现在要将endDateTime作为参数透传进去。
于是要修改抽奖因子实体类:
RaffleAwardEntity raffleAwardEntity = raffleStrategy.performRaffle(raffleFactorEntity);
/**
* @author 印东升
* @description 抽奖因子实体
* @create 2026-04-10 11:12
*/
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class RaffleFactorEntity {
/**
* 用户id
*/
private String userId;
/**
* 策略id
*/
private Long strategyId;
/**
* 结束时间
*/
private Date endDateTime;
}
自然的,整个调用链从上层的活动Controller到模板类中的performRaffle,再到规则树的扣减库存节点,最后在仓储中设置过期时间。这整个调用链很复杂,基本上要从活动到抽奖全部透传一遍。
@Override
public Boolean subtractionAwardStock(String cacheKey, Date endDateTime) {
long surplus = redisService.decr(cacheKey);
if (surplus < 0) {
redisService.setValue(cacheKey, 0);
return false;
}
String lockKey = cacheKey + Constants.UNDERLINE + surplus;
Boolean lock = false;
if (null != endDateTime) {
long expireMills = endDateTime.getTime() – System.currentTimeMillis() + TimeUnit.DAYS.toMillis(1);
lock = redisService.setNx(lockKey, expireMills, TimeUnit.MILLISECONDS);
} else {
lock = redisService.setNx(lockKey);
}
if (!lock) {
log.info("策略奖品库存加锁失败:{}", lockKey);
}
return lock;
}
而sku的库存缓存我们前面已经写了,上面这个和sku的基本上类似。
这里其实还可以优化,所有的对应活动的缓存其实都可以清理,但是除去sku库存锁和award库存锁,其余的剩下的并不多,只有奖品列表、规则树、活动信息等缓存,最多也就十来个,但是这两个库存锁基本上和库存量相当,所以这里只清理库存锁缓存也是没问题的。
3.次数锁显示优化
前面我们配置了奖品的次数锁规则,这里我们就需要把这个锁显示给用户了,因此我们需要告诉用户还有多少次这个奖品就能解锁,并且要让用户知道这个奖品解锁没有。
从上向下改,首先响应给前端的DTO中就要新增三个次数锁相关的字段:
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RaffleAwardListResponseDTO {
//奖品Id
private Integer awardId;
//奖品标题
private String awardTitle;
//奖品副标题【抽奖1次后解锁】
private String awardSubtitle;
//排序编号
private Integer sort;
//奖品次数规则 – 抽奖N次后解锁,未配置则为空
private Integer awardRuleLockCount;
//奖品是否解锁
private Boolean isAwardUnlock;
//等待解锁次数-规则的抽奖N次解锁 – 用户已经抽奖次数
private Integer waitUnlockCount;
}
由此我们也需要修改奖品列表的Controller接口:
/**
* 查询奖品列表
* <a href="http://localhost:8091/api/v1/raffle/query_raffle_award_list">/api/v1/raffle/query_raffle_award_list</a>
* 请求参数 raw json
*
* @param request {"strategyId":1000001}
* @return 奖品列表
*/
@RequestMapping(value = "query_raffle_award_list", method = RequestMethod.POST)
@Override
public Response<List<RaffleAwardListResponseDTO>> queryRaffleAwardList(@RequestBody RaffleAwardListRequestDTO request) {
try {
log.info("查询抽奖奖品列表配置开始 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
//1.参数校验
if (StrUtil.isBlank(request.getUserId()) || null == request.getActivityId()) {
return Response.<List<RaffleAwardListResponseDTO>>builder()
.code(ResponseCode.ILLEGAL_PARAMETER.getCode())
.info(ResponseCode.ILLEGAL_PARAMETER.getInfo())
.build();
}
//2.查询奖品配置
List<StrategyAwardEntity> strategyAwardEntities = raffleAward.queryRaffleStrategyAwardListByActivityId(request.getActivityId());
//3.获取规则配置
String[] treeIds = strategyAwardEntities.stream()
.map(StrategyAwardEntity::getRuleModels)
.filter(ruleModel -> ruleModel != null && !ruleModel.isEmpty())
.toArray(String[]::new);
//4.查询规则配置,获取奖品的解锁规则,抽奖N次后解锁
Map<String, Integer> ruleLockCountMap = raffleRule.queryAwardRuleLockCount(treeIds);
//5.查询抽奖次数 – 用户已经参与抽奖的次数
Integer dayPartakeCount = raffleActivityAccountQuotaService.queryRaffleActivityAccountDayPartakeCount(request.getActivityId(), request.getUserId());
//6.遍历填充数据
List<RaffleAwardListResponseDTO> raffleAwardListResponseDTOS = new ArrayList<>(strategyAwardEntities.size());
for (StrategyAwardEntity strategyAward : strategyAwardEntities) {
Integer awardRuleLockCount = ruleLockCountMap.get(strategyAward.getRuleModels());
raffleAwardListResponseDTOS.add(RaffleAwardListResponseDTO.builder()
.awardId(strategyAward.getAwardId())
.awardTitle(strategyAward.getAwardTitle())
.awardSubtitle(strategyAward.getAwardSubtitle())
.sort(strategyAward.getSort())
.awardRuleLockCount(awardRuleLockCount)
.isAwardUnlock(null == awardRuleLockCount || dayPartakeCount > awardRuleLockCount)
.waitUnlockCount(null == awardRuleLockCount || awardRuleLockCount <= dayPartakeCount ? 0 : awardRuleLockCount – dayPartakeCount)
.build());
}
Response<List<RaffleAwardListResponseDTO>> response = Response.<List<RaffleAwardListResponseDTO>>builder()
.code(ResponseCode.SUCCESS.getCode())
.info(ResponseCode.SUCCESS.getInfo())
.data(raffleAwardListResponseDTOS)
.build();
log.info("查询抽奖奖品列表配置完成 userId:{} activityId:{}", request.getUserId(), request.getActivityId());
return response;
} catch (Exception e) {
log.error("查询抽奖奖品列表配置失败 userId:{} activityId:{}", request.getUserId(), request.getActivityId(),e);
return Response.<List<RaffleAwardListResponseDTO>>builder()
.code(ResponseCode.UN_ERROR.getCode())
.info(ResponseCode.UN_ERROR.getInfo())
.build();
}
}
其中主要新增的是下面这段逻辑,本质是通过查询规则配置,筛选出所有有次数锁规则的奖品,然后根据配置的锁次数和当日用户的抽奖次数来判断是否解锁,或者还有几次解锁。注意这里我们的设定是这几个奖品的次数锁每日刷新,用户每天都要抽一定次数才能重新解锁。
//2.查询奖品配置
List<StrategyAwardEntity> strategyAwardEntities = raffleAward.queryRaffleStrategyAwardListByActivityId(request.getActivityId());
//3.获取规则配置
String[] treeIds = strategyAwardEntities.stream()
.map(StrategyAwardEntity::getRuleModels)
.filter(ruleModel -> ruleModel != null && !ruleModel.isEmpty())
.toArray(String[]::new);
//4.查询规则配置,获取奖品的解锁规则,抽奖N次后解锁
Map<String, Integer> ruleLockCountMap = raffleRule.queryAwardRuleLockCount(treeIds);
//5.查询抽奖次数 – 用户已经参与抽奖的次数
Integer dayPartakeCount = raffleActivityAccountQuotaService.queryRaffleActivityAccountDayPartakeCount(request.getActivityId(), request.getUserId());
三、前端对接
http://localhost:3000/?userId=xiaofuge&activityId=100301

缓存中也生成了相应的锁,并且有过期时间了:

网硕互联帮助中心



评论前必须登录!
注册