抽奖活动领取徽章接口处理

This commit is contained in:
tianfeng 2025-12-31 15:47:00 +08:00
parent 593336abc5
commit 0723bb5f8c
6 changed files with 431 additions and 1 deletions

View File

@ -324,4 +324,20 @@ public class LotteryActivityRestController extends BaseController {
return activityRewardConfigService.claimReward(cmd);
}
/**
* 获取活动奖励配置列表-徽章
*/
@GetMapping("/reward-configs-badge")
public List<ActivityRewardConfigCO> getRewardConfigsBadge(ActivityRewardConfigCmd cmd) {
return activityRewardConfigService.listRewardConfigsBadge(cmd);
}
/**
* 领取活动奖励-徽章
*/
@PostMapping("/claim-reward-badge")
public ClaimRewardResultCO claimRewardBadge(@RequestBody @Validated ClaimActivityRewardCmd cmd) {
return activityRewardConfigService.claimRewardBadge(cmd);
}
}

View File

@ -0,0 +1,179 @@
package com.red.circle.other.app.command.activity;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.google.common.collect.Lists;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.other.app.dto.clientobject.activity.ActivityRewardConfigCO;
import com.red.circle.other.app.dto.clientobject.activity.MyTotalDrawCountCO;
import com.red.circle.other.app.dto.cmd.activity.ActivityRewardConfigCmd;
import com.red.circle.other.app.service.activity.LotteryActivityRestService;
import com.red.circle.other.domain.gateway.props.ActivitySourceGroupGateway;
import com.red.circle.other.infra.database.rds.dao.activity.UserActivityRewardClaimDAO;
import com.red.circle.other.infra.database.rds.entity.activity.UserActivityRewardClaim;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
import com.red.circle.other.inner.enums.activity.PropsActivityTypeEnum;
import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsGroup;
import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsRule;
import com.red.circle.other.inner.model.dto.activity.props.ActivityRewardProps;
import com.red.circle.tool.core.json.JacksonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
* 查询活动奖励配置执行器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ActivityRewardConfigBadgeQryExe {
private final ActivitySourceGroupGateway activitySourceGroupGateway;
private final UserActivityRewardClaimDAO userActivityRewardClaimDAO;
private final UserActivityRechargeService userActivityRechargeService;
private final LotteryActivityRestService lotteryActivityRestService;
public List<ActivityRewardConfigCO> execute(ActivityRewardConfigCmd cmd) {
PropsActivityTypeEnum activityType = PropsActivityTypeEnum.valueOf(cmd.getActivityType());
SysOriginPlatformEnum sysOrigin = SysOriginPlatformEnum.valueOf(cmd.getReqSysOrigin().getOrigin());
List<ActivityPropsRule> ruleConfigs = activitySourceGroupGateway.listRule(sysOrigin, activityType);
if (CollectionUtil.isEmpty(ruleConfigs)) {
return Lists.newArrayList();
}
List<Long> ruleIds = ruleConfigs.stream().map(ActivityPropsRule::getId).collect(Collectors.toList());
Map<Long, UserActivityRewardClaim> claimMap = getUserClaimMap(cmd.getReqUserId(), ruleIds);
MyTotalDrawCountCO drawCount = lotteryActivityRestService.getMyTotalDrawCount(cmd);
BigDecimal finalUserTotalAmount = drawCount != null && drawCount.getUsedCount() != null ? new BigDecimal(drawCount.getUsedCount()) : BigDecimal.ZERO;
return ruleConfigs.stream()
.map(rule -> buildRewardConfig(rule, cmd.getReqSysOrigin().getOrigin(), claimMap, finalUserTotalAmount))
.filter(Objects::nonNull)
.collect(Collectors.toList());
}
private Map<Long, UserActivityRewardClaim> getUserClaimMap(Long userId, List<Long> ruleIds) {
List<UserActivityRewardClaim> claims = userActivityRewardClaimDAO.selectList(
new LambdaQueryWrapper<UserActivityRewardClaim>()
.eq(UserActivityRewardClaim::getUserId, userId)
.in(UserActivityRewardClaim::getRuleId, ruleIds)
);
return claims.stream()
.collect(Collectors.toMap(UserActivityRewardClaim::getRuleId, claim -> claim, (a, b) -> a));
}
private ActivityRewardConfigCO buildRewardConfig(ActivityPropsRule rule, String sysOrigin,
Map<Long, UserActivityRewardClaim> claimMap,
BigDecimal userTotalAmount) {
ActivityPropsGroup group = activitySourceGroupGateway.getActivityPropsGroup(
sysOrigin,
rule.getResourceGroupId()
);
if (Objects.isNull(group)) {
return null;
}
ActivityRewardConfigCO config = new ActivityRewardConfigCO()
.setRuleId(rule.getId())
.setSysOrigin(rule.getSysOrigin())
.setActivityType(rule.getActivityType())
.setResourceGroupId(rule.getResourceGroupId())
.setJsonData(rule.getJsonData())
.setSort(rule.getSort())
.setGroupId(group.getId())
.setGroupName(group.getName())
.setShelfStatus(group.getShelfStatus());
int claimStatus = calculateClaimStatus(rule, claimMap, userTotalAmount);
config.setClaimStatus(claimStatus);
if (CollectionUtil.isNotEmpty(group.getActivityRewardProps())) {
List<ActivityRewardConfigCO.ActivityRewardPropsCO> propsList = group.getActivityRewardProps().stream()
.map(this::convertToRewardPropsCO)
.collect(Collectors.toList());
config.setRewardProps(propsList);
}
return config;
}
/**
* 计算领取状态
* -1: 不可领取充值金额不足
* 0: 可领取充值金额达标且未领取
* 1: 已领取
*/
private int calculateClaimStatus(ActivityPropsRule rule,
Map<Long, UserActivityRewardClaim> claimMap,
BigDecimal userTotalAmount) {
UserActivityRewardClaim claim = claimMap.get(rule.getId());
if (Objects.nonNull(claim)) {
return claim.getStatus();
}
BigDecimal requiredAmount = parseRequiredAmount(rule.getJsonData());
if (Objects.isNull(requiredAmount)) {
return -1;
}
if (userTotalAmount.compareTo(requiredAmount) < 0) {
return -1;
}
return 0;
}
private BigDecimal parseRequiredAmount(String jsonData) {
if (StrUtil.isBlank(jsonData)) {
return null;
}
try {
Map<String, Object> dataMap = JacksonUtils.readValue(jsonData, Map.class);
Object quantity = dataMap.get("quantity");
if (Objects.isNull(quantity)) {
return null;
}
if (quantity instanceof Number) {
return new BigDecimal(quantity.toString());
}
if (quantity instanceof String) {
return new BigDecimal((String) quantity);
}
return null;
} catch (Exception e) {
log.error("解析jsonData失败: {}", jsonData, e);
return null;
}
}
private ActivityRewardConfigCO.ActivityRewardPropsCO convertToRewardPropsCO(ActivityRewardProps props) {
return new ActivityRewardConfigCO.ActivityRewardPropsCO()
.setId(props.getId())
.setGroupId(props.getGroupId())
.setType(props.getType())
.setDetailType(props.getDetailType())
.setContent(props.getContent())
.setSort(props.getSort())
.setCover(props.getCover())
.setQuantity(props.getQuantity())
.setAmount(props.getAmount())
.setRemark(props.getRemark());
}
}

