java抽奖系统(七)

8. 抽奖活动

8.1 新建抽奖活动

创建的活动信息包含:

i. 活动名称

ii. 活动描述

iii. 圈选奖品:勾选对应奖品,并设置奖品等级(⼀⼆三等奖),及奖品数量

iv. 圈选⼈员:勾选参与抽奖⼈员

库表关联分析如下所示:

时序图如下:

将完整的信息放入到redis目的:

1、为了抽奖的时候确定抽奖的活动是哪一个。

2、活动+关联奖品+关联人员的完整信息,就是为了抽奖,根据关联的人员去进行抽奖。为了达到高效的目的,将完整的信息放入到缓存中、

8.2 后端代码实现

约定前后端交互接⼝:

8.2.1 创建活动的请求参数

java 复制代码
@Data
public class CreateActivityParam implements Serializable {
    //活动名称
    @NotBlank(message = "活动名称不能为空!")
    private String activityName;
    //活动描述
    @NotBlank(message = "活动描述不能为空!")
    private String description;
    //活动关联奖品列表
    @NotEmpty(message = "活动关联奖品列表不能为空!")
    //list是容器,使用notempty的注解
    @Valid
    //这个valid注解就是备注列表中的对象的参数的相关不能为空的注解可以生效
    private List<CreatePrizeByActivityParam> activityPrizeList;
    //活动关联人员列表
    @NotEmpty(message = "活动关联人员列表不能为空!")
    @Valid
    private List<CreateUserByActivityParam> activityUserList;
}

8.2.2 创建活动奖品关联表的请求参数

java 复制代码
@Data
public class CreatePrizeByActivityParam implements Serializable {
    /**
     * 活动关联的奖品id
     */
    @NotNull(message = "活动关联的奖品id不能为空!")
    private Long prizeId;
    /**
     * 奖品数量
     */
    @NotNull(message = "奖品数量不能为空!")
    private Long prizeAmount;
    /**
     * 奖品等奖
     */
    @NotBlank(message = "奖品等奖不能为空!")
    private String prizeTiers;
}

8.2.3 创建活动人员关联表的请求参数

java 复制代码
@Data
public class CreateUserByActivityParam implements Serializable {
    /**
     * 活动关联的人员id
     */
    @NotNull(message = "活动关联的人员id不能为空!")
    private Long userId;
    /**
     * 姓名
     */
    @NotBlank(message = "姓名不能为空!")
    private String userName;
}

8.2.4 活动奖品状态的枚举

java 复制代码
@AllArgsConstructor
@Getter
public enum ActivityPrizeStatusEnum {
    INIT(1, "初始化"),

    COMPLETED(2, "已被抽取");


    private final Integer code;

    private final String message;

    public static ActivityPrizeStatusEnum forName(String name) {
        for (ActivityPrizeStatusEnum activityPrizeStatusEnum : ActivityPrizeStatusEnum.values()) {
            if (activityPrizeStatusEnum.name().equalsIgnoreCase(name)) {
                return activityPrizeStatusEnum;
            }
        }
        return null;
    }
}

8.2.5 活动奖品等级的枚举

java 复制代码
@AllArgsConstructor
@Getter
public enum ActivityPrizeTiersEnum {

    FIRST_PRIZE(1, "一等奖"),

    SECOND_PRIZE(2, "二等奖"),

    THIRD_PRIZE(3, "三等奖");

    private final Integer code;

    private final String message;

    public static ActivityPrizeTiersEnum forName(String name) {
        for (ActivityPrizeTiersEnum activityPrizeTiersEnum : ActivityPrizeTiersEnum.values()) {
            if (activityPrizeTiersEnum.name().equalsIgnoreCase(name)) {
                return activityPrizeTiersEnum;
            }
        }
        return null;
    }

}

8.2.6 controller接口设计

java 复制代码
@RestController
public class ActivityController {

    @Autowired
    private ActivityService activityService;
    private static final Logger logger = LoggerFactory.getLogger(ActivityController.class);

    //创建活动
    @RequestMapping("/activity/create")
    public CommonResult<CreateActivityResult> createActivity(
            @Validated @RequestBody CreateActivityParam param) {
        logger.info("ActivityController createActivity CreateActivityParam:{}",
                JacksonUtil.writeValueAsString(param));
        return CommonResult.success(
                convertToCreateActivityResult(activityService.createActivity(param)));
    }


