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

1. 活动创建

需求回顾

创建的活动信息包含:

  • 活动名称
  • 活动描述
  • 关联的一批奖品,关联时需要选择奖品等级(一等奖、二等奖、三等奖),及奖品库存
  • 圈选一批人员参与抽奖

 tip:什么时候设置奖品数量和奖品等级?

肯定是在创建活动的时候,这两项不能作为基本属性放到奖品表中(那样无意义,每次创建活动数量和等级可能都不一样),因此需要将其放到活动奖品奖品关联表中,如下:

时序图

tip:为了快速获取抽奖信息,所以将其存放到 Redis 缓存中

约定前后端交互接口

[请求]  /activity/create  POST

{

    "activityName":"测试活动抽奖",

    "description":"测试活动抽奖",

    "activityPrizeList":[{"prizeId":2, "prizeAmount":1, "prizeTiers":"FIRST_PRIZE"}, {"prizeId":3, "prizeAmount":2, "prizeTiers":"SECOND_PRIZE"}],

    "activityUserList":[{"userId":4, "userName":"郭靖"}, {"userId":5, "userName":"黄蓉"}, {"userId":6, "userName":"杨康"}]

}

[响应]

{

        "code": 200,

        "data": {

                "activityId": 1

        },

        "msg": ""

}

Controller 层接口设计

package com.example.lotterysystem.controller;import com.example.lotterysystem.common.errorcode.ControllerErrorCodeConstants;
import com.example.lotterysystem.common.exception.ControllerException;
import com.example.lotterysystem.common.pojo.CommonResult;
import com.example.lotterysystem.common.utils.JacksonUtil;
import com.example.lotterysystem.controller.param.CreateActivityParam;
import com.example.lotterysystem.controller.result.CreateActivityResult;
import com.example.lotterysystem.service.ActivityService;
import com.example.lotterysystem.service.dto.CreateActivityDTO;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
public class ActivityController {@Autowiredprivate ActivityService activityService;private static final Logger logger = LoggerFactory.getLogger(ActivityController.class);@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)));}private CreateActivityResult convertToCreateActivityResult(CreateActivityDTO createActivityDTO) {if (null == createActivityDTO) {throw new ControllerException(ControllerErrorCodeConstants.CREATE_ACTIVITY_ERROR);}CreateActivityResult result = new CreateActivityResult();result.setActivityId(createActivityDTO.getActivityId());return result;}
}

CreateActivityParam

package com.example.lotterysystem.controller.param;import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotEmpty;
import lombok.Data;import java.io.Serializable;
import java.util.List;@Data
public class CreateActivityParam implements Serializable {// 活动名称@NotBlank(message = "活动名称不能为空!")private String activityName;// 活动描述@NotBlank(message = "活动描述不能为空!")private String description;// 活动关联奖品列表@NotEmpty(message = "活动关联奖品列表不能为空!")@Valid // 上面 NotEmpty 只能确保 list 不能为空,想要 list 里面 CreatePrizeByActivityParam 的字段不能为空,需要加上 Validprivate List<CreatePrizeByActivityParam> activityPrizelist;// 活动关联人员列表@NotEmpty(message = "活动关联人员列表不能为空!")@Validprivate List<CreateUserByActivityParam> activityUserlist;
}

CreateUserByActivityParam

package com.example.lotterysystem.controller.param;import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;import java.io.Serializable;@Data
public class CreateUserByActivityParam implements Serializable {// 活动关联的人员id@NotNull(message = "活动关联的人员 id 不能为空!")private Long userId;// 活动关联的人员姓名@NotBlank(message = "活动关联的人员姓名不能为空!")private String userName;
}

ControllerErrorCodeConstants

// package com.example.lotterysystem.common.errorcode;// ------ 活动模块错误码 ------ErrorCode CREATE_ACTIVITY_ERROR = new ErrorCode(300, "活动创建失败!");

Service 层接口设计

package com.example.lotterysystem.service;import com.example.lotterysystem.controller.param.CreateActivityParam;
import com.example.lotterysystem.service.dto.CreateActivityDTO;public interface ActivityService {// 创建活动CreateActivityDTO createActivity(CreateActivityParam param);
}

CreateActivityDTO

package com.example.lotterysystem.service.dto;import lombok.Data;@Data
public class CreateActivityDTO {// 活动idprivate Long activityId;
}

接口实现

