✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯你正在阅读「Java项目-企悦抽」系列文章🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨🔥弹简特 个人主页
❄️个人专栏直通车:
- 🔌接口测试从入门到跑路
- ☕一个后端的 JavaEE 续命指南
- 🛜网络原理续命手册
- ☕Java项目-轻聊
✨靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:
文章目录
- 一、前言
- 二、抽奖需求分析
- 1、原型图
- 2、需求
- 三、获取活动详细信息接口实现
- 1、业务逻辑
- 2、约定前后端接口
- 2.1 时序图
- 2.2 前后端交互接口
- 3、代码实现
- 3.1 控制层
- 3.2 服务层和持久层
- 4、Postman测试
- 补充知识:幂等性
- 通俗例子
- 专业定义
- 常见非幂等 & 幂等对比
- 为什么后端/接口一定要做幂等?
- 常用实现方式
一、前言
铁汁们,咱们企悦抽项目已经开始进入尾声了,从本期开始,我们将实现我们本项目的最后一个模块:抽奖模块,也是核心模块 同时 是一个难点,我们会分三期将我们的抽奖模块进行实现,分别是:1、提供完整活动信息、2、保存中奖信息、扭转状态、邮箱通知用户、3、返回中奖信息
那么话不多说我们开始吧
二、抽奖需求分析
1、原型图
抽奖流程如下:
2、需求
根据我们的原型图,可以知道,前端的功能是什么?
我们前端的功能就是:
- 前端控制我们的抽奖流程
- 确定中奖人:比如有10个人,3个奖品,那么在开始抽奖,点击“点我确定”的时候,前端需要在10个人里面每一次挑选一个人去中奖。
对于后端来说有三个任务(三个核心接口):
- 提供完整活动信息:既然要在抽奖页查询抽奖那么要将活动、活动关联的奖品、活动关联的人员 等等活动的完整信息展示给前端才行。
本篇博客实现 - 保存中奖信息、扭转状态、邮箱通知用户:前端点击“点我确定”得出中奖人员之后,需要将中奖人员、中什么奖、是几等奖等发给后端存起来,用于到时候前端点击“已抽完、下一步”查看中奖名单的时候显示对应的信息。(此时注意需要扭转活动、奖品、人员状态,因为他们是有状态的,比如活动是否结束?奖品是否抽完?人员是否中过奖?中过奖就不能参与)(与此同时需要完成通知行为:将中奖信息通过邮箱返回给用户)
下一期博客实现 - 返回中奖信息:在第2步中,存储完中奖信息之后,我们提供一个返回中奖信息的接口。
下下一期博客实现
那么接下来我们就先实现第一个任务:
三、获取活动详细信息接口实现
根据需求 我们需要将活动的详细信息返回,用于到时候的前端抽奖显示
1、业务逻辑
如图所示:咱们正是有了这样的一个业务逻辑,所以对于存储到Redis中是可能出现失败的,此时你查询的时候就得先查询数据库了
OK,那么我们知道了业务逻辑之后,接下里就是实现代码了,代码的实现也是很简单的,你只要搞清楚每一层需要做什么即可,我们会直接上手,遇到比较繁琐的处理我们会画图解释:
2、约定前后端接口
2.1 时序图
2.2 前后端交互接口
请求
/activity-detail/find?activityId=24GET
响应{"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":""}
3、代码实现
3.1 控制层
做什么?
- 有一个接口和前端打交道:需要前端传递一个活动ID
- 首先有参数,就打印日志分析该参数
- 其次就是调用服务层
- 将服务层返回的DTO变为前端需要的Result实体
- 写一个方法就是类型转换:将服务层返回一点DTO变为前端需要的Result实体
首先你要知道,我们查询的数据是活动的完整数据,那么活动的完整数据我们在之前创建活动的时候,已经整合在了一个DTO中,回顾一下,如下所示:
@DatapublicclassActivityDetailDTO{// 活动信息/** * 活动id */privateLongactivityId;/** * 活动名称 */privateStringactivityName;/** * 活动描述 */privateStringdesc;/** * 活动状态 */privateActivityStatusEnumstatus;publicBooleanvalid(){returnstatus.equals(ActivityStatusEnum.RUNNING);}// 奖品信息(列表)privateList<PrizeDTO>prizeDTOList;// 人员信息(列表)privateList<UserDTO>userDTOList;@DatapublicstaticclassPrizeDTO{/** * 奖品Id */privateLongprizeId;/** * 奖品名 */privateStringname;/** * 图片索引 */privateStringimageUrl;/** * 价格 */privateBigDecimalprice;/** * 描述 */privateStringdescription;/** * 奖品等级 */privateActivityPrizeTiersEnumtiers;/** * 奖品数量 */privateLongprizeAmount;/** * 奖品状态 */privateActivityPrizeStatusEnumstatus;publicBooleanvalid(){returnstatus.equals(ActivityPrizeStatusEnum.INIT);}}@DatapublicstaticclassUserDTO{/** * 用户id */privateLonguserId;/** * 姓名 */privateStringuserName;/** * 状态 */privateActivityUserStatusEnumstatus;publicBooleanvalid(){returnstatus.equals(ActivityUserStatusEnum.INIT);}}}分析一下他的结构:
那么现在我们控制层需要调用服务层得到这个DTO,那么最终将你这个DTO变为前端需要的Result实体类,此时我们就得定义这个Result实体,如下所示:
@DatapublicclassActivityDetailResultimplementsSerializable{/** * 活动id */privateLongactivityId;/** * 活动名称 */privateStringactivityName;/** * 活动描述 */privateStringdescription;/** * 活动是否有效 */privateBooleanvalid;/** * 奖品信息(列表) */privateList<Prize>prizes;/** * 人员信息(列表) */privateList<User>users;@DatapublicstaticclassPrize{/** * 奖品Id */privateLongprizeId;/** * 奖品名 */privateStringname;/** * 图片索引 */privateStringimageUrl;/** * 价格 */privateBigDecimalprice;/** * 描述 */privateStringdescription;/** * 奖品等奖 */privateStringprizeTierName;/** * 奖品数量 */privateLongprizeAmount;/** * 奖品是否有效 */privateBooleanvalid;}@DatapublicstaticclassUser{/** * 用户id */privateLonguserId;/** * 姓名 */privateStringuserName;/** * 人员是否被抽取 */privateBooleanvalid;}}
需要注意的一点:就是DTO中和我们的Result中的对于状态的处理:
OK,那么接下来就是解释一下控制层的代码了(代码很简单,其实核心重要的是业务要理清楚,如果不懂业务,那么代码是不知道怎么写的哦~)
其实我们项目做到现在,每一次类型转换,代码都会稍微多一点,但是整体真的不难,核心思路都是一样,一定要多练,光看是不行滴~!
3.2 服务层和持久层
咱们服务层干什么呢?
- 去Redis中查询数据,有我们就返回给前端
- 没有就去mysql中查询数据:活动基本表、活动关联的奖品表、活动关联的人员表、奖品表,所以我们后续写四条sql
- 将得到的mysql中数据整合,这个整合我们之前已经实现了
- 将整合的数据存到Redis中,然后再返回给前端
代码
/** * 获取活动详细信息 * @param activityId 活动ID * @return */@OverridepublicActivityDetailDTOgetActivityDetail(LongactivityId){// 校验活动ID是否为空if(null==activityId){logger.warn("查询活动详细信息失败,activityId为空!");returnnull;}// 1. 先从Redis缓存中查询活动详情:这个方法我们之前在创建活动信息的时候就实现过了ActivityDetailDTOdetailDTO=getActivityFromCache(activityId);if(null!=detailDTO){logger.info("查询活动详细信息成功!detailDTO={}",JackSonUtil.writeValueAsString(detailDTO));returndetailDTO;}// 2. 缓存不存在,查询数据库// 2.1 查询活动主表信息ActivityDOaDO=activityMapper.selectById(activityId);// 2.2 查询活动关联奖品表信息List<ActivityPrizeDO>apDOList=activityPrizeMapper.selectByActivityId(activityId);// 2.3 查询活动参与人员表信息List<ActivityUserDO>auDOList=activityUserMapper.selectByActivityId(activityId);// 2.4 遍历活动奖品关联表,收集所有奖品IDList<Long>prizeIds=newArrayList<>();if(apDOList!=null&&!apDOList.isEmpty()){for(ActivityPrizeDOactivityPrizeDO:apDOList){LongprizeId=activityPrizeDO.getPrizeId();prizeIds.add(prizeId);}}// 2.5 根据奖品ID集合,批量查询奖品详情表List<PrizeDO>pDOList=prizeMapper.batchSelectByIds(prizeIds);// 3. 数据组装:将多张表数据整合为活动详情DTOdetailDTO=convertToActivityDetailDTO(aDO,auDOList,pDOList,apDOList);// 4. 存入Redis缓存,方便下次查询cacheActivity(detailDTO);// 5. 返回最终的活动详情信息returndetailDTO;}解释:
4、Postman测试
接下来我们实现第二个接口:抽奖相关的,那么该接口我们将在下一篇博客中完成~
咱们下一篇博客见:抽奖接口相关的
补充知识:幂等性
一句话:同一个操作,执行一次 和 重复执行很多次,结果完全一样,不会出问题。
通俗例子
开灯
按一次开灯 → 灯亮;
再按十次 → 还是亮。
开灯这个操作就是幂等的。付款转账(核心业务场景)
接口调用1次:扣100元;
网络超时重试,自动再调用3次:还是只扣100,不会扣300。
这就是接口要做幂等的原因。
专业定义
一个接口/操作,执行任意多次,对系统产生的最终效果和执行一次完全相同。
常见非幂等 & 幂等对比
| 操作 | 是否幂等 | 说明 |
|---|---|---|
| 新增订单 | 否 | 调一次生成1单,调3次生成3单 |
| 查询数据 | 是 | 查1次、查10次,数据不变 |
| 修改状态为已完成 | 是 | 改一次完成,再改多少次还是完成 |
| 删除一条数据 | 是 | 删一次没了,再删还是没变化 |
为什么后端/接口一定要做幂等?
解决网络超时、重复提交、前端重复点击、消息重复消费带来的重复下单、重复扣款、重复创建数据问题。
常用实现方式
- 唯一订单号/业务主键去重
- 数据库唯一索引
- 分布式锁
- 状态机判断(已处理就不再处理)
- Token防重提交
OK,到这里咱们抽奖模块的第一个接口——获取活动详细信息就实现完毕啦!核心思路就是:先查 Redis,没有再查 MySQL,最后整合活动、奖品、人员数据并回写缓存,为后续抽奖做好数据准备。
下一篇,我们就正式进入抽奖接口的实现,这里面会涉及状态扭转、中奖信息保存、邮箱通知等硬核内容,咱们下一篇见!
老铁们,如果本篇内容对你有帮助,不妨点赞、收藏,也欢迎在评论区留言交流,你的每一份支持都是我持续创作的最大动力~👋