    private CreateActivityResult convertToCreateActivityResult(CreateActivityDTO createActivityDTO) {
        if (createActivityDTO == null) {
            throw new ControllerException(ControllerErrorCodeConstants.CREATE_ACTIVITY_ERROR);
        }
        CreateActivityResult result = new CreateActivityResult();
        result.setActivityId(createActivityDTO.getActivityId());
        return result;
    }
}

8.2.7 service接口及其实现

java 复制代码
public interface ActivityService {
    //创建活动
    CreateActivityDTO createActivity(CreateActivityParam param);
}


@Service
public class ActivityServiceImpl implements ActivityService {
    private static final Logger logger = LoggerFactory.getLogger(ActivityServiceImpl.class);

    /**
     * 活动缓存前置,为了区分业务
     */
    private final String ACTIVITY_PREFIX = "ACTIVITY_";
    /**
     * 活动缓存过期时间
     */
    private final Long ACTIVITY_TIMEOUT = 60 * 60 * 24 * 60L;//60天
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private RedisUtil redisUtil;
    @Override
    @Transactional(rollbackFor = Exception.class)
    // 因为涉及多表,所以要进行本地事务回滚,防治出现下面情况
    //第二部,活动信息保存了
    //但是第三四部,活动相关联的表的信息保存失败,就要进行活动回滚
    public CreateActivityDTO createActivity(CreateActivityParam param) {
        // 校验活动信息是否正确
        checkActivityInfo(param);
        //校验失败就直接抛异常抛出去

        // 保存活动信息
        ActivityDO activityDO = new ActivityDO();
        activityDO.setActivityName(param.getActivityName());
        activityDO.setDescription(param.getDescription());
        activityDO.setStatus(ActivityStatusEnum.RUNNING.name());
        activityMapper.insert(activityDO);

        // 保存活动关联的奖品信息
        List<CreatePrizeByActivityParam> prizeParams = param.getActivityPrizeList();
        List<ActivityPrizeDO> activityPrizeDOList = prizeParams.stream()
                .map(prizeParam -> {
                    ActivityPrizeDO activityPrizeDO = new ActivityPrizeDO();
                    activityPrizeDO.setActivityId(activityDO.getId());
                    activityPrizeDO.setPrizeId(prizeParam.getPrizeId());
                    activityPrizeDO.setPrizeAmount(prizeParam.getPrizeAmount());
                    activityPrizeDO.setPrizeTiers(prizeParam.getPrizeTiers());
                    activityPrizeDO.setStatus(ActivityPrizeStatusEnum.INIT.name());
                    return activityPrizeDO;
                }).collect(Collectors.toList());
        activityPrizeMapper.batchInsert(activityPrizeDOList);
        // 保存活动关联的人员信息
        List<CreateUserByActivityParam> userParams = param.getActivityUserList();
        List<ActivityUserDO> activityUserDOList = userParams.stream()
                .map(userParam -> {
                    ActivityUserDO activityUserDO = new ActivityUserDO();
                    activityUserDO.setActivityId(activityDO.getId());
                    activityUserDO.setUserId(userParam.getUserId());
                    activityUserDO.setUserName(userParam.getUserName());
                    activityUserDO.setStatus(ActivityUserStatusEnum.INIT.name());
                    return activityUserDO;
                }).collect(Collectors.toList());
        activityUserMapper.batchInsert(activityUserDOList);
        // 整合完整的活动信息,存放 redis
        // activityId: ActivityDetailDTO:活动+奖品+人员
        // 先获取奖品基本属性列表
        // 获取需要查询的奖品id
        List<Long> prizeIds = param.getActivityPrizeList().stream()
                .map(CreatePrizeByActivityParam::getPrizeId)
                .distinct()
                .collect(Collectors.toList());
        List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIds);

        ActivityDetailDTO detailDTO = convertToActivityDetailDTO(activityDO, activityUserDOList,
                prizeDOList, activityPrizeDOList);
        //放入redis缓存
        cacheActivity(detailDTO);

