feat: add gm vip gift backend

This commit is contained in:
z 2026-05-05 01:45:59 +08:00
parent 1d724dadd9
commit 23a23d2ee9
31 changed files with 1451 additions and 0 deletions

View File

@ -0,0 +1,38 @@
package com.red.circle.console.adapter.gm;
import com.red.circle.console.app.dto.clienobject.gm.GmVipGiftCO;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftCmd;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftPageQryCmd;
import com.red.circle.console.app.service.gm.GmVipGiftService;
import com.red.circle.console.infra.annotations.OpsOperationReqLog;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.web.controller.BaseController;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* GM VIP赠送.
*/
@RequiredArgsConstructor
@RestController
@RequestMapping("/gm/vip-gift")
public class GmVipGiftRestController extends BaseController {
private final GmVipGiftService gmVipGiftService;
@OpsOperationReqLog("GM VIP赠送")
@PostMapping
public GmVipGiftCO gift(@RequestBody @Validated GmVipGiftCmd cmd) {
return gmVipGiftService.gift(cmd);
}
@GetMapping("/page")
public PageResult<GmVipGiftCO> page(GmVipGiftPageQryCmd cmd) {
return gmVipGiftService.page(cmd);
}
}

View File

@ -0,0 +1,501 @@
package com.red.circle.console.app.service.gm;
import com.red.circle.console.app.dto.clienobject.gm.GmVipGiftCO;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftCmd;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftPageQryCmd;
import com.red.circle.console.infra.database.rds.entity.admin.User;
import com.red.circle.console.infra.database.rds.entity.app.vip.GmVipGiftRecord;
import com.red.circle.console.infra.database.rds.entity.app.vip.RunningWaterUserProps;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserPropsBackpack;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserVipState;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipLevelConfig;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipOrderRecord;
import com.red.circle.console.infra.database.rds.service.admin.UserService;
import com.red.circle.console.infra.database.rds.service.app.vip.GmVipGiftRecordService;
import com.red.circle.console.infra.database.rds.service.app.vip.RunningWaterUserPropsService;
import com.red.circle.console.infra.database.rds.service.app.vip.UserPropsBackpackService;
import com.red.circle.console.infra.database.rds.service.app.vip.UserVipStateService;
import com.red.circle.console.infra.database.rds.service.app.vip.VipLevelConfigService;
import com.red.circle.console.infra.database.rds.service.app.vip.VipOrderRecordService;
import com.red.circle.console.inner.error.ConsoleErrorCode;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.other.inner.endpoint.material.props.PropsBackpackClient;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.sequence.IdWorkerUtils;
import java.math.BigDecimal;
import java.sql.Timestamp;
import java.time.Duration;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* GM VIP赠送.
*/
@Service
@Slf4j
@RequiredArgsConstructor
public class GmVipGiftServiceImpl implements GmVipGiftService {
private static final int VIP_MIN_LEVEL = 1;
private static final int VIP_MAX_LEVEL = 5;
private static final int DEFAULT_DURATION_DAYS = 30;
private static final String STATUS_ACTIVE = "ACTIVE";
private static final String STATUS_SUCCESS = "SUCCESS";
private static final String ORDER_TYPE_GM_GIFT = "GM_GIFT";
private static final String WATER_ORIGIN = "VIP_PURCHASE";
private static final String WATER_ORIGIN_DESC = "VIP purchase";
private static final Timestamp MYSQL_DATETIME_MAX = Timestamp.valueOf("2038-01-19 00:00:00");
private final GmVipGiftRecordService giftRecordService;
private final VipLevelConfigService vipLevelConfigService;
private final UserVipStateService userVipStateService;
private final VipOrderRecordService vipOrderRecordService;
private final UserPropsBackpackService userPropsBackpackService;
private final RunningWaterUserPropsService runningWaterUserPropsService;
private final UserProfileClient userProfileClient;
private final PropsBackpackClient propsBackpackClient;
private final UserService userService;
@Override
@Transactional(rollbackFor = Exception.class)
public GmVipGiftCO gift(GmVipGiftCmd cmd) {
validateGift(cmd);
Timestamp now = now();
User applicant = getAdminUser(cmd.getReqUserId());
UserProfileDTO userProfile = getUserProfile(cmd.getUserId());
VipLevelConfig config = getVipConfig(userProfile.getOriginSys(), cmd.getVipLevel());
UserVipState currentState = getUserVipState(userProfile.getOriginSys(), userProfile.getId());
boolean currentActive = isActive(currentState, now);
int fromLevel = currentActive ? Objects.requireNonNullElse(currentState.getLevel(), 0) : 0;
ResponseAssert.isFalse(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
currentActive && fromLevel > config.getLevel());
Timestamp startAt = currentActive && Objects.nonNull(currentState.getStartAt())
? currentState.getStartAt()
: now;
Timestamp expireAt = resolveExpireAt(currentState, config, currentActive, now);
Long orderId = IdWorkerUtils.getId();
String eventId = "GM-VIP-GIFT-" + orderId;
VipOrderRecord order = buildOrder(orderId, eventId, userProfile, fromLevel, config, startAt,
expireAt, now);
vipOrderRecordService.save(order);
upsertUserVipState(currentState, userProfile, config, startAt, expireAt, now);
giftVipResources(userProfile.getId(), config, now, cmd.getReqUserId());
GmVipGiftRecord record = buildGiftRecord(cmd, applicant, userProfile, fromLevel, config,
orderId, eventId, startAt, expireAt, now);
giftRecordService.save(record);
removeUserCache(userProfile.getId());
return toCO(record, userProfile);
}
@Override
public PageResult<GmVipGiftCO> page(GmVipGiftPageQryCmd cmd) {
PageResult<GmVipGiftRecord> page = giftRecordService.query()
.eq(Objects.nonNull(cmd.getUserId()), GmVipGiftRecord::getUserId, cmd.getUserId())
.eq(Objects.nonNull(cmd.getApplicantId()), GmVipGiftRecord::getApplicantId,
cmd.getApplicantId())
.eq(Objects.nonNull(cmd.getVipLevel()), GmVipGiftRecord::getVipLevel,
cmd.getVipLevel())
.eq(Objects.nonNull(cmd.getStatus()), GmVipGiftRecord::getStatus, cmd.getStatus())
.ge(Objects.nonNull(cmd.getStartTime()), GmVipGiftRecord::getCreateTime,
toTimestamp(cmd.getStartTime()))
.le(Objects.nonNull(cmd.getEndTime()), GmVipGiftRecord::getCreateTime,
toTimestamp(cmd.getEndTime()))
.orderByDesc(GmVipGiftRecord::getId)
.page(cmd.getPageQuery());
Map<Long, UserProfileDTO> userProfileMap = mapUserProfiles(page.getRecords());
return page.convert(record -> toCO(record, userProfileMap.get(record.getUserId())));
}
private void validateGift(GmVipGiftCmd cmd) {
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getReqUserId());
ResponseAssert.notNull(ResponseErrorCode.REQUEST_PARAMETER_ERROR, cmd.getUserId(),
cmd.getVipLevel());
ResponseAssert.isTrue(ResponseErrorCode.REQUEST_PARAMETER_ERROR,
cmd.getVipLevel() >= VIP_MIN_LEVEL && cmd.getVipLevel() <= VIP_MAX_LEVEL);
}
private User getAdminUser(Long userId) {
User user = userService.getById(Math.toIntExact(userId));
ResponseAssert.notNull(ConsoleErrorCode.USER_NOT_FOUND, user);
return user;
}
private UserProfileDTO getUserProfile(Long userId) {
UserProfileDTO userProfile = ResponseAssert.requiredSuccess(userProfileClient.getByUserId(userId));
ResponseAssert.notNull(ConsoleErrorCode.USER_NOT_FOUND, userProfile);
ResponseAssert.notBlank(ConsoleErrorCode.USER_INFO_NOT_FOUND, userProfile.getOriginSys());
return userProfile;
}
private VipLevelConfig getVipConfig(String sysOrigin, Integer vipLevel) {
VipLevelConfig config = vipLevelConfigService.query()
.eq(VipLevelConfig::getSysOrigin, sysOrigin)
.eq(VipLevelConfig::getLevel, vipLevel)
.eq(VipLevelConfig::getEnabled, Boolean.TRUE)
.last(PageConstant.LIMIT_ONE)
.getOne();
ResponseAssert.notNull(ConsoleErrorCode.NOT_FOUND_INFO, config);
if (Objects.isNull(config.getDurationDays()) || config.getDurationDays() <= 0) {
config.setDurationDays(DEFAULT_DURATION_DAYS);
}
return config;
}
private UserVipState getUserVipState(String sysOrigin, Long userId) {
return userVipStateService.query()
.eq(UserVipState::getSysOrigin, sysOrigin)
.eq(UserVipState::getUserId, userId)
.last(PageConstant.LIMIT_ONE)
.getOne();
}
private boolean isActive(UserVipState state, Timestamp now) {
return Objects.nonNull(state)
&& STATUS_ACTIVE.equals(state.getStatus())
&& Objects.nonNull(state.getExpireAt())
&& state.getExpireAt().after(now)
&& Objects.nonNull(state.getLevel())
&& state.getLevel() > 0;
}
private Timestamp resolveExpireAt(UserVipState currentState, VipLevelConfig config,
boolean currentActive, Timestamp now) {
Timestamp plusDuration = plusDays(now, config.getDurationDays());
if (!currentActive) {
return plusDuration;
}
if (Objects.equals(currentState.getLevel(), config.getLevel())) {
return plusDays(max(currentState.getExpireAt(), now), config.getDurationDays());
}
return max(currentState.getExpireAt(), plusDuration);
}
private VipOrderRecord buildOrder(Long orderId, String eventId, UserProfileDTO userProfile,
Integer fromLevel, VipLevelConfig config, Timestamp startAt, Timestamp expireAt,
Timestamp now) {
VipOrderRecord order = new VipOrderRecord()
.setId(orderId)
.setEventId(eventId)
.setSysOrigin(userProfile.getOriginSys())
.setUserId(userProfile.getId())
.setOrderType(ORDER_TYPE_GM_GIFT)
.setFromLevel(fromLevel)
.setToLevel(config.getLevel())
.setDurationDays(config.getDurationDays())
.setOriginalPriceGold(Objects.requireNonNullElse(config.getPriceGold(), 0L))
.setPaidGold(0L)
.setStatus(STATUS_SUCCESS)
.setStartAt(startAt)
.setExpireAt(expireAt)
.setErrorMessage("");
order.setCreateTime(now);
order.setUpdateTime(now);
return order;
}
private void upsertUserVipState(UserVipState currentState, UserProfileDTO userProfile,
VipLevelConfig config, Timestamp startAt, Timestamp expireAt, Timestamp now) {
UserVipState state = Objects.nonNull(currentState)
? currentState
: new UserVipState().setId(IdWorkerUtils.getId());
state.setSysOrigin(userProfile.getOriginSys());
state.setUserId(userProfile.getId());
state.setLevel(config.getLevel());
state.setLevelCode(config.getLevelCode());
state.setDisplayName(config.getDisplayName());
state.setStatus(STATUS_ACTIVE);
state.setStartAt(startAt);
state.setExpireAt(expireAt);
copyResources(state, config);
state.setUpdateTime(now);
if (Objects.nonNull(currentState)) {
userVipStateService.updateSelectiveById(state);
return;
}
state.setCreateTime(now);
userVipStateService.save(state);
}
private void copyResources(UserVipState state, VipLevelConfig config) {
state.setBadgeResourceId(config.getBadgeResourceId());
state.setBadgeName(config.getBadgeName());
state.setBadgeCover(config.getBadgeCover());
state.setBadgeUrl(config.getBadgeUrl());
state.setAvatarFrameResourceId(config.getAvatarFrameResourceId());
state.setAvatarFrameName(config.getAvatarFrameName());
state.setAvatarFrameCover(config.getAvatarFrameCover());
state.setAvatarFrameUrl(config.getAvatarFrameUrl());
state.setEntryEffectResourceId(config.getEntryEffectResourceId());
state.setEntryEffectName(config.getEntryEffectName());
state.setEntryEffectCover(config.getEntryEffectCover());
state.setEntryEffectUrl(config.getEntryEffectUrl());
state.setChatBubbleResourceId(config.getChatBubbleResourceId());
state.setChatBubbleName(config.getChatBubbleName());
state.setChatBubbleCover(config.getChatBubbleCover());
state.setChatBubbleUrl(config.getChatBubbleUrl());
state.setFloatPictureResourceId(config.getFloatPictureResourceId());
state.setFloatPictureName(config.getFloatPictureName());
state.setFloatPictureCover(config.getFloatPictureCover());
state.setFloatPictureUrl(config.getFloatPictureUrl());
}
private void giftVipResources(Long userId, VipLevelConfig config, Timestamp now, Long opUser) {
giveProp(userId, config.getBadgeResourceId(), "BADGE", config.getDurationDays(), now, opUser);
giveProp(userId, config.getAvatarFrameResourceId(), "AVATAR_FRAME", config.getDurationDays(),
now, opUser);
giveProp(userId, config.getEntryEffectResourceId(), "RIDE", config.getDurationDays(), now,
opUser);
giveProp(userId, config.getChatBubbleResourceId(), "CHAT_BUBBLE", config.getDurationDays(),
now, opUser);
giveProp(userId, config.getFloatPictureResourceId(), "FLOAT_PICTURE", config.getDurationDays(),
now, opUser);
}
private void giveProp(Long userId, Long propsId, String type, Integer days, Timestamp now,
Long opUser) {
if (Objects.isNull(propsId) || propsId <= 0 || Objects.isNull(days) || days <= 0) {
return;
}
UserPropsBackpack oldBackpack = getUserProp(userId, propsId);
boolean useProps = resolveUseProps(userId, propsId, type, oldBackpack, now);
Timestamp expireTime = resolvePropExpireTime(oldBackpack, days, now);
if (Boolean.TRUE.equals(useProps)) {
userPropsBackpackService.update()
.set(UserPropsBackpack::getUseProps, Boolean.FALSE)
.eq(UserPropsBackpack::getUserId, userId)
.eq(UserPropsBackpack::getType, type)
.eq(UserPropsBackpack::getUseProps, Boolean.TRUE)
.execute();
}
if (Objects.nonNull(oldBackpack)) {
oldBackpack.setType(type);
oldBackpack.setExpireTime(expireTime);
oldBackpack.setUseProps(useProps);
oldBackpack.setAllowGive(Boolean.FALSE);
oldBackpack.setUpdateTime(now);
oldBackpack.setUpdateUser(opUser);
userPropsBackpackService.updateSelectiveById(oldBackpack);
} else {
UserPropsBackpack backpack = new UserPropsBackpack()
.setUserId(userId)
.setPropsId(propsId)
.setType(type)
.setExpireTime(expireTime)
.setUseProps(useProps)
.setAllowGive(Boolean.FALSE);
backpack.setCreateTime(now);
backpack.setUpdateTime(now);
backpack.setCreateUser(opUser);
backpack.setUpdateUser(opUser);
userPropsBackpackService.save(backpack);
}
RunningWaterUserProps water = new RunningWaterUserProps()
.setUserId(userId)
.setPropsId(propsId)
.setPropsCandy(BigDecimal.ZERO)
.setType(type)
.setOrigin(WATER_ORIGIN)
.setOriginDesc(WATER_ORIGIN_DESC)
.setDays(days);
water.setCreateTime(now);
water.setUpdateTime(now);
water.setCreateUser(opUser);
water.setUpdateUser(0L);
runningWaterUserPropsService.save(water);
notifyPropsGift(userId, type, days, propsId);
}
private UserPropsBackpack getUserProp(Long userId, Long propsId) {
return userPropsBackpackService.query()
.eq(UserPropsBackpack::getUserId, userId)
.eq(UserPropsBackpack::getPropsId, propsId)
.last(PageConstant.LIMIT_ONE)
.getOne();
}
private boolean resolveUseProps(Long userId, Long propsId, String type,
UserPropsBackpack oldBackpack, Timestamp now) {
if (Objects.nonNull(oldBackpack)) {
return Boolean.TRUE.equals(oldBackpack.getUseProps());
}
List<UserPropsBackpack> notExpiredSameType = userPropsBackpackService.query()
.eq(UserPropsBackpack::getUserId, userId)
.eq(UserPropsBackpack::getType, type)
.gt(UserPropsBackpack::getExpireTime, now)
.list();
return notExpiredSameType.stream()
.noneMatch(item -> Boolean.TRUE.equals(item.getUseProps())
&& !Objects.equals(item.getPropsId(), propsId));
}
private Timestamp resolvePropExpireTime(UserPropsBackpack oldBackpack, Integer days,
Timestamp now) {
if (Objects.isNull(oldBackpack) || Objects.isNull(oldBackpack.getExpireTime())
|| oldBackpack.getExpireTime().before(now)) {
return capExpireTime(plusDays(now, days));
}
return capExpireTime(plusDays(oldBackpack.getExpireTime(), days));
}
private GmVipGiftRecord buildGiftRecord(GmVipGiftCmd cmd, User applicant,
UserProfileDTO userProfile, Integer fromLevel, VipLevelConfig config, Long orderId,
String eventId, Timestamp startAt, Timestamp expireAt, Timestamp now) {
return touchNew(new GmVipGiftRecord()
.setId(IdWorkerUtils.getId())
.setUserId(userProfile.getId())
.setSysOrigin(userProfile.getOriginSys())
.setFromLevel(fromLevel)
.setVipLevel(config.getLevel())
.setLevelCode(config.getLevelCode())
.setDisplayName(config.getDisplayName())
.setDurationDays(config.getDurationDays())
.setPriceGold(Objects.requireNonNullElse(config.getPriceGold(), 0L))
.setStartAt(startAt)
.setExpireAt(expireAt)
.setOrderId(orderId)
.setEventId(eventId)
.setStatus(STATUS_SUCCESS)
.setApplicantId(applicant.getId().longValue())
.setApplicantName(resolveAdminName(applicant))
.setRemarks(cmd.getRemarks())
.setExecutedTime(now), now);
}
private String resolveAdminName(User user) {
if (Objects.isNull(user)) {
return null;
}
if (Objects.nonNull(user.getNickname()) && !user.getNickname().isBlank()) {
return user.getNickname();
}
return user.getLoginName();
}
private Map<Long, UserProfileDTO> mapUserProfiles(Collection<GmVipGiftRecord> records) {
if (CollectionUtils.isEmpty(records)) {
return Map.of();
}
Set<Long> userIds = records.stream()
.map(GmVipGiftRecord::getUserId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollectionUtils.isEmpty(userIds)) {
return Map.of();
}
return ResponseAssert.requiredSuccess(userProfileClient.mapByUserIds(userIds));
}
private GmVipGiftCO toCO(GmVipGiftRecord record, UserProfileDTO userProfile) {
GmVipGiftCO co = new GmVipGiftCO();
co.setId(record.getId());
co.setUserId(record.getUserId());
co.setSysOrigin(record.getSysOrigin());
co.setFromLevel(record.getFromLevel());
co.setFromLevelName(levelName(record.getFromLevel()));
co.setVipLevel(record.getVipLevel());
co.setLevelCode(record.getLevelCode());
co.setDisplayName(record.getDisplayName());
co.setDurationDays(record.getDurationDays());
co.setPriceGold(record.getPriceGold());
co.setStartAt(record.getStartAt());
co.setExpireAt(record.getExpireAt());
co.setOrderId(record.getOrderId());
co.setEventId(record.getEventId());
co.setStatus(record.getStatus());
co.setStatusName(statusName(record.getStatus()));
co.setApplicantId(record.getApplicantId());
co.setApplicantName(record.getApplicantName());
co.setRemarks(record.getRemarks());
co.setExecutedTime(record.getExecutedTime());
co.setCreateTime(record.getCreateTime());
co.setUpdateTime(record.getUpdateTime());
co.setUserProfile(userProfile);
return co;
}
private String levelName(Integer level) {
return Objects.isNull(level) || level <= 0 ? "非VIP" : "VIP" + level;
}
private String statusName(String status) {
return STATUS_SUCCESS.equals(status) ? "已赠送" : status;
}
private void notifyPropsGift(Long userId, String type, Integer days, Long propsId) {
try {
propsBackpackClient.givePropsNotify(userId, type, days, propsId);
} catch (Exception exception) {
log.warn("notify vip gift props failed, userId={}, type={}, propsId={}", userId, type,
propsId, exception);
}
}
private void removeUserCache(Long userId) {
try {
userProfileClient.removeCacheAll(userId);
} catch (Exception exception) {
log.warn("remove user cache after vip gift failed, userId={}", userId, exception);
}
}
private <T extends com.red.circle.framework.mybatis.entity.TimestampBaseEntity> T touchNew(T entity,
Timestamp now) {
entity.setCreateTime(now);
entity.setUpdateTime(now);
return entity;
}
private Timestamp max(Timestamp first, Timestamp second) {
if (Objects.isNull(first)) {
return second;
}
if (Objects.isNull(second)) {
return first;
}
return first.after(second) ? first : second;
}
private Timestamp plusDays(Timestamp value, int days) {
return Timestamp.from(value.toInstant().plus(Duration.ofDays(days)));
}
private Timestamp capExpireTime(Timestamp expireTime) {
if (Objects.isNull(expireTime)) {
return null;
}
return expireTime.after(MYSQL_DATETIME_MAX) ? MYSQL_DATETIME_MAX : expireTime;
}
private Timestamp toTimestamp(Long value) {
return Objects.isNull(value) ? null : new Timestamp(value);
}
private Timestamp now() {
return new Timestamp(System.currentTimeMillis());
}
}

