抽奖系统(4)——活动模块

目录

创建活动

时序图:

前后端交互接口:

Controller层接口设计:

创建活动的请求参数

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

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

活动奖品状态的枚举

活动奖品等级的枚举

活动人员状态枚举

Service层设计与实现:

Dao层设计:

接口测试:

相关事务的回滚说明:

抽奖活动列表创建(翻页):

时序图:

前后端交互接口:

Controller层接口设计:

[FindActivityListResult 查询结果:](#FindActivityListResult 查询结果:)

PageParam请求参数:

Service层接口设计与实现:

Dao层接口设计:

查询活动详情信息:

Controller层接口设计:

GetActivityDetailResult返回结果:

Service层接口设计与实现:

Dao层设计:


附加:本项目的抽奖系统测试计划测试计划(抽奖系统)-CSDN博客

创建活动

创建活动的信息包含:

  • 活动名称
  • 活动描述
  • 圈选奖品:勾选对应奖品、并设置奖品等级(一二三等奖)、以及奖品数量
  • 圈选人员:勾选参与抽奖人员

时序图:

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

  1. 为了抽奖的时候确定抽奖的活动是哪一个。
  2. 活动+关联奖品+关联人员的完整信息,就是为了抽奖,根据关联的人员去进行抽奖。为了达到高效的目的,将完整的信息放入到缓存中

前后端交互接口:

Controller层接口设计:

java 复制代码
@RestController
public class ActivityController {

    private static final Logger logger = LoggerFactory.getLogger(ActivityController.class);

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

创建活动的请求参数

java 复制代码
@Data
public class CreateActivityParam implements Serializable {
    //  活动名称
    @NotBlank(message = "活动不能为空!")
    private String activityName;

    //  活动描述
    @NotBlank(message = "活动描述不能为空!")
    private String description;

    // 活动关联奖品列表
    @NotNull(message = "活动奖品列表不能为空!")
    //list 是个容器需使用notblank
    @Valid //上面的NotEmpty 只能确保 list集合不为空 想要确保 list<CreatePrizeByActivityParam> 里面的元素不为空 需要加 @Valid
    private List<CreatePrizeByActivityParam> activityPrizeList;

    // 活动关联人员列表
    @NotNull(message = "活动人员列表不能为空!")
    @Valid
    private List<CreateUserByActivityParam> activityUserList;

}

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

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

    /**
     * 奖品数量
     */
    @NotNull(message = "奖品数量不能为空!")
    private Long prizeAmount;

    /**
     * 奖品等级
     */
    @NotBlank(message = "奖品等级不能为空!")
    private String prizeTiers;
}

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

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

}

活动奖品状态的枚举

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

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

    /**
     * -- GETTER --
     *  获取状态码
     */
    private final Integer code;

    /**
     * -- GETTER --
     *  获取描述信息
     */
    private final String message;

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

}

活动奖品等级的枚举

java 复制代码
@AllArgsConstructor
@Getter
public enum ActivityPrizeTiersEnum {
    FIRST_PRIZE(1, "一等奖"),

    SECOND_PRIZE(2, "二等奖"),

    THIRD_PRIZE(3, "三等奖");

    private final Integer code;

    private final String message;

    /**
     * 根据名称获取枚举
     * @param name
     * @return
     */
    public static ActivityPrizeTiersEnum forName(String name) {
        for (ActivityPrizeTiersEnum activityPrizeTiersEnum : ActivityPrizeTiersEnum.values()) {
            if (activityPrizeTiersEnum.name().equalsIgnoreCase(name)) {
                return activityPrizeTiersEnum;
            }
        }
        return null;
    }

}

活动人员状态枚举

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

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

    private final  Integer code;
    private final  String message;
    public static ActivityUserStatusEnum forName(String name){
        for(ActivityUserStatusEnum activityUserStatusEnum:ActivityUserStatusEnum.values()){
            if(activityUserStatusEnum.name().equals(name)){
                return activityUserStatusEnum;
            }
        }
        return null;
    }

}

Service层设计与实现:

java 复制代码
@Service
public interface ActivityService {
    //创建活动
    CreateActivityDTO createActivity(CreateActivityParam param);
}
java 复制代码
@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 * 3L ;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;

    @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
        // ActivityDetailDTO 用于存放完整的活动信息(包括 活动数据、奖品数据、人员数据)
        // 使用 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);

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