        // 构造返回
        CreateActivityDTO createActivityDTO = new CreateActivityDTO();
        createActivityDTO.setActivityId(activityDO.getId());
        return createActivityDTO;
    }

    /**
     * 缓存完整的活动信息 ActivityDetailDTO
     *
     * @param detailDTO
     */
    private void cacheActivity(ActivityDetailDTO detailDTO) {
        // key: ACTIVITY_12
        // value: ActivityDetailDTO(json)
        if (detailDTO == null || detailDTO.getActivityId() == null) {
            logger.warn("要缓存的活动信息不存在!");
            return;
        }

        try {
            redisUtil.set(ACTIVITY_PREFIX + detailDTO.getActivityId(),
                    JacksonUtil.writeValueAsString(detailDTO), ACTIVITY_TIMEOUT);
        } catch (Exception e) {
            logger.error("缓存活动异常,ActivityServiceImpl ActivityDetailDTO={}",
                    JacksonUtil.writeValueAsString(detailDTO), e);
        }
    }
    /**
     * 根据活动id从缓存中获取活动详细信息
     * @param activityId
     * @return
     */
    private ActivityDetailDTO getActivityFromCache(Long activityId) {
        if (activityId == null) {
            logger.warn("获取缓存活动数据的activityId为空!");
            return null;
        }
        try {
            String str = redisUtil.get(ACTIVITY_PREFIX + activityId);
            if (!StringUtils.hasText(str)) {
                logger.info("获取的缓存活动数据为空!key={}", ACTIVITY_PREFIX + activityId);
                return null;
            }
            return JacksonUtil.readValue(str, ActivityDetailDTO.class);
        } catch (Exception e) {
            logger.error("从缓存中获取活动信息异常,key={}", ACTIVITY_PREFIX + activityId, e);
            return null;
        }
    }


    /**
     * 根据基本DO整合完整的活动信息ActivityDetailDTO
     *
     * @param activityDO
     * @param activityUserDOList
     * @param prizeDOList
     * @param activityPrizeDOList
     * @return
     */
    private ActivityDetailDTO convertToActivityDetailDTO(ActivityDO activityDO,
                                                         List<ActivityUserDO> activityUserDOList,
                                                         List<PrizeDO> prizeDOList,
                                                         List<ActivityPrizeDO> activityPrizeDOList) {
        ActivityDetailDTO detailDTO = new ActivityDetailDTO();
        detailDTO.setActivityId(activityDO.getId());
        detailDTO.setActivityName(activityDO.getActivityName());
        detailDTO.setDesc(activityDO.getDescription());
        detailDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));

        // apDO: {prizeId,amount, status}, {prizeId,amount, status}
        // pDO: {prizeid, name....},{prizeid, name....},{prizeid, name....}
        List<ActivityDetailDTO.PrizeDTO> prizeDTOList = activityPrizeDOList
                .stream()
                .map(apDO -> {
                    ActivityDetailDTO.PrizeDTO prizeDTO = new ActivityDetailDTO.PrizeDTO();
                    prizeDTO.setPrizeId(apDO.getPrizeId());
                    Optional<PrizeDO> optionalPrizeDO = prizeDOList.stream()
                            .filter(prizeDO -> prizeDO.getId().equals(apDO.getPrizeId()))
                            .findFirst();
                    //将第二行的每一个pdo和第一个的apdo根据奖品id进行比较,
                    //得到同id的第二行的第一个pdo
                    // 如果PrizeDO为空,不执行当前方法,不为空才执行
                    optionalPrizeDO.ifPresent(prizeDO -> {
                        prizeDTO.setName(prizeDO.getName());
                        prizeDTO.setImageUrl(prizeDO.getImageUrl());
                        prizeDTO.setPrice(prizeDO.getPrice());
                        prizeDTO.setDescription(prizeDO.getDescription());
                    });
                    prizeDTO.setTiers(ActivityPrizeTiersEnum.forName(apDO.getPrizeTiers()));
                    prizeDTO.setPrizeAmount(apDO.getPrizeAmount());
                    prizeDTO.setStatus(ActivityPrizeStatusEnum.forName(apDO.getStatus()));
                    return prizeDTO;
                }).collect(Collectors.toList());
        detailDTO.setPrizeDTOList(prizeDTOList);

        List<ActivityDetailDTO.UserDTO> userDTOList = activityUserDOList.stream()
                .map(auDO -> {
                    ActivityDetailDTO.UserDTO userDTO = new ActivityDetailDTO.UserDTO();
                    userDTO.setUserId(auDO.getUserId());
                    userDTO.setUserName(auDO.getUserName());
                    userDTO.setStatus(ActivityUserStatusEnum.forName(auDO.getStatus()));
                    return userDTO;
                }).collect(Collectors.toList());
        detailDTO.setUserDTOList(userDTOList);
        return detailDTO;
    }




    //校验活动有效性
    private void checkActivityInfo(CreateActivityParam param) {
        //虽然在controller层已经进行非空判断,但是为了解耦,在service层要进行再次判断
        if (param == null) {
            throw new ServiceException(ServiceErrorCodeConstants.CREATE_ACTIVITY_INFO_IS_EMPTY);
        }
        //校验维度一
        // 人员id在人员表中是否存在
        // 1 2 3  ->  1 2
        List<Long> userIds = param.getActivityUserList()
                .stream()
                .map(CreateUserByActivityParam::getUserId)
                .distinct()//去重id
                .collect(Collectors.toList());
        //数据表中真实存在的id
        List<Long> existUserIds = userMapper.selectExistByIds(userIds);
        if (CollectionUtils.isEmpty(existUserIds)) {
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
        }
        userIds.forEach(id -> {
            if (!existUserIds.contains(id)) {
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_USER_ERROR);
            }
        });
        //校验维度二
        // 奖品id在奖品表中是否存在
        List<Long> prizeIds = param.getActivityPrizeList()
                .stream()
                .map(CreatePrizeByActivityParam::getPrizeId)
                .distinct()
                .collect(Collectors.toList());
        List<Long> existPrizeIds = prizeMapper.selectExistByIds(prizeIds);
        if (CollectionUtils.isEmpty(existPrizeIds)) {
            throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
        }
        prizeIds.forEach(id -> {
            if (!existPrizeIds.contains(id)) {
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);
            }
        });
        //校验维度三
        // 人员数量大于等于奖品数量,奖励不会被抽完
        // 2个奖品 2 1
        int userAmount = param.getActivityUserList().size();
        long prizeAmount = param.getActivityPrizeList()
                .stream()
                .mapToLong(CreatePrizeByActivityParam::getPrizeAmount) // 2 1
                .sum();
        if (userAmount < prizeAmount) {
            throw new ServiceException(ServiceErrorCodeConstants.USER_PRIZE_AMOUNT_ERROR);
        }
        // 校验活动奖品等奖有效性
        param.getActivityPrizeList().forEach(prize -> {
            if (ActivityPrizeTiersEnum.forName(prize.getPrizeTiers()) == null) {
                throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_TIERS_ERROR);
            }
        });
    }
}