View File

@ -0,0 +1,71 @@
package com.red.circle.console.app.dto.clienobject.gm;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.ClientObject;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import java.io.Serial;
import java.sql.Timestamp;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* GM VIP赠送记录.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GmVipGiftCO extends ClientObject {
@Serial
private static final long serialVersionUID = 1L;
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
@JsonSerialize(using = ToStringSerializer.class)
private Long userId;
private String sysOrigin;
private Integer fromLevel;
private String fromLevelName;
private Integer vipLevel;
private String levelCode;
private String displayName;
private Integer durationDays;
private Long priceGold;
private Timestamp startAt;
private Timestamp expireAt;
@JsonSerialize(using = ToStringSerializer.class)
private Long orderId;
private String eventId;
private String status;
private String statusName;
@JsonSerialize(using = ToStringSerializer.class)
private Long applicantId;
private String applicantName;
private String remarks;
private Timestamp executedTime;
private Timestamp createTime;
private Timestamp updateTime;
private UserProfileDTO userProfile;
}

View File

@ -0,0 +1,35 @@
package com.red.circle.console.app.dto.cmd.gm;
import com.red.circle.framework.core.dto.CommonCommand;
import jakarta.validation.constraints.NotNull;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* GM VIP赠送.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GmVipGiftCmd extends CommonCommand {
@Serial
private static final long serialVersionUID = 1L;
/**
* APP用户ID.
*/
@NotNull
private Long userId;
/**
* VIP等级: 1-5.
*/
@NotNull
private Integer vipLevel;
/**
* 备注.
*/
private String remarks;
}

