sendUserNameMap) {
+ SysPushRecordLogCO result = new SysPushRecordLogCO();
+ result.setId(record.getId());
+ result.setTitle(record.getTitle());
+ result.setContent(record.getContent());
+ result.setSysOrigin(record.getSysOrigin());
+ result.setPlatform(record.getPlatform());
+ result.setBusinessScene(getBusinessSceneDesc(record.getBusinessScene()));
+ result.setFixedUserIds(record.getFixedUserIds());
+ result.setLink(record.getLink());
+ result.setOrigin(record.getOrigin());
+ result.setPushStatus(getPushStatusDesc(record.getPushStatus()));
+ result.setDeviceType(record.getDeviceType());
+ result.setFailQuantity(record.getFailQuantity());
+ result.setSuccessQuantity(record.getSuccessQuantity());
+ result.setSendUserName(sendUserNameMap.get(record.getCreateUser()));
+ result.setCreateTime(record.getCreateTime());
+ return result;
+ }
+
+ private String getBusinessSceneDesc(String businessScene) {
+ String desc = MessageBusinessSceneEnum.getDesc(businessScene);
+ return StringUtils.isBlank(desc) ? businessScene : desc;
+ }
+
+ private String getPushStatusDesc(String pushStatus) {
+ if (Objects.equals(PUSH_STATUS_SUCCESS, pushStatus)) {
+ return "成功";
+ }
+ if (PUSH_FAIL_STATUSES.contains(pushStatus)) {
+ return "失败";
+ }
+ return pushStatus;
+ }
+
+ private PageQuery ensurePageQuery(PageCommand pageCommand) {
+ PageQuery pageQuery = pageCommand.getPageQuery();
+ if (Objects.isNull(pageQuery)) {
+ pageQuery = new PageQuery();
+ pageCommand.setPageQuery(pageQuery);
+ }
+ if (Objects.isNull(pageQuery.getCursor()) || pageQuery.getCursor() < 1) {
+ pageQuery.setCursor(1);
+ }
+ if (Objects.isNull(pageQuery.getLimit()) || pageQuery.getLimit() < 1) {
+ pageQuery.setLimit(DEFAULT_PAGE_LIMIT);
+ }
+ return pageQuery;
+ }
+
+ private int resolvePageLimit(PageQuery pageQuery) {
+ if (Objects.isNull(pageQuery) || Objects.isNull(pageQuery.getLimit())
+ || pageQuery.getLimit() < 1) {
+ return DEFAULT_PAGE_LIMIT;
+ }
+ return pageQuery.getLimit();
+ }
+}
diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java
index f7d08a50..f383a8de 100644
--- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java
+++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/dto/clienobject/sys/SysPushRecordLogCO.java
@@ -1,11 +1,12 @@
package com.red.circle.console.app.dto.clienobject.sys;
-import com.fasterxml.jackson.databind.annotation.JsonSerialize;
-import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
-import java.io.Serial;
-import java.io.Serializable;
-import lombok.Data;
-import lombok.experimental.Accessors;
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
+import java.io.Serial;
+import java.io.Serializable;
+import java.sql.Timestamp;
+import lombok.Data;
+import lombok.experimental.Accessors;
/**
*
@@ -89,9 +90,14 @@ public class SysPushRecordLogCO implements Serializable {
*/
private Integer successQuantity;
- /**
- * 发送用户.
- */
- private String sendUserName;
-
-}
+ /**
+ * 发送用户.
+ */
+ private String sendUserName;
+
+ /**
+ * 创建时间.
+ */
+ private Timestamp createTime;
+
+}
diff --git a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java
index 8697fa4d..fec2aaf1 100644
--- a/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java
+++ b/rc-service/rc-service-console/console-client/src/main/java/com/red/circle/console/app/service/app/sys/SysPushService.java
@@ -1,36 +1,43 @@
-package com.red.circle.console.app.service.app.sys;
-
-
-/**
- * 系统推送服务.
- *
- * @author pengliang
- * @since 2020/3/6 15:53
- */
-public interface SysPushService {
- /*
- *//**
- * 创建推送.
- *//*
- void create(CreateRecordCmd createRecordParam);
-
- *//**
- * 获取push日志分页列表
- *//*
- PageResult pageLogs(SysPushRecordLogQryCmd query);
-
- *//**
- * 定时推送任务分页列表
- *//*
- PageResult pageTask(SysPushTaskQryCmd query);
-
- *//**
- * 保存推送任务
- *//*
- void saveTask(SysPushTaskCmd param);
-
- *//**
- * 删除任务
- *//*
- void deletePushTask(Long id);*/
-}
+package com.red.circle.console.app.service.app.sys;
+
+import com.red.circle.console.app.dto.clienobject.sys.SysPushRecordLogCO;
+import com.red.circle.console.app.dto.clienobject.sys.SysPushTaskCO;
+import com.red.circle.framework.dto.PageResult;
+import com.red.circle.other.inner.model.cmd.sys.CreateRecordCmd;
+import com.red.circle.other.inner.model.cmd.sys.SysPushRecordLogQryCmd;
+import com.red.circle.other.inner.model.cmd.sys.SysPushTaskCmd;
+import com.red.circle.other.inner.model.cmd.sys.SysPushTaskQryCmd;
+
+/**
+ * 系统推送服务.
+ *
+ * @author pengliang
+ * @since 2020/3/6 15:53
+ */
+public interface SysPushService {
+
+ /**
+ * 创建推送.
+ */
+ void create(CreateRecordCmd createRecordParam);
+
+ /**
+ * 获取push日志分页列表.
+ */
+ PageResult pageLogs(SysPushRecordLogQryCmd query);
+
+ /**
+ * 定时推送任务分页列表.
+ */
+ PageResult pageTask(SysPushTaskQryCmd query);
+
+ /**
+ * 保存推送任务.
+ */
+ void saveTask(SysPushTaskCmd param);
+
+ /**
+ * 删除任务.
+ */
+ void deletePushTask(Long id);
+}
diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java
new file mode 100644
index 00000000..a2d82f62
--- /dev/null
+++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/dao/sys/SysPushRecordLogDAO.java
@@ -0,0 +1,11 @@
+package com.red.circle.console.infra.database.rds.dao.sys;
+
+import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
+import com.red.circle.framework.mybatis.dao.BaseDAO;
+
+/**
+ * Push 日志 DAO.
+ */
+public interface SysPushRecordLogDAO extends BaseDAO {
+
+}
diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java
new file mode 100644
index 00000000..27888038
--- /dev/null
+++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/entity/sys/SysPushRecordLog.java
@@ -0,0 +1,64 @@
+package com.red.circle.console.infra.database.rds.entity.sys;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
+import java.io.Serial;
+import java.io.Serializable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+
+/**
+ * Push 日志.
+ */
+@Data
+@EqualsAndHashCode(callSuper = false)
+@Accessors(chain = true)
+@TableName("sys_push_record_log")
+public class SysPushRecordLog extends TimestampBaseEntity implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ @TableId(value = "id", type = IdType.ASSIGN_ID)
+ private Long id;
+
+ @TableField("title")
+ private String title;
+
+ @TableField("content")
+ private String content;
+
+ @TableField("sys_origin")
+ private String sysOrigin;
+
+ @TableField("platform")
+ private String platform;
+
+ @TableField("business_scene")
+ private String businessScene;
+
+ @TableField("fixed_user_ids")
+ private String fixedUserIds;
+
+ @TableField("link")
+ private String link;
+
+ @TableField("origin")
+ private String origin;
+
+ @TableField("push_status")
+ private String pushStatus;
+
+ @TableField("device_type")
+ private String deviceType;
+
+ @TableField("fail_quantity")
+ private Integer failQuantity;
+
+ @TableField("success_quantity")
+ private Integer successQuantity;
+}
diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java
new file mode 100644
index 00000000..3bec5277
--- /dev/null
+++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/SysPushRecordLogService.java
@@ -0,0 +1,11 @@
+package com.red.circle.console.infra.database.rds.service.sys;
+
+import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
+import com.red.circle.framework.mybatis.service.BaseService;
+
+/**
+ * Push 日志服务.
+ */
+public interface SysPushRecordLogService extends BaseService {
+
+}
diff --git a/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java
new file mode 100644
index 00000000..cb20383d
--- /dev/null
+++ b/rc-service/rc-service-console/console-infrastructure/src/main/java/com/red/circle/console/infra/database/rds/service/sys/impl/SysPushRecordLogServiceImpl.java
@@ -0,0 +1,16 @@
+package com.red.circle.console.infra.database.rds.service.sys.impl;
+
+import com.red.circle.console.infra.database.rds.dao.sys.SysPushRecordLogDAO;
+import com.red.circle.console.infra.database.rds.entity.sys.SysPushRecordLog;
+import com.red.circle.console.infra.database.rds.service.sys.SysPushRecordLogService;
+import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * Push 日志服务实现.
+ */
+@Service
+public class SysPushRecordLogServiceImpl extends
+ BaseServiceImpl implements SysPushRecordLogService {
+
+}
diff --git a/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java b/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java
index 0e4a6599..814b85e2 100644
--- a/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java
+++ b/rc-service/rc-service-live/live-start/src/main/java/com/red/circle/LiveServiceApplication.java
@@ -1,23 +1,28 @@
-package com.red.circle;
-
-import com.red.circle.component.redis.RedisAutoConfiguration;
-import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
-import org.springframework.cloud.openfeign.EnableFeignClients;
-import org.springframework.scheduling.annotation.EnableScheduling;
+package com.red.circle;
+
+import com.red.circle.component.redis.RedisAutoConfiguration;
+import com.red.circle.framework.cloud.annotation.RedCircleCloudApplication;
+import com.red.circle.live.adapter.app.LiveMicRestController;
+import com.red.circle.live.adapter.app.LiveRoomRestController;
+import com.red.circle.live.adapter.app.LiveUserRestController;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.context.annotation.Import;
+import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author pengliang on 2023/12/7
*/
@Slf4j
-@EnableScheduling
-@EnableDiscoveryClient
-@EnableFeignClients
-@SpringBootApplication(exclude = RedisAutoConfiguration.class)
-public class LiveServiceApplication {
+@EnableScheduling
+@EnableDiscoveryClient
+@EnableFeignClients
+@SpringBootApplication(exclude = RedisAutoConfiguration.class)
+@Import({LiveMicRestController.class, LiveRoomRestController.class, LiveUserRestController.class})
+public class LiveServiceApplication {
public static void main(String[] args) {
log.info("Begin to start Spring Boot Application");
diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java
index 236de5dd..2a1dd4ea 100644
--- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java
+++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommon.java
@@ -126,11 +126,12 @@ public class GameLuckyGiftCommon {
private final RankingActivityService rankingActivityService;
private final GiftMqMessage giftMqMessage;
private final GiftAppConvertor giftAppConvertor;
- private final ActivityRechargeTicketService activityRechargeTicketService;
- private final RoomDailyTaskProgressService roomDailyTaskProgressService;
- private final RoomMemberService roomMemberService;
- private final RedisService redisService;
- private final RoomProfileManagerService roomProfileManagerService;
+ private final ActivityRechargeTicketService activityRechargeTicketService;
+ private final RoomDailyTaskProgressService roomDailyTaskProgressService;
+ private final RoomMemberService roomMemberService;
+ private final RedisService redisService;
+ private final RoomProfileManagerService roomProfileManagerService;
+ private final LuckyGiftSendMetricsService luckyGiftSendMetricsService;
/**
* 发送幸运礼物抽奖mq.
@@ -290,26 +291,35 @@ public class GameLuckyGiftCommon {
Integer totalAwardAmount = apiResult.getOrderResults().stream().mapToInt(OrderResult::getRewardNum).sum();
- // 保存幸运礼物流水
- Long rewardAmount = Long.valueOf(String.valueOf(totalAwardAmount));
- saveLuckGiftCount(param, rewardAmount);
-
- if (totalAwardAmount <= 0) {
- return;
- }
-
- // 发送中奖金币
- BigDecimal balance = sendGold(param.getUserId(), param.getGiftId(), param.getSysOrigin(), rewardAmount);
-
- // 发送中奖通知
- int multiple = getRewardMultiple(param, rewardAmount);
- sendMsg(param, multiple, balance, rewardAmount);
-
- // 魔法礼物中奖后处理
- GiftConfigDTO giftConfigDTO = getGiftConfigDTO(cmd.getGiftId());
- if (GiftTabEnum.MAGIC.name().equals(giftConfigDTO.getGiftTab())) {
- String timeId = IdWorker.getIdStr();
- GiveAwayGiftBatchEvent giftBatchEvent = buildGiftBatchEvent(cmd, timeId, param, giftConfigDTO);
+ GiftConfigDTO giftConfigDTO = getGiftConfigDTO(cmd.getGiftId());
+ luckyGiftSendMetricsService.settle(
+ "legacy:" + cmd.getId(),
+ cmd.getRoomId(),
+ cmd.getUserId(),
+ giftConfigDTO.getGiftCandy(),
+ cmd.getQuantity(),
+ Objects.isNull(cmd.getAcceptUserId()) ? Collections.emptyList() : List.of(cmd.getAcceptUserId())
+ );
+
+ // 保存幸运礼物流水
+ Long rewardAmount = Long.valueOf(String.valueOf(totalAwardAmount));
+ saveLuckGiftCount(param, rewardAmount);
+
+ if (totalAwardAmount <= 0) {
+ return;
+ }
+
+ // 发送中奖金币
+ BigDecimal balance = sendGold(param.getUserId(), param.getGiftId(), param.getSysOrigin(), rewardAmount);
+
+ // 发送中奖通知
+ int multiple = getRewardMultiple(param, rewardAmount);
+ sendMsg(param, multiple, balance, rewardAmount);
+
+ // 魔法礼物中奖后处理
+ if (GiftTabEnum.MAGIC.name().equals(giftConfigDTO.getGiftTab())) {
+ String timeId = IdWorker.getIdStr();
+ GiveAwayGiftBatchEvent giftBatchEvent = buildGiftBatchEvent(cmd, timeId, param, giftConfigDTO);
giftMqMessage.sendGift(timeId, giftBatchEvent);
diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java
index 68759e74..0f7a0e93 100644
--- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java
+++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessor.java
@@ -58,6 +58,7 @@ public class LuckyGiftResultProcessor {
private final RedisService redisService;
private final RoomProfileManagerService roomProfileManagerService;
private final GameLuckyGiftCommon gameLuckyGiftCommon;
+ private final LuckyGiftSendMetricsService luckyGiftSendMetricsService;
public void process(GameLuckyGiftBusinessEvent event, LuckyGiftGoClient.LuckyGiftGoDrawResponse response) {
GiftConfigDTO giftConfig = giftCacheService.getById(event.getGiftId());
@@ -85,6 +86,18 @@ public class LuckyGiftResultProcessor {
long payAmount = resolvePayAmount(event, giftUnitPrice);
BigDecimal balanceAfter = BigDecimal.valueOf(Objects.requireNonNullElse(response.getBalanceAfter(), 0L));
+ luckyGiftSendMetricsService.settle(
+ event.getBusinessId(),
+ event.getRoomId(),
+ event.getUserId(),
+ giftUnitPrice,
+ event.getQuantity(),
+ Optional.ofNullable(event.getUsers()).orElse(Collections.emptyList()).stream()
+ .map(GameLuckyGiftUser::getAcceptUserId)
+ .filter(Objects::nonNull)
+ .toList()
+ );
+
for (LuckyGiftGoClient.LuckyGiftGoDrawResult result : response.getResults()) {
GameLuckyGiftUser user = userMap.get(result.getId());
if (user == null) {
diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java
new file mode 100644
index 00000000..8ffbd519
--- /dev/null
+++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsService.java
@@ -0,0 +1,149 @@
+package com.red.circle.other.app.common.gift;
+
+import com.red.circle.component.redis.service.RedisService;
+import com.red.circle.other.domain.enums.VipBenefitType;
+import com.red.circle.other.domain.gateway.user.UserProfileGateway;
+import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
+import com.red.circle.other.domain.model.user.ability.RegionConfig;
+import com.red.circle.other.infra.common.team.TeamTargetCommon;
+import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
+import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
+import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
+import com.red.circle.other.infra.database.rds.service.user.user.ConsumptionLevelService;
+import com.red.circle.tool.core.collection.CollectionUtils;
+import com.red.circle.tool.core.num.ArithmeticUtils;
+import com.red.circle.tool.core.text.StringUtils;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+/**
+ * 幸运礼物送出后的统计补偿.
+ * 只补房间流水、魅力值、主播收礼积分,固定按 10% 结算。
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class LuckyGiftSendMetricsService {
+
+ private static final String PROCESSED_KEY_PREFIX = "lucky_gift:send:metrics:processed:";
+ private static final BigDecimal LUCKY_GIFT_METRIC_RATIO = new BigDecimal("0.1");
+
+ private final RedisService redisService;
+ private final TeamTargetCommon teamTargetCommon;
+ private final TeamMemberService teamMemberService;
+ private final UserRegionGateway userRegionGateway;
+ private final UserProfileGateway userProfileGateway;
+ private final ConsumptionLevelService consumptionLevelService;
+ private final RoomProfileManagerService roomProfileManagerService;
+
+ public void settle(
+ String idempotencyKey,
+ Long roomId,
+ Long sendUserId,
+ BigDecimal giftUnitPrice,
+ Integer quantity,
+ List acceptUserIds
+ ) {
+ if (StringUtils.isBlank(idempotencyKey) || sendUserId == null || giftUnitPrice == null
+ || quantity == null || quantity <= 0 || CollectionUtils.isEmpty(acceptUserIds)) {
+ return;
+ }
+
+ if (!redisService.setIfAbsent(PROCESSED_KEY_PREFIX + idempotencyKey, "1", 7,
+ TimeUnit.DAYS)) {
+ return;
+ }
+
+ BigDecimal singleGiftAmount = giftUnitPrice.multiply(BigDecimal.valueOf(quantity));
+ BigDecimal singleMetricAmount = calculateMetricAmount(singleGiftAmount);
+ if (ArithmeticUtils.lteZero(singleMetricAmount)) {
+ return;
+ }
+
+ settleRoomValue(roomId, sendUserId, singleGiftAmount, acceptUserIds.size());
+ settleCharm(acceptUserIds, singleMetricAmount);
+ settleHostPoints(sendUserId, acceptUserIds, singleMetricAmount);
+ }
+
+ private void settleRoomValue(Long roomId, Long sendUserId, BigDecimal singleGiftAmount,
+ int acceptSize) {
+ if (roomId == null || acceptSize <= 0) {
+ return;
+ }
+
+ RoomProfile roomProfile = roomProfileManagerService.getProfileById(roomId);
+ if (roomProfile == null || !Objects.equals(roomProfile.getUserId(), sendUserId)
+ || teamMemberService.getByMemberId(sendUserId) == null) {
+ return;
+ }
+
+ BigDecimal roomMetricAmount = calculateMetricAmount(
+ singleGiftAmount.multiply(BigDecimal.valueOf(acceptSize)));
+ if (ArithmeticUtils.lteZero(roomMetricAmount)) {
+ return;
+ }
+ teamTargetCommon.incrementRoomValue(sendUserId, roomMetricAmount);
+ }
+
+ private void settleCharm(List acceptUserIds, BigDecimal singleMetricAmount) {
+ for (Long acceptUserId : acceptUserIds) {
+ if (acceptUserId == null) {
+ continue;
+ }
+
+ BigDecimal vipBenefit = Objects.requireNonNullElse(
+ userProfileGateway.getUserVipBenefit(acceptUserId, VipBenefitType.XP_RATE),
+ VipBenefitType.XP_RATE.getDefaultValue()
+ );
+ BigDecimal charmAmount = singleMetricAmount.multiply(vipBenefit)
+ .setScale(0, RoundingMode.DOWN);
+ if (ArithmeticUtils.lteZero(charmAmount)) {
+ continue;
+ }
+ consumptionLevelService.incrConsumptionDiamond(acceptUserId, charmAmount);
+ }
+ }
+
+ private void settleHostPoints(Long sendUserId, List acceptUserIds,
+ BigDecimal singleMetricAmount) {
+ RegionConfig sendRegion = userRegionGateway.getRegionConfigByUserId(sendUserId);
+ if (sendRegion == null || StringUtils.isBlank(sendRegion.getRegionCode())) {
+ return;
+ }
+
+ Set anchorUserIds = new HashSet<>(
+ teamMemberService.listMemberIds(new HashSet<>(acceptUserIds)));
+ for (Long acceptUserId : acceptUserIds) {
+ if (acceptUserId == null || !anchorUserIds.contains(acceptUserId)) {
+ continue;
+ }
+
+ RegionConfig acceptRegion = userRegionGateway.getRegionConfigByUserId(acceptUserId);
+ if (acceptRegion == null || StringUtils.isBlank(acceptRegion.getRegionCode())) {
+ continue;
+ }
+
+ if (!userRegionGateway.checkBusinessRegionCode(sendRegion.getRegionCode(),
+ acceptRegion.getRegionCode())) {
+ continue;
+ }
+
+ teamTargetCommon.incrementGiftValue(acceptUserId, singleMetricAmount);
+ }
+ }
+
+ private BigDecimal calculateMetricAmount(BigDecimal amount) {
+ if (ArithmeticUtils.lteZero(amount)) {
+ return BigDecimal.ZERO;
+ }
+ return amount.multiply(LUCKY_GIFT_METRIC_RATIO).setScale(0, RoundingMode.DOWN);
+ }
+}
diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java
new file mode 100644
index 00000000..a4a76109
--- /dev/null
+++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/GameLuckyGiftCommonTest.java
@@ -0,0 +1,205 @@
+package com.red.circle.other.app.common.gift;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.red.circle.component.redis.service.RedisService;
+import com.red.circle.external.inner.endpoint.message.ImGroupClient;
+import com.red.circle.mq.rocket.business.producer.GiftMqMessage;
+import com.red.circle.mq.rocket.business.service.MessageSenderService;
+import com.red.circle.other.app.convertor.material.GiftAppConvertor;
+import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
+import com.red.circle.other.app.service.activity.ActivityRechargeTicketService;
+import com.red.circle.other.app.service.activity.RankingActivityService;
+import com.red.circle.other.app.service.game.BaishunRuntimeConfigResolver;
+import com.red.circle.other.app.service.task.RoomDailyTaskProgressService;
+import com.red.circle.other.domain.gateway.user.UserProfileGateway;
+import com.red.circle.other.domain.model.user.UserProfile;
+import com.red.circle.other.infra.config.LuckyGiftApiConfig;
+import com.red.circle.other.infra.database.cache.entity.game.luckgift.GameLuckyGiftInventoryCache;
+import com.red.circle.other.infra.database.cache.entity.game.luckgift.LuckyGiftInventoryTemplateCache;
+import com.red.circle.other.infra.database.cache.service.other.EnumConfigCacheService;
+import com.red.circle.other.infra.database.cache.service.other.GameLuckyGiftCacheService;
+import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
+import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
+import com.red.circle.other.infra.database.rds.entity.game.GameLuckyGiftCount;
+import com.red.circle.other.infra.database.rds.service.game.GameLuckyGiftCountService;
+import com.red.circle.other.infra.database.rds.service.game.GameLuckyGiftRuleConfigService;
+import com.red.circle.other.infra.database.rds.service.game.LuckyGiftProbabilityDetailsService;
+import com.red.circle.other.infra.database.rds.service.game.LuckyGiftProbabilityService;
+import com.red.circle.other.infra.database.rds.service.live.RoomMemberService;
+import com.red.circle.other.infra.database.rds.service.sys.EnumConfigService;
+import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
+import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
+import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient;
+import java.math.BigDecimal;
+import java.util.Collections;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+
+class GameLuckyGiftCommonTest {
+
+ @Test
+ void run_shouldSettleLegacyMetricsWhenAwardIsZero() {
+ TestFixture fixture = new TestFixture();
+ fixture.mockGiftAndSender();
+
+ GameLuckyGiftParamCmd cmd = fixture.newCmd().setAcceptUserId(3001L);
+
+ fixture.common.run(cmd);
+
+ verify(fixture.luckyGiftSendMetricsService).settle(
+ "legacy:9001",
+ 1001L,
+ 2001L,
+ BigDecimal.valueOf(100),
+ 2,
+ List.of(3001L)
+ );
+
+ ArgumentCaptor countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
+ verify(fixture.gameLuckyGiftCountService).save(countCaptor.capture());
+ assertEquals(Long.valueOf(0), countCaptor.getValue().getAwardAmount());
+ assertEquals(Long.valueOf(200), countCaptor.getValue().getPayAmount());
+ }
+
+ @Test
+ void run_shouldHandleNullAcceptUserIdWhenAwardIsZero() {
+ TestFixture fixture = new TestFixture();
+ fixture.mockGiftAndSender();
+
+ GameLuckyGiftParamCmd cmd = fixture.newCmd().setAcceptUserId(null);
+
+ assertDoesNotThrow(() -> fixture.common.run(cmd));
+ verify(fixture.luckyGiftSendMetricsService).settle(
+ "legacy:9001",
+ 1001L,
+ 2001L,
+ BigDecimal.valueOf(100),
+ 2,
+ Collections.emptyList()
+ );
+ }
+
+ private static final class TestFixture {
+
+ private final MessageSenderService senderService = mock(MessageSenderService.class);
+ private final ImGroupClient imGroupClient = mock(ImGroupClient.class);
+ private final WalletGoldClient walletGoldClient = mock(WalletGoldClient.class);
+ private final GiftCacheService giftCacheService = mock(GiftCacheService.class);
+ private final EnumConfigService enumConfigService = mock(EnumConfigService.class);
+ private final UserProfileGateway userProfileGateway = mock(UserProfileGateway.class);
+ private final EnumConfigCacheService enumConfigCacheService = mock(EnumConfigCacheService.class);
+ private final UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
+ private final GameLuckyGiftCountService gameLuckyGiftCountService = mock(GameLuckyGiftCountService.class);
+ private final GameLuckyGiftCacheService gameLuckyGiftCacheService = mock(GameLuckyGiftCacheService.class);
+ private final LuckyGiftProbabilityService luckyGiftProbabilityService = mock(
+ LuckyGiftProbabilityService.class);
+ private final GameLuckyGiftRuleConfigService gameLuckyGiftRuleConfigService = mock(
+ GameLuckyGiftRuleConfigService.class);
+ private final LuckyGiftProbabilityDetailsService luckyGiftProbabilityDetailsService = mock(
+ LuckyGiftProbabilityDetailsService.class);
+ private final LuckyGiftApiConfig luckyGiftApiConfig = mock(LuckyGiftApiConfig.class);
+ private final BaishunRuntimeConfigResolver baishunRuntimeConfigResolver = mock(
+ BaishunRuntimeConfigResolver.class);
+ private final RankingActivityService rankingActivityService = mock(RankingActivityService.class);
+ private final GiftMqMessage giftMqMessage = mock(GiftMqMessage.class);
+ private final GiftAppConvertor giftAppConvertor = mock(GiftAppConvertor.class);
+ private final ActivityRechargeTicketService activityRechargeTicketService = mock(
+ ActivityRechargeTicketService.class);
+ private final RoomDailyTaskProgressService roomDailyTaskProgressService = mock(
+ RoomDailyTaskProgressService.class);
+ private final RoomMemberService roomMemberService = mock(RoomMemberService.class);
+ private final RedisService redisService = mock(RedisService.class);
+ private final RoomProfileManagerService roomProfileManagerService = mock(
+ RoomProfileManagerService.class);
+ private final LuckyGiftSendMetricsService luckyGiftSendMetricsService = mock(
+ LuckyGiftSendMetricsService.class);
+
+ private final GameLuckyGiftCommon common = new GameLuckyGiftCommon(
+ senderService,
+ imGroupClient,
+ walletGoldClient,
+ giftCacheService,
+ enumConfigService,
+ userProfileGateway,
+ enumConfigCacheService,
+ userProfileAppConvertor,
+ gameLuckyGiftCountService,
+ gameLuckyGiftCacheService,
+ luckyGiftProbabilityService,
+ gameLuckyGiftRuleConfigService,
+ luckyGiftProbabilityDetailsService,
+ luckyGiftApiConfig,
+ baishunRuntimeConfigResolver,
+ rankingActivityService,
+ giftMqMessage,
+ giftAppConvertor,
+ activityRechargeTicketService,
+ roomDailyTaskProgressService,
+ roomMemberService,
+ redisService,
+ roomProfileManagerService,
+ luckyGiftSendMetricsService
+ );
+
+ private void mockGiftAndSender() {
+ GiftConfigDTO giftConfig = new GiftConfigDTO()
+ .setId(11L)
+ .setStandardId(99L)
+ .setGiftCandy(BigDecimal.valueOf(100))
+ .setGiftPhoto("gift.png");
+ when(giftCacheService.getById(11L)).thenReturn(giftConfig);
+
+ LuckyGiftInventoryTemplateCache template = new LuckyGiftInventoryTemplateCache()
+ .setGiftGiveTotal(0)
+ .setComboLoss(0)
+ .setCaches(List.of(new GameLuckyGiftInventoryCache().setMultiple(1).setQuantity(1)));
+ when(gameLuckyGiftCacheService.getInventoryTemplate(99L, 2)).thenReturn(template);
+
+ UserProfile senderProfile = new UserProfile();
+ senderProfile.setId(2001L);
+ senderProfile.setAccount("sender001");
+ when(userProfileGateway.getByUserId(2001L)).thenReturn(senderProfile);
+
+ UserProfileDTO senderProfileDTO = new UserProfileDTO();
+ senderProfileDTO.setId(2001L);
+ senderProfileDTO.setAccount("sender001");
+ senderProfileDTO.setUserNickname("sender");
+ senderProfileDTO.setUserAvatar("sender.png");
+ when(userProfileAppConvertor.toUserProfileDTO(senderProfile)).thenReturn(senderProfileDTO);
+
+ LuckyGiftApiConfig.ApiAccount apiAccount = new LuckyGiftApiConfig.ApiAccount();
+ apiAccount.setUrl("http://127.0.0.1:1");
+ apiAccount.setAppId("test-app");
+ apiAccount.setAppChannel("test-channel");
+ apiAccount.setAppKey("test-key");
+ when(baishunRuntimeConfigResolver.resolveLuckyGiftApiAccount("LIKEI", 99L))
+ .thenReturn(apiAccount);
+ }
+
+ private GameLuckyGiftParamCmd newCmd() {
+ return new GameLuckyGiftParamCmd()
+ .setId(9001L)
+ .setUserId(2001L)
+ .setRoomId(1001L)
+ .setRoomAccount("room-account")
+ .setSysOrigin("LIKEI")
+ .setGiftId(11L)
+ .setQuantity(2)
+ .setGiftCombos(0)
+ .setRegionCode("SA")
+ .setGiftsCount(0)
+ .setComboLossCount(0)
+ .setOpenComboLoss(false)
+ .setSysComboLoss(0)
+ .setOpenGiftGiveTotal(false)
+ .setSysGiftGiveTotal(0)
+ .setStandardId(99L);
+ }
+ }
+}
diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java
index 744498fa..c0c781f5 100644
--- a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java
+++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftResultProcessorTest.java
@@ -41,6 +41,7 @@ class LuckyGiftResultProcessorTest {
RedisService redisService = mock(RedisService.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
GameLuckyGiftCommon gameLuckyGiftCommon = mock(GameLuckyGiftCommon.class);
+ LuckyGiftSendMetricsService luckyGiftSendMetricsService = mock(LuckyGiftSendMetricsService.class);
LuckyGiftResultProcessor processor = new LuckyGiftResultProcessor(
gameLuckyGiftCountService,
giftCacheService,
@@ -51,7 +52,8 @@ class LuckyGiftResultProcessorTest {
roomMemberService,
redisService,
roomProfileManagerService,
- gameLuckyGiftCommon
+ gameLuckyGiftCommon,
+ luckyGiftSendMetricsService
);
GiftConfigDTO giftConfig = new GiftConfigDTO()
@@ -110,6 +112,15 @@ class LuckyGiftResultProcessorTest {
processor.process(event, response);
+ verify(luckyGiftSendMetricsService).settle(
+ "biz-001",
+ 3003L,
+ 1001L,
+ BigDecimal.valueOf(173),
+ 4,
+ List.of(2002L)
+ );
+
ArgumentCaptor countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture());
GameLuckyGiftCount saved = countCaptor.getValue();
diff --git a/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java
new file mode 100644
index 00000000..688eb1ed
--- /dev/null
+++ b/rc-service/rc-service-other/other-application/src/test/java/com/red/circle/other/app/common/gift/LuckyGiftSendMetricsServiceTest.java
@@ -0,0 +1,127 @@
+package com.red.circle.other.app.common.gift;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.red.circle.component.redis.service.RedisService;
+import com.red.circle.other.domain.enums.VipBenefitType;
+import com.red.circle.other.domain.gateway.user.UserProfileGateway;
+import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
+import com.red.circle.other.domain.model.user.ability.RegionConfig;
+import com.red.circle.other.infra.common.team.TeamTargetCommon;
+import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
+import com.red.circle.other.infra.database.mongo.entity.team.team.TeamMember;
+import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
+import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
+import com.red.circle.other.infra.database.rds.service.user.user.ConsumptionLevelService;
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class LuckyGiftSendMetricsServiceTest {
+
+ private static final long ROOM_ID = 1001L;
+ private static final long SEND_USER_ID = 2001L;
+ private static final long ACCEPT_ANCHOR_ID = 3001L;
+ private static final long ACCEPT_OTHER_ANCHOR_ID = 3002L;
+
+ private RedisService redisService;
+ private TeamTargetCommon teamTargetCommon;
+ private TeamMemberService teamMemberService;
+ private UserRegionGateway userRegionGateway;
+ private UserProfileGateway userProfileGateway;
+ private ConsumptionLevelService consumptionLevelService;
+ private RoomProfileManagerService roomProfileManagerService;
+ private LuckyGiftSendMetricsService service;
+
+ @BeforeEach
+ void setUp() {
+ redisService = mock(RedisService.class);
+ teamTargetCommon = mock(TeamTargetCommon.class);
+ teamMemberService = mock(TeamMemberService.class);
+ userRegionGateway = mock(UserRegionGateway.class);
+ userProfileGateway = mock(UserProfileGateway.class);
+ consumptionLevelService = mock(ConsumptionLevelService.class);
+ roomProfileManagerService = mock(RoomProfileManagerService.class);
+ service = new LuckyGiftSendMetricsService(
+ redisService,
+ teamTargetCommon,
+ teamMemberService,
+ userRegionGateway,
+ userProfileGateway,
+ consumptionLevelService,
+ roomProfileManagerService
+ );
+ }
+
+ @Test
+ void settle_shouldAddRoomValueCharmAndHostPointsWithTenPercent() {
+ when(redisService.setIfAbsent("lucky_gift:send:metrics:processed:biz-1", "1", 7,
+ TimeUnit.DAYS)).thenReturn(true);
+
+ RoomProfile roomProfile = new RoomProfile();
+ roomProfile.setId(ROOM_ID);
+ roomProfile.setUserId(SEND_USER_ID);
+ when(roomProfileManagerService.getProfileById(ROOM_ID)).thenReturn(roomProfile);
+ when(teamMemberService.getByMemberId(SEND_USER_ID)).thenReturn(new TeamMember());
+ when(teamMemberService.listMemberIds(Set.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID)))
+ .thenReturn(List.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID));
+
+ when(userProfileGateway.getUserVipBenefit(ACCEPT_ANCHOR_ID, VipBenefitType.XP_RATE))
+ .thenReturn(new BigDecimal("1.25"));
+ when(userProfileGateway.getUserVipBenefit(ACCEPT_OTHER_ANCHOR_ID, VipBenefitType.XP_RATE))
+ .thenReturn(BigDecimal.ONE);
+
+ RegionConfig sendRegion = new RegionConfig().setRegionCode("SA");
+ RegionConfig sameRegion = new RegionConfig().setRegionCode("SA");
+ RegionConfig diffRegion = new RegionConfig().setRegionCode("PK");
+ when(userRegionGateway.getRegionConfigByUserId(SEND_USER_ID)).thenReturn(sendRegion);
+ when(userRegionGateway.getRegionConfigByUserId(ACCEPT_ANCHOR_ID)).thenReturn(sameRegion);
+ when(userRegionGateway.getRegionConfigByUserId(ACCEPT_OTHER_ANCHOR_ID)).thenReturn(diffRegion);
+ when(userRegionGateway.checkBusinessRegionCode("SA", "SA")).thenReturn(true);
+ when(userRegionGateway.checkBusinessRegionCode("SA", "PK")).thenReturn(false);
+
+ service.settle(
+ "biz-1",
+ ROOM_ID,
+ SEND_USER_ID,
+ BigDecimal.valueOf(100),
+ 2,
+ List.of(ACCEPT_ANCHOR_ID, ACCEPT_OTHER_ANCHOR_ID)
+ );
+
+ verify(teamTargetCommon).incrementRoomValue(SEND_USER_ID, BigDecimal.valueOf(40));
+ verify(consumptionLevelService).incrConsumptionDiamond(ACCEPT_ANCHOR_ID,
+ BigDecimal.valueOf(25));
+ verify(consumptionLevelService).incrConsumptionDiamond(ACCEPT_OTHER_ANCHOR_ID,
+ BigDecimal.valueOf(20));
+ verify(teamTargetCommon).incrementGiftValue(ACCEPT_ANCHOR_ID, BigDecimal.valueOf(20));
+ verify(teamTargetCommon, never()).incrementGiftValue(ACCEPT_OTHER_ANCHOR_ID,
+ BigDecimal.valueOf(20));
+ }
+
+ @Test
+ void settle_shouldSkipWhenAlreadyProcessed() {
+ when(redisService.setIfAbsent("lucky_gift:send:metrics:processed:biz-2", "1", 7,
+ TimeUnit.DAYS)).thenReturn(false);
+
+ service.settle(
+ "biz-2",
+ ROOM_ID,
+ SEND_USER_ID,
+ BigDecimal.valueOf(100),
+ 1,
+ List.of(ACCEPT_ANCHOR_ID)
+ );
+
+ verify(teamTargetCommon, never()).incrementRoomValue(SEND_USER_ID, BigDecimal.TEN);
+ verify(consumptionLevelService, never()).incrConsumptionDiamond(ACCEPT_ANCHOR_ID,
+ BigDecimal.TEN);
+ verify(teamTargetCommon, never()).incrementGiftValue(ACCEPT_ANCHOR_ID, BigDecimal.TEN);
+ }
+}