View File

@ -0,0 +1,201 @@
package com.red.circle.other.app.command.activity;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.other.app.dto.clientobject.activity.ClaimRewardResultCO;
import com.red.circle.other.app.dto.clientobject.activity.MyTotalDrawCountCO;
import com.red.circle.other.app.dto.cmd.activity.ClaimActivityRewardCmd;
import com.red.circle.other.app.service.activity.LotteryActivityRestService;
import com.red.circle.other.domain.gateway.props.ActivitySourceGroupGateway;
import com.red.circle.other.infra.database.rds.dao.activity.UserActivityRewardClaimDAO;
import com.red.circle.other.infra.database.rds.entity.activity.LotteryDeviceRecord;
import com.red.circle.other.infra.database.rds.entity.activity.UserActivityRewardClaim;
import com.red.circle.other.infra.database.rds.service.activity.LotteryDeviceRecordService;
import com.red.circle.other.infra.database.rds.service.activity.UserActivityRechargeService;
import com.red.circle.other.infra.database.rds.service.user.device.LatestMobileDeviceService;
import com.red.circle.other.inner.asserts.DynamicErrorCode;
import com.red.circle.other.inner.asserts.OtherErrorCode;
import com.red.circle.other.inner.endpoint.activity.LotteryTicketClient;
import com.red.circle.other.inner.endpoint.material.props.BadgeBackpackClient;
import com.red.circle.other.inner.enums.activity.PropsActivityTypeEnum;
import com.red.circle.other.inner.enums.material.BadgeBackpackExpireTypeEnum;
import com.red.circle.other.inner.enums.sys.SysBadgeConfigTypeEnum;
import com.red.circle.other.inner.model.cmd.material.GiveBadgeCmd;
import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsGroup;
import com.red.circle.other.inner.model.dto.activity.props.ActivityPropsRule;
import com.red.circle.other.inner.model.dto.activity.props.ActivityRewardProps;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.json.JacksonUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/**
* 领取活动奖励执行器
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class ClaimActivityRewardBadgeExe {
private final ActivitySourceGroupGateway activitySourceGroupGateway;
private final UserActivityRechargeService userActivityRechargeService;
private final UserActivityRewardClaimDAO userActivityRewardClaimDAO;
private final LotteryTicketClient lotteryTicketClient;
private final LatestMobileDeviceService latestMobileDeviceService;
private final LotteryDeviceRecordService lotteryDeviceRecordService;
private final LotteryActivityRestService lotteryActivityRestService;
private final BadgeBackpackClient badgeBackpackClient;
@Transactional(rollbackFor = Exception.class)
public ClaimRewardResultCO execute(ClaimActivityRewardCmd cmd) {
PropsActivityTypeEnum activityType = PropsActivityTypeEnum.valueOf(cmd.getActivityType());
SysOriginPlatformEnum sysOrigin = SysOriginPlatformEnum.valueOf(cmd.getReqSysOrigin().getOrigin());
// 2. 校验设备唯一性防止小号薅羊毛
checkDeviceUnique(cmd.getActivityId(), cmd.getReqUserId());
List<ActivityPropsRule> ruleConfigs = activitySourceGroupGateway.listRule(sysOrigin, activityType);
ActivityPropsRule targetRule = ruleConfigs.stream()
.filter(rule -> Objects.equals(rule.getId(), cmd.getRuleId()))
.findFirst()
.orElse(null);
ResponseAssert.notNull(DynamicErrorCode.NO_RELATED_INFORMATION_FOUND, targetRule);
long count = userActivityRewardClaimDAO.selectCount(
new LambdaQueryWrapper<UserActivityRewardClaim>()
.eq(UserActivityRewardClaim::getUserId, cmd.getReqUserId())
.eq(UserActivityRewardClaim::getRuleId, cmd.getRuleId())
);
ResponseAssert.isFalse(CommonErrorCode.REPEATED_SUBMISSION, count > 0);
BigDecimal requiredAmount = parseRequiredAmount(targetRule.getJsonData());
ResponseAssert.notNull(DynamicErrorCode.NO_RELATED_INFORMATION_FOUND, requiredAmount);
MyTotalDrawCountCO drawCount = lotteryActivityRestService.getMyTotalDrawCount(cmd);
BigDecimal finalUserTotalAmount = drawCount != null && drawCount.getUsedCount() != null ? new BigDecimal(drawCount.getUsedCount()) : BigDecimal.ZERO;
ResponseAssert.isFalse(OtherErrorCode.USER_RECHARGE_INVALID, finalUserTotalAmount.compareTo(requiredAmount) < 0);
ActivityPropsGroup group = activitySourceGroupGateway.getActivityPropsGroup(
sysOrigin.name(),
ruleConfigs.get(0).getResourceGroupId()
);
List<ActivityRewardProps> activityRewardProps = group.getActivityRewardProps();
ResponseAssert.isFalse(DynamicErrorCode.NO_RELATED_INFORMATION_FOUND, activityRewardProps.isEmpty());
ActivityRewardProps rewardProps = activityRewardProps.get(0);
badgeBackpackClient.giveBadge(new GiveBadgeCmd()
.setAcceptUserId(cmd.getReqUserId())
.setBadgeId(Long.parseLong(rewardProps.getContent()))
.setExpireType(BadgeBackpackExpireTypeEnum.TEMPORARY)
.setDays(rewardProps.getQuantity())
.setBadgeType(SysBadgeConfigTypeEnum.ACTIVITY)
);
UserActivityRewardClaim claim = new UserActivityRewardClaim()
.setUserId(cmd.getReqUserId())
.setActivityId(cmd.getActivityId())
.setRuleId(cmd.getRuleId())
.setActivityType(cmd.getActivityType())
.setStatus(1)
.setLotteryTicketIds(String.join(",", "BADGE"))
.setCreateTime(TimestampUtils.now())
.setUpdateTime(TimestampUtils.now());
userActivityRewardClaimDAO.insert(claim);
return new ClaimRewardResultCO()
.setSuccess(true)
.setLotteryTicketIds(List.of(claim.getId()));
}
/**
* 校验设备唯一性防止小号薅羊毛
*/
private void checkDeviceUnique(Long activityId, Long userId) {
String fingerprint = latestMobileDeviceService.getEnhancedDeviceFingerprintByUserId(userId);
if (com.red.circle.tool.core.text.StringUtils.isBlank(fingerprint)) {
log.warn("Failed to get device fingerprint, userId: {}", userId);
return;
}
LotteryDeviceRecord existingRecord = lotteryDeviceRecordService
.getByActivityAndFingerprint(activityId, fingerprint);
if (existingRecord != null && !existingRecord.getUserId().equals(userId)) {
log.warn("Duplicate device detected, activityId: {}, currentUser: {}, boundUser: {}, fingerprint: {}",
activityId, userId, existingRecord.getUserId(), fingerprint);
ResponseAssert.isTrue(OtherErrorCode.DEVICE_ALREADY_USED, false);
}
}
private BigDecimal parseRequiredAmount(String jsonData) {
if (StrUtil.isBlank(jsonData)) {
return null;
}
try {
Map<String, Object> dataMap = JacksonUtils.readValue(jsonData, Map.class);
Object quantity = dataMap.get("quantity");
if (Objects.isNull(quantity)) {
return null;
}
if (quantity instanceof Number) {
return new BigDecimal(quantity.toString());
}
if (quantity instanceof String) {
return new BigDecimal((String) quantity);
}
return null;
} catch (Exception e) {
log.error("parse jsonData error: {}", jsonData, e);
return null;
}
}
private Integer parseTicketCount(String jsonData) {
if (StrUtil.isBlank(jsonData)) {
return 1;
}
try {
Map<String, Object> dataMap = JacksonUtils.readValue(jsonData, Map.class);
Object ticketCount = dataMap.get("ticketCount");
if (Objects.isNull(ticketCount)) {
return 1;
}
if (ticketCount instanceof Number) {
return ((Number) ticketCount).intValue();
}
if (ticketCount instanceof String) {
return Integer.parseInt((String) ticketCount);
}
return 1;
} catch (Exception e) {
log.error("parse ticketCount error: {}", jsonData, e);
return 1;
}
}
}