View File

@ -0,0 +1,29 @@
package com.red.circle.console.app.dto.cmd.gm;
import com.red.circle.framework.core.dto.PageCommand;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* GM VIP赠送记录分页查询.
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class GmVipGiftPageQryCmd extends PageCommand {
@Serial
private static final long serialVersionUID = 1L;
private Long userId;
private Long applicantId;
private Integer vipLevel;
private String status;
private Long startTime;
private Long endTime;
}

View File

@ -0,0 +1,16 @@
package com.red.circle.console.app.service.gm;
import com.red.circle.console.app.dto.clienobject.gm.GmVipGiftCO;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftCmd;
import com.red.circle.console.app.dto.cmd.gm.GmVipGiftPageQryCmd;
import com.red.circle.framework.dto.PageResult;
/**
* GM VIP赠送.
*/
public interface GmVipGiftService {
GmVipGiftCO gift(GmVipGiftCmd cmd);
PageResult<GmVipGiftCO> page(GmVipGiftPageQryCmd cmd);
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.GmVipGiftRecord;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* GM VIP赠送记录 DAO.
*/
public interface GmVipGiftRecordDAO extends BaseDAO<GmVipGiftRecord> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.RunningWaterUserProps;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* 用户道具流水 DAO.
*/
public interface RunningWaterUserPropsDAO extends BaseDAO<RunningWaterUserProps> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserPropsBackpack;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* 用户道具背包 DAO.
*/
public interface UserPropsBackpackDAO extends BaseDAO<UserPropsBackpack> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserVipState;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* 用户VIP当前状态 DAO.
*/
public interface UserVipStateDAO extends BaseDAO<UserVipState> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipLevelConfig;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* VIP等级配置 DAO.
*/
public interface VipLevelConfigDAO extends BaseDAO<VipLevelConfig> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.dao.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipOrderRecord;
import com.red.circle.framework.mybatis.dao.BaseDAO;
/**
* VIP订单记录 DAO.
*/
public interface VipOrderRecordDAO extends BaseDAO<VipOrderRecord> {
}

View File

@ -0,0 +1,79 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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.sql.Timestamp;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* GM VIP赠送记录.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("gm_vip_gift_record")
public class GmVipGiftRecord extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("sys_origin")
private String sysOrigin;
@TableField("from_level")
private Integer fromLevel;
@TableField("vip_level")
private Integer vipLevel;
@TableField("level_code")
private String levelCode;
@TableField("display_name")
private String displayName;
@TableField("duration_days")
private Integer durationDays;
@TableField("price_gold")
private Long priceGold;
@TableField("start_at")
private Timestamp startAt;
@TableField("expire_at")
private Timestamp expireAt;
@TableField("order_id")
private Long orderId;
@TableField("event_id")
private String eventId;
@TableField("status")
private String status;
@TableField("applicant_id")
private Long applicantId;
@TableField("applicant_name")
private String applicantName;
@TableField("remarks")
private String remarks;
@TableField("executed_time")
private Timestamp executedTime;
}