package com.example.lotterysystem.service.impl;import com.example.lotterysystem.common.errorcode.ServiceErrorCodeConstants;
import com.example.lotterysystem.common.exception.ServiceException;
import com.example.lotterysystem.common.utils.JacksonUtil;
import com.example.lotterysystem.common.utils.RedisUtil;
import com.example.lotterysystem.controller.param.CreateActivityParam;
import com.example.lotterysystem.controller.param.CreatePrizeByActivityParam;
import com.example.lotterysystem.controller.param.CreateUserByActivityParam;
import com.example.lotterysystem.dao.dataobject.ActivityDO;
import com.example.lotterysystem.dao.dataobject.ActivityPrizeDO;
import com.example.lotterysystem.dao.dataobject.ActivityUserDO;
import com.example.lotterysystem.dao.dataobject.PrizeDO;
import com.example.lotterysystem.dao.mapper.*;
import com.example.lotterysystem.service.ActivityService;
import com.example.lotterysystem.service.dto.ActivityDetailDTO;
import com.example.lotterysystem.service.dto.CreateActivityDTO;
import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;
import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;
import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.View;import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;@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;@Autowiredprivate UserMapper userMapper;@Autowiredprivate PrizeMapper prizeMapper;@Autowiredprivate ActivityMapper activityMapper;@Autowiredprivate ActivityUserMapper activityUserMapper;@Autowiredprivate ActivityPrizeMapper activityPrizeMapper;@Autowiredprivate RedisUtil redisUtil;@Autowiredprivate View error;@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.setStaus(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)// 先获取奖品基本属性表// 获取需要查询的奖品idList<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);cacheActivity(detailDTO);// 构造返回CreateActivityDTO createActivityDTO = new CreateActivityDTO();createActivityDTO.setActivityId(activityDO.getId());return createActivityDTO;}// 缓存完整的活动信息 ActivityDetailDTOprivate void cacheActivity(ActivityDetailDTO detailDTO) {// key: ACTIVITY_12// value: ActivityDetailDTO(需先转为 json)if (null == detailDTO || null == detailDTO.getActivityId()) {logger.warn("要缓存的活动信息不存在!");return;}/*** 前面创建三张表时,遇到异常会进行回滚,但这里缓存完整活动信息时遇到异常是不需要回滚的* 因为即使 redis 没有缓存成功,当抽奖端查询活动信息时依然能根据前面创建好的三张表重新将活动缓存到 redis 中* 因此此处用 try catch 包住该段代码,使其即使抛出异常,也不会触发回滚*/try {redisUtil.set(ACTIVITY_PREFIX+detailDTO.getActivityId(),JacksonUtil.writeValueAsString(detailDTO),ACTIVITY_TIMEOUT);} catch (Exception e) {logger.error("缓存活动异常,ActivityDetailDTO={}", JacksonUtil.writeValueAsString(detailDTO), e);}}// 根据活动 id 从缓存中获取活动详细信息private ActivityDetailDTO getActivityFromCache(Long activityId) {if (null == activityId) {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;}}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.getStaus()));// 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<> 防止对象存在空指针异常//.stream() 对 prizeDOList 进行流式处理,拿到每一个 prizeDO 和外层 apDO 的 id 去进行对比// .findFirst() 返回遇到的第一个 id 相等的对象Optional<PrizeDO> optionalPrizeDO = prizeDOList.stream().filter(prizeDO -> prizeDO.getId().equals(apDO.getPrizeId())).findFirst();// ifPresent:如果 PrizeDO 不为空,才执行该方法,不用自己写 if 判断 PrizeDO 是否为空了optionalPrizeDO.ifPresent(prizeDO -> {// 下面四个属性需要遍历 pDO,根据 apDO 中的 prizeId 和 pDO 中的 prizeId 找到对应的属性进行设置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);// auDO 就是 activityUserDOList<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) {if(null == param) {throw new ServiceException(ServiceErrorCodeConstants.CREATE_ACTIVITY_INFO_IS_EMPTY);}// 人员 id 在人员表中是否存在List<Long> userIds = param.getActivityUserlist().stream().map(CreateUserByActivityParam::getUserId).distinct() // 去重.collect(Collectors.toList());// 假设传过去的参数为 1 2 3,若人员表中只存在 1 2,则只返回 1 2List<Long> existUserIds = userMapper.selectExistByIds(userIds);userIds.forEach(id -> {// 若 userMapper 返回的人员表参数中不包含某个创建的活动关联人员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);prizeIds.forEach(id -> {if (!existPrizeIds.contains(id)) {throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_ERROR);}});// 人员数量是否大于等于奖品数量int userAmount = param.getActivityUserlist().size();long prizeAmount = param.getActivityPrizelist().stream().mapToLong(CreatePrizeByActivityParam::getPrizeAmount).sum();if (userAmount < prizeAmount) {throw new ServiceException(ServiceErrorCodeConstants.USER_PRIZE_AMOUNT_ERROR);}// 校验活动奖品等级有效性(看传入参数是否为一等二等三等奖)param.getActivityPrizelist().forEach(prize -> {if (null == ActivityPrizeTiersEnum.forName(prize.getPrizeTiers())) {throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_TIERS_ERROR);}});}
}

插件:GenerateAllSetter 快速生成 Set 方法

活动/活动奖品/活动人员状态/奖品等级 枚举类(Enum)

package com.example.lotterysystem.service.enums;import lombok.AllArgsConstructor;
import lombok.Getter;// 活动状态
@AllArgsConstructor
@Getter
public enum ActivityStatusEnum {RUNNING(1, "活动进行中"),COMPLETED(2, "活动已完成");private final Integer code;private final String message;public static ActivityStatusEnum forName(String name) {for (ActivityStatusEnum activityStatusEnum : ActivityStatusEnum.values()) {if (activityStatusEnum.name().equalsIgnoreCase(name)) {return activityStatusEnum;}}return null;}
}package com.example.lotterysystem.service.enums;import lombok.AllArgsConstructor;
import lombok.Getter;// 活动奖品状态
@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;}
}package com.example.lotterysystem.service.enums;import lombok.AllArgsConstructor;
import lombok.Getter;// 活动人员状态
@AllArgsConstructor
@Getter
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().equalsIgnoreCase(name)) {return activityUserStatusEnum;}}return null;}
}package com.example.lotterysystem.service.enums;import lombok.AllArgsConstructor;
import lombok.Getter;// 奖品等级
@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;}
}

ActivityDetailDTO

package com.example.lotterysystem.service.dto;import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;
import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;
import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;
import lombok.Data;import java.math.BigDecimal;
import java.util.List;@Data
public class ActivityDetailDTO {// 活动信息// 活动idprivate Long activityId;// 活动名称private String activityName;// 活动描述private String desc;// 活动状态private ActivityStatusEnum status;// 通过该方法判断当前活动是否有效public Boolean valid() {return status.equals(ActivityStatusEnum.RUNNING);}// 奖品信息(列表)private List<PrizeDTO> prizeDTOList;// 人员信息(列表)private List<UserDTO> userDTOList;@Datapublic static class PrizeDTO {// 奖品idprivate Long prizeId;// 奖品名private String name;// 图片索引private String imageUrl;// 价格private BigDecimal price;// 描述private String description;// 奖品等级private ActivityPrizeTiersEnum tiers;// 奖品数量private Long prizeAmount;// 奖品状态private ActivityPrizeStatusEnum status;// 通过该方法判断当前奖品是否被抽取public Boolean valid() {return status.equals(ActivityPrizeStatusEnum.INIT);}}@Datapublic static class UserDTO {// 用户idprivate Long userId;// 姓名private String userName;// 状态private ActivityUserStatusEnum status;// 通过该方法判断当前人员是否中奖public Boolean valid() {return status.equals(ActivityUserStatusEnum.INIT);}}
}

