文章目录
-
- 一、抽奖设计
- 二、RabbitMQ的配置与使用
- 三、抽奖请求处理
- 四、MQ异步抽奖逻辑执行
-
- [4.1 消费 MQ 消息](#4.1 消费 MQ 消息)
- [4.2 请求验证(核对抽奖信息有效性)](#4.2 请求验证(核对抽奖信息有效性))
- [4.3 状态转换](#4.3 状态转换)
-
- [> 活动/奖品/参与者状态转换设计](#> 活动/奖品/参与者状态转换设计)
- [> 常规写法的问题](#> 常规写法的问题)
- [> 问题与解决](#> 问题与解决)
- [> 优化写法](#> 优化写法)
- [4.4 结果记录](#4.4 结果记录)
- [4.5 中奖者通知](#4.5 中奖者通知)
- [4.6 事务一致性------异常回滚](#4.6 事务一致性——异常回滚)
- [4.7 保证消息消费成功(加入死信队列)](#4.7 保证消息消费成功(加入死信队列))
- 五、中奖名单
- 六、抽奖页面前端设计
-
- [6.1 需求回顾](#6.1 需求回顾)
- [6.2 新增查询活动详情接口](#6.2 新增查询活动详情接口)
- [6.3 抽奖页面前端交互逻辑](#6.3 抽奖页面前端交互逻辑)
一、抽奖设计
抽奖过程是系统的核心环节,设计目标为公平、透明、高效。整体流程分为以下阶段:

- 参与者注册与奖品建立:管理员通过管理端新增用户(姓名、联系方式等),并提前创建奖品信息。
- 抽奖活动设置:管理员创建活动,输入活动名称、描述,圈选参与人员,圈选奖品并设置等级与数量。活动发布后在管理端展示。
- 抽奖请求处理:前端随机选择参与者,管理员发起抽奖请求,包含活动ID、奖品ID、中奖人员名单。请求经校验后发送至RabbitMQ消息队列,等待MQ消费者真正处理抽奖逻辑,抽奖请求处理接口不再完成任何事情,直接返回,实现异步处理。
- 抽奖结果公布:前端展示中奖名单(通过随机闪烁动画确定)。
- 抽奖逻辑执行:MQ消费者接收消息,系统执行抽奖核心逻辑:验证请求有效性、扭转活动/奖品/人员状态、记录中奖结果。
- 中奖者通知:并发发送邮件和短信通知中奖者。
- 异常处理:若抽奖过程发生异常,通过事务回滚保证数据一致性,并利用死信队列对失败消息进行重试或记录。
技术实现细节:
- 异步处理:将抽奖逻辑放入消息队列异步执行,提高性能,保证幂等性,不影响前端流程。
- 状态扭转处理:采用设计模式(策略模式 + 责任链模式),提升状态扭转的扩展性与维护性,适配多维度状态关联场景。
- 并发处理:中奖者通知采用并发设计,解耦多系统通知逻辑,提升处理效率。
- 事务处理:确保抽奖逻辑执行时的数据库原子性与事务一致性,异常时触发回滚。
二、RabbitMQ的配置与使用
- 引入Spring Boot AMQP依赖,配置RabbitMQ连接信息(host、port、用户名、密码)。
- 设置消息确认机制为自动(auto),开启重试(最多5次)。
bash
## mq ##
spring.rabbitmq.host=ip
spring.rabbitmq.port=端口
spring.rabbitmq.username=admin
spring.rabbitmq.password=admin
#消息确认机制,默认auto
spring.rabbitmq.listener.simple.acknowledge-mode=auto
#设置失败重试5次
spring.rabbitmq.listener.simple.retry.enabled=true
spring.rabbitmq.listener.simple.retry.max-attempts=5
- 定义DirectRabbitConfig配置类,声明队列(DirectQueue)、交换机(DirectExchange)、路由键(DirectRouting),并绑定。
- 配置消息转换器为Jackson2JsonMessageConverter,实现消息的JSON序列化。
三、抽奖请求处理
时序图 :

约定前后端交互接口:
请求:
请求地址 :/draw-prize
请求方法 :POST
请求体:
json
{
"winnerList": [
{
"userId": 15,
"userName": "胡一博"
},
{
"userId": 21,
"userName": "范闲"
}
],
"activityId": 23,
"prizeId": 13,
"prizeTiers": "FIRST_PRIZE",
"winningTime": "2024-05-21T11:55:10.000Z"
}
响应体:
json
{
"code": 200,
"data": true,
"msg": ""
}
Controller层接口设计:
DrawPrizeController提供/draw-prize接口,接收@Validated的DrawPrizeParam参数。- 调用
DrawPrizeService.drawPrize()方法,返回CommonResult.success(true)。
请求参数封装:DrawPrizeParam:
- 包含活动ID、奖品ID、中奖时间、中奖用户列表(
List<Winner>)。 - 使用
@NotNull等注解进行参数校验。
Service层接口设计:
DrawPrizeService定义drawPrize(DrawPrizeParam param)方法。
接口实现:
DrawPrizeServiceImpl中注入RabbitTemplate。- 将
DrawPrizeParam序列化为JSON,与消息ID、创建时间一起封装为Map,通过rabbitTemplate.convertAndSend()发送到指定交换机与路由键。
接口实现示例:
java
@Override
public void drawPrize(DrawPrizeParam param) {
Map<String, String> map = new HashMap<>();
map.put("messageId",String.valueOf(UUID.randomUUID()));
map.put("messageData", JacksonUtil.writeValueAsString(param));
// 发消息: 交换机, 绑定的key, 哪个队列, 消息体
rabbitTemplate.convertAndSend(EXCHANGE_NAME,ROUTING,map);
log.info("mq消息发送成功, map: {}",JacksonUtil.writeValueAsString(map));
}
四、MQ异步抽奖逻辑执行
时序图

4.1 消费 MQ 消息
- 消费者类
MqReceiver使用@RabbitListener监听队列DirectQueue。 - 收到消息后,解析出
DrawPrizeParam,按顺序执行以下步骤:- 核对抽奖信息有效性。
- 扭转活动状态(奖品、人员、活动)。
- 保存中奖结果。
- 并发处理后续流程(通知)。
- 异常时捕获并调用回滚方法,保证事务一致性。
消费者类示例:
java
@RabbitHandler
public void process(Map<String, String> message){
// 成功接收到队列消息
logger.info("Mq成功接收到消息, message: {}", JacksonUtil.writeValueAsString(message));
String paramString = message.get("messageData");
DrawPrizeParam param = JacksonUtil.readValue(paramString,DrawPrizeParam.class);
// 处理抽奖流程
try {
// 校验抽奖请求是否有效
// 如果有两个一样的抽奖请求,
if(!drawPrizeService.checkDrawPrizeParam(param)){
return;
}
// 状态扭转处理
statusConvert(param);
// 保存中奖者名单
List<WinningRecordDO> winningRecordDOList =
drawPrizeService.saveWinnerRecords(param);
// 通知中奖者
syncExecute(winningRecordDOList);
}catch (ServiceException e){
logger.error("处理 MQ 消息异常: {} : {}",e.getCode(),e.getMessage(),e);
// 如果异常, 需要保证事务一致性, 需要回滚; 并且抛出异常
rollback(param);
throw e;
}catch (Exception e){
logger.error("处理 MQ 消息异常: ",e);
rollback(param);
throw e;
}
}
4.2 请求验证(核对抽奖信息有效性)
- 校验内容 :
- 活动是否存在、奖品是否属于该活动。
- 奖品剩余数量是否足够中奖人数。
- 活动状态是否为"进行中"。
- 奖品状态是否为"初始化"(未被抽取)。
- Service层新增接口 :
checkDrawPrizeValid(DrawPrizeParam param)。 - 实现 :通过
ActivityPrizeMapper和ActivityMapper查询对应记录,比对状态与数量,不满足则抛出ServiceException。
验证代码示例:
java
@Override
public Boolean checkDrawPrizeParam(DrawPrizeParam param) {
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(
param.getActivityId(), param.getPrizeId()
);
// 活动或奖品是否存在
if(null == activityDO || null == activityPrizeDO){
log.info("校验抽奖请求失败!失败原因: {}"
, ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY.getMsg());
// throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_OR_PRIZE_IS_EMPTY);
return false;
}
// 活动是否有效
if(activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())){
// throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED);
log.info("校验抽奖请求失败!失败原因: {}"
, ServiceErrorCodeConstants.ACTIVITY_COMPLETED.getMsg());
return false;
}
// 奖品是否有效
if(activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())){
// throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED);
log.info("校验抽奖请求失败!失败原因: {}"
, ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED.getMsg());
return false;
}
// 中奖者列表和奖品数量
if(activityPrizeDO.getPrizeAmount() != param.getWinnerList().size()){
// throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR);
log.info("校验抽奖请求失败!失败原因: {}"
, ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR.getMsg());
return false;
}
return true;
}
4.3 状态转换
> 活动/奖品/参与者状态转换设计
- 涉及三个维度的状态:活动(进行中/已完成)、活动奖品(初始化/已被抽取)、活动人员(初始化/已被抽取)。
- 抽奖成功后,对应奖品状态改为"已被抽取",中奖人员状态改为"已被抽取",若所有奖品抽完,活动状态改为"已完成"。

> 常规写法的问题
- 存在多个处理对象的顺序关系需要维护:奖品+活动状态扭转,活动需要依赖奖品状态改变而改变。可以看出请求依赖于多个决策点,因此处理顺序很重要,不易维护。
- 需要动态改变算法或行为:是否可以扭转状态的条件,若将来会发生改变,在这里不易维护。
- 系统的灵活性和可扩展性无法体现。
- 处理请求的复杂性不易维护。
> 问题与解决
- 引入策略模式 与责任链模式 进行优化:
- 策略模式 :定义
AbstractActivityOperator抽象类,子类实现具体状态转换逻辑(PrizeOperator、UserOperator、ActivityOperator)。 - 责任链模式 :
ActivityStatusManager管理所有操作符,按指定顺序(sequence)遍历,每个操作符判断是否需要处理(needConvert),若需要则执行转换(convertStatus),并从链中移除,避免重复处理。
- 策略模式 :定义

> 优化写法
- 状态转换调用 :在
MqReceiver中,构造ActivityStatusConvertDTO(包含活动ID、奖品ID、中奖用户ID列表及目标状态),调用activityStatusManager.handleEvent()。 - ActivityStatusManagerImpl实现 :
- 维护
Map<String, AbstractActivityOperator>(Spring自动注入所有操作符)。 handleEvent方法中,按sequence分两次遍历操作符(先执行sequence=1的奖品和人员状态转换,再执行sequence=2的活动状态转换),每次调用processStatusConversion方法。- 转换成功则更新缓存(调用
activityService.cacheActivity()刷新Redis)。
- 维护
- 操作符示例 :
PrizeOperator:sequence=1,判断奖品状态是否需要更新,调用activityPrizeMapper.updateStatus()。UserOperator:sequence=1,批量更新中奖用户状态。ActivityOperator:sequence=2,判断所有奖品是否抽完,若是则更新活动状态。
状态扭转示例:
java
@Override
@Transactional(rollbackFor = Exception.class)
public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
// map<String, AbstractActivityOperator>
if(CollectionUtils.isEmpty(operatorMap)){
logger.warn("operatorMap 为空");
return;
}
Map<String, AbstractActivityOperator> currMap = new HashMap<>(operatorMap);
Boolean update = false;
// 先处理: 人员, 奖品
update = processConvertStatus(convertActivityStatusDTO, currMap, PROCESS_TYPE_USER_PRIZE);
// 后处理: 活动
update = processConvertStatus(convertActivityStatusDTO, currMap, PROCESS_TYPE_ACTIVITY) || update;
if(update){
// 更新缓存
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
}
/**
* 扭转状态
*
* @param convertActivityStatusDTO
* @param currMap
* @param sequence
* @return
*/
private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO
, Map<String, AbstractActivityOperator> currMap
, int sequence) {
Boolean update = false;
// 遍历 currMap
Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = currMap.entrySet().iterator();
while (iterator.hasNext()){
AbstractActivityOperator operator = iterator.next().getValue();
// Operatior 是否需要转换
if(operator.sequence() != sequence
|| !operator.needConvert(convertActivityStatusDTO)){
continue;
}
// 需要转换
if(!operator.convert(convertActivityStatusDTO)){
logger.error("{} 状态转换失败! ",operator.getClass().getName());
throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_STATUS_CONVERT_ERROR);
}
// currMap 删除当前Operatior
iterator.remove();
update = true;
}
// 返回
return update;
}
4.4 结果记录
时序图:

- Service层新增接口 :
saveWinningRecords(DrawPrizeParam param)返回中奖记录列表。 - 实现 :
- 根据活动ID、奖品ID查询活动奖品、活动、奖品详情。
- 根据中奖用户ID列表查询用户信息。
- 组装
WinningRecordDO列表(包含中奖者ID、姓名、邮箱、电话,活动名称,奖品名称、等级,中奖时间)。 - 批量插入数据库。
- 缓存处理 :
- 以
WINNING_RECORD_PREFIX + activityId + "_" + prizeId为key,存入本轮中奖记录,过期时间1天。 - 若活动已完成(所有奖品抽完),则查询该活动全部中奖记录,以
WINNING_RECORD_PREFIX + activityId为key存入Redis,过期时间2天。
- 以
保存结果示例:
java
@Override
public List<WinningRecordDO> saveWinnerRecords(DrawPrizeParam param) {
// 查询相关信息,活动,人员,奖品,活动关联奖品......
ActivityDO activityDO = activityMapper.selectById(param.getActivityId());
List<UserDO> userDOList = userMapper.batchSelectByIds(
param.getWinnerList()
.stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
PrizeDO prizeDO = prizeMapper.selectById(param.getPrizeId());
ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
// 构造中奖者记录
List<WinningRecordDO> winningRecordDOList = userDOList
.stream()
.map(userDO -> {
WinningRecordDO winningRecordDO = new WinningRecordDO();
winningRecordDO.setActivityId(activityDO.getId());
winningRecordDO.setActivityName(activityDO.getActivityName());
winningRecordDO.setPrizeId(prizeDO.getId());
winningRecordDO.setPrizeName(prizeDO.getName());
winningRecordDO.setPrizeTier(activityPrizeDO.getPrizeTiers());
winningRecordDO.setWinnerId(userDO.getId());
winningRecordDO.setWinnerName(userDO.getUserName());
winningRecordDO.setWinnerEmail(userDO.getEmail());
winningRecordDO.setWinnerPhoneNumber(userDO.getPhoneNumber());
winningRecordDO.setWinningTime(param.getWinningTime());
return winningRecordDO;
}).collect(Collectors.toList());
// 保存中奖记录
winningRecordMapper.batchInsert(winningRecordDOList);
// 缓存中奖者记录
// 缓存奖品的中奖信息(key:前缀 + activityId + prizeId, winningRecordDOList(奖品维度))
cacheWinningRecords( param.getActivityId() + "_" + param.getPrizeId()
, winningRecordDOList
, WINNING_RECORDS_TIMEOUT);
// 缓存活动维度的中奖记录(key:前缀 + activityId, winningRecordDOList(活动维度中奖名单))
// 当活动已完成去存放活动维度中奖记录
if(activityDO.getStatus()
.equalsIgnoreCase(ActivityStatusEnum.COMPLETED.name())){
List<WinningRecordDO> allList = winningRecordMapper.selectByActivityId(param.getActivityId());
cacheWinningRecords( String.valueOf(param.getActivityId())
, allList
, WINNING_RECORDS_TIMEOUT);
}
return winningRecordDOList;
}
4.5 中奖者通知
- 为提高效率,邮件和短信通知采用并发执行。
- 邮件服务 :
- 引入
spring-boot-starter-mail,配置QQ邮箱SMTP。 MailUtil封装sendSampleMail方法,发送简单文本邮件。
- 引入
- 短信通知 :
- 使用阿里云短信服务,申请通知模板(区别于验证码模板)。
SMSUtil中配置AccessKey,调用API发送短信。- 因为是个人没用企业认证,暂时无法使用此类短信通知。
- 线程池配置 :
- 在
application.properties中配置核心线程数、最大线程数、队列容量等。 ExecutorConfig创建ThreadPoolTaskExecutorbean,用于并发执行通知任务。
- 在
- 并发通知实现 :
- 在
MqReceiver的syncExecute方法中,使用线程池分别执行pushWinningList(邮件)和sendMessage(短信)任务。 - 两个任务遍历中奖记录,调用对应工具类发送通知,异常不会影响主线程。
- 在
邮件服务示例:
java
/**
* 发邮件
*
* @param to: 目标邮箱地址
* @param subject: 标题
* @param context: 正文
* @return
*/
public Boolean sendSampleMail(String to, String subject, String context) {
SimpleMailMessage message = new SimpleMailMessage();
message.setFrom(from);
message.setTo(to);
message.setSubject(subject);
message.setText(context);
try {
mailSender.send(message);
} catch (Exception e) {
logger.error("向{}发送邮件失败!", to, e);
return false;
}
return true;
}
4.6 事务一致性------异常回滚
- 抽奖过程涉及数据库多表更新及Redis缓存写入,需保证原子性。
- 回滚策略 :
- 在
MqReceiver.process()中捕获ServiceException和通用Exception,调用rollbackWinning方法。 rollbackWinning步骤:- 判断状态是否已扭转(通过查询奖品状态是否变为COMPLETED)。
- 若已扭转,调用
activityStatusManager.rollbackHandleEvent(),将状态回滚至初始(活动->RUNNING,奖品->INIT,人员->INIT),并刷新缓存。 - 判断中奖记录是否已入库(通过查询)。
- 若已入库,调用
drawPrizeService.removeRecords()删除数据库记录及对应缓存。
- 在
- 状态回滚接口 :
ActivityStatusManager新增rollbackHandleEvent,遍历所有操作符,构造回滚目标状态(与正向相反),执行convertStatus,并更新缓存。
回滚实现代码:
java
/**
* 恢复状态
*
* @param param
*/
private void rollbackStatus(DrawPrizeParam param) {
// 涉及状态的恢复工作
ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
convertActivityStatusDTO.setActivityId(param.getActivityId());
convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
convertActivityStatusDTO.setPrizeId(param.getPrizeId());
convertActivityStatusDTO.setTargetPrizeStatus(ActivityPrizeStatusEnum.INIT);
convertActivityStatusDTO.setUserIds(
param.getWinnerList()
.stream()
.map(DrawPrizeParam.Winner::getUserId)
.collect(Collectors.toList())
);
convertActivityStatusDTO.setTargetUserStatus(ActivityUserStatusEnum.INIT);
activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);
}
@Override
public void rollbackHandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
for (AbstractActivityOperator operator : operatorMap.values()){
operator.convert(convertActivityStatusDTO);
}
// 缓存更新
activityService.cacheActivity(convertActivityStatusDTO.getActivityId());
}
4.7 保证消息消费成功(加入死信队列)
- 为防止消息处理失败后丢失,引入死信队列。
- 配置 :
- 在
DirectRabbitConfig中声明死信队列DLX_QUEUE、死信交换机DLX_EXCHANGE及绑定。 - 正常队列
DirectQueue绑定死信交换机:通过QueueBuilder.durable().deadLetterExchange().deadLetterRoutingKey()设置。
- 在
- 消费失败处理 :
- 在
MqReceiver.process()中,发生任何异常并完成回滚后,重新抛出异常,使消息消费失败,自动转入死信队列。
- 在
- 死信消费者 :
DLxReceiver监听死信队列,收到消息后,可将其重新发送到正常队列,或记录到数据库待后续处理。- 提示:实际应用中可维护一张异常消息表,由定时任务重新投递,避免实时重试导致循环。
五、中奖名单
时序图:

约定前后端交互接口:
请求:
请求地址 :/winning-records/show
请求方法 :POST
请求体:
json
{
"activityId": 23
}
响应体:
json
{
"code": 200,
"data": [
{
"winnerId": 15,
"winnerName": "胡一博",
"prizeName": "华为手机",
"prizeTier": "一等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
},
{
"winnerId": 21,
"winnerName": "范闲",
"prizeName": "华为手机",
"prizeTier": "一等奖",
"winningTime": "2024-05-21T11:55:10.000+00:00"
}
],
"msg": ""
}
Controller层接口设计:
DrawPrizeController提供/winning-records/show接口,接收ShowWinningRecordsParam,调用drawPrizeService.showWinningRecords(),返回CommonResult<List<WinningRecordResult>>。
Service层接口设计:
DrawPrizeService已有showWinningRecords方法(之前用于回滚判断),直接复用。
实现 (DrawPrizeServiceImpl):
- 根据参数构造缓存key:若指定奖品ID则为
WINNING_RECORD_PREFIX + activityId + "_" + prizeId,否则为WINNING_RECORD_PREFIX + activityId。 - 尝试从Redis获取,命中则反序列化返回。
- 未命中则查询数据库(按活动ID或活动ID+奖品ID),将结果转换为DTO列表,并存入Redis(过期时间同前),最后返回。
接口实现示例:
java
@Override
public List<WinningRecordDTO> getRecords(ShowWinningRecordsParam param) {
// 查询redis: 奖品,活动
String key = null == param.getPrizeId()
? String.valueOf(param.getActivityId())
: param.getActivityId() + "_" + param.getPrizeId();
List<WinningRecordDO> winningRecordDOList = getWinningRecords(key);
if(CollectionUtils.isEmpty(winningRecordDOList)){
return convetToWinningRecordDTOList(winningRecordDOList);
}
// 如果redis不存在, 查库
winningRecordDOList = winningRecordMapper.selectByActivityIdOrPrizeId(param.getActivityId(),param.getPrizeId());
// 整合存放记录到redis中
if(CollectionUtils.isEmpty(winningRecordDOList)){
log.info("查询的中奖记录为空, param: {}",JacksonUtil.writeValueAsString(param));
return Arrays.asList();
}
cacheWinningRecords(key , winningRecordDOList , WINNING_RECORDS_TIMEOUT);
// 构造返回
return convetToWinningRecordDTOList(getWinningRecords(key));
}
六、抽奖页面前端设计
6.1 需求回顾
- 抽奖页面功能:
- 仅进行中的活动且管理员可抽奖。
- 每轮中奖人数等于当前奖品数量。
- 每人只能中一次奖。
- 多轮抽奖包含三个环节:展示奖品信息 → 人名闪动 → 展示中奖名单。
- 支持查看上一奖项、下一步(已抽完)操作。
- 刷新页面后,已抽奖项不能重新抽取,应直接展示中奖名单。
- 活动已完成时,展示全部中奖名单,并支持"分享结果"按钮复制链接(打开后隐藏操作按钮)。
6.2 新增查询活动详情接口
时序图:

约定前后端交互接口:
请求:
请求地址 :/activity-detail/find?activityId=24
请求方法 :GET
响应体:
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
}
],
"users": [
{
"userId": 44,
"userName": "郭靖",
"valid": true
}
]
},
"msg": ""
}
Controller层接口设计:
ActivityController新增/activity-detail/find接口,调用activityService.getActivityDetail(activityId),返回FindActivityDetailResult。
Service层接口设计:
ActivityService新增getActivityDetail(Long activityId),返回ActivityDetailDTO。
实现 (ActivityServiceImpl):
- 首先从Redis获取缓存(key为
ACTIVITY_PREFIX + activityId),若存在直接返回。 - 否则查询数据库:通过
ActivityMapper、ActivityPrizeMapper、PrizeMapper、ActivityUserMapper组装完整详情,调用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;
}
}
6.3 抽奖页面前端交互逻辑
抽奖页面示例展示:

抽奖结束中奖名单展示:

- 页面初始化时从URL获取活动ID和活动是否有效标识。
- 若活动有效且用户为管理员,调用
reloadConf获取活动详情,初始化奖品列表(steps)和人员列表(names),并进入第一个奖品的状态(state='showPic')。 - 点击"开始抽奖"进入
showBlink状态,人名随机闪烁。 - 点击"点我暂停"确定中奖名单,从人员列表中随机抽取当前奖品数量的人员,保存至
data.list,并调用/draw-prize接口异步提交中奖结果,同时更新data.valid=false。 - 进入
showList状态展示本轮中奖名单,按钮变为"已抽完,下一步"。 - 点击"下一步"切换至下一个奖品或展示全部中奖记录。
- 点击"查看上一奖项"可回退。
- 若刷新页面,重新加载活动详情时,已抽奖品
valid为false,直接展示中奖名单(通过showListByBackEnd从后端查询),防止重复抽取。 - 活动已完成时,直接调用
showRecords展示全量中奖名单,并生成"分享结果"按钮,点击复制带参数(隐藏按钮)的链接。