抽奖系统(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 {

    @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("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 的字段不能为空,需要加上 Valid
    private List<CreatePrizeByActivityParam> activityPrizelist;

    // 活动关联人员列表
    @NotEmpty(message = "活动关联人员列表不能为空!")
    @Valid
    private 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 {
    // 活动id
    private 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;

    @Autowired
    private UserMapper userMapper;
    @Autowired
    private PrizeMapper prizeMapper;
    @Autowired
    private ActivityMapper activityMapper;
    @Autowired
    private ActivityUserMapper activityUserMapper;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private RedisUtil redisUtil;
    @Autowired
    private 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)

        // 先获取奖品基本属性表
        // 获取需要查询的奖品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);
        cacheActivity(detailDTO);

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

    // 缓存完整的活动信息 ActivityDetailDTO
    private 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 就是 activityUserDO
        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) {
        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 2
        List<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 {
    // 活动信息
    // 活动id
    private 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;


    @Data
    public static class PrizeDTO {
        // 奖品id
        private 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);
        }
    }

    @Data
    public static class UserDTO {
        // 用户id
        private 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{
    // 关联的活动id
    private Long activityId;
    // 关联的奖品id
    private 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{
    // 关联的活动id
    private Long activityId;
    // 关联的人员id
    private 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 = selectedPrizes
                data.activityUserList = selectedUsers
                submitActivity(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/postMessage
                    window.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;

    @Data
    public static class ActivityInfo implements Serializable {
        // 活动id
        private 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 {
    // 活动id
    private 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;


    @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 层接口设计

// 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,则重置为1
      if (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');
                // 在添加新内容前,先清空listContainer
                listContainer.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>

测试

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

探索 Stable-Diffusion-Webui-Forge:更快的AI图像生成体验

目录 简介&#x1f31f; 主要特点&#x1f4e5; 安装步骤1. 下载2. 配置环境和安装依赖3. 模型目录说明 &#x1f680; 运行 Stable-Diffusion-Webui-Forge1. 进入项目目录2. 运行项目3. 打开页面 &#x1f3a8; 使用体验常见问题&#x1f4dd; 小结 简介 Stable-Diffusion-We…

电梯系统的UML文档04

这个版本的类图是直接从4.2节中用例图的描述得来的&#xff0c;这个视图中的类覆盖了系统所有的功能。我们用电梯类和电梯控制器类&#xff08;ElevatorControl&#xff09;移动或停止电梯&#xff1b;用门类开门或关门&#xff1b;用指示器类让乘客知道电梯的位置和方向&#…

我的创作纪念日——我与CSDN一起走过的365天

目录 一、机缘&#xff1a;旅程的开始 二、收获&#xff1a;沿路的花朵 三、日常&#xff1a;不断前行中 四、成就&#xff1a;一点小确幸 五、憧憬&#xff1a;梦中的重点 一、机缘&#xff1a;旅程的开始 最开始开始写博客是在今年一二月份的时候&#xff0c;也就是上一…

详解Redis的Zset类型及相关命令

目录 Zset简介 ZADD ZCARD ZCOUNT ZRANGE ZREVRANGE ZRANGEBYSCORE ZPOPMAX BZPOPMAX ZPOPMIN BZPOPMIN ZRANK ZREVRANK ZSCORE ZREM ZREMRANGEBYRANK ZREMRANGEBYSCORE ZINCRBY ZINTERSTORE 内部编码 应用场景 Zset简介 有序集合相对于字符串、列表、哈希…

Flask:后端框架使用

文章目录 1、介绍2、demo演示3、Flask请求和响应 3.1 演示demo3.2 request获取请求体数据3.3 requests发送请求3.4 响应返回和接收 4、特殊路由 4.1 路由重定向4.2 路由拦截器 1、介绍 Flask是由python语言编写的轻量级Web应用框架&#xff0c;主要应用于后端框架&#xff…

【Golang/nacos】nacos配置的增删查改,以及服务注册的golang实例及分析

前言 本文分析的实例来源于nacos在github上的开源仓库 nacos配置的增删查改 先具体来看一段代码&#xff0c;我将逐步分析每一段的作用 package mainimport ("fmt""time""github.com/nacos-group/nacos-sdk-go/clients""github.com/naco…

Nvidia Blackwell架构深度剖析:深入了解RTX 50系列GPU的升级

在CES 2025上&#xff0c;英伟达推出了基于Blackwell架构的GeForce RTX 50系列显卡&#xff0c;包括RTX 5090、RTX 5080、RTX 5070 Ti和RTX 5070。一段时间以来&#xff0c;我们已经知晓了该架构的各种细节&#xff0c;其中许多此前还只是传闻。不过&#xff0c;英伟达近日在20…

面试--你的数据库中密码是如何存储的?

文章目录 三种分类使用 MD5 加密存储加盐存储Base64 编码:常见的对称加密算法常见的非对称加密算法https 传输加密 在开发中需要存储用户的密码&#xff0c;这个密码一定是加密存储的&#xff0c;如果是明文存储那么如果数据库被攻击了&#xff0c;密码就泄露了。 我们要对数据…

【24】Word:小郑-准考证❗

目录 题目 准考证.docx 邮件合并-指定考生生成准考证 Word.docx 表格内容居中表格整体相较于页面居中 考试时一定要做一问保存一问❗ 题目 准考证.docx 插入→表格→将文本转换成表格→✔制表符→确定选中第一列→单击右键→在第一列的右侧插入列→布局→合并单元格&#…

WOA-CNN-GRU-Attention、CNN-GRU-Attention、WOA-CNN-GRU、CNN-GRU四模型对比多变量时序预测

WOA-CNN-GRU-Attention、CNN-GRU-Attention、WOA-CNN-GRU、CNN-GRU四模型对比多变量时序预测 目录 WOA-CNN-GRU-Attention、CNN-GRU-Attention、WOA-CNN-GRU、CNN-GRU四模型对比多变量时序预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 基于WOA-CNN-GRU-Attention、…

Spring Boot整合WebSocket

目录 ?引言 1.WebSocket 基础知识 ?1.1 什么是 WebSocket&#xff1f; ?1.2 WebSocket 的应用场景 ?2.Spring Boot WebSocket 整合步骤 2.1 创建 Spring Boot 项目 2.2 添加 Maven 依赖 2.3 配置 WebSocket 2.4 创建 WebSocket 控制器 2.5 创建前端页面 引言 在…

K8S 集群搭建和访问 Kubernetes 仪表板(Dashboard)

一、环境准备 服务器要求&#xff1a; 最小硬件配置&#xff1a;2核CPU、4G内存、30G硬盘。 服务器可以访问外网。 软件环境&#xff1a; 操作系统&#xff1a;Anolis OS 7.9 Docker&#xff1a;19.03.9版本 Kubernetes&#xff1a;v1.18.0版本 内核版本&#xff1a;5.4.203-…

2024:成长、创作与平衡的年度全景回顾

文章目录 1.前言2.突破自我&#xff1a;2024年个人成长与关键突破3.创作历程&#xff1a;从构想到落笔&#xff0c;2024年的文字旅程4.生活与学业的双重奏&#xff1a;如何平衡博客事业与个人生活5.每一步都是前行&#xff1a;2024年度的挑战与收获6.总结 1.前言 回首2024年&a…

计算机网络 (45)动态主机配置协议DHCP

前言 计算机网络中的动态主机配置协议&#xff08;DHCP&#xff0c;Dynamic Host Configuration Protocol&#xff09;是一种网络管理协议&#xff0c;主要用于自动分配IP地址和其他网络配置参数给连接到网络的设备。 一、基本概念 定义&#xff1a;DHCP是一种网络协议&#xf…

学习记录1

[SUCTF 2019]EasyWeb 直接给了源代码&#xff0c;分析一下 <?php function get_the_flag(){// webadmin will remove your upload file every 20 min!!!! $userdir "upload/tmp_".md5($_SERVER[REMOTE_ADDR]);if(!file_exists($userdir)){mkdir($userdir);}if…

git操作(Windows中GitHub)

使用git控制GitHub中的仓库版本&#xff0c;并在Windows桌面中创建与修改代码&#xff0c;与GitHub仓库进行同步。 创建自己的GitHub仓库 创建一个gen_code实验性仓库用来学习和验证git在Windows下的使用方法&#xff1a; gen_code仓库 注意&#xff0c;创建仓库时不要设置…

Redis的安装和使用--Windows系统

Redis下载地址&#xff1a; windows版本readis下载&#xff08;GitHub&#xff09;&#xff1a; https://github.com/tporadowski/redis/releases &#xff08;推荐使用&#xff09; https://github.com/MicrosoftArchive/redis/releases 官网下载&#xff08;无Windows版本…

【odbc】odbc连接kerberos认证的 hive和spark thriftserver

hive odbc驱动&#xff0c;以下两种都可以 教程&#xff1a;使用 ODBC 和 PowerShell 查询 Apache HiveHive ODBC Connector 2.8.0 for Cloudera Enterprise spark thriftserver本质就是披着hiveserver的外壳的spark server 完成kerberos认证: &#xff08;1&#xff09;可以…

AllData数据中台核心菜单十一:数据集成平台

&#x1f525;&#x1f525; AllData大数据产品是可定义数据中台&#xff0c;以数据平台为底座&#xff0c;以数据中台为桥梁&#xff0c;以机器学习平台为中层框架&#xff0c;以大模型应用为上游产品&#xff0c;提供全链路数字化解决方案。 ✨奥零数据科技官网&#xff1a;…

随遇随记篇

vue 函数 unref() 获取原始值 ref 定义的属性 需要 .value 才能拿到值&#xff0c;unref 直接返回原始值&#xff1b;若属性不是ref 定义的&#xff0c;也是直接返回原始值&#xff1b; /* description: 是否必填*/required?: boolean | Ref<boolean>.....let value …