Dao层设计:

java 复制代码
@Mapper
public interface ActivityMapper {

    /**
     * 创建活动
     * @param activityDO
     * @return
     */
    @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 {
    /**
     * 批量插入奖品信息
     * @param activityPrizeDOList
     * @return
     */
    @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 {
    /**
     * 批量插入
     * @param activityUserDOList
     * @return
     */
    @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);
}
java 复制代码
/**
 * 活动信息的数据对象类
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityDO extends BaseDO {
    /**
     * 活动的名称
     */
    private String activityName;

    /**
     * 活动的描述
     */
    private String description;

    /**
     * 活动的状态
     */
    private String status;
}




/**
 * ActivityPrizeDO 类表示活动奖品的数据对象。
 * 该类继承自 BaseDO,包含活动与奖品的关联信息。
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityPrizeDO extends BaseDO {
    /**
     * 关联的活动id
     */
    private Long activityId;

    /**
     * 关联的奖品id
     */
    private Long prizeId;

    /**
     * 关联奖品数量
     */
    private Long prizeAmount;

    /**
     * 关联的奖品状态
     */
    private String status;

    /**
     * 关联的奖品等级
     */
    private String prizeTiers;
}




/**
 * 活动用户数据对象,继承自BaseDO。
 * 包含活动与用户的关联信息。
 */
@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityUserDO extends BaseDO {
    // 关联的活动ID
    private Long activityId;

    // 关联的用户ID
    private Long userId;

    // 用户名称
    private String userName;

    // 关联人状态
    private String status;
}

接口测试:

相关事务的回滚说明:

项目中事务的使用可以简单理解为 "一系列操作要么全成功,要么全失败",就像生活中 "转账" 时,钱从 A 账户转出和转入 B 账户必须同时完成,缺一步都得恢复原样。结合项目场景,事务的作用和实现方式可以拆成部分:​

  1. 抽奖时的 "原子操作"(本地事务)​

比如用户抽中一台手机,系统要做两件事:​

  • 手机库存减 1(确保不会超卖);
  • 生成一条 "用户中奖" 记录(证明用户确实中了)。

这两步必须绑在一起:​

  • 如果库存减 1 成功,但记录没生成(比如突然断电),系统会自动把库存加回去,避免 "少了一台手机却没人中";
  • 如果记录生成了但库存没减,也会自动删除记录,防止 "用户中了奖但库存没变化,导致其他人还能抽"。

代码里用@Transactional注解实现这种绑定,就像给这两步加了个 "保护罩",出问题就全退回原样。
在抽奖系统中,用 Redis 缓存活动状态时,结合事务特性保证 "活动状态不变" 的原理,其实是通过 "关联校验" 和 "原子操作" 实现的,就像给活动状态加了一把 "联动锁",具体可以拆成三个关键点:​

  1. 活动状态和关联信息的 "绑定存储"​

Redis 里缓存的活动状态(比如 "进行中""已结束")不是孤立的,而是和奖品状态、人员抽奖状态 "绑在一起" 存储:​

  • 活动状态用一个键存(比如ACTIVITY_STATUS_1001:RUNNING);
  • 对应的奖品状态(比如某奖品是否已抽完)用PRIZE_STATUS_1001_2001:NOT_DRAWN(2001 是奖品 ID);
  • 人员抽奖状态(比如用户是否已抽过)用USER_DRAW_STATUS_1001_3001:NOT_DRAWN(3001 是用户 ID)。

这就像活动状态是 "总开关",奖品和人员状态是 "分开关",总开关的变化必须先看分开关是否都到位。​

  1. 事务保证 "校验和更新的原子性"​

当系统想改活动状态(比如从 "进行中" 改成 "已结束"),会用 Redis 的事务(MULTI/EXEC)做两件事,确保中间没人插队修改:​

  • 第一步:批量校验分状态

用WATCH命令盯着奖品和人员的状态键,比如检查 "所有奖品是否都已抽完"(PRIZE_STATUS_*都是DRAWN),且 "没有用户正在抽奖"(USER_DRAW_STATUS_*没有DRAWING)。​

  • 第二步:更新活动状态