ServiceErrorCodeConstants

// package com.example.lotterysystem.common.errorcode;// ------ 活动模块错误码 ------ErrorCode CREATE_ACTIVITY_INFO_IS_EMPTY = new ErrorCode(300, "创建的活动信息为空!");ErrorCode ACTIVITY_USER_ERROR = new ErrorCode(301, "活动关联的人员不存在!");ErrorCode ACTIVITY_PRIZE_ERROR = new ErrorCode(302, "活动关联的奖品不存在!");ErrorCode USER_PRIZE_AMOUNT_ERROR = new ErrorCode(303, "活动关联的人员数量必须等于或大于奖品数量!");ErrorCode ACTIVITY_PRIZE_TIERS_ERROR = new ErrorCode(304, "活动奖品等级设置错误!");

Dao 层接口设计

ActivityMapper

package com.example.lotterysystem.dao.mapper;import com.example.lotterysystem.dao.dataobject.ActivityDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;@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);
}

ActivityDO

package com.example.lotterysystem.dao.dataobject;import lombok.Data;
import lombok.EqualsAndHashCode;@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityDO extends BaseDO{// 活动名称private String activityName;// 活动描述private String description;// 活动状态private String staus;}

ActivityPrizeMapper

package com.example.lotterysystem.dao.mapper;import com.example.lotterysystem.dao.dataobject.ActivityPrizeDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;import java.util.List;@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);}

ActivityPrizeDO

package com.example.lotterysystem.dao.dataobject;import lombok.Data;
import lombok.EqualsAndHashCode;@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityPrizeDO extends BaseDO{// 关联的活动idprivate Long activityId;// 关联的奖品idprivate Long prizeId;// 奖品数量private Long prizeAmount;// 奖品等级private String prizeTiers;// 奖品状态private String status;
}

PrizeMapper 新增接口

// package com.example.lotterysystem.dao.mapper;@Select("<script>" +" select id from prize" +" where id in" +" <foreach item='item' collection='items' open='(' separator=',' close=')'>" +" #{item}" +" </foreach>" +" </script>")List<Long> selectExistByIds(@Param("items") List<Long> ids);@Select("<script>" +" select * from prize" +" where id in" +" <foreach item='item' collection='items' open='(' separator=',' close=')'>" +" #{item}" +" </foreach>" +" </script>")List<PrizeDO> batchSelectByIds(@Param("items") List<Long> ids);

ActivityUserMapper

package com.example.lotterysystem.dao.mapper;import com.example.lotterysystem.dao.dataobject.ActivityUserDO;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;import java.util.List;@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);
}

ActivityUserDO

package com.example.lotterysystem.dao.dataobject;import lombok.Data;
import lombok.EqualsAndHashCode;@Data
@EqualsAndHashCode(callSuper = true)
public class ActivityUserDO extends BaseDO{// 关联的活动idprivate Long activityId;// 关联的人员idprivate Long userId;// 姓名private String userName;// 关联人员状态private String status;
}

Postman 测试

先启动 redis 服务

使用无效的人员数据进行测试:

使用无效的奖品数据测试:

使用抽奖人员比奖品少的数据进行测试:

使用有效数据测试:

查看数据库

活动表里存放了新建的活动:

活动奖品关联表中存放了活动关联的奖品:

活动人员关联表中存放了活动关联的人员:

查看 Redis,存放了新建的活动

活动创建页面前端实现

在创建活动之前,需要先查询到奖品列表和用户列表才能提供给前端进行圈选

