✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯 你正在阅读「Java项目-企悦抽」系列文章 🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
- 🔌 接口测试从入门到跑路
- ☕ 一个后端的 JavaEE 续命指南
- 🛜 网络原理续命手册
- ☕ Java项目-轻聊
✨ 靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:

文章目录
- 一、前言
- 二、抽奖需求分析
- 三、获取活动详细信息接口实现
-
- 1、业务逻辑
- 2、约定前后端接口
-
- [2.1 时序图](#2.1 时序图)
- [2.2 前后端交互接口](#2.2 前后端交互接口)
- 3、代码实现
-
- [3.1 控制层](#3.1 控制层)
- [3.2 服务层和持久层](#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响应
json{ "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中,回顾一下,如下所示:
java
@Data
public class ActivityDetailDTO {
// 活动信息
/**
* 活动id
*/
private Long activityId;
/**
* 活动名称
*/
private String activityName;
/**
* 活动描述
*/
private String desc;
/**
* 活动状态
*/
private ActivityStatusEnum status;
public Boolean valid() {
return status.equals(ActivityStatusEnum.RUNNING);
}
// 奖品信息(列表)
private List<PrizeDTO> prizeDTOList;
// 人员信息(列表)
private List<UserDTO> userDTOList;
@Data
public static class PrizeDTO {
/**
* 奖品Id
*/
private Long prizeId;
/**
* 奖品名
*/
private String name;
/**
* 图片索引
*/
private String imageUrl;
/**
* 价格
*/
private BigDecimal price;
/**
* 描述
*/
private String description;
/**
* 奖品等级
*/
private ActivityPrizeTiersEnum tiers;
/**
* 奖品数量
*/
private Long prizeAmount;
/**
* 奖品状态
*/
private ActivityPrizeStatusEnum status;
public Boolean valid() {
return status.equals(ActivityPrizeStatusEnum.INIT);
}
}
@Data
public static class UserDTO {
/**
* 用户id
*/
private Long userId;
/**
* 姓名
*/
private String userName;
/**
* 状态
*/
private ActivityUserStatusEnum status;
public Boolean valid() {
return status.equals(ActivityUserStatusEnum.INIT);
}
}
}
分析一下他的结构:

那么现在我们控制层需要调用服务层得到这个DTO,那么最终将你这个DTO变为前端需要的Result实体类,此时我们就得定义这个Result实体,如下所示:
java
@Data
public class ActivityDetailResult implements Serializable {
/**
* 活动id
*/
private Long activityId;
/**
* 活动名称
*/
private String activityName;
/**
* 活动描述
*/
private String description;
/**
* 活动是否有效
*/
private Boolean valid;
/**
* 奖品信息(列表)
*/
private List<Prize> prizes;
/**
* 人员信息(列表)
*/
private List<User> users;
@Data
public static class Prize {
/**
* 奖品Id
*/
private Long prizeId;
/**
* 奖品名
*/
private String name;
/**
* 图片索引
*/
private String imageUrl;
/**
* 价格
*/
private BigDecimal price;
/**
* 描述
*/
private String description;
/**
* 奖品等奖
*/
private String prizeTierName;
/**
* 奖品数量
*/
private Long prizeAmount;
/**
* 奖品是否有效
*/
private Boolean valid;
}
@Data
public static class User {
/**
* 用户id
*/
private Long userId;
/**
* 姓名
*/
private String userName;
/**
* 人员是否被抽取
*/
private Boolean valid;
}
}

需要注意的一点:就是DTO中和我们的Result中的对于状态的处理:

OK,那么接下来就是解释一下控制层的代码了(代码很简单,其实核心重要的是业务要理清楚,如果不懂业务,那么代码是不知道怎么写的哦~)




其实我们项目做到现在,每一次类型转换,代码都会稍微多一点,但是整体真的不难,核心思路都是一样,一定要多练,光看是不行滴~!
3.2 服务层和持久层
咱们服务层干什么呢?
- 去Redis中查询数据,有我们就返回给前端
- 没有就去mysql中查询数据:活动基本表、活动关联的奖品表、活动关联的人员表、奖品表,所以我们后续写四条sql
- 将得到的mysql中数据整合,这个整合我们之前已经实现了
- 将整合的数据存到Redis中,然后再返回给前端
代码
java
/**
* 获取活动详细信息
* @param activityId 活动ID
* @return
*/
@Override
public ActivityDetailDTO getActivityDetail(Long activityId) {
// 校验活动ID是否为空
if (null == activityId) {
logger.warn("查询活动详细信息失败,activityId为空!");
return null;
}
// 1. 先从Redis缓存中查询活动详情:这个方法我们之前在创建活动信息的时候就实现过了
ActivityDetailDTO detailDTO = getActivityFromCache(activityId);
if (null != detailDTO) {
logger.info("查询活动详细信息成功!detailDTO={}", JackSonUtil.writeValueAsString(detailDTO));
return detailDTO;
}
// 2. 缓存不存在,查询数据库
// 2.1 查询活动主表信息
ActivityDO aDO = activityMapper.selectById(activityId);
// 2.2 查询活动关联奖品表信息
List<ActivityPrizeDO> apDOList = activityPrizeMapper.selectByActivityId(activityId);
// 2.3 查询活动参与人员表信息
List<ActivityUserDO> auDOList = activityUserMapper.selectByActivityId(activityId);
// 2.4 遍历活动奖品关联表,收集所有奖品ID
List<Long> prizeIds = new ArrayList<>();
if (apDOList != null && !apDOList.isEmpty()) {
for (ActivityPrizeDO activityPrizeDO : apDOList) {
Long prizeId = activityPrizeDO.getPrizeId();
prizeIds.add(prizeId);
}
}
// 2.5 根据奖品ID集合,批量查询奖品详情表
List<PrizeDO> pDOList = prizeMapper.batchSelectByIds(prizeIds);
// 3. 数据组装:将多张表数据整合为活动详情DTO
detailDTO = convertToActivityDetailDTO(aDO, auDOList, pDOList, apDOList);
// 4. 存入Redis缓存,方便下次查询
cacheActivity(detailDTO);
// 5. 返回最终的活动详情信息
return detailDTO;
}
解释:



4、Postman测试

接下来我们实现第二个接口:抽奖相关的,那么该接口我们将在下一篇博客中完成~
咱们下一篇博客见:抽奖接口相关的
补充知识:幂等性
一句话 :同一个操作,执行一次 和 重复执行很多次,结果完全一样,不会出问题。
通俗例子
-
开灯
按一次开灯 → 灯亮;
再按十次 → 还是亮。
开灯这个操作就是幂等的。
-
付款转账(核心业务场景)
接口调用1次 :扣100元;
网络超时重试,自动再调用3次 :还是只扣100,不会扣300 。
这就是接口要做幂等的原因。
专业定义
一个接口/操作,执行任意多次 ,对系统产生的最终效果和执行一次完全相同。
常见非幂等 & 幂等对比
| 操作 | 是否幂等 | 说明 |
|---|---|---|
| 新增订单 | 否 | 调一次生成1单,调3次生成3单 |
| 查询数据 | 是 | 查1次、查10次,数据不变 |
| 修改状态为已完成 | 是 | 改一次完成,再改多少次还是完成 |
| 删除一条数据 | 是 | 删一次没了,再删还是没变化 |
为什么后端/接口一定要做幂等?
解决网络超时、重复提交、前端重复点击、消息重复消费 带来的重复下单、重复扣款、重复创建数据问题。
常用实现方式
- 唯一订单号/业务主键去重
- 数据库唯一索引
- 分布式锁
- 状态机判断(已处理就不再处理)
- Token防重提交
OK,到这里咱们抽奖模块的第一个接口------获取活动详细信息就实现完毕啦!核心思路就是:先查 Redis,没有再查 MySQL,最后整合活动、奖品、人员数据并回写缓存,为后续抽奖做好数据准备。
下一篇,我们就正式进入抽奖接口的实现,这里面会涉及状态扭转、中奖信息保存、邮箱通知等硬核内容,咱们下一篇见!
老铁们,如果本篇内容对你有帮助,不妨点赞、收藏,也欢迎在评论区留言交流,你的每一份支持都是我持续创作的最大动力~👋
