diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/enums/material/PropsCommodityType.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/enums/material/PropsCommodityType.java index 9816567d..0ba68072 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/enums/material/PropsCommodityType.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/enums/material/PropsCommodityType.java @@ -70,7 +70,12 @@ public enum PropsCommodityType { /** * 红包封面. */ - RED_PACKET; + RED_PACKET, + + /** + * CP戒指. + */ + CP_RING; public boolean eq(String name) { diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpRingDTO.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpRingDTO.java new file mode 100644 index 00000000..ed8b9516 --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpRingDTO.java @@ -0,0 +1,42 @@ +package com.red.circle.other.inner.model.dto.user; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import lombok.Data; +import lombok.experimental.Accessors; + +import java.io.Serial; +import java.io.Serializable; + +/** + * CP戒指信息. + */ +@Data +@Accessors(chain = true) +public class CpRingDTO implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + /** + * 道具资源ID. + */ + @JsonSerialize(using = ToStringSerializer.class) + private Long propsId; + + /** + * 戒指名称. + */ + private String name; + + /** + * 戒指封面图. + */ + private String cover; + + /** + * 过期时间戳(毫秒). + */ + private Long expireTime; + +} diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpSimpleUserProfileCO.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpSimpleUserProfileCO.java index 984a591e..fabaa90c 100644 --- a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpSimpleUserProfileCO.java +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/model/dto/user/CpSimpleUserProfileCO.java @@ -82,4 +82,54 @@ public class CpSimpleUserProfileCO implements Serializable { private Timestamp createTime; + /** + * 当前CP等级(未达到Lv1门槛为null). + */ + private Integer cpLevel; + + /** + * 下一级所需累计CP值(满级为null). + */ + private Long nextLevelExp; + + /** + * 爱心样式等级(0=无, 1-3). + */ + private Integer loveHeartLevel; + + /** + * 邻麦动效等级(0=无, 1-3). + */ + private Integer micEffectLevel; + + /** + * 资料卡背景等级(0=无, 1-3). + */ + private Integer profileCardLevel; + + /** + * 赠礼飘窗等级(0=无, 1-3). + */ + private Integer giftWindowLevel; + + /** + * 是否解锁非邻麦动效. + */ + private Boolean hasNonMicEffect; + + /** + * 是否解锁CP头饰. + */ + private Boolean hasCpHeadwear; + + /** + * 我方当前佩戴的CP戒指(未佩戴为null). + */ + private CpRingDTO selfRing; + + /** + * CP方当前佩戴的CP戒指(未佩戴为null). + */ + private CpRingDTO cpRing; + } diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/material/UserSwitchUsePropsCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/material/UserSwitchUsePropsCmdExe.java index 5241f55a..ceabc7e1 100644 --- a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/material/UserSwitchUsePropsCmdExe.java +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/material/UserSwitchUsePropsCmdExe.java @@ -2,10 +2,15 @@ package com.red.circle.other.app.command.material; import com.red.circle.other.app.dto.cmd.material.SwitchUsePropsCmd; import com.red.circle.other.domain.gateway.user.UserProfileGateway; -import java.util.Objects; +import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; +import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService; +import com.red.circle.other.inner.enums.material.PropsCommodityType; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Component; +import java.util.List; +import java.util.Objects; + /** * 用户切换使用道具. * @@ -16,6 +21,8 @@ import org.springframework.stereotype.Component; public class UserSwitchUsePropsCmdExe { private final UserProfileGateway userProfileGateway; + private final CpRelationshipService cpRelationshipService; + private final CpRelationshipCacheService cpRelationshipCacheService; public void execute(SwitchUsePropsCmd cmd) { if (Objects.equals(cmd.getUnload(), Boolean.TRUE)) { @@ -23,13 +30,21 @@ public class UserSwitchUsePropsCmdExe { cmd.requiredReqUserId(), Objects.toString(cmd.getType()) ); - return; + } else { + userProfileGateway.switchUseProps( + cmd.requiredReqUserId(), + cmd.getPropsId() + ); + } + + // CP戒指切换后清除双方缓存 + if (Objects.equals(cmd.getType(), PropsCommodityType.CP_RING)) { + List cpUserIds = cpRelationshipService.getCpUserId(cmd.requiredReqUserId()); + cpRelationshipCacheService.remove(List.of(cmd.requiredReqUserId())); + if (cpUserIds != null && !cpUserIds.isEmpty()) { + cpRelationshipCacheService.remove(cpUserIds); + } } - userProfileGateway.switchUseProps( - cmd.requiredReqUserId(), - cmd.getPropsId() - ); } - } diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/cp/impl/CpRelationshipCacheServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/cp/impl/CpRelationshipCacheServiceImpl.java index f2dafca0..ec80bdd5 100644 --- a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/cp/impl/CpRelationshipCacheServiceImpl.java +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/cp/impl/CpRelationshipCacheServiceImpl.java @@ -4,11 +4,18 @@ import com.alibaba.fastjson.JSON; import com.red.circle.component.redis.service.RedisService; import com.red.circle.other.infra.database.cache.key.user.UserKey; import com.red.circle.other.infra.database.cache.service.cp.CpRelationshipCacheService; +import com.red.circle.other.infra.database.rds.entity.props.PropsBackpack; +import com.red.circle.other.infra.database.rds.entity.props.PropsSourceRecord; import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo; +import com.red.circle.other.infra.database.rds.entity.user.user.CpLevelConfig; import com.red.circle.other.infra.database.rds.entity.user.user.CpRelationship; +import com.red.circle.other.infra.database.rds.service.props.PropsBackpackService; +import com.red.circle.other.infra.database.rds.service.props.PropsSourceRecordService; import com.red.circle.other.infra.database.rds.service.user.user.BaseInfoService; import com.red.circle.other.infra.database.rds.service.user.user.CpRelationshipService; import com.red.circle.other.infra.database.rds.service.user.user.CpValueService; +import com.red.circle.other.inner.enums.material.PropsCommodityType; +import com.red.circle.other.inner.model.dto.user.CpRingDTO; import com.red.circle.other.inner.model.dto.user.CpSimpleUserProfileCO; import com.red.circle.tool.core.collection.CollectionUtils; import com.red.circle.tool.core.date.DateUtils; @@ -35,6 +42,8 @@ public class CpRelationshipCacheServiceImpl implements CpRelationshipCacheServic private final RedisService redisService; private final BaseInfoService baseInfoService; private final CpValueService cpValueService; + private final PropsBackpackService propsBackpackService; + private final PropsSourceRecordService propsSourceRecordService; private static final long CACHE_EXPIRE_SECONDS = 7200L; @@ -84,7 +93,7 @@ public class CpRelationshipCacheServiceImpl implements CpRelationshipCacheServic String firstDay = cpRelationship.getCreateTime().toInstant() .atZone(ZoneId.systemDefault()).toLocalDate().toString(); - return new CpSimpleUserProfileCO() + CpSimpleUserProfileCO co = new CpSimpleUserProfileCO() .setMeUserId(baseInfo.getId()) .setMeAccount(baseInfo.getAccount()) .setMeUserAvatar(baseInfo.getUserAvatar()) @@ -98,6 +107,25 @@ public class CpRelationshipCacheServiceImpl implements CpRelationshipCacheServic .setCreateTime(cpRelationship.getCreateTime()) .setDays(days) .setFirstDay(firstDay); + + // 填充等级权益 + CpLevelConfig levelConfig = CpLevelConfig.of(cpValue); + if (levelConfig != null) { + co.setCpLevel(levelConfig.getLevel()) + .setNextLevelExp(levelConfig.nextExpThreshold()) + .setLoveHeartLevel(levelConfig.getLoveHeartLevel()) + .setMicEffectLevel(levelConfig.getMicEffectLevel()) + .setProfileCardLevel(levelConfig.getProfileCardLevel()) + .setGiftWindowLevel(levelConfig.getGiftWindowLevel()) + .setHasNonMicEffect(levelConfig.isHasNonMicEffect()) + .setHasCpHeadwear(levelConfig.isHasCpHeadwear()); + } + + // 实时查双方背包中正在使用的CP戒指 + co.setSelfRing(buildCpRing(userId)); + co.setCpRing(buildCpRing(cpRelationship.getCpUserId())); + + return co; }) .filter(Objects::nonNull) .sorted(Comparator.comparing(CpSimpleUserProfileCO::getCpValue).reversed() @@ -117,6 +145,23 @@ public class CpRelationshipCacheServiceImpl implements CpRelationshipCacheServic return UserKey.CP.getKey(userId); } + private CpRingDTO buildCpRing(Long userId) { + PropsBackpack backpack = propsBackpackService + .getUserNotExpiredUseProps(userId, PropsCommodityType.CP_RING); + if (backpack == null) { + return null; + } + PropsSourceRecord source = propsSourceRecordService.getById(backpack.getPropsId()); + if (source == null) { + return null; + } + return new CpRingDTO() + .setPropsId(backpack.getPropsId()) + .setName(source.getName()) + .setCover(source.getCover()) + .setExpireTime(backpack.getExpireTime().getTime()); + } + @Override public void remove(List userIds) { userIds.forEach(userId -> { diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/user/user/CpLevelConfig.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/user/user/CpLevelConfig.java new file mode 100644 index 00000000..fa968fee --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/user/user/CpLevelConfig.java @@ -0,0 +1,89 @@ +package com.red.circle.other.infra.database.rds.entity.user.user; + +import lombok.Getter; + +import java.math.BigDecimal; +import java.util.Arrays; +import java.util.Comparator; +import java.util.List; + +/** + * CP等级配置. + *

+ * 字段说明: + * - expThreshold : 达到该等级所需的累计CP值(对应 user_cp_value.cp_val) + * - loveHeartLevel : 爱心样式等级(0=无, 1-3=对应样式,展示在资料卡和用户头像旁) + * - micEffectLevel : 邻麦动效等级(0=无, 1-3,仅在左右相邻麦位时触发) + * - profileCardLevel: 资料卡动态背景等级(0=无, 1-3,展示在CP资料卡内层元素) + * - giftWindowLevel : 赠礼飘窗等级(0=无, 1-3,双方赠送CP礼物时触发) + * - hasNonMicEffect : 是否解锁非邻麦动效(true=解锁,非相邻时也可展示动效) + * - hasCpHeadwear : 是否解锁CP头饰(true=解锁,展示在双方头像周期性飞出效果) + * + * @author tf + */ +@Getter +public enum CpLevelConfig { + + LV1 (1, 500000L, 1, 1, 0, 0, false, false), + LV2 (2, 1000000L, 2, 1, 0, 0, false, false), + LV3 (3, 3000000L, 2, 2, 1, 0, false, false), + LV4 (4, 5000000L, 2, 2, 1, 1, false, false), + LV5 (5, 15500000L, 3, 2, 2, 1, true, true ), + LV6 (6, 40000000L, 3, 3, 2, 1, true, true ), + LV7 (7, 70000000L, 3, 3, 3, 1, true, true ), + LV8 (8, 100000000L, 3, 3, 3, 2, true, true ), + LV9 (9, 130000000L, 3, 3, 3, 3, true, true ), + LV10(10, 200000000L, 3, 3, 3, 3, true, true ); + + private final int level; + private final long expThreshold; + private final int loveHeartLevel; + private final int micEffectLevel; + private final int profileCardLevel; + private final int giftWindowLevel; + private final boolean hasNonMicEffect; + private final boolean hasCpHeadwear; + + CpLevelConfig(int level, long expThreshold, + int loveHeartLevel, int micEffectLevel, + int profileCardLevel, int giftWindowLevel, + boolean hasNonMicEffect, boolean hasCpHeadwear) { + this.level = level; + this.expThreshold = expThreshold; + this.loveHeartLevel = loveHeartLevel; + this.micEffectLevel = micEffectLevel; + this.profileCardLevel = profileCardLevel; + this.giftWindowLevel = giftWindowLevel; + this.hasNonMicEffect = hasNonMicEffect; + this.hasCpHeadwear = hasCpHeadwear; + } + + /** 按 expThreshold 降序,用于 of() 从高到低匹配. */ + private static final List DESC = Arrays.stream(values()) + .sorted(Comparator.comparingLong(CpLevelConfig::getExpThreshold).reversed()) + .toList(); + + /** + * 根据累计CP值推算当前等级,未达到Lv1门槛返回 null. + */ + public static CpLevelConfig of(BigDecimal cpVal) { + if (cpVal == null) { + return null; + } + long val = cpVal.longValue(); + return DESC.stream() + .filter(lv -> val >= lv.expThreshold) + .findFirst() + .orElse(null); + } + + /** + * 获取下一等级经验门槛,满级返回 null. + */ + public Long nextExpThreshold() { + int nextOrdinal = this.ordinal() + 1; + CpLevelConfig[] all = values(); + return nextOrdinal < all.length ? all[nextOrdinal].expThreshold : null; + } + +}