java抽奖系统(八)

9. 抽奖模块

9.1 抽奖设计

        抽奖过程是抽奖系统中最重要的核⼼环节,它需要确保公平、透明且⾼效。以下是详细的抽奖过程设计:

        对于前端来说,负责控制抽奖的流程,确定中奖的人员

        对于后端来说:

                接口1:查询完整的活动信息

                接口2: 前端确定好中奖的人员之后奖人员信息发送给后端,后端完整的保存好中奖信息。(扭转活动,奖品,人员状态,有的奖品抽完了,有的人员中奖了就不能参加下一个等级的抽奖了,这个活动是不是已经抽奖结束了)(完成中奖人员通知的操作)

                接口三:查询中奖列表。

9.2 查询活动完整信息接口

        activityDetailDTO类里面包含抽奖活动所需要的所有参数。

        前端:向后端发起请求,从redis中查询相关的数据(如果没有存储,或者redis中的缓存过期,就会查询相关的数据库表,整合所需要的所有数据,再一次存放到redis中),最后奖数据返回给前端。

controller:

 @RequestMapping("/activity-detail/find")
    public CommonResult<GetActivityDetailResult> getActivityDetail(Long activityId) {
        logger.info("ActivityController getActivityDetail activityId:{}", activityId);
        ActivityDetailDTO detailDTO = activityService.getActivityDetail(activityId);
        return CommonResult.success(convertToGetActivityDetailResult(detailDTO));
    }

    private GetActivityDetailResult convertToGetActivityDetailResult(ActivityDetailDTO detailDTO) {
        if (detailDTO == null) {
            throw new ControllerException(ControllerErrorCodeConstants.GET_ACTIVITY_DETAIL_ERROR);
        }

        GetActivityDetailResult result = new GetActivityDetailResult();
        result.setActivityId(detailDTO.getActivityId());
        result.setActivityName(detailDTO.getActivityName());
        result.setDescription(detailDTO.getDesc());
        result.setValid(detailDTO.valid());
        // 抽奖顺序:一等奖、二、三
        result.setPrizes(
                detailDTO.getPrizeDTOList().stream()
                        .sorted(Comparator.comparingInt(prizeDTO -> prizeDTO.getTiers().getCode()))
                        //按照奖品code1,2,3等奖来进行从小到大的排序
                        .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.setPrizeTierName(prizeDTO.getTiers().getMessage());
                            prize.setPrizeAmount(prizeDTO.getPrizeAmount());
                            prize.setValid(prizeDTO.valid());
                            return prize;
                        }).collect(Collectors.toList())
        );
        result.setUsers(
                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;
    }

service:

ActivityDetailDTO getActivityDetail(Long activityId);

