新增用户vip权限开关功能

This commit is contained in:
tianfeng 2026-03-17 18:28:08 +08:00
parent 8388db8f8e
commit b58ff31f76
14 changed files with 485 additions and 1 deletions

View File

@ -0,0 +1,47 @@
package com.red.circle.other.adapter.app.user.account;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import com.red.circle.framework.web.controller.BaseController;
import com.red.circle.other.app.dto.clientobject.user.vip.UserVipAbilityCO;
import com.red.circle.other.app.dto.cmd.user.vip.UserVipAbilitySettingCmd;
import com.red.circle.other.app.service.user.vip.AppUserVipAbilityService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 用户 VIP 设置服务
*
* @author tf
* @eo.api-type http
* @eo.groupName 用户服务.VIP管理
* @eo.path /user/vip
*/
@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping(value = "/user/vip", produces = MediaType.APPLICATION_JSON_VALUE)
public class AppUserVipAbilityRestController extends BaseController {
private final AppUserVipAbilityService appUserVipAbilityService;
/**
* 获取用户 VIP 设置
*/
@GetMapping("/ability")
public UserVipAbilityCO getUserVipAbility(AppExtCommand appExtCommand) {
Long userId = appExtCommand.getReqUserId();
return appUserVipAbilityService.getUserVipAbility(userId);
}
/**
* 更新用户 VIP 设置
*/
@PostMapping(value = "/ability/update")
public void updateUserVipAbility(@RequestBody @Validated UserVipAbilitySettingCmd cmd) {
appUserVipAbilityService.updateUserVipAbility(cmd);
}
}

View File

@ -0,0 +1,63 @@
package com.red.circle.other.app.command.user.vip;
import com.red.circle.other.app.dto.cmd.user.vip.UserVipAbilitySettingCmd;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.infra.database.mongo.entity.user.profile.UserVipAbilitySetting;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserVipAbilitySettingService;
import com.red.circle.other.domain.enums.VipAbilityType;
import com.red.circle.other.domain.enums.ContactMode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Date;
@Component
@RequiredArgsConstructor
public class UserVipAbilityCmdExe {
private final UserVipAbilitySettingService userVipAbilitySettingService;
private final UserProfileGateway userProfileGateway;
public void updateUserVipAbility(UserVipAbilitySettingCmd cmd) {
Long userId = cmd.getReqUserId();
// 获取用户 VIP 等级
String vipLevel = userProfileGateway.getUserVipName(userId);
// 功能开关结合 VIP 等级判断是否可用
Boolean kickPrevention = VipAbilityType.KICK_PREVENTION.hasAbility(vipLevel) && Boolean.TRUE.equals(cmd.getKickPrevention());
Boolean mysteriousInvisibility = VipAbilityType.MYSTERIOUS_INVISIBILITY.hasAbility(vipLevel) && Boolean.TRUE.equals(cmd.getMysteriousInvisibility());
Boolean antiBlock = VipAbilityType.ANTI_BLOCK.hasAbility(vipLevel) && Boolean.TRUE.equals(cmd.getAntiBlock());
// ContactMode 单选互斥逻辑
String contactMode = cmd.getContactMode();
if (contactMode == null) {
contactMode = ContactMode.EVERYONE.name();
}
// DO_NOT_DISTURB 与其他模式互斥
if (ContactMode.DO_NOT_DISTURB.name().equals(contactMode)) {
// 用户选择拒绝所有人联系
contactMode = ContactMode.DO_NOT_DISTURB.name();
} else {
// VIP 等级不可选择高级模式
if (ContactMode.VIP2_ABOVE.name().equals(contactMode) && (vipLevel == null || vipLevel.compareTo("EARL") < 0)) {
contactMode = ContactMode.EVERYONE.name();
} else if (ContactMode.VIP5_ABOVE.name().equals(contactMode) && (vipLevel == null || vipLevel.compareTo("KING") < 0)) {
contactMode = ContactMode.EVERYONE.name();
}
}
// 构建 MongoDB 对象
UserVipAbilitySetting setting = new UserVipAbilitySetting();
setting.setUserId(userId);
setting.setContactMode(contactMode);
setting.setKickPrevention(kickPrevention);
setting.setMysteriousInvisibility(mysteriousInvisibility);
setting.setAntiBlock(antiBlock);
setting.setUpdateTime(new Date());
// 执行 upsert
userVipAbilitySettingService.upsert(setting);
}
}

View File