View File

@ -19,7 +19,7 @@ public class MyTotalDrawCountQryExe {
private final LotteryTicketService lotteryTicketService;
public MyTotalDrawCountCO execute(AppExtCommand cmd) {
Long userId = cmd.requiredReqUserId();
Long userId = cmd.getReqUserId();
List<LotteryTicket> tickets = lotteryTicketService.query()
.eq(LotteryTicket::getUserId, userId)

View File

@ -1,6 +1,8 @@
package com.red.circle.other.app.service.activity.impl;
import com.red.circle.other.app.command.activity.ActivityRewardConfigBadgeQryExe;
import com.red.circle.other.app.command.activity.ActivityRewardConfigQryExe;
import com.red.circle.other.app.command.activity.ClaimActivityRewardBadgeExe;
import com.red.circle.other.app.command.activity.ClaimActivityRewardExe;
import com.red.circle.other.app.dto.clientobject.activity.ActivityRewardConfigCO;
import com.red.circle.other.app.dto.clientobject.activity.ClaimRewardResultCO;
@ -22,6 +24,8 @@ public class ActivityRewardConfigServiceImpl implements ActivityRewardConfigServ
private final ActivityRewardConfigQryExe activityRewardConfigQryExe;
private final ClaimActivityRewardExe claimActivityRewardExe;
private final ActivityRewardConfigBadgeQryExe activityRewardConfigBadgeQryExe;
private final ClaimActivityRewardBadgeExe claimActivityRewardBadgeExe;
private final DistributedLockUtil distributedLockUtil;
private static final String CLAIM_LOCK_PREFIX = "activity:reward:claim:lock";
@ -44,4 +48,24 @@ public class ActivityRewardConfigServiceImpl implements ActivityRewardConfigServ
() -> claimActivityRewardExe.execute(cmd)
);
}
@Override
public List<ActivityRewardConfigCO> listRewardConfigsBadge(ActivityRewardConfigCmd cmd) {
return activityRewardConfigBadgeQryExe.execute(cmd);
}
@Override
public ClaimRewardResultCO claimRewardBadge(ClaimActivityRewardCmd cmd) {
String lockKey = DistributedLockUtil.buildLockKey(
CLAIM_LOCK_PREFIX,
cmd.getReqUserId(),
cmd.getRuleId()
);
return distributedLockUtil.executeWithLock(
lockKey,
() -> claimActivityRewardBadgeExe.execute(cmd)
);
}
}

View File

@ -21,4 +21,14 @@ public interface ActivityRewardConfigService {
* 领取活动奖励
*/
ClaimRewardResultCO claimReward(ClaimActivityRewardCmd cmd);
/**
* 获取活动奖励配置列表
*/
List<ActivityRewardConfigCO> listRewardConfigsBadge(ActivityRewardConfigCmd cmd);
/**
* 领取活动奖励
*/
ClaimRewardResultCO claimRewardBadge(ClaimActivityRewardCmd cmd);
}