8. 抽奖活动
8.1 新建抽奖活动
创建的活动信息包含:
i. 活动名称
ii. 活动描述
iii. 圈选奖品:勾选对应奖品,并设置奖品等级(⼀⼆三等奖),及奖品数量
iv. 圈选⼈员:勾选参与抽奖⼈员
库表关联分析如下所示:
时序图如下:
将完整的信息放入到redis目的:
1、为了抽奖的时候确定抽奖的活动是哪一个。
2、活动+关联奖品+关联人员的完整信息,就是为了抽奖,根据关联的人员去进行抽奖。为了达到高效的目的,将完整的信息放入到缓存中、
8.2 后端代码实现
约定前后端交互接⼝:
8.2.1 创建活动的请求参数
@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 创建活动奖品关联表的请求参数
@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 创建活动人员关联表的请求参数
@Data
public class CreateUserByActivityParam implements Serializable {
/**
* 活动关联的人员id
*/
@NotNull(message = "活动关联的人员id不能为空!")
private Long userId;
/**
* 姓名
*/
@NotBlank(message = "姓名不能为空!")
private String userName;
}
8.2.4 活动奖品状态的枚举
@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 活动奖品等级的枚举
@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接口设计
@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接口及其实现
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层
@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:
@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:
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:
@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进行测试:
返回结果如下:
{
"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:谢谢观看。