View File

@ -0,0 +1,49 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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.math.BigDecimal;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户道具流水.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("running_water_user_props")
public class RunningWaterUserProps extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("props_id")
private Long propsId;
@TableField("props_candy")
private BigDecimal propsCandy;
@TableField("type")
private String type;
@TableField("origin")
private String origin;
@TableField("days")
private Integer days;
@TableField("origin_desc")
private String originDesc;
}

View File

@ -0,0 +1,46 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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.sql.Timestamp;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 用户道具背包.
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = false)
@TableName("user_props_backpack")
public class UserPropsBackpack extends TimestampBaseEntity {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("user_id")
private Long userId;
@TableField("props_id")
private Long propsId;
@TableField("type")
private String type;
@TableField("expire_time")
private Timestamp expireTime;
@TableField("is_use_props")
private Boolean useProps;
@TableField("allow_give")
private Boolean allowGive;
}

View File

@ -0,0 +1,116 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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 java.io.Serial;
import java.io.Serializable;
import java.sql.Timestamp;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* 用户VIP当前状态.
*/
@Data
@Accessors(chain = true)
@TableName("user_vip_state")
public class UserVipState implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@TableField("sys_origin")
private String sysOrigin;
@TableField("user_id")
private Long userId;
@TableField("level")
private Integer level;
@TableField("level_code")
private String levelCode;
@TableField("display_name")
private String displayName;
@TableField("status")
private String status;
@TableField("start_at")
private Timestamp startAt;
@TableField("expire_at")
private Timestamp expireAt;
@TableField("badge_resource_id")
private Long badgeResourceId;
@TableField("badge_name")
private String badgeName;
@TableField("badge_cover")
private String badgeCover;
@TableField("badge_url")
private String badgeUrl;
@TableField("avatar_frame_resource_id")
private Long avatarFrameResourceId;
@TableField("avatar_frame_name")
private String avatarFrameName;
@TableField("avatar_frame_cover")
private String avatarFrameCover;
@TableField("avatar_frame_url")
private String avatarFrameUrl;
@TableField("entry_effect_resource_id")
private Long entryEffectResourceId;
@TableField("entry_effect_name")
private String entryEffectName;
@TableField("entry_effect_cover")
private String entryEffectCover;
@TableField("entry_effect_url")
private String entryEffectUrl;
@TableField("chat_bubble_resource_id")
private Long chatBubbleResourceId;
@TableField("chat_bubble_name")
private String chatBubbleName;
@TableField("chat_bubble_cover")
private String chatBubbleCover;
@TableField("chat_bubble_url")
private String chatBubbleUrl;
@TableField("float_picture_resource_id")
private Long floatPictureResourceId;
@TableField("float_picture_name")
private String floatPictureName;
@TableField("float_picture_cover")
private String floatPictureCover;
@TableField("float_picture_url")
private String floatPictureUrl;
@TableField("create_time")
private Timestamp createTime;
@TableField("update_time")
private Timestamp updateTime;
}