如果校验通过,就把活动状态改成ENDED;如果校验失败(比如还有奖品没抽完),事务会自动放弃更新,活动状态保持不变。​

就像你想关门(改活动状态),必须先确认所有人都已离开(人员状态)且东西都收拾好了(奖品状态),中途有人突然回来(状态变化),门就关不上。​

  1. 缓存和数据库的 "状态同步机制"​

Redis 里的状态最终要和数据库保持一致,这里的事务会同时管两头:​

  • 当系统在 Redis 里用事务确认 "可以改活动状态" 时,会同时触发数据库的事务:先改数据库里的活动状态,再改 Redis 的缓存状态,中间任何一步失败,两边都会回滚(比如数据库改成功但 Redis 崩了,就把数据库状态改回去)。
  • 如果有人直接改了数据库里的奖品状态(比如手动标记某奖品已抽完),系统会发一个 "更新信号" 到 Redis,让 Redis 里的对应状态同步变化,避免缓存和数据库 "对不上" 导致校验出错。

举个例子:为什么活动状态不会乱变?​

比如某活动规定 "所有奖品抽完才结束":​

  • 当最后一个奖品被抽中时,系统会先在 Redis 里用事务校验:检查该奖品状态是否变为 "已抽取"(PRIZE_STATUS_1001_2001:DRAWN),且没有其他奖品还没抽(PRIZE_STATUS_1001_*全是DRAWN)。
  • 校验通过后,事务才会把活动状态从RUNNING改成ENDED,并同步到数据库。
  • 如果校验时发现还有一个奖品没抽(分状态不对),事务就会放弃,活动状态继续保持RUNNING。

简单说,原理就是 "用 Redis 事务把活动状态和关联状态绑成一个整体,要么一起变,要么都不变",确保活动状态的变化符合业务规则,不会因为中途出问题或数据不同步导致混乱。

抽奖活动列表创建(翻页):

时序图:

前后端交互接口:

java 复制代码
请求] /activity/find-list?currentPage=1&pageSize=10 GET

[响应]
{
    "code":200,
    "data";
        "total":10
        "records":[
            {
                "activityId": 23,
                "activityName":"抽奖测试",
                "description":"年会抽奖活动",
                "valid": true
            },
            {

                "activityId": 22,
                1"activityName":"抽奖测试",
                "description":"年会抽奖活动",
                "valid": true
            }
    "msg"111
}

Controller层接口设计:

java 复制代码
  /**
     * 查询活动列表
     * @param param
     * @return
     */
    @RequestMapping("/activity/find-list")
    public CommonResult<FindActivityListResult> findActivityList(PageParam param){
        logger.info("查询活动列表开始 findActivityList  PageParam :{}",
                JacksonUtil.writeValueAsString(param));
        return CommonResult.success(convertToFindActivityListResult(
                activityService.findActivityList(param)));
    }

    /**
     * 查询活动列表
     * 将FindPrizeListResult 转换成FindActivityListResult
     * @param activityList
     * @return
     */
    private FindActivityListResult convertToFindActivityListResult(PageListDTO<ActivityDTO> activityList) {
        // 判断活动列表是否为空,为空时抛出状态码为 301 的"查询活动列表失败"异常
        if (null == activityList) {
            throw new ControllerException(ControllerErrorCodeConstants.FIND_ACTIVITY_LIST_ERROR);
        }
        // 创建 FindActivityListResult对象,用于存储转换后的数据
        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;
    }

FindActivityListResult 查询结果:

java 复制代码
/**
 * 分页查询结果
 *
 * @author:
 */
@Data
public class FindActivityListResult implements Serializable {
    /**
     * 总量
     */
    private Integer total;

    /**
     * 当前列表
     */
    private List<ActivityInfo> records;

    @Data
    public static class ActivityInfo implements Serializable {

        /**
         * 活动id
         */
        private Long activityId;

        /**
         * 活动名称
         */
        private String activityName;

        /**
         * 活动描述
         */
        private String description;

        /**
         * 活动是否有效
         */
        private Boolean valid;

    }
}

PageParam请求参数:

java 复制代码
@Getter
@Setter
/**
 * 分页参数
 */
public class PageParam implements Serializable {
    //当前页码(默认从第一页开始)
    private Integer currentPage = 1;
    //当前页数量(默认显示10条数据)
    private Integer pageSize = 10;
    //计算偏移量
    public Integer offset(){
        //如果当前是第一页,1-1=0,0*10=0,就是从数据库中第0个数据开始
        //如果当前是第二页,2-1=1,1*10=10,就是从数据库中第10个数据开始
        return (currentPage - 1) * pageSize;
    }

}

Service层接口设计与实现:

java 复制代码
@Service
public interface ActivityService {
     /**
     * 翻页查找活动列表
     *
     */
    PageListDTO<ActivityDTO> findActivityList(PageParam param);
}

接口实现:

java 复制代码
  /**
     * 根据分页参数查询活动列表
     * <p>
     * 此方法首先查询所有活动的总数,然后根据当前页码和页面大小获取当前页的活动列表
     * 它将查询结果转换为DTO列表,并返回一个包含总数和当前页DTO列表的PageListDTO对象
     *
     * @param param 分页参数,包含当前页码和每页大小
     * @return 返回一个PageListDTO对象,包含活动总数和当前页的活动DTO列表
     */
    @Override
    public PageListDTO<ActivityDTO> findActivityList(PageParam param) {
        // 查询活动总数
        int total = activityMapper.count();

        //获取当前页列表
        List<ActivityDO> activityDOList = activityMapper.selectActivityList(param.offset(), param.getPageSize());
        //将ActivityDO转换为ActivityDTO
        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;
                }).toList();

        //返回包含总数和当前页DTO列表的PageListDTO对象
        return new PageListDTO<>(total, activityDTOList);

    }