@ -0,0 +1,57 @@
package com.red.circle.other.app.command.user.vip;
import com.red.circle.other.app.dto.clientobject.user.vip.UserVipAbilityCO;
import com.red.circle.other.app.convertor.user.UserVipAbilityAppConvertor;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.infra.database.mongo.entity.user.profile.UserVipAbilitySetting;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserVipAbilitySettingService;
import com.red.circle.other.domain.enums.VipAbilityType;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Objects;
/**
* 查询用户 VIP 设置
*/
@Component
@RequiredArgsConstructor
public class UserVipAbilityQryExe {
private final UserVipAbilitySettingService userVipAbilitySettingService;
private final UserVipAbilityAppConvertor convertor;
private final UserProfileGateway userProfileGateway; // 使用 Gateway
public UserVipAbilityCO execute(Long userId) {
UserVipAbilitySetting setting = userVipAbilitySettingService.findByUserId(userId);
if (setting == null) {
setting = new UserVipAbilitySetting();
setting.setUserId(userId);
setting.setContactMode("EVERYONE");
setting.setKickPrevention(true);
setting.setMysteriousInvisibility(true);
setting.setAntiBlock(true);
}
// Gateway 获取 VIP 等级
String vipLevel = userProfileGateway.getUserVipName(userId);
// 权限开关结合等级判断是否真正可用
if (vipLevel != null) {
setting.setKickPrevention(VipAbilityType.KICK_PREVENTION.hasAbility(vipLevel)
&& Boolean.TRUE.equals(setting.getKickPrevention()));
setting.setMysteriousInvisibility(VipAbilityType.MYSTERIOUS_INVISIBILITY.hasAbility(vipLevel)
&& Boolean.TRUE.equals(setting.getMysteriousInvisibility()));
setting.setAntiBlock(VipAbilityType.ANTI_BLOCK.hasAbility(vipLevel)
&& Boolean.TRUE.equals(setting.getAntiBlock()));
} else {
setting.setKickPrevention(false);
setting.setMysteriousInvisibility(false);
setting.setAntiBlock(false);
}
UserVipAbilityCO co = convertor.toUserVipAbilityCO(setting);
co.setVipLevel(vipLevel);
return co;
}
}

View File

@ -0,0 +1,15 @@
package com.red.circle.other.app.convertor.user;
import com.red.circle.framework.core.convertor.ConvertorModel;
import com.red.circle.other.app.dto.clientobject.user.vip.UserVipAbilityCO;
import com.red.circle.other.infra.database.mongo.entity.user.profile.UserVipAbilitySetting;
import org.mapstruct.Mapper;
/**
* 用户 VIP 设置实体转换器
*/
@Mapper(componentModel = ConvertorModel.SPRING)
public interface UserVipAbilityAppConvertor {
UserVipAbilityCO toUserVipAbilityCO(UserVipAbilitySetting setting);
}

View File

@ -0,0 +1,26 @@
package com.red.circle.other.app.service.user.vip;
import com.red.circle.other.app.command.user.vip.UserVipAbilityQryExe;
import com.red.circle.other.app.dto.clientobject.user.vip.UserVipAbilityCO;
import com.red.circle.other.app.dto.cmd.user.vip.UserVipAbilitySettingCmd;
import com.red.circle.other.app.command.user.vip.UserVipAbilityCmdExe;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class AppUserVipAbilityServiceImpl implements AppUserVipAbilityService {
private final UserVipAbilityCmdExe userVipAbilityCmdExe;
private final UserVipAbilityQryExe userVipAbilityQryExe;
@Override
public UserVipAbilityCO getUserVipAbility(Long userId) {
return userVipAbilityQryExe.execute(userId);
}
@Override
public void updateUserVipAbility(UserVipAbilitySettingCmd cmd) {
userVipAbilityCmdExe.updateUserVipAbility(cmd);
}
}

View File

