✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨你正在阅读「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?activityId24GET响应{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);}// 奖品信息列表privateListPrizeDTOprizeDTOList;// 人员信息列表privateListUserDTOuserDTOList;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;/** * 奖品信息列表 */privateListPrizeprizes;/** * 人员信息列表 */privateListUserusers;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(nullactivityId){logger.warn(查询活动详细信息失败activityId为空);returnnull;}// 1. 先从Redis缓存中查询活动详情:这个方法我们之前在创建活动信息的时候就实现过了ActivityDetailDTOdetailDTOgetActivityFromCache(activityId);if(null!detailDTO){logger.info(查询活动详细信息成功detailDTO{},JackSonUtil.writeValueAsString(detailDTO));returndetailDTO;}// 2. 缓存不存在查询数据库// 2.1 查询活动主表信息ActivityDOaDOactivityMapper.selectById(activityId);// 2.2 查询活动关联奖品表信息ListActivityPrizeDOapDOListactivityPrizeMapper.selectByActivityId(activityId);// 2.3 查询活动参与人员表信息ListActivityUserDOauDOListactivityUserMapper.selectByActivityId(activityId);// 2.4 遍历活动奖品关联表收集所有奖品IDListLongprizeIdsnewArrayList();if(apDOList!null!apDOList.isEmpty()){for(ActivityPrizeDOactivityPrizeDO:apDOList){LongprizeIdactivityPrizeDO.getPrizeId();prizeIds.add(prizeId);}}// 2.5 根据奖品ID集合批量查询奖品详情表ListPrizeDOpDOListprizeMapper.batchSelectByIds(prizeIds);// 3. 数据组装将多张表数据整合为活动详情DTOdetailDTOconvertToActivityDetailDTO(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最后整合活动、奖品、人员数据并回写缓存为后续抽奖做好数据准备。下一篇我们就正式进入抽奖接口的实现这里面会涉及状态扭转、中奖信息保存、邮箱通知等硬核内容咱们下一篇见老铁们如果本篇内容对你有帮助不妨点赞、收藏也欢迎在评论区留言交流你的每一份支持都是我持续创作的最大动力