文章目录
-
- 一、活动创建
-
- [1.1 时序图](#1.1 时序图)
- [1.2 约定前后端交互接口](#1.2 约定前后端交互接口)
- [1.3 Controller层接口设计](#1.3 Controller层接口设计)
- [1.4 Service层接口设计](#1.4 Service层接口设计)
- [1.4 活动创建页面前端实现](#1.4 活动创建页面前端实现)
- 二、活动列表展示
-
- [2.1 时序图](#2.1 时序图)
- [2.1 约定前后端交互接口](#2.1 约定前后端交互接口)
- [2.2 Controller层接口设计](#2.2 Controller层接口设计)
- [2.3 Service层接口设计](#2.3 Service层接口设计)
- [2.4 活动列表页面前端实现](#2.4 活动列表页面前端实现)
- 三、Redis缓存实现
-
- [3.1 活动创建时的缓存写入](#3.1 活动创建时的缓存写入)
- [3.2 活动详情查询的缓存优先策略](#3.2 活动详情查询的缓存优先策略)
- [3.3 活动状态扭转时的缓存更新](#3.3 活动状态扭转时的缓存更新)
- [3.4 缓存失效与容错处理](#3.4 缓存失效与容错处理)
- [3.5 缓存数据结构](#3.5 缓存数据结构)

一、活动创建
1.1 时序图

1.2 约定前后端交互接口
请求
请求地址 :/activity/create
请求方法 :POST
请求体:
json
{
"activityName": "抽奖测试",
"description": "年会抽奖活动",
"activityPrizeList": [
{
"prizeId": 13,
"prizeAmount": 1,
"prizeTiers": "FIRST_PRIZE"
},
{
"prizeId": 12,
"prizeAmount": 1,
"prizeTiers": "SECOND_PRIZE"
}
],
"activityUserList": [
{
"userId": 25,
"userName": "郭靖"
},
{
"userId": 23,
"userName": "杨康"
}
]
}
响应
响应体:
json
{
"code": 200,
"data": {
"activityId": 23
},
"msg": ""
}
1.3 Controller层接口设计
- 定义
ActivityController类,使用@RestController注解。 - 提供
/activity/create接口,接收@Validated修饰的CreateActivityParam参数。 - 调用
ActivityService.createActivity()方法处理业务逻辑,返回CommonResult<CreateActivityResult>。 - 异常时抛出
ControllerException。
接口代码示例:
java
/**
* 创建活动
*
* @param param
* @return
*/
@PostMapping("/activity/create")
public CommonResult<CreateActivityResult> createActivity(
@Validated @RequestBody CreateActivityParam param){
log.info("方法: createActivity 参数: param : {}", JacksonUtil.writeValueAsString(param));
return CommonResult.success(
converToCreateActivityResult(
activityService.createActivity(param)));
}
请求参数封装:CreateActivityParam
- 包含活动名称(
activityName)、描述(description)、奖品列表(activityPrizeList)、人员列表(activityUserList)。 - 使用
@NotBlank、@NotEmpty、@Valid等注解进行参数校验。 - 奖品列表项
CreatePrizeByActivityParam:奖品ID、数量、等级(枚举ActivityPrizeTiersEnum)。 - 人员列表项
CreateUserByActivityParam:用户ID、用户名。
奖品等级枚举:ActivityPrizeTiersEnum
- 定义一等奖(FIRST_PRIZE)、二等奖(SECOND_PRIZE)、三等奖(THIRD_PRIZE)。
- 提供code和message,以及根据name或message获取枚举的静态方法。
响应结果封装:CreateActivityResult
- 仅包含活动ID(
activityId)字段。
1.4 Service层接口设计
- 定义
ActivityService接口,声明createActivity(CreateActivityParam request)方法,返回CreateActivityDTO。
数据传输对象:CreateActivityDTO
- 仅包含活动ID字段,用于Service层返回给Controller。
接口实现:ActivityServiceImpl
- 实现
createActivity方法,核心逻辑如下:- 校验活动信息(具体校验内容在源码中,如活动名称非空等)。
- 创建活动记录:构建
ActivityDO,设置名称、描述、状态为"进行中"(RUNNING),调用ActivityMapper.insert()入库。 - 关联奖品:遍历
activityPrizeList,构建ActivityPrizeDO列表,设置活动ID、奖品ID、数量、等级、状态为"初始化"(INIT),调用ActivityPrizeMapper.batchInsert()批量插入。 - 关联人员:遍历
activityUserList,构建ActivityUserDO列表,设置活动ID、用户ID、用户名、状态为"初始化",调用ActivityUserMapper.batchInsert()批量插入。 - 查询奖品详细信息:从奖品表中根据奖品ID列表查询奖品完整信息。
- 缓存活动完整信息:将活动、关联奖品、关联人员整合为
ActivityDetailDTO,存入Redis(key前缀ACTIVITY_,过期时间3天)。 - 返回活动ID。
- 使用
@Transactional保证上述操作的事务一致性。
接口实现代码示例:
java
@Override
@Transactional(rollbackFor = Exception.class) // 涉及了多表,需要保证事务
public CreateActivityDTO createActivity(CreateActivityParam param) {
// 校验活动信息是否正确
checkActivityInfo(param);
// 保存活动信息到库里
ActivityDO activityDO = new ActivityDO();
activityDO.setActivityName(param.getActivityName());
activityDO.setDescription(param.getDescription());
activityDO.setStatus(ActivityStatusEnum.RUNNING.name());
activityMapper.insert(activityDO);
// 保存活动关联的奖品信息
List<CreatePrizeByActivityParam> prizeParams = param.getActivityPrizeList();
List<ActivityPrizeDO> activityPrizeDOList = prizeParams
.stream()
.map(prizeParam -> {
ActivityPrizeDO activityPrizeDO = new ActivityPrizeDO();
activityPrizeDO.setActivityId(activityDO.getId());
activityPrizeDO.setPrizeId(prizeParam.getPrizeId());
activityPrizeDO.setPrizeAmount(prizeParam.getPrizeAmount());
activityPrizeDO.setPrizeTiers(prizeParam.getPrizeTiers());
activityPrizeDO.setStatus(ActivityPrizeStatusEnum.INIT.name());
return activityPrizeDO;
}).collect(Collectors.toList());
activityPrizeMapper.batchInsert(activityPrizeDOList);
// 保存活动关联的人员信息
List<CreateUserByActivityParam> userParams = param.getActivityUserList();
List<ActivityUserDO> activityUserDOList = userParams
.stream()
.map(userParam -> {
ActivityUserDO activityUserDO = new ActivityUserDO();
activityUserDO.setActivityId(activityDO.getId());
activityUserDO.setUserId(userParam.getUserId());
activityUserDO.setUserName(userParam.getUserName());
activityUserDO.setStatus(ActivityUserStatusEnum.INIT.name());
return activityUserDO;
}).collect(Collectors.toList());
activityUserMapper.batchInsert(activityUserDOList);
// 整合完整的活动信息
// 将完成的活动信息存放到 redis 缓存中
// 规定 key:activityId: ActivityDetailDTO: 活动+奖品+人员
// 需要先获取奖品基本属性列表
List<Long> prizeIds = param.getActivityPrizeList()
.stream()
.map(CreatePrizeByActivityParam::getPrizeId)
.distinct()
.collect(Collectors.toList());
List<PrizeDO> prizeDOList = prizeMapper.batchSelectByIds(prizeIds);
ActivityDetailDTO detailDTO = converToActivityDetailDTO(activityDO,activityUserDOList
,activityPrizeDOList,prizeDOList);
// 存放进缓存
cacheActivity(detailDTO);
// 构造返回
CreateActivityDTO createActivityDTO = new CreateActivityDTO();
createActivityDTO.setActivityId(activityDO.getId());
return createActivityDTO;
}
状态枚举类
- ActivityStatusEnum :活动状态,包括进行中(
RUNNING)、已完成(COMPLETED)。 - ActivityPrizeStatusEnum :活动奖品状态,包括初始化(
INIT)、已被抽取(COMPLETED)。 - ActivityUserStatusEnum :活动人员状态,包括初始化(
INIT)、已被抽取(COMPLETED)。
Dao层接口设计
- ActivityMapper :提供
insert(ActivityDO)方法,使用@Options获取自动生成的主键。 - ActivityPrizeMapper :提供
batchInsert(List<ActivityPrizeDO>)方法,批量插入活动奖品关联数据。 - ActivityUserMapper :提供
batchInsert(List<ActivityUserDO>)方法,批量插入活动人员关联数据。 - PrizeMapper :提供
selectByIdList(List<Long>)方法,根据奖品ID列表查询奖品详情。
数据对象(DO)
- ActivityDO :继承
BaseDO,包含活动名称、描述、状态字段。 - ActivityPrizeDO :继承
BaseDO,包含活动ID、奖品ID、奖品数量、奖品等级、状态字段。 - ActivityUserDO :继承
BaseDO,包含活动ID、用户ID、用户名、状态字段。
1.4 活动创建页面前端实现
- 页面加载时通过AJAX请求获取奖品列表和人员列表(普通用户),并填充至模态框。
- 用户可在模态框中勾选奖品并设置数量、等级,勾选参与人员。
- 点击"创建活动"按钮时,通过jQuery Validate插件验证表单必填项。
- 验证通过后,将选中的奖品和人员数据与活动名称、描述组合,通过
POST /activity/create接口提交。 - 成功创建后,通过
window.parent.postMessage通知父页面(活动列表页)刷新列表。
二、活动列表展示
2.1 时序图

2.1 约定前后端交互接口
请求接口:
请求地址 :/activity/find-list?currentPage=1&pageSize=10
请求方法 :GET
响应数据:
响应体:
json
{
"code": 200,
"data": {
"total": 10,
"records": [
{
"activityId": 23,
"activityName": "抽奖测试",
"description": "年会抽奖活动",
"valid": true
}
]
},
"msg": ""
}
2.2 Controller层接口设计
ActivityController中提供/activity/find-list接口,接收PageListParam参数(当前页、每页大小)。- 调用
ActivityService.findActivityList()获取分页数据,返回CommonResult<FindActivityListResult>。 - 将
PageListDTO<ActivityDTO>转换为FindActivityListResult,其中ActivityDTO转换为ActivityInfo(包含活动ID、名称、描述、是否有效)。
接口代码示例:
java
@RequestMapping("/activity/find-list")
public CommonResult<FIndActivityListResult> findActivityList(PageParam param){
log.info("findActivityList param: {}",JacksonUtil.writeValueAsString(param));
return CommonResult.success(
convetToFIndActivityListResult(
activityService.findActivityList(param)));
}
分页查询参数:PageListParam
- 包含
currentPage(默认1)和pageSize(默认10)。 - 提供
offset()方法计算SQL偏移量。
响应结果封装:FindActivityListResult
- 包含
total(总记录数)和records(当前页活动信息列表)。 - 内部类
ActivityInfo:活动ID、名称、描述、是否有效(isValid)。
2.3 Service层接口设计
ActivityService中增加findActivityList(PageListParam request)方法,返回PageListDTO<ActivityDTO>。
数据传输对象:ActivityDTO
- 包含活动ID、名称、描述、状态(
ActivityStatusEnum)。 - 提供
valid()方法判断活动是否有效(状态为RUNNING)。
接口实现:ActivityServiceImpl
- 实现
findActivityList方法:- 调用
ActivityMapper.count()获取总记录数。 - 调用
ActivityMapper.queryActivitiesByPage(offset, pageSize)获取当前页活动记录。 - 将
ActivityDO列表转换为ActivityDTO列表,设置状态枚举。 - 返回
PageListDTO封装总记录数和当前页数据。
- 调用
接口实现示例:
java
@Override
public PageListDTO<ActivityDTO> findActivityList(PageParam param) {
// 获取总量
int total = activityMapper.count();
// 获取当前页列表
List<ActivityDO> activityDOList = activityMapper.selectActivityList(param.offset(),param.getPageSize());
List<ActivityDTO> activityDTOList = activityDOList
.stream()
.map(activityDO -> {
ActivityDTO activityDTO = new ActivityDTO();
activityDTO.setActivityId(activityDO.getId());
activityDTO.setActivityName(activityDO.getActivityName());
activityDTO.setDescription(activityDO.getDescription());
activityDTO.setStatus(ActivityStatusEnum.forName(activityDO.getStatus()));
return activityDTO;
}).collect(Collectors.toList());
return new PageListDTO<>(total,activityDTOList);
}
Dao层接口设计
- ActivityMapper 新增方法:
int count():统计活动总数。List<ActivityDO> queryActivitiesByPage(@Param("offset") Integer offset, @Param("pageSize") Integer pageSize):分页查询,按ID降序排列。
2.4 活动列表页面前端实现
- 页面加载时通过AJAX请求获取第一页活动数据。
- 根据活动状态动态生成链接:进行中活动显示"活动进行中,去抽奖"链接,已完成活动显示"活动已完成,查看中奖名单"链接。
- 实现分页控件:上一页、下一页、跳转输入框,点击或回车时重新请求对应页码数据。
- 请求时在header中添加
user_token进行身份验证,未登录时跳转登录页。
三、Redis缓存实现
3.1 活动创建时的缓存写入
- 在活动创建完成后,将活动完整信息(包括活动基本信息、关联的奖品列表、参与人员列表)整合为
ActivityDetailDTO对象。 - 调用
RedisUtil.set(ACTIVITY_PREFIX + activityId, detailJson, ACTIVITY_EFFECTIVE_TIME)将该对象序列化为JSON后存入Redis。 - 缓存key以
ACTIVITY_为前缀,后跟活动ID,过期时间设置为3天,平衡缓存命中率与数据实时性。
缓存写入示例:
java
/**
* 缓存完整的活动信息: ActivityDetailDTO
*
* @param detailDTO
*/
private void cacheActivity(ActivityDetailDTO detailDTO) {
// key: ACTIVITY_activityId
// value: ActivityDetailDTO(JSON)
if(null == detailDTO || null == detailDTO.getActivityId()){
log.warn("要缓存的信息不存在");
return;
}
try {
redisUtil.set(Constants.ACTIVITY_PREFIS + detailDTO.getActivityId()
, JacksonUtil.writeValueAsString(detailDTO)
, Constants.ACTIVITY_TIMEOUT);
}catch (Exception e){
log.error("缓存活动异常: ActivityDetailDTO={}"
,JacksonUtil.writeValueAsString(detailDTO)
,e);
}
}
3.2 活动详情查询的缓存优先策略
- 在
getActivityDetail接口中,首先根据活动ID构造缓存key,尝试从Redis获取缓存的ActivityDetailDTO。 - 若缓存命中,直接返回,避免数据库查询。
- 若缓存未命中,则查询数据库,组装活动详情后,再通过
cacheActivity方法将结果写入Redis,供后续请求使用。
查询活动缓存示例:
java
/**
* 根据活动ID 从缓存中获取活动详细信息
* @param activityId
* @return
*/
private ActivityDetailDTO getActivityFromCache(Long activityId){
if(null == activityId){
log.warn("获取缓存活动数据activityId为空");
return null;
}
try {
String str = redisUtil.get(Constants.ACTIVITY_PREFIS + activityId);
if(!StringUtils.hasLength(str)){
log.warn("获取缓存活动数据为空! key: {}",Constants.ACTIVITY_PREFIS);
return null;
}
return JacksonUtil.readValue(str,ActivityDetailDTO.class);
}catch (Exception e){
log.error("从缓存中获取活动信息异常,key: {}",Constants.ACTIVITY_PREFIS);
return null;
}
}
3.3 活动状态扭转时的缓存更新
- 当活动状态发生改变(如奖品被抽取、活动完成)时,在数据库事务成功提交后,调用
cacheActivity(activityId)方法强制刷新Redis中的活动详情。 - 确保缓存中的数据与数据库保持最终一致性,避免用户看到过期状态(如已抽取的奖品仍显示为可抽)。
- 缓存更新操作放在事务外部,避免因Redis异常导致事务回滚。
3.4 缓存失效与容错处理
- 所有缓存操作均通过
RedisUtil封装,其内部对异常进行捕获并记录错误日志,但不会抛出异常影响主业务流程。 - 当Redis服务不可用或操作超时时,业务自动降级为直接查询数据库,保证核心功能的可用性。
- 缓存数据均设置合理的过期时间,防止内存泄漏,同时允许数据在过期后重新从数据库加载,实现冷热数据分离。
3.5 缓存数据结构
- 活动详情缓存使用JSON字符串格式存储,内容包含活动ID、名称、描述、状态,以及奖品列表(每个奖品包含ID、名称、图片、数量、等级、是否有效)和人员列表(每个人员包含ID、姓名、是否有效)。
- 这种结构化的存储方式便于前端直接使用,减少多次查询开销。