@ -0,0 +1,65 @@
package com.red.circle.other.app.dto.clientobject.user.vip;
import lombok.Data;
/**
* 用户 VIP 设置返回 DTO
* <p>
* 用于前端展示和判断用户 VIP 权限与功能开关状态
* 所有功能开关都会结合用户 VIP 等级判断是否真正可用
* </p>
*/
@Data
public class UserVipAbilityCO {
/** 用户 ID */
private Long userId;
/**
* 用户 VIP 等级标识
* <p>
* 可选值
* VISCOUNT - 子爵 (VIP1)
* EARL - 伯爵 (VIP2)
* MARQUIS - 侯爵 (VIP3)
* DUKE - 公爵 (VIP4)
* KING - 国王 (VIP5)
* EMPEROR - 皇帝 (VIP6)
* </p>
* 如果用户未开通 VIP则返回 null
*/
private String vipLevel;
/**
* 联系权限模式
* <p>
* 枚举值
* EVERYONE - 所有人可联系默认
* VIP2_ABOVE - VIP2 及以上可联系
* VIP5_ABOVE - VIP5 及以上可联系
* DO_NOT_DISTURB - 拒绝所有人联系单选互斥
* </p>
*/
private String contactMode;
/**
* 防踢开关
* <p>
* - VIP5 及以上用户才可开启
*/
private Boolean kickPrevention;
/**
* 排行榜隐身开关
* <p>
* - VIP6 用户才可开启
*/
private Boolean mysteriousInvisibility;
/**
* 防拉黑开关
* <p>
* - VIP6 用户才可开启
*/
private Boolean antiBlock;
}

View File

@ -0,0 +1,47 @@
package com.red.circle.other.app.dto.cmd.user.vip;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
/**
* 用户 VIP 设置命令 DTO
* <p>
* 用于前端请求修改用户 VIP 功能开关或联系权限
* 所有更新都会在服务端结合用户 VIP 等级进行校验
* 保证功能开关和联系模式不会超过用户等级权限
* </p>
*/
@Data
public class UserVipAbilitySettingCmd extends AppExtCommand {
/**
* 联系权限模式
* <p>
* 可选值
* EVERYONE - 所有人可联系默认
* VIP2_ABOVE - VIP2 及以上可联系
* VIP5_ABOVE - VIP5 及以上可联系
* DO_NOT_DISTURB - 拒绝所有人联系单选互斥
* </p>
* 注意
* - DO_NOT_DISTURB 与其他模式互斥
* - 高等级模式会受 VIP 等级限制如果等级不够服务端会回退为默认 EVERYONE
*/
private String contactMode;
/**
* 防踢开关请求值
*/
private Boolean kickPrevention;
/**
* 排行榜隐身开关请求值
*/
private Boolean mysteriousInvisibility;
/**
* 防拉黑开关请求值
*/
private Boolean antiBlock;
}

View File

@ -0,0 +1,20 @@
package com.red.circle.other.app.service.user.vip;
import com.red.circle.other.app.dto.clientobject.user.vip.UserVipAbilityCO;
import com.red.circle.other.app.dto.cmd.user.vip.UserVipAbilitySettingCmd;
/**
* 用户 VIP 设置服务
*/
public interface AppUserVipAbilityService {
/**
* 获取用户 VIP 设置
*/
UserVipAbilityCO getUserVipAbility(Long userId);
/**
* 更新用户 VIP 设置
*/
void updateUserVipAbility(UserVipAbilitySettingCmd cmd);
}

View File

@ -0,0 +1,8 @@
package com.red.circle.other.domain.enums;
public enum ContactMode {
EVERYONE,
VIP2_ABOVE,
VIP5_ABOVE,
DO_NOT_DISTURB
}

View File

@ -203,4 +203,11 @@ public interface UserProfileGateway {
*/
boolean getUserVipAbility(Long userId, VipAbilityType abilityType);
/**
* 根据 userId 查询用户当前 VIP 等级
*
* @param userId 用户ID
* @return VIP 等级标识VISCOUNT ~ EMPEROR VIP 返回 null
*/
String getUserVipName(Long userId);
}

View File

@ -0,0 +1,27 @@
package com.red.circle.other.infra.database.mongo.entity.user.profile;
import lombok.Data;
import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import java.util.Date;
@Data
@Document("user_vip_ability_setting")
public class UserVipAbilitySetting {
@Id
private Long userId;
/**
* 联系模式枚举: EVERYONE, VIP2_ABOVE, VIP5_ABOVE, DO_NOT_DISTURB
*/
private String contactMode;
/** 功能开关 */
private Boolean kickPrevention;
private Boolean mysteriousInvisibility;
private Boolean antiBlock;
private Date updateTime;
}

View File

@ -0,0 +1,28 @@
package com.red.circle.other.infra.database.mongo.service.user.profile;
import com.red.circle.other.domain.enums.ContactMode;
import com.red.circle.other.domain.enums.VipAbilityType;
import com.red.circle.other.infra.database.mongo.entity.user.profile.UserVipAbilitySetting;
public interface UserVipAbilitySettingService {
/**
* 判断用户是否开启某个功能
*/
boolean isEnabled(Long userId, VipAbilityType abilityType);
/**
* 获取用户联系权限
*/
ContactMode getContactMode(Long userId);
/**
* 创建或更新用户设置
*/
void upsert(UserVipAbilitySetting setting);
/**
* 根据 userId 查询记录
*/
UserVipAbilitySetting findByUserId(Long userId);
}

