【抽奖系统开发实战】Spring Boot 抽奖模块全解析:MQ 异步处理、缓存信息、状态扭转与异常回滚

【抽奖系统开发实战】Spring Boot 抽奖模块全解析:MQ 异步处理、缓存信息、状态扭转与异常回滚

文章目录

一、抽奖设计

抽奖过程是系统的核心环节,设计目标为公平、透明、高效。整体流程分为以下阶段:

在这里插入图片描述
  • 参与者注册与奖品建立:管理员通过管理端新增用户(姓名、联系方式等),并提前创建奖品信息。
  • 抽奖活动设置:管理员创建活动,输入活动名称、描述,圈选参与人员,圈选奖品并设置等级与数量。活动发布后在管理端展示。
  • 抽奖请求处理:前端随机选择参与者,管理员发起抽奖请求,包含活动ID、奖品ID、中奖人员名单。请求经校验后发送至RabbitMQ消息队列,等待MQ消费者真正处理抽奖逻辑,抽奖请求处理接口不再完成任何事情,直接返回,实现异步处理。
  • 抽奖结果公布:前端展示中奖名单(通过随机闪烁动画确定)。
  • 抽奖逻辑执行:MQ消费者接收消息,系统执行抽奖核心逻辑:验证请求有效性、扭转活动/奖品/人员状态、记录中奖结果。
  • 中奖者通知:并发发送邮件和短信通知中奖者。
  • 异常处理:若抽奖过程发生异常,通过事务回滚保证数据一致性,并利用死信队列对失败消息进行重试或记录。

技术实现细节:

  • 异步处理:将抽奖逻辑放入消息队列异步执行,提高性能,保证幂等性,不影响前端流程。
  • 状态扭转处理:采用设计模式(策略模式 + 责任链模式),提升状态扭转的扩展性与维护性,适配多维度状态关联场景。
  • 并发处理:中奖者通知采用并发设计,解耦多系统通知逻辑,提升处理效率。
  • 事务处理:确保抽奖逻辑执行时的数据库原子性与事务一致性,异常时触发回滚。

二、RabbitMQ的配置与使用

  • 引入Spring Boot AMQP依赖,配置RabbitMQ连接信息(host、port、用户名、密码)。
  • 设置消息确认机制为自动(auto),开启重试(最多5次)。
## 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
请求体

{"winnerList":[{"userId":15,"userName":"胡一博"},{"userId":21,"userName":"范闲"}],"activityId":23,"prizeId":13,"prizeTiers":"FIRST_PRIZE","winningTime":"2024-05-21T11:55:10.000Z"}

响应体

{"code":200,"data":true,"msg":""}

Controller层接口设计

  • DrawPrizeController提供/draw-prize接口,接收@ValidatedDrawPrizeParam参数。
  • 调用DrawPrizeService.drawPrize()方法,返回CommonResult.success(true)

请求参数封装:DrawPrizeParam

  • 包含活动ID、奖品ID、中奖时间、中奖用户列表(List<Winner>)。
  • 使用@NotNull等注解进行参数校验。

Service层接口设计

  • DrawPrizeService定义drawPrize(DrawPrizeParam param)方法。

接口实现

  • DrawPrizeServiceImpl中注入RabbitTemplate
  • DrawPrizeParam序列化为JSON,与消息ID、创建时间一起封装为Map,通过rabbitTemplate.convertAndSend()发送到指定交换机与路由键。

接口实现示例:

@OverridepublicvoiddrawPrize(DrawPrizeParam param){Map<String,String> map =newHashMap<>(); 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,按顺序执行以下步骤:
    1. 核对抽奖信息有效性。
    2. 扭转活动状态(奖品、人员、活动)。
    3. 保存中奖结果。
    4. 并发处理后续流程(通知)。
  • 异常时捕获并调用回滚方法,保证事务一致性。

消费者类示例:

@RabbitHandlerpublicvoidprocess(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)
  • 实现:通过ActivityPrizeMapperActivityMapper查询对应记录,比对状态与数量,不满足则抛出ServiceException

验证代码示例:

@OverridepublicBooleancheckDrawPrizeParam(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);returnfalse;}// 活动是否有效if(activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())){// throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_COMPLETED); log.info("校验抽奖请求失败!失败原因: {}",ServiceErrorCodeConstants.ACTIVITY_COMPLETED.getMsg());returnfalse;}// 奖品是否有效if(activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())){// throw new ServiceException(ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED); log.info("校验抽奖请求失败!失败原因: {}",ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED.getMsg());returnfalse;}// 中奖者列表和奖品数量if(activityPrizeDO.getPrizeAmount()!= param.getWinnerList().size()){// throw new ServiceException(ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR); log.info("校验抽奖请求失败!失败原因: {}",ServiceErrorCodeConstants.WINNER_PRIZE_AMOUNT_ERROR.getMsg());returnfalse;}returntrue;}

4.3 状态转换

> 活动/奖品/参与者状态转换设计
  • 涉及三个维度的状态:活动(进行中/已完成)、活动奖品(初始化/已被抽取)、活动人员(初始化/已被抽取)。
  • 抽奖成功后,对应奖品状态改为“已被抽取”,中奖人员状态改为“已被抽取”,若所有奖品抽完,活动状态改为“已完成”。
在这里插入图片描述
> 常规写法的问题
  1. 存在多个处理对象的顺序关系需要维护:奖品+活动状态扭转,活动需要依赖奖品状态改变而改变。可以看出请求依赖于多个决策点,因此处理顺序很重要,不易维护。
  2. 需要动态改变算法或行为:是否可以扭转状态的条件,若将来会发生改变,在这里不易维护。
  3. 系统的灵活性和可扩展性无法体现。
  4. 处理请求的复杂性不易维护。
> 问题与解决
  • 引入策略模式责任链模式进行优化:
    • 策略模式:定义AbstractActivityOperator抽象类,子类实现具体状态转换逻辑(PrizeOperatorUserOperatorActivityOperator)。
    • 责任链模式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,判断所有奖品是否抽完,若是则更新活动状态。

状态扭转示例:

