大营销平台 —— 细节优化和前端对接

一、前言

至此,活动领域就基本上完成了,后续还有一个用户返利和结算,这个在下一节实现,这一节先对前面写的活动领域和策略领域进行优化,调整一些细节问题,并且补充一部分内容。

最后对接前端,前后端联调。

二、细节优化

1.Redis编码问题

前面我们在Redis中存储的值都是序列化后的乱码,我们还是希望它可以用JSON格式存储,这样既方便我们调试,也便于运维。

在Redis配置类中解开解码器的注释,这样就可以变成JSON格式了。注意,必须是用我这个解码器,那个 RedisCodec() 有问题,不会自动把数组中的对象格式化,甚至连数组中的map对象都不行,我用这个出了很多问题,一直报NPE,AI都没找出来这个问题,最后是我亲自打了无数个断点才找出来的。

java 复制代码
config.setCodec(new JsonJacksonCodec());
java 复制代码
@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 复制代码
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去自动解析数组中的元素了。

java 复制代码
  private final Decoder<Object> decoder = (buf, state) ->
                JSON.parseObject(new ByteBufInputStream(buf), Object.class, Feature.SupportAutoType);

2.活动结束后清理缓存

目前我们的缓存全部是持久化的,没有设置过期时间,这就导致即使活动都已经结束了,但是这个过期活动的缓存还存在Redis里面占地方,所以我们应当将策略部分的键值对(sku库存和奖品库存)设置过期时间至活动结束,基于安全性考虑,我们会多延长一天,也就是在活动结束后一天清理。

既然是在活动结束后一天清理,那必然要先知道活动多久结束,前面我们在写策略时没有考虑到这一点,因此现在要将endDateTime作为参数透传进去。

于是要修改抽奖因子实体类:

java 复制代码
RaffleAwardEntity raffleAwardEntity = raffleStrategy.performRaffle(raffleFactorEntity);
java 复制代码
/**
 * @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,再到规则树的扣减库存节点,最后在仓储中设置过期时间。这整个调用链很复杂,基本上要从活动到抽奖全部透传一遍。

java 复制代码
@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中就要新增三个次数锁相关的字段:

java 复制代码
@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接口:

java 复制代码
/**
     * 查询奖品列表
     * <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();
        }
    }

其中主要新增的是下面这段逻辑,本质是通过查询规则配置,筛选出所有有次数锁规则的奖品,然后根据配置的锁次数和当日用户的抽奖次数来判断是否解锁,或者还有几次解锁。注意这里我们的设定是这几个奖品的次数锁每日刷新,用户每天都要抽一定次数才能重新解锁。

java 复制代码
            //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());

三、前端对接

bash 复制代码
http://localhost:3000/?userId=xiaofuge&activityId=100301

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

相关推荐
掉鱼的猫2 小时前
Hot-Plug 实战:运行期管理 Solon 插件
java·hotfix
markinmarkin2 小时前
Spring 中有哪些方式可以把Bean 注入到IOC 容器?
java·spring
带刺的坐椅2 小时前
Hot-Plug 实战:运行期管理 Solon 插件
java·solon·插件·热插拔
大黄说说2 小时前
SQL Server 执行计划怎么看?快速定位 SQL 慢的根源
java·linux·数据库
雪之下雪乃的代码日记3 小时前
Python快速入门(Java开发者版)
java·开发语言·笔记·python
敲个大西瓜3 小时前
Springboot核心面试题
java·spring boot·后端
器灵科技3 小时前
Seedance2.5 VS MiniMax H3 同日上线:AI短剧创作者该怎么选?
java·人工智能·阿里云·prompt·aigc
xbgRS3 小时前
Mybatis源码-核心流程及核心类
java·mybatis
zzh___zzh3 小时前
Java Stream API 常用方法笔记
java·笔记
萌动的小火苗4 小时前
1、python基础面试题
java·开发语言·python