View File

@ -0,0 +1,65 @@
package com.red.circle.other.infra.database.mongo.service.user.profile.impl;
import com.red.circle.other.domain.enums.ContactMode;
import com.red.circle.other.domain.enums.VipAbilityType;
import com.red.circle.other.infra.database.mongo.entity.user.profile.UserVipAbilitySetting;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserVipAbilitySettingService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.data.mongodb.core.query.Criteria;
import org.springframework.data.mongodb.core.query.Query;
import org.springframework.data.mongodb.core.query.Update;
import org.springframework.stereotype.Service;
import java.util.Date;
@Service
@RequiredArgsConstructor
public class UserVipAbilitySettingServiceImpl implements UserVipAbilitySettingService {
private final MongoTemplate mongoTemplate;
@Override
public boolean isEnabled(Long userId, VipAbilityType abilityType) {
UserVipAbilitySetting setting = mongoTemplate.findById(userId, UserVipAbilitySetting.class);
if (setting == null) {
// 默认值: 第一次开通 VIP 功能开关默认开启 true
return true;
}
switch (abilityType) {
case KICK_PREVENTION: return Boolean.TRUE.equals(setting.getKickPrevention());
case MYSTERIOUS_INVISIBILITY: return Boolean.TRUE.equals(setting.getMysteriousInvisibility());
case ANTI_BLOCK: return Boolean.TRUE.equals(setting.getAntiBlock());
default: return false;
}
}
@Override
public ContactMode getContactMode(Long userId) {
UserVipAbilitySetting setting = mongoTemplate.findById(userId, UserVipAbilitySetting.class);
if (setting == null || setting.getContactMode() == null) {
return ContactMode.EVERYONE;
}
return ContactMode.valueOf(setting.getContactMode());
}
@Override
public void upsert(UserVipAbilitySetting setting) {
setting.setUpdateTime(new Date());
Query query = Query.query(Criteria.where("_id").is(setting.getUserId()));
Update update = new Update()
.set("contactMode", setting.getContactMode())
.set("kickPrevention", setting.getKickPrevention())
.set("mysteriousInvisibility", setting.getMysteriousInvisibility())
.set("antiBlock", setting.getAntiBlock())
.set("updateTime", setting.getUpdateTime());
mongoTemplate.upsert(query, update, UserVipAbilitySetting.class);
}
@Override
public UserVipAbilitySetting findByUserId(Long userId) {
return mongoTemplate.findById(userId, UserVipAbilitySetting.class);
// 或者用 query:
// return mongoTemplate.findOne(Query.query(Criteria.where("_id").is(userId)), UserVipAbilitySetting.class);
}
}

View File

@ -28,6 +28,7 @@ import com.red.circle.other.infra.database.mongo.entity.user.profile.UserSpecial
import com.red.circle.other.infra.database.mongo.service.team.team.TeamMemberService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserRunProfileService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserSpecialIdService;
import com.red.circle.other.infra.database.mongo.service.user.profile.UserVipAbilitySettingService;
import com.red.circle.other.infra.database.rds.entity.props.PropsNobleVipAbility;
import com.red.circle.other.infra.database.rds.entity.user.user.BaseInfo;
import com.red.circle.other.infra.database.rds.entity.user.user.ConsumptionLevel;
@ -86,6 +87,7 @@ public class UserProfileGatewayImpl implements UserProfileGateway {
private final UserRelationshipCacheService userRelationshipCacheService;
private final UserRunProfileCacheService userRunProfileCacheService;
private final UserRegionCacheService userRegionCacheService;
private final UserVipAbilitySettingService userVipAbilitySettingService;
@Override
@ -500,7 +502,9 @@ public class UserProfileGatewayImpl implements UserProfileGateway {
@Override
public boolean getUserVipAbility(Long userId, VipAbilityType abilityType) {
return abilityType.hasAbility(resolveUserVipName(userId));
boolean hasVipLevel = abilityType.hasAbility(resolveUserVipName(userId));
boolean switchOn = userVipAbilitySettingService.isEnabled(userId, abilityType);
return hasVipLevel && switchOn;
}
@Override
@ -642,4 +646,9 @@ public class UserProfileGatewayImpl implements UserProfileGateway {
}).filter(Objects::nonNull).collect(Collectors.toSet());
}
@Override
public String getUserVipName(Long userId) {
return resolveUserVipName(userId);
}
}