@Override
    public ActivityDetailDTO getActivityDetail(Long activityId) {
        if (activityId == null) {
            logger.warn("查询活动详细信息失败,activityId为空!");
            return null;
        }
        // 查询 redis
        ActivityDetailDTO detailDTO = getActivityFromCache(activityId);
        if (detailDTO != null) {
            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);

        // 奖品表: 先获取要查询的奖品id
        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:

@Select("select * from activity_prize where activity_id = #{activityId}")
    List<ActivityPrizeDO> selectByActivityId(@Param("activityId") Long activityId);

@Select("select * from activity_user where activity_id = #{activityId}")
    List<ActivityUserDO> selectByActivityId(@Param("activityId") Long activityId);

postman进行测试:

返回结果如下:

{
    "code": 200,
    "data": {
        "activityId": 26,
        "activityName": "测试抽奖活动2.0",
        "description": "测试抽奖活动2.0",
        "valid": true,
        "prizes": [
            {
                "prizeId": 23,
                "name": "snh48演唱会门票",
                "imageUrl": "bc10a8f2-eb31-4e1e-8a72-2883b3968f0c.jpg",
                "price": 498.00,
                "description": "演唱会门票",
                "prizeTierName": "一等奖",
                "prizeAmount": 1,
                "valid": true
            },
            {
                "prizeId": 21,
                "name": "奥迪a6",
                "imageUrl": "a1452970-b709-4a51-8343-e969f5ac5c64.jpg",
                "price": 260000.00,
                "description": "一个四轮车子",
                "prizeTierName": "二等奖",
                "prizeAmount": 1,
                "valid": true
            }
        ],
        "users": [
            {
                "userId": 44,
                "userName": "王奕",
                "valid": true
            },
            {
                "userId": 45,
                "userName": "周诗雨",
                "valid": true
            }
        ]
    },
    "msg": ""
}

9.3 抽奖接口分析

        抽奖场景:当确定中奖人员信息之后,处于用户体验,需要当前页面马上显示出相关的人员信息,但是前端将相关中奖人确定的信息传入到后端之后,后端要进行(保存中奖信息,扭转对协应的转态,完成通知),之后才会将先关数据发送到前端显示的页面上,这样会极大地消耗时间,这就是同步接口,该接口完全不符合当前的抽奖接口。

        异步接口:

        前端:确定中间人之后前往后端。

        后端:前端来请求之后直接返回数据,对上面三个步骤进行异步处理。

        异步处理流程,会另找时间完成后端所要完成的三个步骤。 

9.3.1 异步处理业务流程图

9.3.2 业务逻辑分析

1、抽奖请求处理(重要)

         随机抽取:前端随机选择后端提供的参与者,确保每次抽取的结果是公平的。

        请求提交:在活动进⾏时,管理员可发起抽奖请求。请求包含活动ID、奖品ID和中奖⼈员等附加 信息。

         消息队列通知:有效的抽奖请求被发送⾄MQ队列中,等待MQ消费者真正处理抽奖逻辑。

         请求返回:抽奖的请求处理接⼝将不再完成任何的事情,直接返回。

2、 抽奖结果公布

        前端展⽰:中奖名单通过前端随机抽取的⼈员,公布展⽰出来。 

3、 抽奖逻辑执⾏(重要)

         消 息消费:MQ消费者收到异步消息,系统开始执⾏以下抽奖逻辑。

4、 中奖结果处理(重要)

        请求验证:

                 ◦ 系统验证抽奖请求的有效性,如是否满⾜系统根据设定的规则(如奖品数量、每⼈中奖次数 限制等)等;

                ◦ 幂等性:若消息多发,已抽取的内容不能再次抽取

        状态扭转:根据中奖结果扭转活动/奖品/参与者状态,如奖品是否已被抽取,⼈员是否已中奖等。

        结果记录:中奖结果被记录在数据库中,并同步更新Redis缓存。

5、 中奖者通知

         通知中奖者:通知中奖者和其他相关系统(如邮件发送服务)。

        奖品领取:中奖者根据通知中的指引领取奖品。

6、 抽奖异常处理

         回滚处理:当抽奖过程中发⽣异常,需要保证事务⼀致性。

        补救措施:抽奖⾏为是⼀次性的,因此异步处理抽奖任务必须保证成功,若过程异常,需采取补 救措施

        技术实现细节:

        异步处理:提⾼抽奖性能,不影响抽奖流程,将抽奖处理放⼊队列中进⾏异步处理,且保证了幂 等性。

        活动状态扭转处理:状态扭转会涉及活动及奖品等多横向维度扭转,不能避免未来不会有其他内 容牵扯进活动中,因此对于状态扭转处理,需要⾼扩展性(设计模式)与维护性。         并发处理:中奖者通知,可能要通知多系统(邮箱和手机两种方式),但相互解耦,可以设计为并发处理,加快抽奖效率 作⽤。

         事务处理:在抽奖逻辑执⾏时,如若发⽣异常,需要确保数据库表原⼦性、事务⼀致性,因此要 做好事务处理。

        本次的异步处理操作使用rabbitmq工具来实现。

9.4  mq

        MQ( Message queue ),从字⾯意思上看,本质是个队列,FIFO先⼊先出,只不过队列中存放的内容 是消息(message)⽽已.消息可以⾮常简单,⽐如只包含⽂本字符串,JSON等,也可以很复杂,⽐如内嵌对象. MQ多⽤于分布式系统之间进⾏通信,系统之间的调⽤通常有两种⽅式:

        1. 同步通信

        直接调⽤对⽅的服务,数据从⼀端发出后⽴即就可以达到另⼀端.

        2. 异步通信

        数据从⼀端发出后,先进⼊⼀个容器进⾏临时存储,当达到某种条件后,再由这个容器发送给另⼀端. 容器的⼀个具体实现就是MQ( message queue)

        RabbitMQ 就是MQ的⼀种实现

9.4.1 MQ主要作用        

        MQ主要⼯作是接收并转发消息,在不同的应⽤场景下可以展现不同的作⽤:

        1. 异步解耦:

        在业务流程中,⼀些操作可能⾮常耗时,但并不需要即时返回结果.可以借助MQ把这些操 作异步化,⽐如⽤⼾注册后发送注册短信或邮件通知,可以作为异步任务处理,⽽不必等待这些操作 完成后才告知⽤⼾注册成功.

        2. 流量削峰:

        在访问量剧增的情况下,应⽤仍然需要继续发挥作⽤,但是是这样的突发流量并不常⻅.如 果以能处理这类峰值为标准⽽投⼊资源,⽆疑是巨⼤的浪费.使⽤MQ能够使关键组件⽀撑突发访问压 ⼒,不会因为突发流量⽽崩溃.⽐如秒杀或者促销活动,可以使⽤MQ来控制流量,将请求排队,然后系 统根据⾃⼰的处理能⼒逐步处理这些请求.

        3. 异步通信:

        在很多时候应⽤不需要⽴即处理消息,MQ提供了异步处理机制,允许应⽤把⼀些消息放⼊ MQ中,但并不⽴即处理它,在需要的时候再慢慢处理.

        4. 消息分发:

        当多个系统需要对同⼀数据做出响应时,可以使⽤MQ进⾏消息分发.⽐如⽀付成功后,⽀ 付系统可以向MQ发送消息,其他系统订阅该消息,⽽⽆需轮询数据库.

        5. 延迟通知:

        在需要在特定时间后发送通知的场景中,可以使⽤MQ的延迟消息功能,⽐如在电⼦商务平 台中,如果⽤⼾下单后⼀定时间内未⽀付,可以使⽤延迟队列在超时后⾃动取消订单

9.4.2 rabbitmq核心概念

       如下图所示,生产者和消费者是通过网络进行连接的,网络之中是有信道的,发送的消息是存放在队列中遵循先进先出的规则,交换机的作用是用来路由消息的,消息由生产者发送到网络中,由交换机经过路由将消息传递给队列(交换机绑定的队列)。

        消费者消费消息,就是去相应对的队列里面进行拿取相应的消息。

        多个消费者可以订阅同一个队列。

9.4.3 . web界⾯操作

RabbitMQ Management访问

RabbitMQ 介绍.pdf 基本步骤见文件

9.4.4 rabbitmq的配置与使用

1、引入依赖:

<!-- RabbitMQ -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-amqp</artifactId>
		</dependency>

2、application.properties 配置 MQ

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

3、DirectRabbitConfig 配置类

@Configuration
public class DirectRabbitConfig {
    public static final String QUEUE_NAME = "DirectQueue";
    public static final String EXCHANGE_NAME = "DirectExchange";
    public static final String ROUTING = "DirectRouting";

    public static final String DLX_QUEUE_NAME = "DlxDirectQueue";
    public static final String DLX_EXCHANGE_NAME = "DlxDirectExchange";
    public static final String DLX_ROUTING = "DlxDirectRouting";

    /**
     * 队列 起名: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);

        // 普通队列绑定死信交换机
//        return QueueBuilder.durable(QUEUE_NAME)
//                .deadLetterExchange(DLX_EXCHANGE_NAME)
//                .deadLetterRoutingKey(DLX_ROUTING).build();
    }

    /**
     * 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);
    }

    /**
     * 死信队列
     *
     * @return
     */
    @Bean
    public Queue dlxQueue() {
        // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
        // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
        // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
        //   return new Queue("DirectQueue",true,true,false);

        // 一般设置一下队列的持久化就好,其余两个就是默认false
        return new Queue(DLX_QUEUE_NAME,true);
    }

    /**
     * 死信交换机
     *
     * @return
     */
    @Bean
    DirectExchange dlxExchange() {
        return new DirectExchange(DLX_EXCHANGE_NAME,true,false);
    }

    /**
     * 绑定死信队列与交换机
     *
     * @return
     */
    @Bean
    Binding bindingDlx() {
        return BindingBuilder.bind(dlxQueue())
                .to(dlxExchange())
                .with(DLX_ROUTING);
    }



    @Bean
    public MessageConverter jsonMessageConverter(){
        return new Jackson2JsonMessageConverter();
    }
}

在当前生产消费的模块中,生产者是前端的抽奖接口,消费者是处理抽奖流程的方法。

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

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

相关文章

VulnHub靶场渗透之:Gigachad

环境搭建 VulnHub是一个丰富的实战靶场集合&#xff0c;里面有许多有趣的实战靶机。 本次靶机介绍&#xff1a;http://www.vulnhub.com/entry/gigachad-1,657/ 下载靶机ova文件&#xff0c;导入虚拟机&#xff0c;启动环境&#xff0c;便可以开始进行靶机实战。 虚拟机无法分…

解决Apache/2.4.39 (Win64) PHP/7.2.18 Server at localhost Port 80问题

配置一下apache里面的配置文件&#xff1a;httpd.conf 和 httpd.vhosts.conf httpd.conf httpd-vhosts.conf 重启服务 展示&#xff1a; 浏览器中中文乱码问题&#xff1a;

.NET重点

B/S C/S什么语言 B/S&#xff1a; 浏览器端&#xff1a;JavaScript&#xff0c;HTML&#xff0c;CSS 服务器端&#xff1a;ASP&#xff08;.NET&#xff09;PHP/JSP 优势&#xff1a;维护方便&#xff0c;易于升级和扩展 劣势&#xff1a;服务器负担沉重 C/S java/.NET/…

Linux下基于最新稳定版ESP-IDF5.3.2开发esp32s3入门任务间的通讯-消息队列【入门四】

继续上一篇任务创建 【Linux下基于最新稳定版ESP-IDF5.3.2开发esp32s3入门任务间的通讯-信号量【入门三】-CSDN博客】 今天要实现消息队列进行任务的通讯 一、从上一篇信号量通讯demo拷贝一份重命名&#xff0c;还是之前的两个任务&#xff0c;重命名了。 xTaskCreatePinned…

【AI日记】24.12.22 容忍与自由 | 环境因素和个人因素

【AI论文解读】【AI知识点】【AI小项目】【AI战略思考】【AI日记】 工作 内容&#xff1a;看 OpenAi 这周的发布会和其他 AI 新闻&#xff0c;大佬视频时间&#xff1a;3 小时 读书 书名&#xff1a;富兰克林自传时间&#xff1a;1 小时评估&#xff1a;读完&#xff0c;总体…

穷举vs暴搜vs深搜vs回溯vs剪枝系列一>电话号码的字母组合

题目&#xff1a; 解析&#xff1a; 代码&#xff1a; private String[] hash {"","","abc","def","ghi","jkl","mno","pqrs","tuv","wxyz"};private StringBuffer p…

设计模式 -- 单例模式

设计模式 -- 单例模式 单例模式C++ 实现饿汉式单例模式懒汉式单例模式使用静态局部变量实现懒汉式单例模式(推荐)使用call_once实现懒汉式单例模式(推荐)使用静态全局部变量和指针的方式实现懒汉式单例模式(不推荐)双重检测懒汉式单例模式单例模式 单例模式:确保在整个程…

CH430N 插上电脑无反应

电路图&#xff0c;此处我用的是3.3V供电&#xff0c;现象就是插上USB,电脑没有反应。搜索也搜索不到 抄板请看自己是多少V供电 后来看到也有类似的 换了芯片后就好了。md新板子第一个芯片就是坏的&#xff0c;服了。

重拾设计模式--状态模式

文章目录 状态模式&#xff08;State Pattern&#xff09;概述状态模式UML图作用&#xff1a;状态模式的结构环境&#xff08;Context&#xff09;类&#xff1a;抽象状态&#xff08;State&#xff09;类&#xff1a;具体状态&#xff08;Concrete State&#xff09;类&#x…

【MySQL】深入了解索引背后的内部结构

目录 索引的认识&#xff1a; 作用&#xff1a; 索引的使用&#xff1a; 索引底层的数据结构&#xff1a; 哈希表 AVL树 红黑树 B树&#xff1a; B树&#xff1a; B树搜索&#xff1a; 索引的认识&#xff1a; 索引是数据库中的一个数据结构&#xff0c;用于加速查询…

【MySQL】--- 数据类型

Welcome to 9ilks Code World (๑•́ ₃ •̀๑) 个人主页: 9ilk (๑•́ ₃ •̀๑) 文章专栏&#xff1a; MySQL &#x1f3e0; 数据类型分类 MySQL是一套整体的对外数据存取方案,既然要存取数据,而数据有不同的类型,因此MySQL也存在不同的数据类型,有不同的用…

电商店铺数据集成到金蝶云星辰V2的实践经验分享

电商店铺数据集成到金蝶云星辰V2的技术案例分享 在电商业务快速发展的背景下&#xff0c;如何高效地将聚水潭平台上的电商店铺数据集成到金蝶云星辰V2系统中&#xff0c;成为了许多企业面临的重要挑战。本文将详细探讨一个实际运行的解决方案——“电商店铺->金蝶客户”&am…

在VBA中结合正则表达式和查找功能给文档添加交叉连接

在VBA中搜索文本有两种方式可用&#xff0c;一种是利用Range.Find对象&#xff08;更常见的形式可能是Selection.Find&#xff0c;Selection是Range的子类&#xff0c;Selection.Find其实就是特殊的Range.Find&#xff09;&#xff0c;另一种方法是利用正则表达式&#xff0c;但…

大腾智能CAD:国产云原生三维设计新选择

在快速发展的工业设计领域&#xff0c;CAD软件已成为不可或缺的核心工具。它通过强大的建模、分析、优化等功能&#xff0c;不仅显著提升了设计效率与精度&#xff0c;还促进了设计思维的创新与拓展&#xff0c;为产品从概念构想到实体制造的全过程提供了强有力的技术支持。然而…

实现Python将csv数据导入到Neo4j

目录 一、获取数据集 1.1 获取数据集 1.2 以“记事本”方式打开文件 1.3 另存为“UTF-8”格式文件 1.4 选择“是” 二、 打开Neo4j并运行 2.1 创建新的Neo4j数据库 2.2 分别设置数据库名和密码 ​编辑 2.3 启动Neo4j数据库 2.4 打开Neo4j数据库 2.5 运行查看该数据库…

MySQL知识汇总(二):select

select语句 -- select语句 select 字段 from 表 -- 查询全部信息 select * from 表 SELECT * FROM student2 -- 查询指定字段 select name from 表 SELECT name FROM student2 -- 起别名 给查询结果用 AS 起个其他的名字&#xff0c;可以是字段也可以是表 SELECT name AS 名字 …

Restaurants WebAPI(二)——DTO/CQRS

文章目录 项目地址一、DTO1.1 创建Restaurant的Dto1.2 修改之前未使用Dto的接口1.2.1 修改GetRestaurantByIdUseCase1.2.2 修改IGetRestaurantByIdUseCase接口1.2.3 再次请求接口1.3 显示Dish List1.3.1创建DishDto1.3.2 在RestaurantDto里添加DishDto1.3.3 使用Include添加Dis…

c++--------c++概念

定义与起源 C是一种高级编程语言&#xff0c;它是C语言的扩展。C由Bjarne Stroustrup在20世纪80年代初开发&#xff0c;最初被称为“C with Classes”。其设计目的是在保持C语言高效性的同时&#xff0c;增加面向对象编程&#xff08;OOP&#xff09;的特性。例如&#xff0c;…

面向对象设计过程的理解和实践

在软件开发的世界里&#xff0c;面向对象设计&#xff08;Object-Oriented Design&#xff0c;简称OOD&#xff09;是一项至关重要的技术。它不仅帮助我们更好地将现实世界的问题转化为软件系统中的对象&#xff0c;还确保这些对象之间能够高效地协同工作&#xff0c;共同实现系…

游泳溺水识别数据集,对9984张原始图片进行YOLO,COCO JSON, VOC XML 格式的标注,平均识别率在91.7%以上

游泳溺水识别数据集&#xff1a; 对9984张原始图片进行YOLO&#xff0c;COCO JSON, VOC XML 格式的标注&#xff0c;平均识别率在91.7&#xff05;以上 &#xff0c;可识别泳池或者水库中是否有人溺水。 数据集分割 训练组98&#xff05; 9818图片 有效集&#xff05;…