Dao层接口设计:

java 复制代码
    /**
     *  分页查询活动列表
     */
    @Select("select * from activity order by id desc limit #{offset}, #{pageSize}")
    List<ActivityDO> selectActivityList(@Param("offset") Integer offset,
                                        @Param("pageSize") Integer pageSize);

接口测试:

查询活动详情信息:

activityDetailDTO类里面包含抽奖活动所需要的所有参数。

前端:向后端发起请求,从redis中查询相关的数据(如果没有存储,或者redis中的缓存过期,就会查询相关的数据库表,整合所需要的所有数据,再一次存放到redis中),最后奖数据返回给前端。

Controller层接口设计:

java 复制代码
 /**
     * 查询活动详情
     * @param activityId
     * @return
     */
    @RequestMapping("/activity-detail/find")
    public CommonResult<GetActivityDetailResult> getActivityDetail(Long activityId) {
        logger.info("getActivityDetail activityId:{}", activityId);
        ActivityDetailDTO detailDTO = activityService.getActivityDetail(activityId);
        return CommonResult.success(convertToGetActivityDetailResult(detailDTO));
    }
    /**
     *  将ActivityDetailDTO 转换成GetActivityDetailResult
     * @param detailDTO
     * @return
     */
    private GetActivityDetailResult convertToGetActivityDetailResult(
            ActivityDetailDTO detailDTO) {
        if(null == detailDTO){
            throw new ControllerException(ControllerErrorCodeConstants.GET_ACTIVITY_DETAIL_ERROR);
        }
        GetActivityDetailResult result = new GetActivityDetailResult();
        result.setActivityId(detailDTO.getActivityId());
        result.setActivityName(detailDTO.getActivityName());
        result.setDescription(detailDTO.getDesc());
        result.setValid(detailDTO.valid());
        // 抽奖顺序:一等奖、二、三
        result.setPrizes(
                detailDTO.getPrizeDTOList().stream()
                        .sorted(Comparator.comparingInt(prizeDTO -> prizeDTO.getTiers().getCode()))
                        .map(prizeDTO -> {
                            GetActivityDetailResult.Prize prize = new GetActivityDetailResult.Prize();
                            prize.setPrizeId(prizeDTO.getPrizeId());
                            prize.setName(prizeDTO.getName());
                            prize.setImageUrl(prizeDTO.getImageUrl());
                            prize.setPrice(prizeDTO.getPrice());
                            prize.setDescription(prizeDTO.getDescription());
                            prize.setPrizeTierName(prizeDTO.getTiers().getMessage());
                            prize.setPrizeAmount(prizeDTO.getPrizeAmount());
                            prize.setValid(prizeDTO.valid());
                            return prize;
                        }).collect(Collectors.toList())
        );
        result.setUsers(
                detailDTO.getUserDTOList().stream()
                        .map(userDTO -> {
                            GetActivityDetailResult.User user = new GetActivityDetailResult.User();
                            user.setUserId(userDTO.getUserId());
                            user.setUserName(userDTO.getUserName());
                            user.setValid(userDTO.valid());
                            return user;
                        }).collect(Collectors.toList())
        );
        return result;
    }

