【项目】【抽奖系统】抽奖 - 查询活动完整信息 - 详解


一、抽奖流程

  1. 参与者注册与奖品建⽴
    1.1. 参与者注册:管理员通过管理端新增⽤⼾, 填写必要的信息,如姓名、联系⽅式等。
    1.2. 奖品建⽴:奖品需要提前建⽴好
  2. 抽奖活动设置
    2.1. 活动创建:管理员在系统中创建抽奖活动,输⼊活动名称、描述、奖品列表等信息。
    2.2. 圈选⼈员: 关联该抽奖活动的参与者。
    2.3. 圈选奖品:圈选该抽奖活动的奖品,设置奖品等级、个数等。
    2.4. 活动发布:活动信息发布后,系统通过管理端界⾯展⽰活动列表。
  3. 抽奖请求处理(重要)
    3.1. 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
    3.2. 请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加
    信息。
    3.3. 消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。
    请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。
  4. 抽奖结果公布
    4.1. 前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。
  5. 抽奖逻辑执⾏(重要)
    5.1. 消息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。
  6. 中奖结果处理(重要)
    6.1. 请求验证:
    6.2. 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数限制等)等;
    6.3. 幂等性:若消息多发,已抽取的内容不能再次抽取
    6.4. 状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,⼈员是否已中奖等。
    6.5. 结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存。
  7. 中奖者通知
    7.1. 通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。
    7.2. 奖品领取:中奖者根据通知中的指引领取奖品。
  8. 抽奖异常处理
    8.1. 回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。
    8.2. 补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补救措施

技术实现细节

  • 异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂等性。
  • 活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。
  • 事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要做好事务处理。

二、查询活动完整信息简介

时序图:

三、参数列表

参数名描述类型默认值条件
activityId活动idLong必须

四、接口规范

[请求] /activity-detail/find?activityId=24 GET
[响应]
{
"code": 200,
"data": {
"activityId": 24,
"activityName": "测试抽奖活动",
"description": "测试抽奖活动",
"valid": true,
"prizes": [
{
"prizeId": 18,
"name": "⼿机",
"description": "⼿机",
"price": 5000.00,
"imageUrl": "e606c8db-218a-40c2-8946-0d9f8570626d.jpg",
"prizeAmount": 1,
"prizeTierName": "⼀等奖",
"valid": true
},
{
"prizeId": 19,
"name": "吹⻛机",
"description": "吹⻛机",
"price": 200.00,
"imageUrl": "63404e12-26f7-4974-9a99-41993586093c.jpg",
"prizeAmount": 1,
"prizeTierName": "⼆等奖",
"valid": true
}
],
"users": [
{
"userId": 44,
"userName": "郭靖",
"valid": true
},
{
"userId": 45,
"userName": "杨康",
"valid": true
}
]
},
"msg": ""
}

五、controller层

com/yj/lottery_system/controller 包下 ActivityController 类中:

  • 非空校验
  • 调用service
  • GetActivityDetailResult:controller层返回类
  • convertTOGetActivityDetailResult:service返回结果 转换 controller返回结果 方法
@RequestMapping("/activity-detail/find")
public CommonResult<GetActivityDetailResult> getActivityDetailFind (Long activityId) {
  //日志打印
  log.info("getActivityDetailFind activityId: {}", JacksonUtil.writeValueAsString(activityId));
  //调用service服务
  ActivityDetailDTO activityDetailDTO = activityService.getActivityDetailFind(activityId);
  return CommonResult.success(
  convertTOGetActivityDetailResult(activityDetailDTO));
  }

5.1 GetActivityDetailResult:controller层返回类

com.yj.lottery_system.controller.result 包下:

  • 其实跟前面创建活动的service层返回的ActivityDetailDTO类差不多,只不过将状态表示直接换成Boolean,不需要提供单独方法
  • 也可以对照请求响应,一一填写
package com.yj.lottery_system.controller.result;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;
/**
* @author: yibo
*/
@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;
    /**
    * 奖品等奖
    */
    private String prizeTierName;
    /**
    * 奖品数量
    */
    private Long prizeAmount;
    /**
    * 奖品是否有效
    */
    private Boolean valid;
    }
    @Data
    public static class User {
    /**
    * 用户id
    */
    private Long userId;
    /**
    * 姓名
    */
    private String userName;
    /**
    * 人员是否被抽取
    */
    private Boolean valid;
    }
    }

5.2 convertTOGetActivityDetailResult:service返回结果 转换 controller返回结果 方法

com/yj/lottery_system/controller 包下 ActivityController 类中:

  • 对于类中的List,使用老方法流来赋值即可。
  • 在对奖品赋值的时候,我们将奖品按照等级排个序
