拉新奖励逻辑更新

This commit is contained in:
tianfeng 2026-03-06 11:31:10 +08:00
parent 7cbcef2772
commit e2849d2cca
5 changed files with 228 additions and 79 deletions

View File

@ -1,5 +1,6 @@
package com.red.circle.other.domain.gateway.user.ability;
import com.red.circle.other.domain.model.user.RechargeCommissionResult;
import java.math.BigDecimal;
/**
@ -15,9 +16,9 @@ public interface InviteUserGateway {
* @param rechargeAmount 充值金额
* @param sysOrigin 系统
* @param inviteUserId 被邀请人(充值人)
* @return 佣金处理结果无邀请关系时返回null
*/
void getRechargeCommission(BigDecimal rechargeAmount, String sysOrigin,
RechargeCommissionResult getRechargeCommission(BigDecimal rechargeAmount, String sysOrigin,
Long inviteUserId);
}

View File

@ -0,0 +1,29 @@
package com.red.circle.other.domain.model.user;
import java.math.BigDecimal;
import lombok.Data;
/**
* 邀请佣金处理结果.
*
* @author tf
*/
@Data
public class RechargeCommissionResult {
/** 邀请人(A)的userId. */
private Long inviterUserId;
/** 被邀请人(B)的userId. */
private Long inviteeUserId;
/** 被邀请人累计充值金额Redis累计用于档位判断. */
private BigDecimal inviteeTotalRecharge;
/** 系统来源. */
private String sysOrigin;
/** 被邀请人是否为7天内新用户. */
private boolean newUser;
}

View File

@ -13,7 +13,12 @@ public enum UserOneTimeTaskTypeEnum {
/**
* 首次充值.
*/
FIRST_CHARGE;
FIRST_CHARGE,
/**
* 邀请奖励.
*/
INVITE_REWARD;
}

View File

