优惠券平台(十七):实现用户查询/取消优惠券预约提醒功能

业务背景

当用户预约了一个或多个优惠券抢购提醒后,如果不再需要提醒,可以取消预约通知。不过,虽然用户可以取消提醒,但已经发送到 MQ 的消息不会被撤回,消费者在时间点到达时依然会收到消息。此时,我们不应该再向用户发出提醒。因此,我们需要开发一个方法来判断用户是否取消了预约。同时,还需支持用户查询其已预约的优惠券列表信息,以便用户管理其预约状态。

取消预约提醒

1. 取消用户预约优惠券提醒

有这样一种情况,用户预约了优惠券提醒后不想再预约场景,那我们就需要把这个提醒删除。

代码如下所示:

@Service
@RequiredArgsConstructor
public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {// ......
​@Overridepublic void cancelCouponRemind(CouponTemplateRemindCancelReqDTO requestParam) {// 验证优惠券是否存在,避免缓存穿透问题并获取优惠券开抢时间CouponTemplateQueryRespDTO couponTemplate = couponTemplateService.findCouponTemplate(new CouponTemplateQueryReqDTO(requestParam.getShopNumber(), requestParam.getCouponTemplateId()));if (couponTemplate.getValidStartTime().before(new Date())) {throw new ClientException("无法取消已开始领取的优惠券预约");}
​LambdaQueryWrapper<CouponTemplateRemindDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateRemindDO.class).eq(CouponTemplateRemindDO::getUserId, UserContext.getUserId()).eq(CouponTemplateRemindDO::getCouponTemplateId, requestParam.getCouponTemplateId());CouponTemplateRemindDO couponTemplateRemindDO = couponTemplateRemindMapper.selectOne(queryWrapper);if (couponTemplateRemindDO == null) {throw new ClientException("优惠券模板预约信息不存在");}// 计算 BitMap 信息Long bitMap = CouponTemplateRemindUtil.calculateBitMap(requestParam.getRemindTime(), requestParam.getType());if ((bitMap & couponTemplateRemindDO.getInformation()) == 0L) {throw new ClientException("您没有预约该时间点的提醒");}bitMap ^= couponTemplateRemindDO.getInformation();queryWrapper.eq(CouponTemplateRemindDO::getInformation, couponTemplateRemindDO.getInformation());if (bitMap.equals(0L)) {// 如果新 BitMap 信息是 0,说明已经没有预约提醒了,可以直接删除if (couponTemplateRemindMapper.delete(queryWrapper) == 0) {// MySQL 乐观锁进行删除,如果删除失败,说明用户可能同时正在进行删除、新增提醒操作throw new ClientException("取消提醒失败,请刷新页面后重试");}} else {// 虽然删除了这个预约提醒,但还有其它提醒,那就更新数据库couponTemplateRemindDO.setInformation(bitMap);if (couponTemplateRemindMapper.update(couponTemplateRemindDO, queryWrapper) == 0) {// MySQL 乐观锁进行更新,如果更新失败,说明用户可能同时正在进行删除、新增提醒操作throw new ClientException("取消提醒失败,请刷新页面后重试");}}}
}

 流程图如下:

业务流程如下所示:

  • 验证优惠券:根据查询优惠券模板方法避免缓存击穿和穿透,并且获取到优惠券模板详情后判断优惠券是否已开始领取,如果是的话抛出异常。
  • 查询预约提醒记录:系统使用 userIdcouponTemplateId 在数据库中查找对应的提醒记录。如果找不到该记录,则抛出异常,提示“优惠券模板预约信息不存在”。如果找到记录,继续执行后续操作。
  • 计算用户想要取消的提醒对应的 BitMap:使用 CouponTemplateRemindUtil.calculateBitMap() 方法,根据用户的 remindTimetype 计算出该提醒对应的 bitMap(位图)。
  • 检查用户是否已经预约该提醒:通过 bitMap & couponTemplateRemindDO.getInformation() 检查数据库中的预约提醒信息是否包含该时间点的提醒。如果结果为 0,说明用户没有预约该时间点的提醒,抛出异常提示“您没有预约该时间点的提醒”。
  • 更新 BitMap 信息:使用异或操作 bitMap ^= couponTemplateRemindDO.getInformation() 取消该时间点的提醒位。此时,bitMap 会去除用户想要取消的提醒对应的位。
  • 判断更新后的 BitMap:如果 bitMap0,说明用户取消了所有提醒,删除该预约提醒记录。如果 bitMap 不为 0:说明用户取消了部分提醒,仍有其他提醒存在。系统更新数据库中的 information 字段,保存剩余的提醒信息。

    需要注意的是 validStartTime 不能小于当前时间。创建完成后,模板信息会被复制到创建优惠券预约提醒的入参中,并在 t_coupon_template_remind 表中生成一条预约提醒记录。

    当用户通过取消预约提醒接口进行操作时,传入的参数依然是上述的模板信息。执行取消操作后,查看数据库时,可以发现对应的预约提醒记录会被逐步修改。如果用户创建了多个时间段的提醒,每次取消会修改记录中的提醒信息,直到最后一个预约时间被取消,才最终删除该记录。

    2. 消息队列判断是否已取消预约

    虽然用户可以取消提醒,但已经发送到 MQ 的消息不会被撤回,消费者在时间点到达时依然会收到消息。这时我们不应该再向用户发出提醒。所以,我们需要开发一个方法,那就是判断用户是否取消了预约。

    @Service
    @RequiredArgsConstructor
    public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {
    ​// ......
    ​@Overridepublic boolean isCancelRemind(CouponTemplateRemindDTO requestParam) {LambdaQueryWrapper<CouponTemplateRemindDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateRemindDO.class).eq(CouponTemplateRemindDO::getUserId, requestParam.getUserId()).eq(CouponTemplateRemindDO::getCouponTemplateId, requestParam.getCouponTemplateId());CouponTemplateRemindDO couponTemplateRemindDO = couponTemplateRemindMapper.selectOne(queryWrapper);if (couponTemplateRemindDO == null) {// 数据库中没该条预约提醒,说明被取消return true;}
    ​// 即使存在数据,也要检查该类型的该时间点是否有提醒Long information = couponTemplateRemindDO.getInformation();Long bitMap = CouponTemplateRemindUtil.calculateBitMap(requestParam.getRemindTime(), requestParam.getType());
    ​// 按位与等于 0 说明用户取消了预约return (bitMap & information) == 0L;}
    }

    通过该方法,我们在消息队列的消费者执行前加入判断,如果已取消则打印一行日志即可。

    代码如下所示:

    @Slf4j
    @Component
    @RequiredArgsConstructor
    public class CouponTemplateRemindExecutor {
    ​private final CouponTemplateRemindService couponTemplateRemindService;
    ​/*** 执行提醒** @param couponTemplateRemindDTO 用户预约提醒请求信息*/public void executeRemindCouponTemplate(CouponTemplateRemindDTO couponTemplateRemindDTO) {// 用户没取消预约,则发出提醒if (couponTemplateRemindService.isCancelRemind(couponTemplateRemindDTO)) {log.info("用户已取消优惠券预约提醒,参数:{}", JSON.toJSONString(couponTemplateRemindDTO));return;}
    ​// ......}
    }

    3. 布隆过滤器优化性能

    是否需要每次消息消费时都查询数据库来检查用户是否取消了提醒呢?如果对每条消息都进行数据库查询,消息消费的效率就会受到数据库的瓶颈影响。

    为了解决这个问题,可以使用布隆过滤器进行初步判断。当用户取消提醒时,我们根据(用户ID、券ID、提醒时间点、提醒类型)的四元组计算哈希值,并将其存入布隆过滤器。消息消费时,如果布隆过滤器中不存在该哈希值,则说明用户没有取消提醒,可以直接发送提醒。如果存在该哈希值,则有两种可能:

    • 用户确实取消了提醒。
    • 布隆过滤器发生了误判。

      由于存在误判的可能性,我们必须进一步查询数据库,确认用户是否真的取消了提醒。不过这种情况很少出现,大部分请求已经被布隆过滤器过滤,剩下需要查询数据库的请求量很小。

      3.1 创建布隆过滤器

      在优惠券查询布隆过滤器的基础上,添加防止取消提醒缓存穿透布隆过滤器。

      代码如下所示:

      @Configuration
      public class RBloomFilterConfiguration {
      ​/*** 优惠券查询缓存穿透布隆过滤器*/@Beanpublic RBloomFilter<String> couponTemplateQueryBloomFilter(RedissonClient redissonClient, @Value("${framework.cache.redis.prefix:}") String cachePrefix) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(cachePrefix + "couponTemplateQueryBloomFilter");bloomFilter.tryInit(640L, 0.001);return bloomFilter;}
      ​/*** 防止取消提醒缓存穿透的布隆过滤器*/@Beanpublic RBloomFilter<String> cancelRemindBloomFilter(RedissonClient redissonClient, @Value("${framework.cache.redis.prefix:}") String cachePrefix) {RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(cachePrefix + "cancelRemindBloomFilter");bloomFilter.tryInit(640L, 0.001);return bloomFilter;}
      }
      3.2 取消预约提醒加入布隆过滤器

      在我们取消优惠券提醒方法的最后,将优惠券模板 ID、用户 ID、预约时间、预约类型获取 Hash 加入布隆过滤器。

      代码如下所示:

      @Service
      @RequiredArgsConstructor
      public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {// ......
      ​private final RBloomFilter<String> cancelRemindBloomFilter;@Overridepublic void cancelCouponRemind(CouponTemplateRemindCancelReqDTO requestParam) {// ......
      ​// 取消提醒这个信息添加到布隆过滤器中cancelRemindBloomFilter.add(String.valueOf(Objects.hash(requestParam.getCouponTemplateId(), UserContext.getUserId(), requestParam.getRemindTime(), requestParam.getType())));}
      }
      3.3 判断取消优惠券提醒

      代码如下所示:

      @Service
      @RequiredArgsConstructor
      public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {// ......
      ​private final RBloomFilter<String> cancelRemindBloomFilter;
      ​@Overridepublic boolean isCancelRemind(CouponTemplateRemindDTO requestParam) {if (!cancelRemindBloomFilter.contains(String.valueOf(Objects.hash(requestParam.getCouponTemplateId(), requestParam.getUserId(), requestParam.getRemindTime(), requestParam.getType())))) {// 布隆过滤器中不存在,说明没取消提醒,此时已经能挡下大部分请求return false;}
      ​// 对于少部分的“取消了预约”,可能是误判,此时需要去数据库中查找LambdaQueryWrapper<CouponTemplateRemindDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateRemindDO.class).eq(CouponTemplateRemindDO::getUserId, requestParam.getUserId()).eq(CouponTemplateRemindDO::getCouponTemplateId, requestParam.getCouponTemplateId());CouponTemplateRemindDO couponTemplateRemindDO = couponTemplateRemindMapper.selectOne(queryWrapper);if (couponTemplateRemindDO == null) {// 数据库中没该条预约提醒,说明被取消return true;}
      ​// 即使存在数据,也要检查该类型的该时间点是否有提醒Long information = couponTemplateRemindDO.getInformation();Long bitMap = CouponTemplateRemindUtil.calculateBitMap(requestParam.getRemindTime(), requestParam.getType());
      ​// 按位与等于 0 说明用户取消了预约return (bitMap & information) == 0L;}
      }

       

      查询预约提醒列表

      1. 查询用户优惠券预约提醒列表

      代码如下所示:

      @Service
      @RequiredArgsConstructor
      public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {// ......
      ​@Overridepublic List<CouponTemplateRemindQueryRespDTO> listCouponRemind(CouponTemplateRemindQueryReqDTO requestParam) {String value = stringRedisTemplate.opsForValue().get(String.format(USER_COUPON_TEMPLATE_REMIND_INFORMATION, requestParam.getUserId()));if (value != null) {return JSON.parseArray(value, CouponTemplateRemindQueryRespDTO.class);}
      ​// 查出用户预约的信息LambdaQueryWrapper<CouponTemplateRemindDO> queryWrapper = Wrappers.lambdaQuery(CouponTemplateRemindDO.class).eq(CouponTemplateRemindDO::getUserId, requestParam.getUserId());List<CouponTemplateRemindDO> couponTemplateRemindDOlist = couponTemplateRemindMapper.selectList(queryWrapper);if (CollUtil.isEmpty(couponTemplateRemindDOlist))return new ArrayList<>();
      ​// 根据优惠券 ID 查询优惠券信息List<Long> couponIds = couponTemplateRemindDOlist.stream().map(CouponTemplateRemindDO::getCouponTemplateId).toList();List<Long> shopNumbers = couponTemplateRemindDOlist.stream().map(CouponTemplateRemindDO::getShopNumber).toList();List<CouponTemplateDO> couponTemplateDOList = couponTemplateService.listCouponTemplateByIds(couponIds, shopNumbers);List<CouponTemplateRemindQueryRespDTO> actualResult = BeanUtil.copyToList(couponTemplateDOList, CouponTemplateRemindQueryRespDTO.class);
      ​// 填充响应结果的其它信息actualResult.forEach(each -> {// 找到当前优惠券对应的预约提醒信息couponTemplateRemindDOlist.stream().filter(i -> i.getCouponTemplateId().equals(each.getId())).findFirst().ifPresent(i -> {// 解析并填充预约提醒信息CouponTemplateRemindUtil.fillRemindInformation(each, i.getInformation());});});
      ​stringRedisTemplate.opsForValue().set(String.format(USER_COUPON_TEMPLATE_REMIND_INFORMATION, requestParam.getUserId()), JSON.toJSONString(actualResult), 1, TimeUnit.MINUTES);return actualResult;}
      }

      逻辑整体来说比较简单,但是有两个难点:

      • 如何将位图中的信息解析为正常的预约记录?

      • 因为用户预约的优惠券可能是跨多个库的,如何完成跨库查询?

        因为我们取消了用户的优惠券模板预约提醒,对应添加的缓存也需要删除,我们这里采用更新数据库删除缓存策略保障数据库和缓存一致性。

        @Service
        @RequiredArgsConstructor
        public class CouponTemplateServiceRemindImpl extends ServiceImpl<CouponTemplateRemindMapper, CouponTemplateRemindDO> implements CouponTemplateRemindService {// ......
        ​private final RBloomFilter<String> cancelRemindBloomFilter;@Overridepublic void cancelCouponRemind(CouponTemplateRemindCancelReqDTO requestParam) {// ......
        ​// 取消提醒这个信息添加到布隆过滤器中cancelRemindBloomFilter.add(String.valueOf(Objects.hash(requestParam.getCouponTemplateId(), UserContext.getUserId(), requestParam.getRemindTime(), requestParam.getType())));// 删除用户预约提醒的缓存信息,通过更新数据库删除缓存策略保障数据库和缓存一致性stringRedisTemplate.delete(String.format(USER_COUPON_TEMPLATE_REMIND_INFORMATION, UserContext.getUserId()));}
        }

        对应的流程图为:

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

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

        相关文章

        【个人开发】macbook m1 Lora微调qwen大模型

        本项目参考网上各类教程整理而成&#xff0c;为个人学习记录。 项目github源码地址&#xff1a;Lora微调大模型 项目中微调模型为&#xff1a;qwen/Qwen1.5-4B-Chat。 去年新发布的Qwen/Qwen2.5-3B-Instruct同样也适用。 微调步骤 step0: 环境准备 conda create --name fin…

        深入理解进程优先级

        目录 引言 一、进程优先级基础 1.1 什么是进程优先级&#xff1f; 1.2 优先级与系统性能 二、查看进程信息 2.1 使用ps -l命令 2.2 PRI与NI的数学关系 三、深入理解Nice值 3.1 Nice值的特点 3.2 调整优先级实践 四、进程特性全景图 五、优化实践建议 结语 引言 在操…

        大数据学习之SparkSql

        95.SPARKSQL_简介 网址&#xff1a; https://spark.apache.org/sql/ Spark SQL 是 Spark 的一个模块&#xff0c;用于处理 结构化的数据 。 SparkSQL 特点 1 易整合 无缝的整合了 SQL 查询和 Spark 编程&#xff0c;随时用 SQL 或 DataFrame API 处理结构化数据。并且支…

        k8s的操作指令和yaml文件

        一、项目的生命周期 创建----》发布----》更新----》回滚----》删除 1.创建 kubectl create deployment nginx1 --imagenginx:1.22 --replicas3 #基于deployment控制器创建pod&#xff0c;控制器的名称是nginx1,pod使用的镜像是nginx:1.22&#xff0c;pod的数量有3个 2.发布 ku…

        解锁 DeepSeek 模型高效部署密码:蓝耘平台全解析

        &#x1f496;亲爱的朋友们&#xff0c;热烈欢迎来到 青云交的博客&#xff01;能与诸位在此相逢&#xff0c;我倍感荣幸。在这飞速更迭的时代&#xff0c;我们都渴望一方心灵净土&#xff0c;而 我的博客 正是这样温暖的所在。这里为你呈上趣味与实用兼具的知识&#xff0c;也…

        k8s部署rabbitmq

        1. 创建provisioner制备器&#xff08;如果已存在&#xff0c;则不需要&#xff09; 1.1 编写nfs-provisioner-rbac.yaml配置文件 apiVersion: v1 kind: ServiceAccount metadata:name: nfs-client-provisionernamespace: wms --- kind: ClusterRole apiVersion: rbac.author…

        评估大模型(LLM)摘要生成能力:方法、挑战与策略

        大语言模型&#xff08;LLMs&#xff09;有着强大的摘要生成能力&#xff0c;为信息快速提取和处理提供了便利。从新闻文章的快速概览到学术文献的要点提炼&#xff0c;LLMs 生成的摘要广泛应用于各个场景。然而&#xff0c;准确评估这些摘要的质量却颇具挑战。如何确定一个摘要…

        dmd-50

        dmd-50 一、查壳 无壳&#xff0c;64位 二、IDA分析 main 下面的内容中数据经过R键转换&#xff0c;你就会知道v41的内容&#xff0c;以及是当v41成立时key是有效的。 v41870438d5b6e29db0898bc4f0225935c0 结合上面的函数知道&#xff1a;v41经过MD5解密后是key 注意是…

        关于图像锐化的一份介绍

        在这篇文章中&#xff0c;我将介绍有关图像锐化有关的知识&#xff0c;具体包括锐化的简单介绍、一阶锐化与二阶锐化等方面内容。 一、锐化 1.1 概念 锐化&#xff08;sharpening&#xff09;就是指将图象中灰度差增大的方法&#xff0c;一次来增强物体的轮廓与边缘。因为发…

        全程Kali linux---CTFshow misc入门(38-50)

        第三十八题&#xff1a; ctfshow{48b722b570c603ef58cc0b83bbf7680d} 第三十九题&#xff1a; 37换成1&#xff0c;36换成0&#xff0c;就得到长度为287的二进制字符串&#xff0c;因为不能被8整除所以&#xff0c;考虑每7位转换一个字符&#xff0c;得到flag。 ctfshow{5281…

        vue3学习四

        七 标签ref属性 设置标签ref属性&#xff0c;类似于设置标签id。 普通标签 <template name"test4"> <p ref"title" id"title" click"showinfo">VIEW4</p> <View3/><script lang"ts" setup>…

        STM32 软件SPI读写W25Q64

        接线图 功能函数 //写SS函数 void My_W_SS(uint8_t BitValue) {GPIO_WriteBit(GPIOA, GPIO_Pin_4, (BitAction)BitValue); }//写SCK函数 void My_W_SCK(uint8_t BitValue) {GPIO_WriteBit(GPIOA, GPIO_Pin_5, (BitAction)BitValue); }//写MOSI函数 void My_W_MOSI(uint8_t Bit…

        pytest-xdist 进行多进程并发测试

        在自动化测试中&#xff0c;运行时间过长往往是令人头疼的问题。你是否遇到过执行 Pytest 测试用例时&#xff0c;整个测试流程缓慢得让人抓狂&#xff1f;别担心&#xff0c;pytest-xdist 正是解决这一问题的利器&#xff01;它支持多进程并发执行&#xff0c;能够显著加快测试…

        CLion2024.3.2版中引入vector头文件报错

        报错如下&#xff1a; 在MacBook端的CLion中引入#include <vector>报 vector file not found&#xff08;引入map、set等也看参考此方案&#xff09;&#xff0c;首先可以在Settings -> Build,Execution,Deployment -> Toolchains中修改C compiler和C compiler的路…

        【RocketMQ 存储】- 同步刷盘和异步刷盘

        文章目录 1. 前言2. 概述3. submitFlushRequest 提交刷盘请求4. FlushDiskWatcher 同步刷盘监视器5. 同步刷盘但是不需要等待刷盘结果6. 小结 本文章基于 RocketMQ 4.9.3 1. 前言 RocketMQ 存储部分系列文章&#xff1a; 【RocketMQ 存储】- RocketMQ存储类 MappedFile【Rock…

        了解传输层TCP协议

        目录 一、TCP协议段格式 二、TCP原理 1.确认应答 2.超时重传 3.连接管理 建立连接 断开连接 4.滑动窗口 5.流量控制 6.拥塞控制 7.延时应答 8.捎带应答 9.面向字节流 10.TCP异常情况 TCP&#xff0c;即Transmission Control Protocol&#xff0c;传输控制协议。人如…

        第 26 场 蓝桥入门赛

        3.电子舞龙【算法赛】 - 蓝桥云课 问题描述 话说这年头&#xff0c;连舞龙都得电子化&#xff01;这不&#xff0c;蓝桥村的老程序员王大爷突发奇想&#xff0c;用LED灯带和一堆传感器鼓捣出了一条“电子舞龙”&#xff0c;它能根据程序指令在村里的广场上“翩翩起舞”。 广…

        老游戏回顾:TL2

        TL2是一部ARPG游戏&#xff0c;是TL的续作游戏&#xff0c;由位于美国西雅图的Runic Games开发&#xff0c;游戏于2012年9月20日上市&#xff0c;简体中文版于2013年4月10日在国内上市。 2有非常独特的艺术风格&#xff0c;这些在1中就已经形成&#xff0c;经过升级将使这款游…

        前端实现 GIF 图片循环播放

        前言 使用 img 加载 GIF 图片&#xff0c;内容只会播放一次&#xff0c;之后就会自动暂停&#xff1b; 通过定时器在一段时间后重新加载图片的方式&#xff0c;会导致浏览器内存不断增大&#xff0c;并且可能会有闪烁、卡顿的问题&#xff1b; ImageDecoder WebCodecs API 的…

        1-2 面向对象编程方法

        1.0 面向对象编程思维 在面向对象风格中&#xff0c;结构体被看做数据&#xff08;data&#xff09;&#xff0c;而操作数据的函数称作方法&#xff08;method&#xff09;。目前函数 和数据是分离的&#xff0c;函数并不直接操作数据&#xff0c;我们需要拿到函数返回的结果&a…