private GetActivityDetailResult convertTOGetActivityDetailResult(ActivityDetailDTO activityDetailDTO) {
if(null == activityDetailDTO) {
throw new ControllerException(ControllerErrorCodeConstants.GET_ACTIVITY_DETAILS_ERROR);
}
GetActivityDetailResult getActivityDetailResult = new GetActivityDetailResult();
getActivityDetailResult.setActivityId(activityDetailDTO.getActivityId());
getActivityDetailResult.setActivityName(activityDetailDTO.getActivityName());
getActivityDetailResult.setDescription(activityDetailDTO.getDesc());
getActivityDetailResult.setValid(activityDetailDTO.valid());
getActivityDetailResult.setPrizes(activityDetailDTO.getPrizeDTOList().stream()
//按照奖品等级排序
.sorted(Comparator.comparingInt(prizeDTO -> prizeDTO.getTires().getCode()))
.map(prizeDTO -> {
GetActivityDetailResult.Prize prize = new GetActivityDetailResult.Prize();
prize.setPrizeId(prizeDTO.getPrizeId());
prize.setDescription(prizeDTO.getDescription());
prize.setImageUrl(prizeDTO.getImageUrl());
prize.setName(prizeDTO.getName());
prize.setPrice(prizeDTO.getPrice());
prize.setPrizeTierName(prizeDTO.getTires().getMessage());
prize.setPrizeAmount(prizeDTO.getPrizeAmount());
prize.setValid(prizeDTO.valid());
return prize;
}).collect(Collectors.toList())
);
getActivityDetailResult.setUsers(activityDetailDTO.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 getActivityDetailResult;
}

5.3 新增错误码

com/yj/lottery_system/common/errorcode 包下 ControllerErrorCodeConstants.java类

ErrorCode GET_ACTIVITY_DETAILS_ERROR = new ErrorCode(302,"查询活动详情失败");

六、service层

6.1 创建接口

com/yj/lottery_system/service 包下 IActivityService接口类中:

/**
* 获取活动详情信息
* @param activityId
* @return
*/
ActivityDetailDTO getActivityDetailFind(Long activityId);

6.2 实现接口

com/yj/lottery_system/service/impl 包下 ActivityServiceImpl 类中:

  • 非空校验
  • 先在 Redis 里面查找,方法创建活动时已写好
  • 有直接返回,没有进行下一步查表
  • 调 dao 查 活动表,拿到活动信息
  • 调 dao 查 活动奖品表,拿到活动关联奖品信息
  • 调 dao 查 活动人员表,拿到活动关联人员信息
  • 调 dao 查 奖品表,拿到活动关联奖品详细信息
  • 整合详细信息,方法创建活动时已写好
  • 存放Redis,方法创建活动时已写好
/**
* 获取活动详情信息
* @param activityId
* @return
*/
@Override
public ActivityDetailDTO getActivityDetailFind(Long activityId) {
//查Redis
if(null == activityId) {
log.warn("查询活动详细信息失败 activityId不存在");
return null;
}
ActivityDetailDTO activityFromCache = getActivityFromCache(activityId);
if(activityFromCache != null) {
log.warn("查询活动详细信息成功 activityFromCache:{}", JacksonUtil.writeValueAsString(activityFromCache));
return activityFromCache;
}
//如果Redis没有,
// 查表 活动表
ActivityDO activityDO = activityMapper.selectById(activityId);
// 活动奖品表
List<ActivityPrizeDO> activityPrizeDOList = activityPrizeMapper.selectByActivityId(activityId);
  // 活动人员表
  List<ActivityUserDO> activityUserDOList = activityUserMapper.selectByActivityId(activityId);
    // 奖品表
    //先拿奖品id
    List<Long> prizeIdList = activityPrizeDOList.stream()
      .map(ActivityPrizeDO::getPrizeId)
      .collect(Collectors.toList());
      List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIdList);
        //整合详细信息,存放Redis
        ActivityDetailDTO activityDetailDTO = convertActivityDetailDTO(activityDO, activityUserDOList, activityPrizeDOList, prizeDOList);
        //存放Redis
        cacheActivity(activityDetailDTO);
        return activityDetailDTO;
        }

七、dao层

com/yj/lottery_system/dao/mapper 包下 ActivityMapper.java 类

/**
* 根据活动id查活动信息
* @param id
* @return
*/
@Select("select * from activity where id = #{id}")
ActivityDO selectById(@Param("id") Long id);

com/yj/lottery_system/dao/mapper 包下 ActivityPrizeMapper.java 类

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

com/yj/lottery_system/dao/mapper 包下 ActivityUserMapper.java 类

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

八、测试

posted @ 2026-01-20 17:43  yangykaifa  阅读(0)  评论(0)    收藏  举报