跳到主要内容Spring Boot 抽奖模块设计:MQ 异步处理、缓存与状态管理 | 极客日志Javajava
Spring Boot 抽奖模块设计:MQ 异步处理、缓存与状态管理
综述由AI生成基于 Spring Boot 的抽奖系统设计与实现。核心采用 RabbitMQ 进行异步处理,解耦请求与业务逻辑。通过策略模式与责任链模式优化活动、奖品及参与者的状态转换,确保逻辑可扩展。利用 Redis 缓存中奖名单提升查询性能。设计了事务回滚机制与死信队列,保障数据一致性与消息可靠性。同时包含前后端交互接口设计及前端抽奖页交互逻辑,实现了公平、透明、高效的抽奖流程。
黑客帝国38 浏览 一、抽奖设计
抽奖过程是系统的核心环节,设计目标为公平、透明、高效。整体流程分为以下阶段:

- 参与者注册与奖品建立:管理员通过管理端新增用户(姓名、联系方式等),并提前创建奖品信息。
- 抽奖活动设置:管理员创建活动,输入活动名称、描述,圈选参与人员,圈选奖品并设置等级与数量。活动发布后在管理端展示。
- 抽奖请求处理:前端随机选择参与者,管理员发起抽奖请求,包含活动 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":""}
DrawPrizeController 提供 /draw-prize 接口,接收 @Validated 的 DrawPrizeParam 参数。
- 调用
DrawPrizeService.drawPrize() 方法,返回 CommonResult.success(true)。
- 包含活动 ID、奖品 ID、中奖时间、中奖用户列表(
List<Winner>)。
- 使用
@NotNull 等注解进行参数校验。
DrawPrizeService 定义 drawPrize(DrawPrizeParam param) 方法。
DrawPrizeServiceImpl 中注入 RabbitTemplate。
- 将
DrawPrizeParam 序列化为 JSON,与消息 ID、创建时间一起封装为 Map,通过 rabbitTemplate.convertAndSend() 发送到指定交换机与路由键。
@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));
rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING, map);
log.info("mq 消息发送成功,map: {}", JacksonUtil.writeValueAsString(map));
}
四、MQ 异步抽奖逻辑执行
4.1 消费 MQ 消息
- 消费者类
MqReceiver 使用 @RabbitListener 监听队列 DirectQueue。
- 收到消息后,解析出
DrawPrizeParam,按顺序执行以下步骤:
- 核对抽奖信息有效性。
- 扭转活动状态(奖品、人员、活动)。
- 保存中奖结果。
- 并发处理后续流程(通知)。
- 异常时捕获并调用回滚方法,保证事务一致性。
@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。
@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());
return false;
}
if (activityDO.getStatus().equals(ActivityStatusEnum.COMPLETED.name())) {
log.info("校验抽奖请求失败!失败原因:{}", ServiceErrorCodeConstants.ACTIVITY_COMPLETED.getMsg());
return false;
}
if (activityPrizeDO.getStatus().equals(ActivityPrizeStatusEnum.COMPLETED.name())) {
log.info("校验抽奖请求失败!失败原因:{}", ServiceErrorCodeConstants.ACTIVITY_PRIZE_COMPLETED.getMsg());
return false;
}
if (activityPrizeDO.getPrizeAmount() != param.getWinnerList().size()) {
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,判断所有奖品是否抽完,若是则更新活动状态。
@Override
@Transactional(rollbackFor = Exception.class)
public void handlerEvent(ConvertActivityStatusDTO convertActivityStatusDTO) {
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());
}
}
private Boolean processConvertStatus(ConvertActivityStatusDTO convertActivityStatusDTO,
Map<String, AbstractActivityOperator> currMap,
int sequence) {
Boolean update = false;
Iterator<Map.Entry<String, AbstractActivityOperator>> iterator = currMap.entrySet().iterator();
while (iterator.hasNext()) {
AbstractActivityOperator operator = iterator.next().getValue();
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);
}
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 天。
@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);
cacheWinningRecords(param.getActivityId() + "_" + param.getPrizeId(), winningRecordDOList, WINNING_RECORDS_TIMEOUT);
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,用于并发执行通知任务。
- 并发通知实现:
- 在
MqReceiver 的 syncExecute 方法中,使用线程池分别执行 pushWinningList(邮件)和 sendMessage(短信)任务。
- 两个任务遍历中奖记录,调用对应工具类发送通知,异常不会影响主线程。
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,并更新缓存。
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
请求体:
{"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":""}
DrawPrizeController 提供 /winning-records/show 接口,接收 ShowWinningRecordsParam,调用 drawPrizeService.showWinningRecords(),返回 CommonResult<List<WinningRecordResult>>。
DrawPrizeService 已有 showWinningRecords 方法(之前用于回滚判断),直接复用。
实现(DrawPrizeServiceImpl):
- 根据参数构造缓存 key:若指定奖品 ID 则为
WINNING_RECORD_PREFIX + activityId + "_" + prizeId,否则为 WINNING_RECORD_PREFIX + activityId。
- 尝试从 Redis 获取,命中则反序列化返回。
- 未命中则查询数据库(按活动 ID 或活动 ID+ 奖品 ID),将结果转换为 DTO 列表,并存入 Redis(过期时间同前),最后返回。
@Override
public List<WinningRecordDTO> getRecords(ShowWinningRecordsParam param) {
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);
}
winningRecordDOList = winningRecordMapper.selectByActivityIdOrPrizeId(param.getActivityId(), param.getPrizeId());
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
{"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":""}
ActivityController 新增 /activity-detail/find 接口,调用 activityService.getActivityDetail(activityId),返回 FindActivityDetailResult。
ActivityService 新增 getActivityDetail(Long activityId),返回 ActivityDetailDTO。
- 首先从 Redis 获取缓存(key 为
ACTIVITY_PREFIX + activityId),若存在直接返回。
- 否则查询数据库:通过
ActivityMapper、ActivityPrizeMapper、PrizeMapper、ActivityUserMapper 组装完整详情,调用 cacheActivity 存入 Redis 后返回。
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 展示全量中奖名单,并生成'分享结果'按钮,点击复制带参数(隐藏按钮)的链接。
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online