@Override@Transactional(rollbackFor =Exception.class)publicvoidhandlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO){// map<String, AbstractActivityOperator>if(CollectionUtils.isEmpty(operatorMap)){ logger.warn("operatorMap 为空");return;}Map<String,AbstractActivityOperator> currMap =newHashMap<>(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 */privateBooleanprocessConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO ,Map<String,AbstractActivityOperator> currMap ,int sequence){Boolean update =false;// 遍历 currMapIterator<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());thrownewServiceException(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天。

保存结果示例:

@OverridepublicList<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 =newWinningRecordDO(); 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创建ThreadPoolTaskExecutor bean,用于并发执行通知任务。
  • 并发通知实现
    • MqReceiversyncExecute方法中,使用线程池分别执行pushWinningList(邮件)和sendMessage(短信)任务。
    • 两个任务遍历中奖记录,调用对应工具类发送通知,异常不会影响主线程。

邮件服务示例:

/** * 发邮件 * * @param to: 目标邮箱地址 * @param subject: 标题 * @param context: 正文 * @return */publicBooleansendSampleMail(Stringto,String subject,String context){SimpleMailMessage message =newSimpleMailMessage(); message.setFrom(from); message.setTo(to); message.setSubject(subject); message.setText(context);try{ mailSender.send(message);}catch(Exception e){ logger.error("向{}发送邮件失败!",to, e);returnfalse;}returntrue;}

4.6 事务一致性——异常回滚

  • 抽奖过程涉及数据库多表更新及Redis缓存写入,需保证原子性。
  • 回滚策略
    • MqReceiver.process()中捕获ServiceException和通用Exception,调用rollbackWinning方法。
    • rollbackWinning步骤:
      1. 判断状态是否已扭转(通过查询奖品状态是否变为COMPLETED)。
      2. 若已扭转,调用activityStatusManager.rollbackHandleEvent(),将状态回滚至初始(活动->RUNNING,奖品->INIT,人员->INIT),并刷新缓存。
      3. 判断中奖记录是否已入库(通过查询)。
      4. 若已入库,调用drawPrizeService.removeRecords()删除数据库记录及对应缓存。
  • 状态回滚接口ActivityStatusManager新增rollbackHandleEvent,遍历所有操作符,构造回滚目标状态(与正向相反),执行convertStatus,并更新缓存。

回滚实现代码:

/** * 恢复状态 * * @param param */privatevoidrollbackStatus(DrawPrizeParam param){// 涉及状态的恢复工作ConvertActivityStatusDTO convertActivityStatusDTO =newConvertActivityStatusDTO(); 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);}@OverridepublicvoidrollbackHandlerEvent(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
请求体

{"activityId":23}

响应体

{"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(过期时间同前),最后返回。

接口实现示例:

@OverridepublicList<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)){returnconvetToWinningRecordDTOList(winningRecordDOList);}// 如果redis不存在, 查库 winningRecordDOList = winningRecordMapper.selectByActivityIdOrPrizeId(param.getActivityId(),param.getPrizeId());// 整合存放记录到redis中if(CollectionUtils.isEmpty(winningRecordDOList)){ log.info("查询的中奖记录为空, param: {}",JacksonUtil.writeValueAsString(param));returnArrays.asList();}cacheWinningRecords(key , winningRecordDOList ,WINNING_RECORDS_TIMEOUT);// 构造返回returnconvetToWinningRecordDTOList(getWinningRecords(key));}

六、抽奖页面前端设计

6.1 需求回顾

  • 抽奖页面功能:
    • 仅进行中的活动且管理员可抽奖。
    • 每轮中奖人数等于当前奖品数量。
    • 每人只能中一次奖。
    • 多轮抽奖包含三个环节:展示奖品信息 → 人名闪动 → 展示中奖名单。
    • 支持查看上一奖项、下一步(已抽完)操作。
    • 刷新页面后,已抽奖项不能重新抽取,应直接展示中奖名单。
    • 活动已完成时,展示全部中奖名单,并支持“分享结果”按钮复制链接(打开后隐藏操作按钮)。

6.2 新增查询活动详情接口

时序图

在这里插入图片描述

约定前后端交互接口

请求:

请求地址/activity-detail/find?activityId=24
请求方法GET

响应体

{"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),若存在直接返回。
  • 否则查询数据库:通过ActivityMapperActivityPrizeMapperPrizeMapperActivityUserMapper组装完整详情,调用cacheActivity存入Redis后返回。

接口实现示例:

/** * 根据活动ID 从缓存中获取活动详细信息 * @param activityId * @return */privateActivityDetailDTOgetActivityFromCache(Long activityId){if(null== activityId){ log.warn("获取缓存活动数据activityId为空");returnnull;}try{String str = redisUtil.get(Constants.ACTIVITY_PREFIS+ activityId);if(!StringUtils.hasLength(str)){ log.warn("获取缓存活动数据为空! key: {}",Constants.ACTIVITY_PREFIS);returnnull;}returnJacksonUtil.readValue(str,ActivityDetailDTO.class);}catch(Exception e){ log.error("从缓存中获取活动信息异常,key: {}",Constants.ACTIVITY_PREFIS);returnnull;}}

6.3 抽奖页面前端交互逻辑

抽奖页面示例展示:

在这里插入图片描述

抽奖结束中奖名单展示:

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

Read more

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

小米 “养龙虾”:手机 Agent 落地,智能家居十年困局被撬开

3月6日,小米正式推出国内首个手机端类 OpenClaw Agent 应用 ——Xiaomi miclaw,开启小范围邀请封测。这款被行业与网友戏称为小米 “开养龙虾” 的新品,绝非大模型浪潮下又一款语音助手的常规升级,而是基于自研 MiMo 大模型、具备系统级权限、全场景上下文理解能力的端侧智能体。 作为深耕智能家居领域的行业媒体,《智哪儿》始终认为:智能家居行业过去十年的迭代,始终没能跳出 “被动执行” 的底层困局。而 miclaw 的落地,不止是小米在端侧 AI 赛道的关键落子,更是为整个智能家居行业的底层逻辑重构,提供了可落地的参考范本。需要清醒认知的是,目前该产品仍处于小范围封测阶段,复杂场景执行成功率、端侧功耗表现、第三方生态适配进度等核心体验,仍有待大规模用户实测验证。本文将结合具象场景、量化数据与多维度视角,客观拆解 miclaw 的突破价值、现实挑战,以及它对智能家居行业的长期影响。 01 复盘行业困局:智能家居十年 始终困在 “被动执行”

By Ne0inhk
Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座

Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 discord_interactions 的鸿蒙化适配指南 - 在 OpenHarmony 打造高效的社交机器人交互底座 在现代社交应用与办公协同工具的开发中,集成强大的机器人(Bot)交互能力是提升活跃度的关键。discord_interactions 库为 Flutter 开发者提供了一套完整的、遵循 Discord 官方协议的交互模型,涵盖了从 Slash Commands(斜杠命令)到 Webhook 签名验证的核心功能。本文将深入解析如何在 OpenHarmony(鸿蒙)环境下,结合鸿蒙的安全机制与网络特性,完美适配 discord_interactions 到你的鸿蒙应用中。 前言 随着鸿蒙系统(HarmonyOS)进入原生应用开发的新纪元,跨平台社交工具的适配需求日益增长。discord_interactions 作为一个纯

By Ne0inhk

Flutter 三方库 eip55 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、符合 Web3 标准的以太坊地址校验与防串改引擎

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 eip55 的鸿蒙化适配指南 - 在鸿蒙系统上构建极致、严谨、符合 Web3 标准的以太坊地址校验与防串改引擎 在鸿蒙(OpenHarmony)系统的区块链钱包应用、数字资产管理工具(如鸿蒙版 NFT 浏览器)或需要处理加密货币转账的场景中,如何确保用户输入的以太坊(Ethereum)地址既符合基本格式,又通过了大小写混合的校验和(Checksum)验证,防止因为单个字符手误导致的资产永久丢失?eip55 为开发者提供了一套工业级的、基于 EIP-55 提案的地址转换与验证方案。本文将深入实战其在鸿蒙 Web3 安全基座中的应用。 前言 什么是 EIP-55?它是由以太坊创始人 Vitalik Buterin 提出的地址校验和提案。通过在地址字符串中引入特定的。大小写混合模式(基于 Keccak-256 哈希)

By Ne0inhk
【CANN】Pi0机器人大模型 × 昇腾A2 测评

【CANN】Pi0机器人大模型 × 昇腾A2 测评

【CANN】Pi0机器人大模型 × 昇腾A2 测评 * 写在最前面 🌈你好呀!我是 是Yu欸🚀 感谢你的陪伴与支持~ 欢迎添加文末好友🌌 在所有感兴趣的领域扩展知识,不定期掉落福利资讯(*^▽^*) 写在最前面 版权声明:本文为原创,遵循 CC 4.0 BY-SA 协议。转载请注明出处。 Pi0机器人VLA大模型测评 哈喽大家好呀!我是 是Yu欸。 最近人形机器人和具身智能真的太火了,大家都在聊 Pi0、聊 VLA 大模型。但是,兄弟们,不管是搞科研还是做落地,咱们始终绕不开一个问题——算力。 今天,我们一起把当下最火的 Pi0 机器人视觉-语言-动作大模型,完完整整地部署在国产算力平台上,也就是华为的昇腾 Atlas 800I A2 服务器上。 在跑通仓库模型的基础上,我们做一次性能测评。 我们要测三个最核心的指标:

By Ne0inhk