@ -3,7 +3,9 @@ package com.red.circle.other.infra.gateway.user;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.RechargeCommissionResult;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.domain.ranking.RankingActivityType;
import com.red.circle.other.domain.ranking.RankingCycleType;
@ -11,26 +13,13 @@ import com.red.circle.other.domain.ranking.RankingDimension;
import com.red.circle.other.infra.database.mongo.entity.activity.RankingActivityRecord;
import com.red.circle.other.infra.database.rds.dao.user.user.UserInviteUserDAO;
import com.red.circle.other.infra.database.rds.entity.user.user.InviteUserRechargeCommission;
import com.red.circle.other.infra.database.rds.entity.user.user.OneTimeTask;
import com.red.circle.other.infra.database.rds.entity.user.user.UserInviteUser;
import com.red.circle.other.infra.database.rds.service.user.user.OneTimeTaskService;
import com.red.circle.other.infra.gateway.RankingActivityGateway;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.other.domain.gateway.user.ability.InviteUserGateway;
import com.red.circle.other.infra.database.cache.service.user.UserInviteCacheService;
import com.red.circle.other.infra.database.rds.entity.user.user.InviteUserRegister;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserRechargeCommissionService;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserRegisterService;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserSummaryService;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import com.red.circle.other.infra.database.cache.service.user.UserInviteCacheService;
import com.red.circle.other.infra.gateway.RankingActivityGateway;
import com.red.circle.other.domain.gateway.user.ability.InviteUserGateway;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.date.ZonedDateTimeAsiaRiyadhUtils;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd;
@ -39,6 +28,15 @@ import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 被邀请的用户充值邀请人获得佣金处理 实现.
*
@ -49,61 +47,57 @@ import org.springframework.stereotype.Component;
@RequiredArgsConstructor
public class InviteUserGatewayImpl implements InviteUserGateway {
private static final String INVITE_RECHARGE_TOTAL_KEY = "invite:recharge:total:%d:%d";
private static final long INVITE_RECHARGE_TOTAL_EXPIRE_DAYS = 30L;
private final WalletGoldClient walletGoldClient;
private final UserInviteCacheService userInviteCacheService;
private final InviteUserSummaryService inviteUserSummaryService;
private final UserInviteUserDAO userInviteUserDAO;
private final UserProfileGateway userProfileGateway;
private final InviteUserRechargeCommissionService inviteUserRechargeCommissionService;
private final OneTimeTaskService oneTimeTaskService;
private final RankingActivityGateway rankingActivityGateway;
private final RedisService redisService;
/**
* 被邀请人充值邀请人获得佣金
*
* @param rechargeAmount 充值金额
* @param sysOrigin 系统
* @param inviteUserId 被邀请人(充值人)
*/
@Override
public void getRechargeCommission(BigDecimal rechargeAmount, String sysOrigin,
public RechargeCommissionResult getRechargeCommission(BigDecimal rechargeAmount, String sysOrigin,
Long inviteUserId) {
//根据被邀请人获得邀请人
UserInviteUser userInviteUser = getByInviteUserId(inviteUserId);
if (Objects.isNull(userInviteUser)) {
return;
return null;
}
//能够赚取佣金的天数
Integer commissionDays = getCommissionDays(sysOrigin);
if (Objects.isNull(commissionDays)) {
log.info("能够赚取佣金的天数 => null, inviteUserId:{}", inviteUserId);
return;
return null;
}
//邀请时间 + 有效天数 > 当前时间 则可以继续获得佣金
Timestamp limitedTime = getLimitedTime(userInviteUser, commissionDays);
if (limitedTime.compareTo(TimestampUtils.now()) < 0) {
log.info("邀请时间 + 有效天数 < 当前时间, inviteUserId:{}", inviteUserId);
return;
return null;
}
//获得佣金比例
Double commissionRate = getCommissionRate(sysOrigin, userInviteUser.getInviteUserId());
Double commissionRate = getCommissionRate(sysOrigin);
if (Objects.isNull(commissionRate)) {
log.info("获得佣金比例 => null, inviteUserId:{}", inviteUserId);
return;
return null;
}
//获得的佣金 = 糖果数 * 佣金比例
BigDecimal commission = getCommission(rechargeAmount, commissionRate);
if (commission.compareTo(BigDecimal.ZERO) <= 0) {
log.info("糖果数 * 佣金比例 <= 0, inviteUserId:{}", inviteUserId);
return;
return null;
}
inviteUserSummaryService.incrCommission(userInviteUser.getUserId(), commission);
inviteUserRechargeCommissionService.add(sysOrigin, userInviteUser.getUserId(), userInviteUser.getInviteUserId(), commission);
inviteUserRechargeCommissionService.add(sysOrigin, userInviteUser.getUserId(),
userInviteUser.getInviteUserId(), commission);
// 累计邀请人数判断inviteUserRechargeCommissionService里是否已有该被邀请人的记录
// 查询该被邀请人在此之前是否有充值记录
@ -114,7 +108,7 @@ public class InviteUserGatewayImpl implements InviteUserGateway {
// 如果记录数 < 1说明是首次充值当前这条刚插入才累计邀请人数+1
if (recordCount < 1) {
log.info("被邀请人首次充值,累计邀请人数+1, inviterUserId:{}, inviteUserId:{}",
log.info("被邀请人首次充值,累计邀请人数+1, inviterUserId:{}, inviteeUserId:{}",
userInviteUser.getUserId(), userInviteUser.getInviteUserId());
inviteUserSummaryService.incrUser(userInviteUser.getUserId(), 1);
}
@ -122,21 +116,39 @@ public class InviteUserGatewayImpl implements InviteUserGateway {
awardCommission(sysOrigin, userInviteUser.getUserId(), commission, userInviteUser.getInviteUserId());
recordRanking(userInviteUser.getUserId(), commission.longValue());
BigDecimal totalRecharge = accumulateRecharge(userInviteUser.getUserId(), inviteUserId, rechargeAmount);
RechargeCommissionResult result = new RechargeCommissionResult();
result.setInviterUserId(userInviteUser.getUserId());
result.setInviteeUserId(inviteUserId);
result.setInviteeTotalRecharge(totalRecharge);
result.setSysOrigin(sysOrigin);
result.setNewUser(isNewUserWithinDays(inviteUserId, 7));
return result;
}
private void recordRanking(Long userId, Long quantity) {
RankingActivityRecord record = new RankingActivityRecord();
record.setSysOrigin(SysOriginPlatformEnum.LIKEI.name());
record.setUserId(userId);
record.setUserSex(1);
record.setActivityType(RankingActivityType.INVITE);
record.setCycleType(RankingCycleType.WEEKLY);
record.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString());
record.setDimension(RankingDimension.CONSUME_AMOUNT);
record.setExpiredTime(new Timestamp(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
// 累计数据
rankingActivityGateway.accumulateRankingData(record, quantity);
private BigDecimal accumulateRecharge(Long inviterUserId, Long inviteeUserId, BigDecimal rechargeAmount) {
String key = String.format(INVITE_RECHARGE_TOTAL_KEY, inviterUserId, inviteeUserId);
Long total = redisService.increment(key, rechargeAmount.longValue());
if (total != null && total <= rechargeAmount.longValue()) {
redisService.expire(key, INVITE_RECHARGE_TOTAL_EXPIRE_DAYS, TimeUnit.DAYS);
}
return total != null ? BigDecimal.valueOf(total) : rechargeAmount;
}
private void recordRanking(Long userId, Long quantity) {
RankingActivityRecord record = new RankingActivityRecord();
record.setSysOrigin(SysOriginPlatformEnum.LIKEI.name());
record.setUserId(userId);
record.setUserSex(1);
record.setActivityType(RankingActivityType.INVITE);
record.setCycleType(RankingCycleType.WEEKLY);
record.setCycleKey(ZonedDateTimeAsiaRiyadhUtils.nowWeekMondayToInt().toString());
record.setDimension(RankingDimension.CONSUME_AMOUNT);
record.setExpiredTime(new Timestamp(System.currentTimeMillis() + 30L * 24 * 60 * 60 * 1000));
rankingActivityGateway.accumulateRankingData(record, quantity);
}
/**
* 奖励佣金
@ -164,18 +176,15 @@ public class InviteUserGatewayImpl implements InviteUserGateway {
.setScale(0, RoundingMode.DOWN);
}
private Double getCommissionRate(String sysOrigin, Long userId) {
Double commissionRate = userInviteCacheService.getCommissionRate(SysOriginPlatformEnum.valueOf(sysOrigin));
private Double getCommissionRate(String sysOrigin) {
return userInviteCacheService.getCommissionRate(SysOriginPlatformEnum.valueOf(sysOrigin));
}
UserProfile userProfile = userProfileGateway.getByUserId(userId);
Duration duration = Duration.between(userProfile.getCreateTime().toLocalDateTime(), LocalDateTime.now());
if (duration.toDays() < 7) {
OneTimeTask oneTimeTask = oneTimeTaskService.queryCompletedFirstCharge(userId, sysOrigin);
if (Objects.isNull(oneTimeTask)) {
commissionRate += 0.03;
}
}
return commissionRate;
/** 判断用户是否为注册N天内的新用户 */
private boolean isNewUserWithinDays(Long userId, int days) {
UserProfile userProfile = userProfileGateway.getByUserId(userId);
Duration duration = Duration.between(userProfile.getCreateTime().toLocalDateTime(), LocalDateTime.now());
return duration.toDays() < days;
}
private UserInviteUser getByInviteUserId(Long inviteUserId) {
@ -196,13 +205,11 @@ public class InviteUserGatewayImpl implements InviteUserGateway {
}
private Integer getCommissionDays(String sysOrigin) {
return userInviteCacheService
.getCommissionDays(SysOriginPlatformEnum.valueOf(sysOrigin));
return userInviteCacheService.getCommissionDays(SysOriginPlatformEnum.valueOf(sysOrigin));
}
private Timestamp getLimitedTime(UserInviteUser userInviteUser, Integer commissionDays) {
return TimestampUtils
.dateTimestampPlusDays(userInviteUser.getCreateTime().toLocalDateTime(), commissionDays);
return TimestampUtils.dateTimestampPlusDays(userInviteUser.getCreateTime().toLocalDateTime(), commissionDays);
}
}

View File

@ -2,6 +2,8 @@ package com.red.circle.other.app.inner.service.user.user.impl;
import com.google.common.base.Throwables;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.common.business.enums.SendPropsOrigin;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.dto.PageResult;
@ -9,14 +11,19 @@ import com.red.circle.framework.mybatis.convertor.MybatisConvertor;
import com.red.circle.other.app.inner.convertor.user.UserInviteInnerConvertor;
import com.red.circle.other.app.inner.service.user.user.InviteUserClientService;
import com.red.circle.other.domain.gateway.user.ability.InviteUserGateway;
import com.red.circle.other.domain.model.user.RechargeCommissionResult;
import com.red.circle.other.infra.database.cache.service.user.UserInviteCacheService;
import com.red.circle.other.infra.database.rds.entity.user.user.OneTimeTask;
import com.red.circle.other.infra.database.rds.query.user.InviteUserQry;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserRechargeCommissionService;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserRegisterService;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserRewardRecordService;
import com.red.circle.other.infra.database.rds.service.user.user.InviteUserSummaryService;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.other.infra.database.rds.service.user.user.OneTimeTaskService;
import com.red.circle.other.infra.enums.user.user.UserOneTimeTaskTypeEnum;
import com.red.circle.other.inner.endpoint.activity.PropsActivityClient;
import com.red.circle.other.inner.endpoint.sys.EnumConfigClient;
import com.red.circle.other.inner.model.cmd.activity.SendActivityRewardCmd;
import com.red.circle.other.inner.model.cmd.user.InviteUserQryCmd;
import com.red.circle.other.inner.model.cmd.user.SysInviteUserConfigCmd;
import com.red.circle.other.inner.model.cmd.user.UserInviteRewardQryCmd;
@ -26,13 +33,23 @@ import com.red.circle.other.inner.model.dto.user.InviteUserRewardRecordDTO;
import com.red.circle.other.inner.model.dto.user.InviteUserSummaryDTO;
import com.red.circle.other.inner.model.dto.user.PageInviteUserRegisterDTO;
import com.red.circle.other.inner.model.dto.user.SysInviteUserConfigDTO;
import java.math.BigDecimal;
import java.util.List;
import java.util.Objects;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import com.red.circle.tool.core.text.StringUtils;
import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd;
import com.red.circle.wallet.inner.model.enums.GoldOrigin;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.concurrent.TimeUnit;
/**
* 邀请用户.
*
@ -43,6 +60,21 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class InviteUserClientServiceImpl implements InviteUserClientService {
// 档位阈值
private static final long TIER1_THRESHOLD = 10_000L;
private static final long TIER2_THRESHOLD = 50_000L;
private static final long TIER3_THRESHOLD = 100_000L;
// 档位道具id
private static final long TIER1_PROPS_ID = 2029448725407711234L;
private static final long TIER2_PROPS_ID = 2029449081176965121L;
private static final long TIER3_PROPS_ID = 2029449364032438273L;
// 档位金币奖励
private static final long TIER1_GOLD = 2000L;
private static final long TIER2_GOLD = 5000L;
private static final long TIER3_GOLD = 5000L;
private final InviteUserGateway inviteUserGateway;
private final UserInviteCacheService userInviteCacheService;
private final UserInviteInnerConvertor userInviteInnerConvertor;
@ -50,17 +82,99 @@ public class InviteUserClientServiceImpl implements InviteUserClientService {
private final InviteUserRegisterService inviteUserRegisterService;
private final InviteUserRewardRecordService inviteUserRewardRecordService;
private final InviteUserRechargeCommissionService rechargeCommissionService;
private final PropsActivityClient propsActivityClient;
private final WalletGoldClient walletGoldClient;
private final OneTimeTaskService oneTimeTaskService;
@Override
public void processRechargeCommission(BigDecimal rechargeAmount, String sysOrigin,
Long inviteUserId) {
try {
inviteUserGateway.getRechargeCommission(rechargeAmount, sysOrigin, inviteUserId);
RechargeCommissionResult result = inviteUserGateway.getRechargeCommission(rechargeAmount, sysOrigin, inviteUserId);
if (Objects.isNull(result)) {
return;
}
checkAndAwardTiers(result);
} catch (Exception e) {
log.error("被邀请人充值邀请人获得佣金累计错误: {}", Throwables.getStackTraceAsString(e));
}
}
/**
* 根据累计充值金额检查并发放档位奖励
*/
private void checkAndAwardTiers(RechargeCommissionResult result) {
if (!result.isNewUser()) {
return;
}
long total = result.getInviteeTotalRecharge().longValue();
if (total >= TIER3_THRESHOLD) {
tryAwardTier(result, 3, TIER3_PROPS_ID, TIER3_GOLD);
tryAwardTier(result, 2, TIER2_PROPS_ID, TIER2_GOLD);
tryAwardTier(result, 1, TIER1_PROPS_ID, TIER1_GOLD);
} else if (total >= TIER2_THRESHOLD) {
tryAwardTier(result, 2, TIER2_PROPS_ID, TIER2_GOLD);
tryAwardTier(result, 1, TIER1_PROPS_ID, TIER1_GOLD);
} else if (total >= TIER1_THRESHOLD) {
tryAwardTier(result, 1, TIER1_PROPS_ID, TIER1_GOLD);
}
}
/**
* 尝试发放指定档位奖励幂等
* 金币发给邀请人A道具发给被邀请人B
*/
private void tryAwardTier(RechargeCommissionResult result, int tier, long propsId, long gold) {
String taskType = UserOneTimeTaskTypeEnum.INVITE_REWARD.name() + "_" + tier;
OneTimeTask alreadyRewarded = oneTimeTaskService.query()
.eq(OneTimeTask::getUserId, result.getInviterUserId())
.eq(OneTimeTask::getSysOrigin, result.getSysOrigin())
.eq(OneTimeTask::getTaskType, taskType)
.last("LIMIT 1")
.getOne();
if (alreadyRewarded != null) {
return;
}
log.info("发放邀请奖励档位{}: inviterUserId={}, inviteeUserId={}", tier,
result.getInviterUserId(), result.getInviteeUserId());
// 写入记录幂等保证
oneTimeTaskService.save(new OneTimeTask()
.setUserId(result.getInviterUserId())
.setSysOrigin(result.getSysOrigin())
.setTaskType(taskType)
.setRewardQuantity(BigDecimal.valueOf(gold))
.setAwardStatus(Boolean.TRUE)
);
// 金币奖励发给邀请人A
walletGoldClient.changeBalance(GoldReceiptCmd.builder()
.appIncome()
.userId(result.getInviterUserId())
.eventId(result.getInviteeUserId())
.sysOrigin(result.getSysOrigin())
.origin(GoldOrigin.INVITE_USER_REWARDS)
.originDescribe(GoldOrigin.INVITE_USER_REWARDS.getDesc())
.amount(BigDecimal.valueOf(gold))
.build());
// 道具奖励发给被邀请人B
propsActivityClient.sendActivityReward(new SendActivityRewardCmd()
.setTrackId(IdWorkerUtils.getId())
.setSysOrigin(SysOriginPlatformEnum.LIKEI)
.setAcceptUserId(result.getInviteeUserId())
.setSourceGroupId(propsId)
.setOrigin(SendPropsOrigin.INVITE_USER_REWARDS)
);
}
@Override
public List<InviteUserRegisterDTO> pageInviteUserRegister(Integer pageNumber, Long userId) {
return userInviteInnerConvertor.toListInviteUserRegisterDTO(
@ -107,10 +221,8 @@ public class InviteUserClientServiceImpl implements InviteUserClientService {
.convert(userInviteInnerConvertor::toInviteUserRewardRecordDTO);
}
@Override
public PageResult<PageInviteUserRegisterDTO> pageInviteUserRegisterAll(InviteUserQryCmd query) {
InviteUserQry inviteUserQry = new InviteUserQry();
inviteUserQry.setSysOrigin(query.getSysOrigin());
inviteUserQry.setUserId(query.getUserId());
@ -128,7 +240,6 @@ public class InviteUserClientServiceImpl implements InviteUserClientService {
inviteUserQry).convert(userInviteInnerConvertor::toPageInviteUserRegisterDTO);
}
@Override
public void updateConfig(SysInviteUserConfigCmd config) {
ResponseAssert.isFalse(CommonErrorCode.TYPE_IS_NOT_IN_SCOPE,
@ -141,12 +252,8 @@ public class InviteUserClientServiceImpl implements InviteUserClientService {
} else {
userInviteCacheService.setCommissionRate(sysOrigin, 0.0);
}
}
/**
* 邀请用户规则
*/
@Override
public SysInviteUserConfigDTO getConfig(String sysOrigin) {
SysInviteUserConfigDTO vo = new SysInviteUserConfigDTO();