// create-activity.html<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>创建抽奖活动</title><link rel="stylesheet" href="https://cdn.staticfile.org/twitter-bootstrap/4.5.2/css/bootstrap.min.css"><link rel="stylesheet" href="./css/base.css"><link rel="stylesheet" href="./css/toastr.min.css"><style>body {font-family: Arial, sans-serif;background-color: #fff;margin: 0;padding: 0;}.container {max-width: 800px;margin: 30px auto;padding: 20px 30px;background-color: #fff;}.prize-checkbox {margin-bottom: 10px;}.modal {display: none; /* 初始状态下模态框不可见 */position: fixed;z-index: 1;left: 0;top: 0;bottom: 0;right: 0;overflow: hidden;overflow-y: auto;background-color: rgba(0, 0, 0, 0.1);}.modal-content {background-color: #fefefe;margin: 5% auto;padding: 20px;border: 1px solid #888;width: 610px;}.modal-content h2{display: flex;justify-content: space-between;align-items: center;font-weight: 600;font-size: 18px;color: #000000;height: 50px;border-bottom: 1px solid #DEDEDE;margin-bottom: 30px;}#prizesContainer{height: 406px;margin-bottom: 40px;overflow-y: auto;padding: 0 26px;}.close {color: #000;float: right;font-size: 28px;cursor: pointer;}.close:hover,.close:focus {color: black;text-decoration: none;cursor: pointer;}.prize-item,.user-item {display: flex;align-items: center;margin-bottom: 10px;justify-content: center;}.custom-p {font-size: 16px;/* 设置字体粗细 */font-weight: bold;margin-right: 90px; /* 右侧外边距 */}.prize-item input[type="checkbox"],.user-item input[type="checkbox"] {margin-right: 99px;}.prize-item label,.user-item label {margin-right: 11px;margin-bottom: 0;width: 200px;}.prize-item select {margin-left: auto; /* 将下拉选择框放置在末尾 */}.prize-item .form-control {width: 96px;height: 36px;line-height: 36px;margin-right: 56px;}.h-title{font-weight: 600;font-size: 30px;letter-spacing: 1px;color: #000000;line-height: 50px;text-align: center;margin-bottom: 40px;}.desc-row{margin-bottom: 60px;}.form-btn-box{display: flex;align-items: center;justify-content: center;}.form-btn-box button{width: 148px;height: 48px;}.pre-btn{margin-right: 20px;}</style>
</head>
<body>
<div class="container"><h2 class="h-title">创建抽奖活动</h2><form id="activityForm"><div class="form-group"><label for="activityName">活动名称</label><input type="text" placeholder="请输入活动名称" class="form-control" class="form-control" id="activityName" name="activityName" required></div><div class="form-group desc-row"><label for="description">活动描述</label><textarea id="description" placeholder="请输入活动描述" rows="5" cols="33" class="form-control" name="description" required></textarea></div><div class="form-btn-box"><button id="buttonPrizes" type="button" class="btn btn-primary pre-btn" onclick="showPrizesModal()">圈选奖品</button><button id="buttonUsers" type="button" class="btn btn-primary pre-btn" onclick="showUsersModal()">圈选人员</button><button type="submit"  class="btn btn-primary" id="createActivity">创建活动</button></div></form>
</div>
<!-- toast提示 -->
<div class="toast"></div>
<!-- 奖品选择模态框 -->
<div id="prizesModal" class="modal"><div class="modal-content"><h2>奖品列表<span class="close"  onclick="hidePrizesModal()">&times;</span></h2><div class="prize-item"><p class="custom-p">勾选</p><p class="custom-p">奖品名</p><p class="custom-p">数量</p><p class="custom-p">奖品等级</p></div><div id="prizesContainer"><!-- 奖品列表将动态插入这里 --></div><div class="form-btn-box"><button type="button" class="btn btn-secondary pre-btn" onclick="hidePrizesModal()">取消</button><button type="button" class="btn btn-primary" onclick="submitPrizes()">确定</button></div></div>
</div>
<!-- 人员选择模态框 -->
<div id="usersModal" class="modal"><div class="modal-content"><h2>人员列表<span class="close"  onclick="hideUsersModal()">&times;</span></h2><div id="usersContainer"><!-- 奖品列表将动态插入这里 --></div><div class="form-btn-box"><button type="button" class="btn btn-secondary pre-btn" onclick="hideUsersModal()">取消</button><button type="button" class="btn btn-primary" onclick="submitUsers()">确定</button></div></div>
</div><!-- JavaScript代码 -->
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script  src="https://cdn.bootcdn.net/ajax/libs/jquery-validate/1.19.3/jquery.validate.min.js"></script>
<script src="./js/toastr.min.js"></script>
<script>var userToken = localStorage.getItem("user_token");// 初始时奖品列表为空,勾选的奖品会存放到这里var selectedPrizes = [];// 显示奖品选择模态框function showPrizesModal() {$('#prizesModal').css('display', 'block');}// 隐藏奖品选择模态框function hidePrizesModal() {$('#prizesModal').css('display', 'none');}// 获取奖品列表的函数function fetchPrizes() {$.ajax({url: '/prize/find-list',type: 'GET',dataType: 'json',data: { currentPage: 1, pageSize: 100},headers: {// jwt"user_token": userToken},success: function(result) {var prizes = result.data.records;var prizesContainer = $('#prizesContainer');prizesContainer.empty(); // 清空当前奖品列表// 遍历完奖品列表后,再一行一行插入到模态框中prizes.forEach(function(prize) {prizesContainer.append($('<div class="prize-item">').append(`<input type="checkbox" id="prize-${prize.prizeId}" name="prize-${prize.prizeId}" value="${prize.prizeId}"><label for="prize-${prize.prizeId}">${prize.prizeName}</label><input class="form-control" type="number" name="quantity-${prize.prizeId}" min="1" value="1"><select class="form-control" name="level-${prize.prizeId}"><option value="FIRST_PRIZE" selected>一等奖</option><option value="SECOND_PRIZE">二等奖</option><option value="THIRD_PRIZE">三等奖</option></select>`));});},error: function(err){console.log(err);if(err!=null && err.status==401){alert("用户未登录, 即将跳转到登录页!");// 跳转登录页window.location.href = "/blogin.html";window.parent.location.href = "/blogin.html";//让父页面一起跳转}}});}// 提交奖品数据的函数function submitPrizes() {selectedPrizes = [];// 将选中的奖品信息存储在selectedPrizes$('.prize-item input[type="checkbox"]:checked').each(function() {var prizeId = +$(this).val();var prizeAmount = +$('input[name="quantity-' + prizeId + '"]').val();var prizeTiers = $('select[name="level-' + prizeId + '"]').val();selectedPrizes.push({prizeId: prizeId,prizeAmount: prizeAmount,prizeTiers: prizeTiers});});// 关闭模态框hidePrizesModal();//  修改按钮var nextButton = document.getElementById('buttonPrizes');if (selectedPrizes.length > 0) {nextButton.textContent = '圈选奖品(已选)';} else {nextButton.textContent = '圈选奖品';}}// 初始时人员列表为空var selectedUsers = [];// 显示人员选择模态框function showUsersModal() {$('#usersModal').css('display', 'block');}// 隐藏人员选择模态框function hideUsersModal() {$('#usersModal').css('display', 'none');}// 获取人员列表的函数function fetchUsers() {$.ajax({url: '/base-user/find-list',type: 'GET',dataType: 'json',data: { identity: 'NORMAL' },headers: {// jwt"user_token": userToken},success: function(result) {var users = result.data;var usersContainer = $('#usersContainer');usersContainer.empty(); // 清空当前人员列表users.forEach(function(user) {console.info(user);usersContainer.append($('<div class="user-item">').append(`<input type="checkbox" id="user-${user.userId}" name="user-${user.userId}" value="${user.userId}"><label for="user-${user.userId}">${user.userName}</label>`));});},error:function(err){console.log(err);if(err!=null && err.status==401){alert("用户未登录, 即将跳转到登录页!");// 跳转登录页window.location.href = "/blogin.html";window.parent.location.href = "/blogin.html";//让父页面一起跳转}}});}// 提交用户数据的函数function submitUsers() {selectedUsers = [];// 将选中的奖品信息存储在selectedUsers$('.user-item input[type="checkbox"]:checked').each(function() {var userId = +$(this).val();var userName = $(this).next('label').text();selectedUsers.push({userId: userId,userName: userName});});// 关闭模态框hideUsersModal();//  修改按钮var nextButton = document.getElementById('buttonUsers');if (selectedUsers.length > 0) {nextButton.textContent = '圈选人员(已选)';} else {nextButton.textContent = '圈选人员';}}// 绑定表单提交事件$('#createActivity').click(function(event){event.stopPropagation()$('#activityForm').validate({rules:{activityName:"required",description:{required:true,}},messages:{activityName:"请输入活动名称",description:"请输入活动描述"},// 验证通过才会触发submitHandler:function(form){console.log('selectedPrizes',selectedPrizes)console.log('selectedUsers',selectedUsers)// 如果未选择奖品则进行toast提示if(selectedPrizes.length==0){alert('请至少选择一个奖品')return false}// 如果未选择人员则进行toast提示if(selectedUsers.length==0){alert('请至少选择一个人员, 人员数量应大于等于奖品总量')return false}// 获取提交表单信息var data = {activityName:'',description:'',activityPrizeList:[],activityUserList:[]}data.activityName = $('#activityName').val()data.description = $('#description').val()data.activityPrizeList = selectedPrizesdata.activityUserList = selectedUserssubmitActivity(data)}})})// 提交活动信息接口function submitActivity(data){$.ajax({url: '/activity/create',type: 'POST',contentType: 'application/json',data: JSON.stringify(data),headers: {// jwt"user_token": userToken},success: function(result) {if (result.code != 200) {alert("创建失败!" + result.msg);} else {alert("创建成功!");// 向父页面传值  https://developer.mozilla.org/zh-CN/docs/Web/API/Window/postMessagewindow.parent.postMessage({from:'activities-list.html',id:'#activitiesList'},'*');}},error:function(err){console.log(err);if(err!=null && err.status==401){alert("用户未登录, 即将跳转到登录页!");// 跳转登录页window.location.href = "/blogin.html";window.parent.location.href = "/blogin.html";//让父页面一起跳转}}});}// 获取奖品/人员列表并填充模态框$(document).ready(function() {fetchPrizes();fetchUsers();});// 显示奖品选择模态框$(document).ready(function() {$('#activityForm').on('click', 'button圈选奖品', function() {showPrizesModal();});});// 显示人员选择模态框$(document).ready(function() {$('#activityForm').on('click', 'button圈选人员', function() {showUsersModal();});});
</script>
</body>
</html>

测试

2. 活动列表展示(翻页)

时序图

约定前后端交互接口

[请求]  /activity/find-list?currentPage=1&pageSize=10  GET

{}

[响应]

{

}

Controller 层接口设计

    // 查询活动列表@RequestMapping("/activity/find-list")public CommonResult<FindActivityListResult> findActivityLIst(PageParam param) {logger.info("findActivityLIst PageParam:{}", JacksonUtil.writeValueAsString(param));return CommonResult.success(convertToFindActivityListResult(activityService.findActivityList(param)));}private FindActivityListResult convertToFindActivityListResult(PageListDTO<ActivityDTO> activityList) {if (null == activityList) {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;}

FindActivityListResult

package com.example.lotterysystem.controller.result;import lombok.Data;import java.io.Serializable;
import java.util.List;// 查询活动列表
@Data
public class FindActivityListResult implements Serializable {// 奖品总量private Integer total;// 当前列表private List<ActivityInfo> records;@Datapublic static class ActivityInfo implements Serializable {// 活动idprivate Long activityId;// 活动名称private String activityName;// 活动描述private String description;// 活动是否有效private Boolean valid;}
}

Service 层接口设计

// package com.example.lotterysystem.service;// 翻页查询活动(摘要)列表PageListDTO<ActivityDTO> findActivityList(PageParam param);

ActivityDTO

package com.example.lotterysystem.service.dto;import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import lombok.Data;@Data
public class ActivityDTO {// 活动idprivate Long activityId;// 活动名称private String activityName;// 活动描述private String description;// 活动状态private ActivityStatusEnum status;// 判断当前的活动是否有效public Boolean valid() {return status.equals(ActivityStatusEnum.RUNNING);}
}

接口实现

// package com.example.lotterysystem.service.impl;@Overridepublic 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 层接口设计

// package com.example.lotterysystem.dao.mapper;// 查询总量@Select("select count(1) from activity")int count();/*** 先根据 id 进行降序排序,然后根据偏移量获取数据库中对应大小的数据* 通过 list 返回** @param offset* @param pageSize* @return*/@Select("select * from activity order by id desc limit #{offset}, #{pageSize}")List<ActivityDO> selectActivityList(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize);

Postman 测试

tip:测试时别忘了在拦截器中忽略该路径

活动列表页前端实现

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><title>活动列表</title><link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/bootstrap/4.5.2/css/bootstrap.min.css"><link rel="stylesheet" href="./css/base.css"><style>body {font-family: Arial, sans-serif;background-color: #f2f2f2;}.activity-list {padding:0 30px;}#activities{height: calc(100vh - 134px);overflow-y: auto;padding-right: 10px;}.activity-item {display: flex;align-items: center;justify-content: space-between;background-color: #f7f7f7;padding: 24px;border-radius: 4px;overflow: hidden;margin-bottom: 10px;border-radius: 8px;padding-bottom: 12px;}.activity-info{width: calc(100% - 120px);}.activity-info h4{width: 100%;font-weight: 600;font-size: 15px;color: #000000;margin-bottom: 4px;}.activity-info p{font-weight: 400;font-size: 14px;color: #666666;margin: 0;line-height: 28px;}.active a{font-weight: 400;font-size: 15px;color: red;margin-bottom: 0;display: block;width: 250px;}.inactive a{font-weight: 400;font-size: 15px;color: gray;margin-bottom: 0;display: block;width: 250px;}.pagination {display: flex;justify-content: flex-end;margin-top: 18px;padding-right: 16px;}.pagination button {margin: 0 5px; /* 按钮之间的间距保持不变 */border-radius: 5px; /* 设置圆角为20像素,可以根据需要调整 */border: 1px solid #007bff;background-color: #fff;padding: 0px 8px; /* 可以添加一些内边距,使按钮看起来更饱满 */cursor: pointer; /* 将鼠标光标改为指针形状,提升用户体验 */font-size: 13px;}.pagination span{margin: 0 10px;font-size: 14px;}.pagination input{width: 80px;text-align: center;}.activity-list h2 {font-weight: 600;font-size: 18px;color: #000000;height: 70px;display: flex;align-items: center;margin-bottom: 0;}</style>
</head>
<body style="background-color: white">
<div class="activity-list"><h2>活动列表</h2><div id="activities"><!-- 活动列表将动态插入这里 --></div><div class="pagination"><button class="btn-outline-primary" onclick="fetchActivities(1)">首页</button><button class="btn-outline-primary" onclick="previousPage()">上一页</button><span>第 <input type="number" id="pageInput" min="1" value="1" /> 页</span><button class="btn-outline-primary" onclick="nextPage()">下一页</button><button class="btn-outline-primary" onclick="fetchActivities(totalPages)">尾页</button></div>
</div><script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.0/jquery.min.js"></script>
<script src="./js/toastr.min.js"></script>
<script>var currentPage = 1;var pageSize = 10;var totalPages;var userToken = localStorage.getItem("user_token");// 发送AJAX请求的函数function fetchActivities(page) {// 如果页码小于1,则重置为1if (page < 1) {page = 1;}// 更新当前页码currentPage = page;// 构建要发送的数据对象var dataToSend = {currentPage: currentPage,pageSize: pageSize};// 发送AJAX请求$.ajax({url: '/activity/find-list',type: 'GET',data: dataToSend, // 将分页参数作为请求数据发送dataType: 'json',headers: {// jwt"user_token": userToken},success: function(result) {if (result.code != 200) {alert("查询活动列表失败!" + result.msg);} else {var activities = result.data.records; // 假设返回的数据中活动列表字段为 'records'var activitiesHtml = '';var listContainer = document.getElementById('activities');// 在添加新内容前,先清空listContainerlistContainer.innerHTML = '';activities.forEach(function(activity) {var url = 'draw.html?activityName='+ encodeURIComponent(activity.activityName)+'&activityId=' + encodeURIComponent(activity.activityId)+'&valid=' + encodeURIComponent(activity.valid);var linkTextActive = `<a href="${url}" target="_blank">活动进行中,去抽奖</a>`;var linkTextInactive = `<a href="${url}" target="_blank">活动已完成,查看中奖名单</a>`;var validClass = activity.valid ? 'active' : 'inactive';var link = activity.valid ? linkTextActive : linkTextInactive;activitiesHtml += `<div class="activity-item"><div class="activity-info"><h4>${activity.activityName}</h4><p>${activity.description}</p></div><div class="${validClass}"><p>${link}</p></div></div>`;});$('#activities').html(activitiesHtml);// 更新分页控件的总页数totalPages = Math.ceil(result.data.total / pageSize);// 更新输入框的值$('#pageInput').val(currentPage);} // else end},error:function(err){console.log(err);if(err!=null && err.status==401){alert("用户未登录, 即将跳转到登录页!");// 跳转登录页window.location.href = "/blogin.html";window.parent.location.href = "/blogin.html";//让父页面一起跳转}}});}function previousPage() {if (currentPage > 1) {fetchActivities(currentPage - 1);} else {alert("已经是第一页");}}function nextPage() {if (currentPage < totalPages) {fetchActivities(currentPage + 1);} else {alert("已经是最后一页");}}$(document).ready(function() {fetchActivities(1);});// 绑定输入框回车事件$('#pageInput').on('keypress', function(e) {if (e.key === 'Enter') {var page = parseInt(this.value);if(page > totalPages){page = totalPages$('#pageInput').val(totalPages);}if (!isNaN(page) && page >= 1 && page <= totalPages) {fetchActivities(page);}}});</script>
</body>
</html>

测试

 3. 查询活动详情接口

时序图

约定前后端交互接口

[请求]  /activity-detail/find?activityId=24 GET

[响应]

{

    "code": 200,

    "data": {

        "activityId": 1,

        "activityName": "测试活动抽奖",

        "desc": "测试活动抽奖",

        "valid": true,

        "prizeDTOList": [

            {

                "prizeId": 2,

                "name": "手机",

                "imageUrl": "0a767457-513d-4b02-bdb4-fbf7f57b5b8c.png",

                "price": 5000.00,

                "description": "手机",

                "tiers": "一等奖",

                "prizeAmount": 1,

                "valid": true

            },

            {

                "prizeId": 3,

                "name": "吹风机",

                "imageUrl": "72f4d9a5-b7c8-48eb-b26e-39d2c685beb6.png",

                "price": 200.00,

                "description": "吹风机",

                "tiers": "二等奖",

                "prizeAmount": 2,

                "valid": true

            }

        ],

        "userDTOList": [

            {

                "userId": 4,

                "userName": "郭靖",

                "valid": true

            },

            {

                "userId": 5,

                "userName": "黄蓉",

                "valid": true

            },

            {

                "userId": 6,

                "userName": "杨康",

                "valid": true

            }

        ]

    },

    "msg": ""

}

Controller 层

ActivityController 新增

    // 查询活动详情@RequestMapping("/activity-detail/find")private CommonResult<GetActivityDetailResult> getActivityDetail(Long activityId) {logger.info("getActivityDetail activityId:{}", activityId);ActivityDetailDTO detailDTO = activityService.getActivityDetail(activityId);return CommonResult.success(convertToGetActivityDetailResult(detailDTO));}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.setDesc(detailDTO.getDesc());result.setValid(detailDTO.valid());// 抽奖顺序:一等奖、二等奖、三等奖result.setPrizeDTOList(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.setTiers(prizeDTO.getTiers().getMessage());prize.setPrizeAmount(prizeDTO.getPrizeAmount());prize.setValid(prizeDTO.valid());return prize;}).collect(Collectors.toList()));result.setUserDTOList(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

package com.example.lotterysystem.controller.result;import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;import lombok.Data;import java.io.Serializable;
import java.math.BigDecimal;
import java.util.List;@Data
public class GetActivityDetailResult implements Serializable {// 活动信息// 活动idprivate Long activityId;// 活动名称private String activityName;// 活动描述private String desc;// 活动是否有效private Boolean valid;// 奖品信息(列表)private List<Prize> prizeDTOList;// 人员信息(列表)private List<User> userDTOList;@Datapublic static class Prize {// 奖品idprivate Long prizeId;// 奖品名private String name;// 图片索引private String imageUrl;// 价格private BigDecimal price;// 描述private String description;// 奖品等级private String tiers;/*** 奖品数量* @see ActivityPrizeTiersEnum#getMessage()*/private Long prizeAmount;// 奖品是否被抽取private Boolean valid;}@Datapublic static class User {// 用户idprivate Long userId;// 姓名private String userName;// 该人员是否抽中private Boolean valid;}
}

ControllerErrorCodeConstants

// package com.example.lotterysystem.common.errorcode;ErrorCode GET_ACTIVITY_DETAIL_ERROR = new ErrorCode(302, "查询活动详细信息失败!");

Service 层

ActivityService 新增接口

// package com.example.lotterysystem.service;// 获取活动详细属性ActivityDetailDTO getActivityDetail(Long activityId);