8.2.8 dao层

java 复制代码
@Mapper
public interface ActivityMapper {
    @Insert("insert into activity (activity_name, description, status)" +
            " values (#{activityName}, #{description}, #{status})")
    @Options(useGeneratedKeys = true, keyProperty ="id", keyColumn ="id")
    int insert(ActivityDO activityDO);
}

@Mapper
public interface ActivityPrizeMapper {
    @Insert("<script>" +
            " insert into activity_prize (activity_id, prize_id, prize_amount, prize_tiers, status)" +
            " values <foreach collection = 'items' item='item' index='index' separator=','>" +
            " (#{item.activityId}, #{item.prizeId}, #{item.prizeAmount}, #{item.prizeTiers}, #{item.status})" +
            " </foreach>" +
            " </script>")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    int batchInsert(@Param("items") List<ActivityPrizeDO> activityPrizeDOList);
}

@Mapper
public interface ActivityUserMapper {
    @Insert("<script>" +
            " insert into activity_user (activity_id, user_id, user_name, status)" +
            " values <foreach collection = 'items' item='item' index='index' separator=','>" +
            " (#{item.activityId}, #{item.userId}, #{item.userName}, #{item.status})" +
            " </foreach>" +
            " </script>")
    @Options(useGeneratedKeys = true, keyProperty = "id", keyColumn = "id")
    int batchInsert(@Param("items") List<ActivityUserDO> activityUserDOList);
}

8.3 论相关事务的回滚

论表与表之间之间事务的回滚:

从管理角度看:

活动信息表完成注入,活动关联奖品表和活动关联人员表,这三表是出于一个事务的状态,其中但凡有一个表的相关信息没有成功注入,就应该回滚到三个表都没有信息注入的状态;

但是前三表信息无误之后,将完整的活动信息保存到redis这个操作是否要和三个表注入处于一个事物?

从后续抽奖角度:

请求查找活动抽奖后的完整信息,前往redis发现这里面表的状态是空白,就会接下来前往后面处于一个事务的三个表对相关信息进行查询,并将这些信息返回到redis之后再回返给请求;所以可以不处于一个事物中;

8.4 后端测试

不合法的人员id和奖品id进行测试:

正确的人员和奖品id进行测试:

redis缓存信息,活动id为26

数据库activity如下所示:

8.5 前端后端交互测试

完善前端页面,代码见码云,进行测试:

创建成功并跳转到活动列表页面:

redis活动信息存储如下:

活动库表信息存储:

8.6 抽奖活动列表创建

活动列表支持翻页

8.6.1 时序图

8.6.2 前后端交互

8.6.3 后端代码实现

controller:

java 复制代码
 @RequestMapping("/activity/find-list")
    public CommonResult<FindActivityListResult> findActivityList(PageParam param) {
        logger.info("ActivityController findActivityList PageParam:{}",
                JacksonUtil.writeValueAsString(param));
        return CommonResult.success(
                convertToFindActivityListResult(
                        activityService.findActivityList(param)));
    }

    private FindActivityListResult convertToFindActivityListResult(
            PageListDTO<ActivityDTO> activityList) {
        if (activityList == null) {
            throw new ControllerException(ControllerErrorCodeConstants.FIND_ACTIVITY_LIST_ERROR);
        }
        FindActivityListResult result = new FindActivityListResult();
        result.setTotal(activityList.getTotal());
        result.setRecords(
                activityList.getRecords()
                        .stream()
                        .map(activityDTO -> {
                            FindActivityListResult.ActivityInfo activityInfo = new FindActivityListResult.ActivityInfo();
                            activityInfo.setActivityId(activityDTO.getActivityId());
                            activityInfo.setActivityName(activityDTO.getActivityName());
                            activityInfo.setDescription(activityDTO.getDescription());
                            activityInfo.setValid(activityDTO.valid());
                            return activityInfo;
                        }).collect(Collectors.toList())
        );
        return result;
    }

service:

java 复制代码
 PageListDTO<ActivityDTO> findActivityList(PageParam param);

    @Override
    public PageListDTO<ActivityDTO> findActivityList(PageParam param) {
        // 获取总量
        int total = activityMapper.count();

        // 获取当前页列表
        List<ActivityDO> activityDOList = activityMapper.selectActivityList(param.offset(),
                param.getPageSize());
        List<ActivityDTO> activityDTOList = activityDOList.stream()
                .map(activityDO -> {
                    ActivityDTO activityDTO = new ActivityDTO();
                    activityDTO.setActivityId(activityDO.getId());
                    activityDTO.setActivityName(activityDO.getActivityName());
                    activityDTO.setDescription(activityDO.getDescription());
                    activityDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
                    return activityDTO;
                }).collect(Collectors.toList());
        return new PageListDTO<>(total, activityDTOList);
    }

dao:

java 复制代码
    @Select("select count(1) from activity")
    int count();

    @Select("select * from activity order by id desc limit #{offset}, #{pageSize}")
    List<ActivityDO> selectActivityList(@Param("offset") Integer offset,
                                        @Param("pageSize") Integer pageSize);

使用postman进行测试:

返回结果如下:

java 复制代码
{
    "code": 200,
    "data": {
        "total": 4,
        "records": [
            {
                "activityId": 27,
                "activityName": "抽奖活动3.0",
                "description": "抽奖活动3.0",
                "valid": true
            },
            {
                "activityId": 26,
                "activityName": "测试抽奖活动2.0",
                "description": "测试抽奖活动2.0",
                "valid": true
            },
            {
                "activityId": 25,
                "activityName": "平安夜抽奖活动",
                "description": "平安夜抽奖活动",
                "valid": true
            },
            {
                "activityId": 24,
                "activityName": "抽奖的活动",
                "description": "测试抽奖的活动",
                "valid": true
            }
        ]
    },
    "msg": ""
}

8.6.4 前端代码完善

前后端交互测试:

ps:谢谢观看。

相关推荐
数据小爬虫@9 分钟前
利用Python爬虫技术获取商品销量详情
开发语言·爬虫·python
字节高级特工18 分钟前
C++---入门
开发语言·c++·算法
北辰浮光41 分钟前
[JavaScript]Ajax-异步交互技术
开发语言·javascript·ajax
亦是远方1 小时前
C++编译过程
java·开发语言·c++
程序猿000001号1 小时前
使用Python的Turtle库绘制图形:初学者指南
开发语言·python
carlcarl09031 小时前
qt通过QAxObject操作word插入表格及数据,图片生成文档
开发语言·c#
萝诗粉1 小时前
c语言在程序开发过程的各个阶段遇到的问题和知识总结
c语言·开发语言
Lojarro1 小时前
【Tomcat】第二站:Tomcat通过反射机制运行项目
java·tomcat
快乐飒男1 小时前
C语言基础13(内存管理)
c语言·开发语言·学习