GetActivityDetailResult返回结果:

java 复制代码
@Data
public class GetActivityDetailResult implements Serializable {
    /**
     * 活动id
     */
    private Long activityId;

    /**
     * 活动名称
     */
    private String activityName;

    /**
     * 活动描述
     */
    private String description;

    /**
     * 活动是否有效
     */
    private Boolean valid;

    /**
     * 奖品信息(列表)
     */
    private List<Prize> prizes;

    /**
     * 人员信息(列表)
     */
    private List<User> users;


    @Data
    public static class Prize {
        /**
         * 奖品Id
         */
        private Long prizeId;
        /**
         * 奖品名
         */
        private String name;

        /**
         * 图片索引
         */
        private String imageUrl;

        /**
         * 价格
         */
        private BigDecimal price;

        /**
         * 描述
         */
        private String description;

        /**
         * 奖品等奖
         * @see ActivityPrizeTiersEnum#getMessage()
         */
        private String prizeTierName;

        /**
         * 奖品数量
         */
        private Long prizeAmount;

        /**
         * 奖品是否有效
         */
        private Boolean valid;
    }

    @Data
    public static class User {
        /**
         * 用户id
         */
        private Long userId;
        /**
         * 姓名
         */
        private String userName;
        /**
         * 人员是否被抽取
         */
        private Boolean valid;
    }

}

Service层接口设计与实现:

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


    /**
     * 翻页查找活动列表
     *
     */
    PageListDTO<ActivityDTO> findActivityList(PageParam param);

    /**
     *  获取活动详情属性
     * @param activityId
     * @return
     */
    ActivityDetailDTO getActivityDetail(Long activityId);
}


@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 * 3L ;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;

    @Autowired
    private RedisUtil redisUtil;

    /**
     * 根据活动Id获取活动详情
     * 此方法首先从缓存中获取活动详情,如果缓存中不存在,则从数据库中查询活动详情并缓存
     *
     * @param activityId 活动Id
     * @return 返回活动详情DTO
     */
    @Override
    public ActivityDetailDTO getActivityDetail(Long activityId) {
        if (null == activityId) {
            logger.warn("查询活动详细信息失败,activityId为空!");
            return null;
        }
        // 查询 redis
        ActivityDetailDTO detailDTO = getActivityFromCache(activityId);
        if (null != detailDTO) {
            logger.info("查询活动详细信息成功!detailDTO={}",
                    JacksonUtil.writeValueAsString(detailDTO));
            return detailDTO;
        }

        // 如果redis不存在,查表
        // 活动表
        ActivityDO aDO = activityMapper.selectById(activityId);

        // 活动奖品表
        List<ActivityPrizeDO> apDOList =  activityPrizeMapper.selectByActivityId(activityId);

        // 活动人员表
        List<ActivityUserDO> auDOList = activityUserMapper.selectByActivityId(activityId);

        // 奖品表: 先获取要查询的奖品id
        List<Long> prizeIds = apDOList.stream()
                .map(ActivityPrizeDO::getPrizeId)
                .collect(Collectors.toList());
        List<PrizeDO> pDOList = prizeMapper.batchSelectByIds(prizeIds);
        // 整合活动详细信息,存放redis
        detailDTO = convertToActivityDetailDTO(aDO, auDOList, pDOList, apDOList);
        cacheActivity(detailDTO);
        // 返回
        return detailDTO;
    }
}

Dao层设计:

java 复制代码
     /**
     *  根据活动id查询奖品信息
     * @param activityId
     * @return
     */
    @Select("select * from activity_prize where activity_id = #{activityId}")
    List<ActivityPrizeDO> selectByActivityId(@Param("activityId") Long activityId);


    /**
     * 根据活动id查询用户信息
     * @param activityId
     * @return
     */
    @Select("select * from activity_user where activity_id = #{activityId}")
    List<ActivityUserDO> selectByActivityId(@Param("activityId") Long activityId);

接口测试: