抽奖系统-奖品-活动

发布于:2025-05-15 ⋅ 阅读:(15) ⋅ 点赞:(0)

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

获取奖品列表

在这里插入图片描述

@Data
public class PageParam implements Serializable {
    private Integer currentPage=1;
    private Integer pageSize=10;
    public Integer offset(){
        return (currentPage-1)*pageSize;
    }
}
@Data
public class FindPrizeListResult {
    private Integer  total;
    private List<PrizeInfo> records;

    @Data
    public static class PrizeInfo{
        private Long PrizeId;
        private String prizeName;
        private BigDecimal price;
        private String description;
        private String imageUrl;
    }
}
@Data
public class PageListDTO<T> {
    private Integer total;
    private List<T> records;
    public PageListDTO(Integer total, List<T> records) {
        this.total = total;
        this.records = records;
    }
}

@Data
public class PrizeDTO {
    private Long id;
    private String name;
    public String description;
    private BigDecimal price;
    private String imageUrl;
}

controller

    @RequestMapping("/prize/find-list")
    public CommonResult<FindPrizeListResult> findPrizeList(PageParam param){
        log.info("查询奖品列表开始,param:{}", JacksonUtil.writeValueAsString(param));
        PageListDTO<PrizeDTO> prizeListDTO = prizeService.findPrizeList(param);
        return CommonResult.success(convertToFindPrizeListResult(prizeListDTO));
    }

    private FindPrizeListResult convertToFindPrizeListResult(PageListDTO<PrizeDTO> pageListDTO) {
            if(null==pageListDTO){
                throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
            }
            FindPrizeListResult findPrizeListResult = new FindPrizeListResult();
            findPrizeListResult.setTotal(pageListDTO.getTotal());
            findPrizeListResult.setRecords(pageListDTO.getRecords().stream().map(
                    prizeDTO -> { 
                        FindPrizeListResult.PrizeInfo prizeInfo = new FindPrizeListResult.PrizeInfo();
                        prizeInfo.setPrizeId(prizeDTO.getId());
                        prizeInfo.setPrizeName(prizeDTO.getName());
                        prizeInfo.setDescription(prizeDTO.getDescription());
                        prizeInfo.setPrice(prizeDTO.getPrice());
                        prizeInfo.setImageUrl(prizeDTO.getImageUrl());
                        return prizeInfo;
                    }).collect(Collectors.toList())
            );
            return findPrizeListResult;
    }

mapper

    @Select("select count(*) from prize")
    int count();

    @Select("select * from prize order by id desc limit #{offset},#{pageSize}")
    List<PrizeDO> selectPrizeList(@Param("offset") Integer offset,@Param("pageSize") Integer pageSize);

service

    @Override
    public PageListDTO<PrizeDTO> findPrizeList(PageParam param) {
        int total = prizeMapper.count();
        List<PrizeDTO> prizeDTOList =new ArrayList<>();
        List<PrizeDO> prizeDOList = prizeMapper.findPrizeList(param.offset(),param.getPageSize());
        for (PrizeDO prizeDO : prizeDOList) {
            PrizeDTO prizeDTO = new PrizeDTO();
            prizeDTO.setId(prizeDO.getId());
            prizeDTO.setName(prizeDO.getName());
            prizeDTO.setDescription(prizeDO.getDescription());
            prizeDTO.setPrice(prizeDO.getPrice());
            prizeDTO.setImageUrl(prizeDO.getImageUrl());
            prizeDTOList.add(prizeDTO);
        }
        return new PageListDTO<>(total,prizeDTOList);
    }

controller

    @RequestMapping("/prize/find-list")
    public CommonResult<FindPrizeListResult> findPrizeList(PageParam param){
        log.info("查询奖品列表开始,param:{}", JacksonUtil.writeValueAsString(param));
        PageListDTO<PrizeDTO> prizeListDTO = prizeService.findPrizeList(param);
        return CommonResult.success(convertToFindPrizeListResult(prizeListDTO));
    }

    private FindPrizeListResult convertToFindPrizeListResult(PageListDTO<PrizeDTO> pageListDTO) {
            if(null==pageListDTO){
                throw new ControllerException(ControllerErrorCodeConstants.FIND_PRIZE_LIST_ERROR);
            }
            FindPrizeListResult findPrizeListResult = new FindPrizeListResult();
            findPrizeListResult.setTotal(pageListDTO.getTotal());
            findPrizeListResult.setRecords(pageListDTO.getRecords().stream().map(
                    prizeDTO -> {
                        FindPrizeListResult.PrizeInfo prizeInfo = new FindPrizeListResult.PrizeInfo();
                        prizeInfo.setPrizeId(prizeDTO.getId());
                        prizeInfo.setPrizeName(prizeDTO.getName());
                        prizeInfo.setDescription(prizeDTO.getDescription());
                        prizeInfo.setPrice(prizeDTO.getPrice());
                        prizeInfo.setImageUrl(prizeDTO.getImageUrl());
                        return prizeInfo;
                    }).collect(Collectors.toList())
            );
            return findPrizeListResult;
    }

在这里插入图片描述

前端页面

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

活动创建需求分析

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
人员只有普通用户

在这里插入图片描述

最后一个状态就是活动有没有正在进行的意思

设置奖品数量的时候,必须在创建活动的时候才有意义
所以还有奖品与活动关联表
在这里插入图片描述
最后一个状态是看这个奖品有没有抽完

对应还有活动人员关联表
在这里插入图片描述
因为user_name常用,所以放在关联表里面

status就是看这个人员有没有中奖

在这里插入图片描述
放在redis中是为了未来的抽奖

活动创建后端实现1-控制层实现及校验活动

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

controller
在这里插入图片描述

service:
在这里插入图片描述
检查service

在这里插入图片描述
检查的mapper

在这里插入图片描述
在这里插入图片描述

活动创建后端实现2-保存信息

在这里插入图片描述
安装一个插件
点击变量,alt+回车,那么就可以生成set和get方法了

在这里插入图片描述

活动插入

在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

活动奖品插入

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

先检查奖品满足几等奖不

在这里插入图片描述
然后再插入两张表
在这里插入图片描述

整合活动信息

为将来抽奖活动准备

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

存入redis

缓存这里我们不抛异常,没有缓存成功就没有缓存成功吧,不影响整个项目的流程,就算缓存没有成功的话,也可以去数据库中查询数据,然后再缓存,所以我们在缓存中的异常捕获掉,这样就不会抛异常,就不会因为缓存失败而回滚了

然后还要写一个获取缓存信息的函数

在这里插入图片描述
然后就是在检查函数那里,
List existUserIds=userMapper.selectExistByIds(userIds);
这里的existUserIds容器一定要判断是不是为空,不然后面执行会报错
在这里插入图片描述
这样就可以了

测试

在这里插入图片描述

在这里插入图片描述
还有就是
在这里插入图片描述

这里的mapper语句写的有问题,原来写的是items.属性

然后还有一些代码要改一下
原:
在这里插入图片描述
现在:
在这里插入图片描述

原:
在这里插入图片描述

现在:
在这里插入图片描述

然后就成功了
在这里插入图片描述
但是redis还没有成功
原:
在这里插入图片描述
现在:
在这里插入图片描述
然后在测试
在这里插入图片描述
在这里插入图片描述

活动创建前端实现

在这里插入图片描述
这里pagesize设的足够大,那么就会显示完全了,因为这里的奖品页面展示没有设置分页

在这里插入图片描述
在这里插入图片描述

总共三个ajax请求
在这里插入图片描述

查询活动列表后端

在这里插入图片描述

[请求] /activity/find-list?currentPage=1&pageSize=10 GET
[响应] 
{
 "code": 200,
 "data": {
 "total": 10,
 "records": [
 {
 "activityId": 23,
 "activityName": "抽奖测试",
 "description": "年会抽奖活动",
 "valid": true
 },
 {
 "activityId": 22,
 "activityName": "抽奖测试",
 "description": "年会抽奖活动",
 "valid": true
 },
 {
 "activityId": 21,
 "activityName": "节⽇抽奖",
 "description": "⽐特年会抽奖活动",
 "valid": true
 }
 ]
 },
 "msg": ""
}

在这里插入图片描述

在这里插入图片描述
controller
在这里插入图片描述

service:
在这里插入图片描述
mapper
在这里插入图片描述
然后测试一下

在这里插入图片描述

活动列表页前端实现

在这里插入图片描述
在这里插入图片描述

抽奖需求分析

在这里插入图片描述
在这里插入图片描述

流程是这样的,先展示奖品,然后闪动抽奖(前端干),然后谁中奖了,最后公布所有中奖名单
人员数量大于奖品数量
在这里插入图片描述

对应还有中奖通知

查询活动详细信息后端实现

在这里插入图片描述
在这里插入图片描述

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

在这里插入图片描述
controller:
在这里插入图片描述
service

在这里插入图片描述

mapper:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

每使用一个方法就换一行,那么这样的话,调试的时候就非常方便了

在这里插入图片描述

抽奖接口设计

在这里插入图片描述
在这里插入图片描述

这样在前端点击的时候就不会卡着半天等响应了,这个都是马上响应的,但是业务的处理是在其他时间处理的

因为马上响应的,所以结果必须得成功,大话已经说出去了
在这里插入图片描述
返回的就是成功
补救措施就是重新落库

  1. 抽奖请求处理(重要)
    • 随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。
    • 请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加
    信息。
    • 消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。
    • 请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。
  2. 抽奖结果公布
    • 前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。
  3. 抽奖逻辑执⾏(重要)
    • 消息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。
  4. 中奖结果处理(重要)
    • 请求验证:
    ◦ 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数
    限制等)等;
    ◦ 幂等性:若消息多发,已抽取的内容不能再次抽取
    • 状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取, ⼈员是否已中奖等。
    • 结果记录:中奖结果被记录在数据库中,并同步更新 Redis 缓存。
  5. 中奖者通知
    • 通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。
    • 奖品领取:中奖者根据通知中的指引领取奖品。
  6. 抽奖异常处理
    • 回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。
    • 补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补
    救措施

技术实现细节
• 异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂
等性。
• 活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内
容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。
• 并发处理:中奖者通知,可能要通知多系统,但相互解耦,可以设计为并发处理,加快抽奖效率
作⽤。
• 事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要
做好事务处理。
通过以上流程,抽奖系统能够确保抽奖过程的公平性和⾼效性,同时提供良好的⽤⼾体验。⽽且还整合了 Redis 和 MQ , 进⼀步提⾼系统的性能

异步处理用的就是RabbitMQ
就是前端把请求放入MQ队列中,然后就什么都不管了

MQ配置

先引入依赖
然后配置

## mq ##
spring.rabbitmq.host=124.71.229.73
spring.rabbitmq.port=5672
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#消息确认机制,默认auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#设置失败重试 5次
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5

spring.rabbitmq.listener.simple.acknowledge-mode=auto
的意思是消费者消费消息没有抛出异常,那么就是消费成功了,不然就是消费失败的

#设置失败重试 5次
的意思就是消费者消费消息失败的话,在重发五次消息

在这里插入图片描述
然后是配置队列,交换机

@Configuration
public class DirectRabbitConfig {
    public static final String QUEUE_NAME = "DirectQueue";
    public static final String EXCHANGE_NAME = "DirectExchange";
    public static final String ROUTING = "DirectRouting";
    /**
     * 队列 起名:DirectQueue
     *
     * @return
     */
    @Bean
    public Queue directQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使⽤,⽽且当连接关闭后队列即被删除。此参考优先级⾼于durable
        // autoDelete:是否⾃动删除,当没有⽣产者或者消费者使⽤此队列,该队列会⾃动删除。
        // return new Queue("DirectQueue",true,true,false);
        // ⼀般设置⼀下队列的持久化就好,其余两个就是默认false
        return new Queue(QUEUE_NAME,true);
    }
    /**
     * Direct交换机 起名:DirectExchange
     *
     * @return
     */
    @Bean
    DirectExchange directExchange() {
        return new DirectExchange(EXCHANGE_NAME,true,false);
    }
    /**
     * 绑定 将队列和交换机绑定, 并设置⽤于匹配键:DirectRouting
     *
     * @return
     */
    @Bean
    Binding bindingDirect() {
        return BindingBuilder.bind(directQueue())
                .to(directExchange())
                .with(ROUTING);
    }
    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

总结


网站公告

今日签到

点亮在社区的每一天
去签到