目录
一、抽奖流程

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

三、参数列表
| 参数名 | 描述 | 类型 | 默认值 | 条件 |
|---|---|---|---|---|
| activityId | 活动id | Long | 必须 |
四、接口规范
[请求] /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);
八、测试

2270

被折叠的 条评论
为什么被折叠?