View File

@ -0,0 +1,113 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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 java.io.Serial;
import java.io.Serializable;
import java.sql.Timestamp;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* VIP等级配置.
*/
@Data
@Accessors(chain = true)
@TableName("vip_level_config")
public class VipLevelConfig implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@TableField("sys_origin")
private String sysOrigin;
@TableField("level")
private Integer level;
@TableField("level_code")
private String levelCode;
@TableField("display_name")
private String displayName;
@TableField("enabled")
private Boolean enabled;
@TableField("duration_days")
private Integer durationDays;
@TableField("price_gold")
private Long priceGold;
@TableField("badge_resource_id")
private Long badgeResourceId;
@TableField("badge_name")
private String badgeName;
@TableField("badge_cover")
private String badgeCover;
@TableField("badge_url")
private String badgeUrl;
@TableField("avatar_frame_resource_id")
private Long avatarFrameResourceId;
@TableField("avatar_frame_name")
private String avatarFrameName;
@TableField("avatar_frame_cover")
private String avatarFrameCover;
@TableField("avatar_frame_url")
private String avatarFrameUrl;
@TableField("entry_effect_resource_id")
private Long entryEffectResourceId;
@TableField("entry_effect_name")
private String entryEffectName;
@TableField("entry_effect_cover")
private String entryEffectCover;
@TableField("entry_effect_url")
private String entryEffectUrl;
@TableField("chat_bubble_resource_id")
private Long chatBubbleResourceId;
@TableField("chat_bubble_name")
private String chatBubbleName;
@TableField("chat_bubble_cover")
private String chatBubbleCover;
@TableField("chat_bubble_url")
private String chatBubbleUrl;
@TableField("float_picture_resource_id")
private Long floatPictureResourceId;
@TableField("float_picture_name")
private String floatPictureName;
@TableField("float_picture_cover")
private String floatPictureCover;
@TableField("float_picture_url")
private String floatPictureUrl;
@TableField("create_time")
private Timestamp createTime;
@TableField("update_time")
private Timestamp updateTime;
}