接口实现

    @Overridepublic ActivityDetailDTO getActivityDetail(Long activityId) {if (null == activityId) {logger.warn("查询活动详细信息失败,因为 activityId 为空!");return null;}// 查询 redisActivityDetailDTO 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);// 查奖品表,还需要奖品的其他属性,这些属性在活动奖品表中没有,因此需要查奖品表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 层代码

ActivityMapper

// package com.example.lotterysystem.dao.mapper;@Select("select * from activity where id = #{id}")ActivityDO selectById(@Param("id") Long id);

ActivityUserMapper

// package com.example.lotterysystem.dao.mapper;@Select("select * from activity_user where activity_id = #{activityId}")List<ActivityUserDO> selectByActivityId(@Param("activityId") Long activityId);

ActivityPrizeMapper

// package com.example.lotterysystem.dao.mapper;@Select("select * from activity_prize where activity_id = #{activityId}")List<ActivityPrizeDO> selectByActivityId(@Param("activityId") Long activityId);

使用 Postman 测试

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/4802.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

广播网络实验

1 实验内容 1、构建星性拓扑下的广播网络,实现hub各端口的数据广播,验证网络的连通性并测试网络效率 2、构建环形拓扑网络,验证该拓扑下结点广播会产生数据包环路 2 实验流程与结果分析 2.1 实验环境 ubuntu、mininet、xterm、wireshark、iperf 2.2 实验方案与结果分析…

Fabric区块链网络搭建:保姆级图文详解

目录 前言1、项目环境部署1.1 基础开发环境1.2 网络部署 2、后台环境2.1、环境配置2.2、运行springboot项目 3、PC端3.1、安装依赖3.2、修改区块链网络连接地址3.3、启动项目 前言 亲爱的家人们&#xff0c;创作很不容易&#xff0c;若对您有帮助的话&#xff0c;请点赞收藏加…

【AI | pytorch】torch.polar的使用

一、torch.polar的使用 torch.polar 是 PyTorch 中用来生成复数张量的一个函数&#xff0c;但它与数学中的复数表达式 ( z re^{i\theta} ) 是等价的。 具体来说&#xff0c;torch.polar(abs, angle) 接受两个实数张量参数&#xff1a; abs&#xff1a;表示复数的模长&#…

.Net Core微服务入门全纪录(六)——EventBus-事件总线

系列文章目录 1、.Net Core微服务入门系列&#xff08;一&#xff09;——项目搭建 2、.Net Core微服务入门全纪录&#xff08;二&#xff09;——Consul-服务注册与发现&#xff08;上&#xff09; 3、.Net Core微服务入门全纪录&#xff08;三&#xff09;——Consul-服务注…

深度学习 · 手撕 DeepLearning4J ,用Java实现手写数字识别 (附UI效果展示)

引言 随着人工智能技术的不断发展&#xff0c;手写数字识别已经成为深度学习领域的一个经典案例。不管是老牌的机器学习模型还是现代的神经网络架构&#xff0c;手写数字识别总是大家学习和实战的起点之一。而对于我们日常使用的Java开发者来说&#xff0c;借助DeepLearning4J…

linux平台RTMP|RTSP播放器如何回调SEI数据?

我们在对接Linux平台RTMP|RTSP播放的时候&#xff0c;有遇到这样的技术需求&#xff0c;合作企业在做无人机视觉分析场景的时候&#xff0c;除了需要低延迟的拿到解码后的RGB|YUV数据&#xff0c;然后投递给他们自己的视觉算法处理模块外&#xff0c;还需要播放器支持SEI的回调…

vue2 - Day05 - VueX

Vuex 是 Vue.js 官方的状态管理库。它是一个让你能在应用中集中管理共享状态的工具。当应用的规模逐渐增大&#xff0c;组件之间的数据传递变得越来越复杂时&#xff0c;Vuex 就成为了救星&#xff0c;提供了一个集中式的存储来管理所有的组件状态&#xff0c;并且保证状态以一…

Linux系统之kill命令的基本使用

Linux系统之kill命令的基本使用 一、kill命令介绍1. kill命令简介2. kill命令的使用场景3. kill命令使用注意事项 二、kill命令的使用帮助1. 查看kill命令帮助信息2. kill命令帮助解释 三、kill常用的信号1. 列出所有的信号2.kill常用的信号 四、kill命令的基本使用1. 运行一个…

HTML之拜年/跨年APP(改进版)

目录&#xff1a; 一&#xff1a;目录 二&#xff1a;效果 三&#xff1a;页面分析/开发逻辑 1.页面详细分析&#xff1a; 2.开发逻辑&#xff1a; 四&#xff1a;完整代码&#xff08;不多废话&#xff09; index.html部分 app.json部分 二&#xff1a;效果 三&#xff1a;页面…

深入探索Python人脸识别技术:从原理到实践

一、引言在当今数字化时代,人脸识别技术已然成为了计算机视觉领域的璀璨明星,广泛且深入地融入到我们生活的各个角落。从门禁系统的安全守护,到金融支付的便捷认证,再到安防监控的敏锐洞察,它的身影无处不在,以其高效、精准的特性,极大地提升了我们生活的便利性与安全性…

JupyterLab 安装以及部分相关配置

安装 JupyterLab pip install jupyter启动 JupyterLab jupyter lab [--port <指定的端口号>] [--no-browser] # --port 指定端口 # --no-browser 启动时不打开浏览器安装中文 首先安装中文包 pip install jupyterlab-language-pack-zh-CN安装完成后重启 JupyterLab 选…

mac m1下载maven安装并配置环境变量

下载地址&#xff1a;Download Apache Maven – Maven 解压到一个没有中文和空格的文件夹 输入pwd查看安装路径 输入cd返回根目录再输入 code .zshrc 若显示 command not found: code你可以通过以下步骤来安装和配置 code 命令&#xff1a; 1. 确保你已经安装了 Visual Studio…

【环境搭建】Metersphere v2.x 容器部署教程踩坑总结

前言 Metersphere部署过程中遇到的问题有点多&#xff0c;原因是其容器的架构蛮复杂的&#xff0c;比较容易踩坑&#xff0c;所以记录一下。 介绍 MeterSphere 是开源持续测试平台&#xff0c;遵循 GPL v3 开源许可协议&#xff0c;涵盖测试管理、接口测试、UI 测试和性能测…

VSCode的配置与使用(C/C++)

从0开始教你在vscode调试一个C文件 一.首先是配置你的编译环境&#xff0c;添加到环境变量&#xff08;默认你是全新的电脑&#xff0c;没有安装vs2019之类的&#xff09; 原因&#xff1a;因为相比于vs2019&#xff0c;vscode只是个代码编辑器&#xff0c;相当于一个彩色的、…

QTableWidget的简单使用

1.最简单的表格示例&#xff1a; ui->tableWidget->setRowCount(2);// 设置行数ui->tableWidget->setColumnCount(3);// 设置列数&#xff0c;一定要放在设置行表头之前QStringList rowHeaderList;// 行表头rowHeaderList << QStringLiteral("姓名"…

七大排序算法

文章目录 排序的概念及引用1.插入排序2.希尔排序(缩小增量排序)3.选择排序4.堆排序5.冒泡排序6.快速排序7.归并排序8.代码排序部分的测试9.代码加效果大致测试时间&#xff08;仅供参考&#xff09; 排序的概念及引用 排序:将数据按照特定的规律排成递增或递减的操作 稳定性:…

前端常见标签

1. <!-- ! 快速生成标签 --> &#xff01;回车会立刻生成模板 2. <!-- CTRL / 生成注释--> 3. 文本标签 <!-- span 文本标签 --> 生成如下&#xff1a; 4. <!-- h1-h6标题标签 --> 大小依次递减 生成&…

PHP教育系统小程序

&#x1f310; 教育系统&#xff1a;全方位学习新体验&#xff0c;引领未来教育风尚 &#x1f680; 教育系统&#xff1a;创新平台&#xff0c;智慧启航 &#x1f4f1; 教育系统&#xff0c;一款深度融合科技与教育的创新平台&#xff0c;匠心独运地采用先进的ThinkPHP框架与U…

MySQL配置my.ini文件

my.ini文件中存储了数据库的文件地址&#xff0c;数据库数据存储地址以及登录密码等基础信息。在遇到忘记密码或者其他基础问题时&#xff0c;修改my.ini文件很方便。但是部分数据库版本默认不生成my.ini文件&#xff0c;需要自己进行配置。 1.停止数据库服务。在搜索框中输入…

【电视盒子】HI3798MV300刷机教程笔记/备份遥控码修复遥控器/ADB/线刷卡刷/电视盒子安装第三方应用软件

心血来潮&#xff0c;看到电视机顶盒满天飞的广告&#xff0c;想改造一下家里的电视盒子&#xff0c;学一下网上的人刷机&#xff0c;但是一切都不知道怎么开始&#xff0c;虽然折腾了一天&#xff0c;以失败告终&#xff0c;还是做点刷机笔记。 0.我的机器 年少不会甄别&…