View File

@ -0,0 +1,71 @@
package com.red.circle.console.infra.database.rds.entity.app.vip;
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 java.io.Serial;
import java.io.Serializable;
import java.sql.Timestamp;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* VIP购买/赠送订单记录.
*/
@Data
@Accessors(chain = true)
@TableName("vip_order_record")
public class VipOrderRecord implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@TableField("event_id")
private String eventId;
@TableField("sys_origin")
private String sysOrigin;
@TableField("user_id")
private Long userId;
@TableField("order_type")
private String orderType;
@TableField("from_level")
private Integer fromLevel;
@TableField("to_level")
private Integer toLevel;
@TableField("duration_days")
private Integer durationDays;
@TableField("original_price_gold")
private Long originalPriceGold;
@TableField("paid_gold")
private Long paidGold;
@TableField("status")
private String status;
@TableField("start_at")
private Timestamp startAt;
@TableField("expire_at")
private Timestamp expireAt;
@TableField("error_message")
private String errorMessage;
@TableField("create_time")
private Timestamp createTime;
@TableField("update_time")
private Timestamp updateTime;
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.GmVipGiftRecord;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* GM VIP赠送记录服务.
*/
public interface GmVipGiftRecordService extends BaseService<GmVipGiftRecord> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.RunningWaterUserProps;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* 用户道具流水服务.
*/
public interface RunningWaterUserPropsService extends BaseService<RunningWaterUserProps> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserPropsBackpack;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* 用户道具背包服务.
*/
public interface UserPropsBackpackService extends BaseService<UserPropsBackpack> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserVipState;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* 用户VIP当前状态服务.
*/
public interface UserVipStateService extends BaseService<UserVipState> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipLevelConfig;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* VIP等级配置服务.
*/
public interface VipLevelConfigService extends BaseService<VipLevelConfig> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.console.infra.database.rds.service.app.vip;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipOrderRecord;
import com.red.circle.framework.mybatis.service.BaseService;
/**
* VIP订单记录服务.
*/
public interface VipOrderRecordService extends BaseService<VipOrderRecord> {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.GmVipGiftRecordDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.GmVipGiftRecord;
import com.red.circle.console.infra.database.rds.service.app.vip.GmVipGiftRecordService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* GM VIP赠送记录服务实现.
*/
@Service
public class GmVipGiftRecordServiceImpl extends
BaseServiceImpl<GmVipGiftRecordDAO, GmVipGiftRecord>
implements GmVipGiftRecordService {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.RunningWaterUserPropsDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.RunningWaterUserProps;
import com.red.circle.console.infra.database.rds.service.app.vip.RunningWaterUserPropsService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* 用户道具流水服务实现.
*/
@Service
public class RunningWaterUserPropsServiceImpl extends
BaseServiceImpl<RunningWaterUserPropsDAO, RunningWaterUserProps>
implements RunningWaterUserPropsService {
}

View File

@ -0,0 +1,16 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.UserPropsBackpackDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserPropsBackpack;
import com.red.circle.console.infra.database.rds.service.app.vip.UserPropsBackpackService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* 用户道具背包服务实现.
*/
@Service
public class UserPropsBackpackServiceImpl extends
BaseServiceImpl<UserPropsBackpackDAO, UserPropsBackpack>
implements UserPropsBackpackService {
}

View File

@ -0,0 +1,15 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.UserVipStateDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.UserVipState;
import com.red.circle.console.infra.database.rds.service.app.vip.UserVipStateService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* 用户VIP当前状态服务实现.
*/
@Service
public class UserVipStateServiceImpl extends BaseServiceImpl<UserVipStateDAO, UserVipState>
implements UserVipStateService {
}

View File

@ -0,0 +1,15 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.VipLevelConfigDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipLevelConfig;
import com.red.circle.console.infra.database.rds.service.app.vip.VipLevelConfigService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* VIP等级配置服务实现.
*/
@Service
public class VipLevelConfigServiceImpl extends BaseServiceImpl<VipLevelConfigDAO, VipLevelConfig>
implements VipLevelConfigService {
}

View File

@ -0,0 +1,15 @@
package com.red.circle.console.infra.database.rds.service.app.vip.impl;
import com.red.circle.console.infra.database.rds.dao.app.vip.VipOrderRecordDAO;
import com.red.circle.console.infra.database.rds.entity.app.vip.VipOrderRecord;
import com.red.circle.console.infra.database.rds.service.app.vip.VipOrderRecordService;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import org.springframework.stereotype.Service;
/**
* VIP订单记录服务实现.
*/
@Service
public class VipOrderRecordServiceImpl extends BaseServiceImpl<VipOrderRecordDAO, VipOrderRecord>
implements VipOrderRecordService {
}

View File

@ -0,0 +1,74 @@
SET NAMES utf8mb4;
CREATE TABLE IF NOT EXISTS `gm_vip_gift_record` (
`id` bigint NOT NULL COMMENT '记录ID',
`user_id` bigint NOT NULL COMMENT 'APP用户ID',
`sys_origin` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '系统来源',
`from_level` int NOT NULL DEFAULT '0' COMMENT '赠送前VIP等级',
`vip_level` int NOT NULL COMMENT '赠送VIP等级',
`level_code` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '等级编码',
`display_name` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '' COMMENT '显示名称',
`duration_days` int NOT NULL DEFAULT '30' COMMENT '赠送天数',
`price_gold` bigint NOT NULL DEFAULT '0' COMMENT '对应购买价格,金币',
`start_at` datetime(3) DEFAULT NULL COMMENT '权益开始时间',
`expire_at` datetime(3) DEFAULT NULL COMMENT '权益到期时间',
`order_id` bigint NOT NULL COMMENT '关联VIP订单ID',
`event_id` varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '赠送幂等事件ID',
`status` varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'SUCCESS' COMMENT '状态',
`applicant_id` bigint NOT NULL COMMENT '后台操作人ID',
`applicant_name` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '后台操作人名称',
`remarks` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '备注',
`executed_time` datetime(3) DEFAULT NULL COMMENT '执行时间',
`create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
`update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间',
`create_user` bigint DEFAULT NULL COMMENT '创建用户',
`update_user` bigint DEFAULT NULL COMMENT '更新用户',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_gm_vip_gift_event` (`event_id`),
KEY `idx_gm_vip_gift_user` (`user_id`),
KEY `idx_gm_vip_gift_applicant` (`applicant_id`),
KEY `idx_gm_vip_gift_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='GM VIP赠送记录';
SET @gm_operation_menu_id := (
SELECT `id` FROM `sys_menu` WHERE `alias` = 'GmOperation' LIMIT 1
);
INSERT INTO `sys_menu` (
`id`, `parent_id`, `menu_name`, `path`, `menu_type`, `icon`, `create_user`, `update_user`,
`sort`, `status`, `router`, `alias`
)
SELECT
1700000403, @gm_operation_menu_id, 'VIP赠送', 'gm/vip-gift', 2, 'lucide:badge',
'0', '0', 3, 0, 'vip-gift', 'GmVipGift'
WHERE @gm_operation_menu_id IS NOT NULL
AND NOT EXISTS (
SELECT 1 FROM `sys_menu` WHERE `alias` = 'GmVipGift'
);
UPDATE `sys_menu`
SET
`parent_id` = @gm_operation_menu_id,
`menu_name` = 'VIP赠送',
`path` = 'gm/vip-gift',
`menu_type` = 2,
`icon` = 'lucide:badge',
`create_user` = '0',
`update_user` = '0',
`sort` = 3,
`status` = 0,
`router` = 'vip-gift'
WHERE `alias` = 'GmVipGift';
INSERT INTO `sys_role_menu` (`role_id`, `menu_id`)
SELECT DISTINCT valid_source_role.`id`, target_menu.`id`
FROM `sys_role_menu` source
JOIN `sys_role` valid_source_role ON valid_source_role.`id` = source.`role_id`
JOIN `sys_menu` target_menu ON target_menu.`alias` = 'GmVipGift'
JOIN `sys_menu` source_menu ON source_menu.`alias` = 'GmGoldOperation'
WHERE source.`menu_id` = source_menu.`id`
AND NOT EXISTS (
SELECT 1 FROM `sys_role_menu` target
WHERE target.`role_id` = source.`role_id`
AND target.`menu_id` = target_menu.`id`
);