新增道具券功能

This commit is contained in:
tianfeng 2025-11-05 18:11:20 +08:00
parent 2bbf29ad0a
commit c74a4b11d1
29 changed files with 2335 additions and 0 deletions

View File

@ -259,6 +259,11 @@ public enum SendPropsOrigin {
*/
LOTTERY_REWARD("Lottery Rewards"),
/**
* 道具券兑换
*/
COUPON_EXCHANGE("Coupon Exchange"),
;
private final String desc;

View File

@ -202,6 +202,11 @@ public enum OfficialNoticeTypeEnum {
*/
GAME_BAISHUN_WIN,
/**
* 收到道具券
*/
RECEIVE_PROP_COUPON,
;

View File

@ -0,0 +1,108 @@
package com.red.circle.other.inner.asserts;
import com.red.circle.framework.dto.IResponseErrorCode;
import com.red.circle.framework.dto.ResErrorCode;
/**
* 道具券相关错误码 范围3200 ~ 3300
*
* @author system
* @date 2025-11-05
*/
@ResErrorCode(describe = "道具券相关", minCode = 3200, maxCode = 3300)
public enum OtherErrorCode implements IResponseErrorCode {
/**
* 道具券不存在
*/
PROP_COUPON_NOT_FOUND(3200, "Prop coupon not found"),
/**
* 道具券不属于当前用户
*/
PROP_COUPON_NOT_BELONG_TO_USER(3201, "Prop coupon does not belong to the current user"),
/**
* 道具券已使用
*/
PROP_COUPON_ALREADY_USED(3202, "Prop coupon has already been used"),
/**
* 道具券已过期
*/
PROP_COUPON_EXPIRED(3203, "Prop coupon has expired"),
/**
* 道具券类型不支持
*/
PROP_COUPON_TYPE_NOT_SUPPORT(3204, "Prop coupon type not supported"),
/**
* 道具券已赠送
*/
PROP_COUPON_ALREADY_SENT(3205, "Prop coupon has already been sent"),
/**
* 接收人不存在
*/
PROP_COUPON_RECEIVER_NOT_FOUND(3206, "Receiver not found"),
/**
* 不能赠送给自己
*/
PROP_COUPON_CANNOT_SEND_TO_SELF(3207, "Cannot send coupon to yourself"),
/**
* 道具不存在
*/
PROP_COUPON_PROP_NOT_FOUND(3208, "Prop not found"),
/**
* 道具券生成失败
*/
PROP_COUPON_GENERATE_FAILED(3209, "Prop coupon generation failed"),
/**
* 操作频繁请稍后再试
*/
PROP_COUPON_OPERATION_TOO_FREQUENT(3210, "Operation too frequent, please try again later"),
/**
* 道具券数量不足
*/
PROP_COUPON_QUANTITY_NOT_ENOUGH(3211, "Prop coupon quantity not enough"),
/**
* 批量赠送用户数量超过限制
*/
PROP_COUPON_BATCH_USER_LIMIT(30, "Batch user limit exceeded"),
/**
* 道具券有效天数超过限制
*/
PROP_COUPON_VALID_DAYS_LIMIT(365, "Valid days limit exceeded"),
;
private final int code;
private final String message;
OtherErrorCode(int code, String message) {
this.code = code;
this.message = message;
}
@Override
public Integer getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
@Override
public String getErrorCodeName() {
return this.name();
}
}

View File

@ -0,0 +1,58 @@
package com.red.circle.other.adapter.app.props;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.clientobject.PropCouponRecordCO;
import com.red.circle.other.app.dto.cmd.PropCouponQueryCmd;
import com.red.circle.other.app.dto.cmd.PropCouponSendCmd;
import com.red.circle.other.app.dto.cmd.PropCouponUseCmd;
import com.red.circle.other.app.service.PropCouponService;
import lombok.RequiredArgsConstructor;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
/**
* 道具券控制器
*
* @author system
* @date 2025-11-05
*/
@RestController
@RequestMapping("/prop/coupon")
@RequiredArgsConstructor
public class PropCouponRestController {
private final PropCouponService propCouponService;
/**
* 使用道具券
*/
@PostMapping("/use")
public void useCoupon(@RequestBody @Validated PropCouponUseCmd cmd) {
propCouponService.useCoupon(cmd);
}
/**
* 赠送道具券
*/
@PostMapping("/send")
public void sendCoupon(@RequestBody @Validated PropCouponSendCmd cmd) {
propCouponService.sendCoupon(cmd);
}
/**
* 查询我的道具券列表
*/
@PostMapping("/list")
public PageResult<PropCouponCO> queryMyCoupons(@RequestBody @Validated PropCouponQueryCmd cmd) {
return propCouponService.queryMyCoupons(cmd);
}
/**
* 查询使用记录
*/
@PostMapping("/record/list")
public PageResult<PropCouponRecordCO> queryUseRecords(@RequestBody @Validated PropCouponQueryCmd cmd) {
return propCouponService.queryUseRecords(cmd);
}
}

View File

@ -0,0 +1,183 @@
package com.red.circle.other.app.command.propcoupon;
import com.red.circle.external.inner.endpoint.message.OfficialNoticeClient;
import com.red.circle.external.inner.model.cmd.message.notice.official.NoticeExtTemplateCustomizeCmd;
import com.red.circle.external.inner.model.enums.message.OfficialNoticeTypeEnum;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.app.dto.cmd.PropCouponGrantCmd;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponSource;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import com.red.circle.other.inner.asserts.OtherErrorCode;
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.sequence.IdWorkerUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 后台赠送道具券执行器
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponGrantCmdExe {
private final PropCouponGateway propCouponGateway;
private final UserProfileClient userProfileClient;
private final OfficialNoticeClient officialNoticeClient;
/**
* 执行后台赠送道具券
*/
@Transactional(rollbackFor = Exception.class)
public void execute(PropCouponGrantCmd cmd) {
// 1. 校验参数
validateParams(cmd);
// 2. 校验用户是否存在
Set<Long> userIds = Set.copyOf(cmd.getUserIds());
Map<Long, UserProfileDTO> userMap = validateUsers(userIds);
// 3. 解析券类型
PropCouponType couponType = PropCouponType.getByCode(cmd.getCouponType());
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_TYPE_NOT_SUPPORT, couponType);
// 4. 批量生成券
List<PropCoupon> propCoupons = generateCoupons(cmd, couponType, userMap.keySet());
// 5. 批量插入券记录
propCouponGateway.batchSave(propCoupons);
// 6. 发送系统消息通知用户
sendNotifications(propCoupons, userMap, couponType);
log.info("后台赠送道具券成功, couponType:{}, propId:{}, userCount:{}, totalCoupons:{}",
couponType.getDesc(), cmd.getPropId(), userMap.size(), propCoupons.size());
}
/**
* 校验参数
*/
private void validateParams(PropCouponGrantCmd cmd) {
// 校验用户数量不能超过限制
ResponseAssert.isFalse(OtherErrorCode.PROP_COUPON_BATCH_USER_LIMIT,
cmd.getUserIds() == null || cmd.getUserIds().size() > 100);
// 校验有效天数不能超过限制
ResponseAssert.isFalse(OtherErrorCode.PROP_COUPON_VALID_DAYS_LIMIT,
cmd.getValidDays() <= 0 || cmd.getValidDays() > 365);
}
/**
* 校验用户是否存在
*/
private Map<Long, UserProfileDTO> validateUsers(Set<Long> userIds) {
Map<Long, UserProfileDTO> userMap = ResponseAssert.requiredSuccess(
userProfileClient.mapByUserIds(userIds)
);
// 校验所有用户都存在
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_RECEIVER_NOT_FOUND,
userMap.size() == userIds.size());
return userMap;
}
/**
* 批量生成券
*/
private List<PropCoupon> generateCoupons(PropCouponGrantCmd cmd, PropCouponType couponType, Set<Long> userIds) {
List<PropCoupon> propCoupons = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
LocalDateTime expireTime = now.plusDays(cmd.getValidDays());
for (Long userId : userIds) {
PropCoupon propCoupon = new PropCoupon();
propCoupon.setCouponNo(generateCouponNo());
propCoupon.setUserId(userId);
propCoupon.setCouponType(couponType);
propCoupon.setPropId(cmd.getPropId());
propCoupon.setPropName(cmd.getPropName());
propCoupon.setValidDays(cmd.getValidDays());
propCoupon.setStatus(PropCouponStatus.UNUSED);
propCoupon.setSource(PropCouponSource.ADMIN_GRANT);
propCoupon.setExpireTime(expireTime);
propCoupon.setCreateTime(now);
propCoupon.setUpdateTime(now);
propCoupons.add(propCoupon);
}
return propCoupons;
}
/**
* 生成券编号唯一
*/
private String generateCouponNo() {
// 使用雪花算法生成唯一ID
long id = IdWorkerUtils.getId();
// 格式: COUPON + 时间戳 + ID
return String.format("COUPON%d%d", System.currentTimeMillis(), id);
}
/**
* 发送系统消息通知用户
*/
private void sendNotifications(List<PropCoupon> propCoupons, Map<Long, UserProfileDTO> userMap, PropCouponType couponType) {
for (PropCoupon propCoupon : propCoupons) {
try {
UserProfileDTO user = userMap.get(propCoupon.getUserId());
if (user != null) {
sendNotification(propCoupon, user, couponType);
}
} catch (Exception e) {
log.error("发送道具券通知失败, userId:{}, couponNo:{}",
propCoupon.getUserId(), propCoupon.getCouponNo(), e);
}
}
}
/**
* 发送单个通知
*/
private void sendNotification(PropCoupon propCoupon, UserProfileDTO user, PropCouponType couponType) {
officialNoticeClient.send(
NoticeExtTemplateCustomizeCmd.builder()
.toAccount(user.getId())
.noticeType(OfficialNoticeTypeEnum.RECEIVE_PROP_COUPON)
.content(String.format("You have received a %s ticket", couponType.name()))
.expand(buildNotificationData(propCoupon))
.build()
);
log.info("发送道具券赠送通知, userId:{}, couponNo:{}, couponType:{}",
user.getId(), propCoupon.getCouponNo(), couponType.getDesc());
}
/**
* 构建通知数据
*/
private String buildNotificationData(PropCoupon propCoupon) {
return String.format("couponNo=%s&couponType=%s&propName=%s&validDays=%d",
propCoupon.getCouponNo(),
propCoupon.getCouponType().getDesc(),
propCoupon.getPropName(),
propCoupon.getValidDays());
}
}

View File

@ -0,0 +1,152 @@
package com.red.circle.other.app.command.propcoupon;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.app.dto.cmd.PropCouponSendCmd;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponSource;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.infra.database.rds.dao.props.PropCouponUseRecordDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponUseRecordDO;
import com.red.circle.other.inner.asserts.OtherErrorCode;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 赠送道具券执行器
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponSendCmdExe {
private final PropCouponGateway propCouponGateway;
private final UserProfileClient userProfileClient;
private final PropCouponUseRecordDAO propCouponUseRecordDAO;
/**
* 执行赠送道具券
*/
@Transactional(rollbackFor = Exception.class)
public void execute(PropCouponSendCmd cmd) {
Long senderId = cmd.requiredReqUserId();
Long receiverId = cmd.getReceiverId();
// 1. 根据券编号查询券信息加锁
PropCoupon propCoupon = propCouponGateway.selectByCouponNo(cmd.getCouponNo());
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_NOT_FOUND, propCoupon);
// 2. 校验券是否有效未使用未过期属于当前用户
validateCoupon(propCoupon, senderId, receiverId);
// 3. 校验接收人是否存在
UserProfileDTO receiver = validateReceiver(receiverId);
// 4. 更新券的持有人为接收人
propCoupon.setUserId(receiverId);
propCoupon.setSource(PropCouponSource.USER_GIFT); // 更新来源为用户赠送
propCoupon.setUpdateTime(LocalDateTime.now());
propCouponGateway.update(propCoupon);
// 5. 记录赠送记录
saveGiftRecord(propCoupon, senderId, receiverId);
// 6. 发送系统消息通知接收人
sendNotification(propCoupon, receiver);
log.info("用户赠送道具券成功, senderId:{}, receiverId:{}, couponNo:{}, couponType:{}",
senderId, receiverId, cmd.getCouponNo(), propCoupon.getCouponType().getDesc());
}
/**
* 校验券是否有效
*/
private void validateCoupon(PropCoupon propCoupon, Long senderId, Long receiverId) {
// 校验券是否属于当前用户
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_NOT_BELONG_TO_USER,
Objects.equals(propCoupon.getUserId(), senderId));
// 校验券状态是否为未使用
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_ALREADY_USED,
Objects.equals(propCoupon.getStatus(), PropCouponStatus.UNUSED));
// 校验券是否已过期
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_EXPIRED,
propCoupon.getExpireTime().isAfter(LocalDateTime.now()));
// 校验不能赠送给自己
ResponseAssert.isFalse(OtherErrorCode.PROP_COUPON_CANNOT_SEND_TO_SELF,
Objects.equals(senderId, receiverId));
}
/**
* 校验接收人是否存在
*/
private UserProfileDTO validateReceiver(Long receiverId) {
UserProfileDTO receiver = ResponseAssert.requiredSuccess(
userProfileClient.getByUserId(receiverId)
);
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_RECEIVER_NOT_FOUND, receiver);
return receiver;
}
/**
* 保存赠送记录
*/
private void saveGiftRecord(PropCoupon propCoupon, Long senderId, Long receiverId) {
PropCouponUseRecordDO recordDO = new PropCouponUseRecordDO();
recordDO.setCouponNo(propCoupon.getCouponNo());
recordDO.setUserId(senderId);
recordDO.setCouponType(propCoupon.getCouponType().getCode());
recordDO.setPropId(propCoupon.getPropId());
recordDO.setPropName(propCoupon.getPropName());
recordDO.setValidDays(propCoupon.getValidDays());
recordDO.setActionType(2); // 2-赠送
recordDO.setReceiverId(receiverId);
recordDO.setRemark("用户赠送道具券");
recordDO.setCreateTime(LocalDateTime.now());
propCouponUseRecordDAO.insert(recordDO);
}
/**
* 发送系统消息通知接收人
*/
private void sendNotification(PropCoupon propCoupon, UserProfileDTO receiver) {
// TODO: 发送系统通知
// 示例代码需要根据实际项目调整:
// officialNoticeClient.send(
// NoticeExtTemplateCustomizeCmd.builder()
// .toAccount(receiver.getId())
// .noticeType(OfficialNoticeTypeEnum.RECEIVE_PROP_COUPON)
// .content("您收到了一张" + propCoupon.getCouponType().getDesc() + "道具券")
// .expand(buildNotificationData(propCoupon))
// .build()
// );
log.info("发送道具券赠送通知, receiverId:{}, couponNo:{}, couponType:{}",
receiver.getId(), propCoupon.getCouponNo(), propCoupon.getCouponType().getDesc());
}
/**
* 构建通知数据
*/
private String buildNotificationData(PropCoupon propCoupon) {
// TODO: 根据实际需求构建通知数据
return String.format("couponNo=%s&couponType=%s&propName=%s&validDays=%d",
propCoupon.getCouponNo(),
propCoupon.getCouponType().getDesc(),
propCoupon.getPropName(),
propCoupon.getValidDays());
}
}

View File

@ -0,0 +1,288 @@
package com.red.circle.other.app.command.propcoupon;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.common.business.enums.PlatformChannelEnum;
import com.red.circle.common.business.enums.SendPropsOrigin;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.other.app.dto.cmd.PropCouponUseCmd;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import com.red.circle.other.infra.database.rds.dao.props.PropCouponUseRecordDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponUseRecordDO;
import com.red.circle.other.inner.asserts.OtherErrorCode;
import com.red.circle.other.inner.asserts.user.UserErrorCode;
import com.red.circle.other.inner.endpoint.live.RoomManagerClient;
import com.red.circle.other.inner.endpoint.material.props.*;
import com.red.circle.other.inner.enums.material.BadgeBackpackExpireTypeEnum;
import com.red.circle.other.inner.enums.material.ConsolePropsTypeEnum;
import com.red.circle.other.inner.enums.material.SysNobleVipTypeEnum;
import com.red.circle.other.inner.enums.sys.SysBadgeConfigTypeEnum;
import com.red.circle.other.inner.model.cmd.material.GiveBadgeCmd;
import com.red.circle.other.inner.model.cmd.material.GivePropsBackpackCmd;
import com.red.circle.other.inner.model.dto.material.PropsCommodityStoreDTO;
import com.red.circle.other.inner.model.dto.material.props.PropsNobleVipAbilityDTO;
import com.red.circle.tool.core.parse.DataTypeUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.Objects;
/**
* 使用道具券执行器
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponUseCmdExe {
private final PropCouponGateway propCouponGateway;
private final PropsBackpackClient propsBackpackClient;
private final BadgeBackpackClient badgeBackpackClient;
private final RoomThemeBackpackClient roomThemeBackpackClient;
private final PropCouponUseRecordDAO propCouponUseRecordDAO;
private final PropsCommodityStoreClient propsCommodityStoreClient;
private final PropsNobleVipClient propsNobleVipClient;
private final RoomManagerClient roomManagerClient;
/**
* 执行使用道具券
*/
@Transactional(rollbackFor = Exception.class)
public void execute(PropCouponUseCmd cmd) {
Long userId = cmd.requiredReqUserId();
// 1. 根据券编号查询券信息加锁
PropCoupon propCoupon = propCouponGateway.selectByCouponNo(cmd.getCouponNo());
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_NOT_FOUND, propCoupon);
// 2. 校验券是否有效
validateCoupon(propCoupon, userId);
// 3. 根据券类型调用对应的赠送道具接口
givePropsToUser(propCoupon, userId);
// 4. 更新券状态为已使用
propCoupon.setStatus(PropCouponStatus.USED);
propCoupon.setUseTime(LocalDateTime.now());
propCoupon.setUpdateTime(LocalDateTime.now());
propCouponGateway.update(propCoupon);
// 5. 记录使用记录
saveUseRecord(propCoupon, userId);
log.info("用户使用道具券成功, userId:{}, couponNo:{}, couponType:{}, propId:{}",
userId, cmd.getCouponNo(), propCoupon.getCouponType().getDesc(), propCoupon.getPropId());
}
/**
* 校验券是否有效
*/
private void validateCoupon(PropCoupon propCoupon, Long userId) {
// 校验券是否属于当前用户
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_NOT_BELONG_TO_USER,
Objects.equals(propCoupon.getUserId(), userId));
// 校验券状态是否为未使用
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_ALREADY_USED,
Objects.equals(propCoupon.getStatus(), PropCouponStatus.UNUSED));
// 校验券是否已过期
ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_EXPIRED,
propCoupon.getExpireTime().isAfter(LocalDateTime.now()));
}
/**
* 根据券类型赠送对应道具
*/
private void givePropsToUser(PropCoupon propCoupon, Long userId) {
PropCouponType couponType = propCoupon.getCouponType();
Long propId = propCoupon.getPropId();
Integer validDays = propCoupon.getValidDays();
switch (couponType) {
case AVATAR_FRAME:
// 赠送头像框
giveProps(userId, propId, ConsolePropsTypeEnum.AVATAR_FRAME.getName(), validDays);
break;
case RIDE:
// 赠送座驾
giveProps(userId, propId, ConsolePropsTypeEnum.RIDE.getName(), validDays);
break;
case VIP:
// 赠送VIP贵族VIP
PropsNobleVipAbilityDTO vipAbility = getVipAbilityBySysOriginSourceId(propId, SysOriginPlatformEnum.LIKEI.name());
giveProps(userId, propId, ConsolePropsTypeEnum.NOBLE_VIP.getName(), validDays);
if (Objects.nonNull(vipAbility.getCarId()) && !Objects
.equals(vipAbility.getCarId(), 0L)) {
giveProps(userId, vipAbility.getCarId(), ConsolePropsTypeEnum.RIDE.getName(), validDays);
}
if (Objects.nonNull(vipAbility.getAvatarFrameId())
&& !Objects.equals(vipAbility.getAvatarFrameId(), 0L)) {
giveProps(userId, vipAbility.getAvatarFrameId(), ConsolePropsTypeEnum.AVATAR_FRAME.getName(), validDays);
}
if (Objects.nonNull(vipAbility.getChatBubbleId())
&& !Objects.equals(vipAbility.getChatBubbleId(), 0L)) {
giveProps(userId, vipAbility.getChatBubbleId(), ConsolePropsTypeEnum.CHAT_BUBBLE.getName(), validDays);
}
if (Objects.nonNull(vipAbility.getDataCardId())
&& !Objects.equals(vipAbility.getDataCardId(), 0L)) {
giveProps(userId, vipAbility.getDataCardId(), ConsolePropsTypeEnum.DATA_CARD.getName(), validDays);
}
roomManagerClient.updateRoomSettingMemberCapacity(userId,
vipAbility.getRoomMaxMember(), vipAbility.getAdminNumber());
//保存vip实际权益有效时间
// Boolean isDuke = Objects.equals(vipAbility.getVipType(), SysNobleVipTypeEnum.DUKE.name());
// Boolean isKing = Objects.equals(vipAbility.getVipType(), SysNobleVipTypeEnum.KING.name());
// Boolean isBuy = Objects.equals(cmd.getVipOrigin(), SendPropsOrigin.BUY_OR_GIVE.name());
// Boolean isActivity = Objects
// .equals(cmd.getVipOrigin(), SendPropsOrigin.ACTIVITY_AWARD.name());
// if ((isDuke || isKing) && (isBuy || isActivity)) {
// propsNobleVipClient.addActualEquity(cmd.getReceiverId(),
// vipAbility.getId(), Long.valueOf(cmd.getExchangeDays()));
// }
break;
case CHAT_BUBBLE:
// 赠送聊天气泡
giveProps(userId, propId, ConsolePropsTypeEnum.CHAT_BUBBLE.getName(), validDays);
break;
case THEME:
// 赠送主题
giveTheme(userId, propId, validDays);
break;
case DATA_CARD:
// 赠送资料卡
giveProps(userId, propId, ConsolePropsTypeEnum.DATA_CARD.getName(), validDays);
break;
case FLOAT_PICTURE:
// 赠送飘屏
giveProps(userId, propId, ConsolePropsTypeEnum.FLOAT_PICTURE.getName(), validDays);
break;
case BADGE:
// 赠送徽章
// giveBadge(userId, propId, validDays);
break;
case SPECIAL_ID:
// 赠送靓号暂不支持
ResponseAssert.failure(OtherErrorCode.PROP_COUPON_TYPE_NOT_SUPPORT);
break;
default:
ResponseAssert.failure(OtherErrorCode.PROP_COUPON_TYPE_NOT_SUPPORT);
}
}
/**
* 赠送普通道具头像框座驾VIP聊天气泡资料卡飘屏
*/
private void giveProps(Long userId, Long propId, String propsType, Integer validDays) {
GivePropsBackpackCmd giveCmd = new GivePropsBackpackCmd()
.setAcceptUserId(userId)
.setPropsId(propId)
.setType(propsType)
.setOrigin(SendPropsOrigin.COUPON_EXCHANGE.name())
.setOriginDesc(SendPropsOrigin.COUPON_EXCHANGE.getDesc())
.setDays(validDays)
.setUseProps(Boolean.TRUE)
.setAllowGive(Boolean.FALSE);
ResponseAssert.requiredSuccess(propsBackpackClient.giveProps(giveCmd));
// 发送通知
propsBackpackClient.givePropsNotify(userId, propsType, validDays, propId);
}
/**
* 赠送主题
*/
private void giveTheme(Long userId, Long themeId, Integer validDays) {
ResponseAssert.requiredSuccess(
roomThemeBackpackClient.sendRoomTheme(
userId,
themeId,
validDays,
SendPropsOrigin.COUPON_EXCHANGE
)
);
// 发送通知
propsBackpackClient.givePropsNotify(userId, ConsolePropsTypeEnum.THEME.getName(), validDays, themeId);
}
/**
* 赠送徽章
*/
private void giveBadge(Long userId, Long badgeId, Integer validDays) {
// 徽章赠送天数大于1000天则为永久赠送
BadgeBackpackExpireTypeEnum expireType;
if (validDays >= 1000) {
expireType = BadgeBackpackExpireTypeEnum.PERMANENT;
} else {
expireType = BadgeBackpackExpireTypeEnum.TEMPORARY;
}
ResponseAssert.requiredSuccess(
badgeBackpackClient.giveBadge(new GiveBadgeCmd()
.setAcceptUserId(userId)
.setBadgeId(badgeId)
.setExpireType(expireType)
.setDays(validDays)
.setBadgeType(SysBadgeConfigTypeEnum.ACTIVITY)
)
);
// 发送通知
propsBackpackClient.givePropsNotify(userId, SysBadgeConfigTypeEnum.ACTIVITY.name(), validDays, badgeId);
}
private PropsNobleVipAbilityDTO getVipAbilityBySysOriginSourceId(Long propId,
String sysOrigin) {
PropsCommodityStoreDTO commodityStore = ResponseAssert.requiredSuccess(
propsCommodityStoreClient.getPropsCommodityStore(sysOrigin, propId));
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_PROP_NOT_FOUND, commodityStore);
PropsNobleVipAbilityDTO nobleVipAbility = ResponseAssert.requiredSuccess(
propsNobleVipClient.getAbilityDTO(commodityStore.getSourceId()));
ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_PROP_NOT_FOUND, nobleVipAbility);
return nobleVipAbility;
}
/**
* 保存使用记录
*/
private void saveUseRecord(PropCoupon propCoupon, Long userId) {
PropCouponUseRecordDO recordDO = new PropCouponUseRecordDO();
recordDO.setCouponNo(propCoupon.getCouponNo());
recordDO.setUserId(userId);
recordDO.setCouponType(propCoupon.getCouponType().getCode());
recordDO.setPropId(propCoupon.getPropId());
recordDO.setPropName(propCoupon.getPropName());
recordDO.setValidDays(propCoupon.getValidDays());
recordDO.setActionType(1); // 1-使用
recordDO.setRemark("用户使用道具券");
recordDO.setCreateTime(LocalDateTime.now());
propCouponUseRecordDAO.insert(recordDO);
}
}

View File

@ -0,0 +1,74 @@
package com.red.circle.other.app.command.propcoupon.query;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.convertor.PropCouponConvertor;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.cmd.PropCouponQueryCmd;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 查询道具券列表执行器
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponListQryExe {
private final PropCouponGateway propCouponGateway;
/**
* 执行查询道具券列表
*/
public PageResult<PropCouponCO> execute(PropCouponQueryCmd cmd) {
// 1. 获取当前用户ID
Long userId = cmd.requiredReqUserId();
// 2. 解析查询条件
PropCouponType couponType = null;
if (cmd.getCouponType() != null) {
couponType = PropCouponType.getByCode(cmd.getCouponType());
}
PropCouponStatus status = null;
if (cmd.getStatus() != null) {
status = PropCouponStatus.getByCode(cmd.getStatus());
}
// 3. 查询券列表分页
List<PropCoupon> propCoupons = propCouponGateway.selectByUserId(
userId,
couponType,
status,
cmd.getCurrent().intValue(),
cmd.getSize().intValue()
);
// 4. 统计总数
Long total = propCouponGateway.countByUserId(userId, couponType, status);
// 5. 转换为CO对象
List<PropCouponCO> couponCOList = propCoupons.stream()
.map(PropCouponConvertor::toClientObject)
.collect(Collectors.toList());
// 6. 返回分页结果
PageResult<PropCouponCO> pageResult = new PageResult<>();
pageResult.setTotal(total);
pageResult.setRecords(couponCOList);
pageResult.setCurrent(cmd.getCurrent());
pageResult.setSize(cmd.getSize());
return pageResult;
}
}

View File

@ -0,0 +1,94 @@
package com.red.circle.other.app.command.propcoupon.query;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.convertor.PropCouponConvertor;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.clientobject.PropCouponRecordCO;
import com.red.circle.other.app.dto.cmd.PropCouponQueryCmd;
import com.red.circle.other.infra.database.rds.dao.props.PropCouponUseRecordDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponUseRecordDO;
import com.red.circle.other.inner.endpoint.user.user.UserProfileClient;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 查询道具券使用记录执行器
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponRecordQryExe {
private final PropCouponUseRecordDAO propCouponUseRecordDAO;
private final UserProfileClient userProfileClient;
/**
* 执行查询使用记录
*/
public PageResult<PropCouponRecordCO> execute(PropCouponQueryCmd cmd) {
// 1. 获取当前用户ID
Long userId = cmd.requiredReqUserId();
// 2. 构建查询条件
LambdaQueryWrapper<PropCouponUseRecordDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PropCouponUseRecordDO::getUserId, userId);
// 如果指定了券类型添加过滤条件
if (cmd.getCouponType() != null) {
wrapper.eq(PropCouponUseRecordDO::getCouponType, cmd.getCouponType());
}
// 按创建时间倒序
wrapper.orderByDesc(PropCouponUseRecordDO::getCreateTime);
// 3. 分页查询
Page<PropCouponUseRecordDO> page = new Page<>(cmd.getCurrent(), cmd.getSize());
IPage<PropCouponUseRecordDO> recordPage = propCouponUseRecordDAO.selectPage(page, wrapper);
// 4. 获取接收人信息用于赠送记录
Set<Long> receiverIds = recordPage.getRecords().stream()
.filter(record -> record.getReceiverId() != null)
.map(PropCouponUseRecordDO::getReceiverId)
.collect(Collectors.toSet());
Map<Long, UserProfileDTO> receiverMap = Map.of();
if (!receiverIds.isEmpty()) {
receiverMap = userProfileClient.mapByUserIds(receiverIds).getBody();
}
// 5. 转换为CO对象
Map<Long, UserProfileDTO> finalReceiverMap = receiverMap;
List<PropCouponRecordCO> recordCOList = recordPage.getRecords().stream()
.map(record -> {
PropCouponRecordCO co = PropCouponConvertor.toRecordClientObject(record);
// 如果是赠送记录填充接收人昵称
if (record.getReceiverId() != null && finalReceiverMap.containsKey(record.getReceiverId())) {
UserProfileDTO receiver = finalReceiverMap.get(record.getReceiverId());
co.setReceiverNickname(receiver.getUserNickname());
}
return co;
})
.collect(Collectors.toList());
// 6. 返回分页结果
PageResult<PropCouponRecordCO> pageResult = new PageResult<>();
pageResult.setTotal(recordPage.getTotal());
pageResult.setRecords(recordCOList);
pageResult.setCurrent(cmd.getCurrent());
pageResult.setSize(cmd.getSize());
return pageResult;
}
}

View File

@ -0,0 +1,176 @@
package com.red.circle.other.app.convertor;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.clientobject.PropCouponRecordCO;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponSource;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponDO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponUseRecordDO;
/**
* 道具券对象转换器
*
* @author system
* @date 2025-11-05
*/
public class PropCouponConvertor {
/**
* Domain转CO
*/
public static PropCouponCO toClientObject(PropCoupon propCoupon) {
if (propCoupon == null) {
return null;
}
PropCouponCO co = new PropCouponCO();
co.setId(propCoupon.getId());
co.setCouponNo(propCoupon.getCouponNo());
co.setCouponType(propCoupon.getCouponType().getCode());
co.setCouponTypeName(propCoupon.getCouponType().getDesc());
co.setPropId(propCoupon.getPropId());
co.setPropName(propCoupon.getPropName());
co.setValidDays(propCoupon.getValidDays());
co.setStatus(propCoupon.getStatus().getCode());
co.setStatusName(propCoupon.getStatus().getDesc());
co.setSource(propCoupon.getSource().getCode());
co.setSourceName(propCoupon.getSource().getDesc());
co.setExpireTime(propCoupon.getExpireTime());
co.setUseTime(propCoupon.getUseTime());
co.setCreateTime(propCoupon.getCreateTime());
return co;
}
/**
* DO转CO
*/
public static PropCouponCO toClientObject(PropCouponDO propCouponDO) {
if (propCouponDO == null) {
return null;
}
PropCouponCO co = new PropCouponCO();
co.setId(propCouponDO.getId());
co.setCouponNo(propCouponDO.getCouponNo());
co.setCouponType(propCouponDO.getCouponType());
PropCouponType couponType = PropCouponType.getByCode(propCouponDO.getCouponType());
if (couponType != null) {
co.setCouponTypeName(couponType.getDesc());
}
co.setPropId(propCouponDO.getPropId());
co.setPropName(propCouponDO.getPropName());
co.setPropIcon(propCouponDO.getPropIcon());
co.setValidDays(propCouponDO.getValidDays());
co.setStatus(propCouponDO.getStatus());
PropCouponStatus status = PropCouponStatus.getByCode(propCouponDO.getStatus());
if (status != null) {
co.setStatusName(status.getDesc());
}
co.setSource(propCouponDO.getSource());
PropCouponSource source = PropCouponSource.getByCode(propCouponDO.getSource());
if (source != null) {
co.setSourceName(source.getDesc());
}
co.setExpireTime(propCouponDO.getExpireTime());
co.setUseTime(propCouponDO.getUseTime());
co.setCreateTime(propCouponDO.getCreateTime());
return co;
}
/**
* DO转RecordCO
*/
public static PropCouponRecordCO toRecordClientObject(PropCouponUseRecordDO recordDO) {
if (recordDO == null) {
return null;
}
PropCouponRecordCO co = new PropCouponRecordCO();
co.setId(recordDO.getId());
co.setCouponNo(recordDO.getCouponNo());
co.setCouponType(recordDO.getCouponType());
PropCouponType couponType = PropCouponType.getByCode(recordDO.getCouponType());
if (couponType != null) {
co.setCouponTypeName(couponType.getDesc());
}
co.setPropId(recordDO.getPropId());
co.setPropName(recordDO.getPropName());
co.setValidDays(recordDO.getValidDays());
co.setActionType(recordDO.getActionType());
// 操作类型名称
if (recordDO.getActionType() != null) {
co.setActionTypeName(recordDO.getActionType() == 1 ? "使用" : "赠送");
}
co.setReceiverId(recordDO.getReceiverId());
co.setRemark(recordDO.getRemark());
co.setCreateTime(recordDO.getCreateTime());
return co;
}
/**
* Domain转DO
*/
public static PropCouponDO toDO(PropCoupon propCoupon) {
if (propCoupon == null) {
return null;
}
PropCouponDO propCouponDO = new PropCouponDO();
propCouponDO.setId(propCoupon.getId());
propCouponDO.setCouponNo(propCoupon.getCouponNo());
propCouponDO.setUserId(propCoupon.getUserId());
propCouponDO.setCouponType(propCoupon.getCouponType().getCode());
propCouponDO.setPropId(propCoupon.getPropId());
propCouponDO.setPropName(propCoupon.getPropName());
propCouponDO.setValidDays(propCoupon.getValidDays());
propCouponDO.setStatus(propCoupon.getStatus().getCode());
propCouponDO.setSource(propCoupon.getSource().getCode());
propCouponDO.setExpireTime(propCoupon.getExpireTime());
propCouponDO.setUseTime(propCoupon.getUseTime());
propCouponDO.setCreateTime(propCoupon.getCreateTime());
propCouponDO.setUpdateTime(propCoupon.getUpdateTime());
return propCouponDO;
}
/**
* DO转Domain
*/
public static PropCoupon toDomain(PropCouponDO propCouponDO) {
if (propCouponDO == null) {
return null;
}
PropCoupon propCoupon = new PropCoupon();
propCoupon.setId(propCouponDO.getId());
propCoupon.setCouponNo(propCouponDO.getCouponNo());
propCoupon.setUserId(propCouponDO.getUserId());
propCoupon.setCouponType(PropCouponType.getByCode(propCouponDO.getCouponType()));
propCoupon.setPropId(propCouponDO.getPropId());
propCoupon.setPropName(propCouponDO.getPropName());
propCoupon.setValidDays(propCouponDO.getValidDays());
propCoupon.setStatus(PropCouponStatus.getByCode(propCouponDO.getStatus()));
propCoupon.setSource(PropCouponSource.getByCode(propCouponDO.getSource()));
propCoupon.setExpireTime(propCouponDO.getExpireTime());
propCoupon.setUseTime(propCouponDO.getUseTime());
propCoupon.setCreateTime(propCouponDO.getCreateTime());
propCoupon.setUpdateTime(propCouponDO.getUpdateTime());
return propCoupon;
}
}

View File

@ -0,0 +1,36 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* 道具券过期定时任务
*
* @author system
* @date 2025-11-05
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class PropCouponExpireTask {
private final PropCouponGateway propCouponGateway;
/**
* 过期券定时任务
* 每小时执行一次
*/
@Scheduled(cron = "0 0 * * * ?")
public void expireCoupons() {
// TODO: 实现过期逻辑
// try {
// Integer count = propCouponGateway.expireOverdueCoupons();
// log.info("道具券过期定时任务执行完成,过期券数量:{}", count);
// } catch (Exception e) {
// log.error("道具券过期定时任务执行失败", e);
// }
}
}

View File

@ -0,0 +1,68 @@
package com.red.circle.other.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.command.propcoupon.PropCouponGrantCmdExe;
import com.red.circle.other.app.command.propcoupon.PropCouponSendCmdExe;
import com.red.circle.other.app.command.propcoupon.PropCouponUseCmdExe;
import com.red.circle.other.app.command.propcoupon.query.PropCouponListQryExe;
import com.red.circle.other.app.command.propcoupon.query.PropCouponRecordQryExe;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.clientobject.PropCouponRecordCO;
import com.red.circle.other.app.dto.cmd.PropCouponGrantCmd;
import com.red.circle.other.app.dto.cmd.PropCouponQueryCmd;
import com.red.circle.other.app.dto.cmd.PropCouponSendCmd;
import com.red.circle.other.app.dto.cmd.PropCouponUseCmd;
import com.red.circle.other.app.util.DistributedLockUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
* 道具券服务实现
*
* @author system
* @date 2025-11-05
*/
@Service
@RequiredArgsConstructor
public class PropCouponServiceImpl implements PropCouponService {
private static final String PROP_COUPON_USE_LOCK_KEY_PREFIX = "prop:coupon:use";
private static final String PROP_COUPON_SEND_LOCK_KEY_PREFIX = "prop:coupon:send";
private final PropCouponUseCmdExe propCouponUseCmdExe;
private final PropCouponSendCmdExe propCouponSendCmdExe;
private final PropCouponGrantCmdExe propCouponGrantCmdExe;
private final PropCouponListQryExe propCouponListQryExe;
private final PropCouponRecordQryExe propCouponRecordQryExe;
private final DistributedLockUtil distributedLockUtil;
@Override
public void useCoupon(PropCouponUseCmd cmd) {
distributedLockUtil.executeWithLock(
DistributedLockUtil.buildLockKey(PROP_COUPON_USE_LOCK_KEY_PREFIX, cmd.getCouponNo()),
() -> propCouponUseCmdExe.execute(cmd), "useCoupon fail.");
}
@Override
public void sendCoupon(PropCouponSendCmd cmd) {
distributedLockUtil.executeWithLock(
DistributedLockUtil.buildLockKey(PROP_COUPON_SEND_LOCK_KEY_PREFIX, cmd.getCouponNo()),
() -> propCouponSendCmdExe.execute(cmd), "sendCoupon fail."
);
}
@Override
public PageResult<PropCouponCO> queryMyCoupons(PropCouponQueryCmd cmd) {
return propCouponListQryExe.execute(cmd);
}
@Override
public PageResult<PropCouponRecordCO> queryUseRecords(PropCouponQueryCmd cmd) {
return propCouponRecordQryExe.execute(cmd);
}
@Override
public void grantCoupons(PropCouponGrantCmd cmd) {
propCouponGrantCmdExe.execute(cmd);
}
}

View File

@ -0,0 +1,94 @@
package com.red.circle.other.app.dto.clientobject;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 道具券客户端对象
*
* @author system
* @date 2025-11-05
*/
@Data
public class PropCouponCO {
/**
* 主键ID
*/
private Long id;
/**
* 券编号
*/
private String couponNo;
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
private Integer couponType;
/**
* 券类型名称
*/
private String couponTypeName;
/**
* 道具ID
*/
private Long propId;
/**
* 道具名称
*/
private String propName;
/**
* 道具图标
*/
private String propIcon;
/**
* 有效天数
*/
private Integer validDays;
/**
* 券状态0未使用 1已使用 2已过期 3已赠送
*/
private Integer status;
/**
* 券状态名称
*/
private String statusName;
/**
* 来源1后台赠送 2活动获得 3用户赠送
*/
private Integer source;
/**
* 来源名称
*/
private String sourceName;
/**
* 过期时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime expireTime;
/**
* 使用时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime useTime;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,82 @@
package com.red.circle.other.app.dto.clientobject;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 道具券使用记录客户端对象
*
* @author system
* @date 2025-11-05
*/
@Data
public class PropCouponRecordCO {
/**
* 主键ID
*/
private Long id;
/**
* 券编号
*/
private String couponNo;
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
private Integer couponType;
/**
* 券类型名称
*/
private String couponTypeName;
/**
* 道具ID
*/
private Long propId;
/**
* 道具名称
*/
private String propName;
/**
* 有效天数
*/
private Integer validDays;
/**
* 操作类型1使用 2赠送
*/
private Integer actionType;
/**
* 操作类型名称
*/
private String actionTypeName;
/**
* 接收人ID赠送时有值
*/
private Long receiverId;
/**
* 接收人昵称
*/
private String receiverNickname;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
}

View File

@ -0,0 +1,52 @@
package com.red.circle.other.app.dto.cmd;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.util.List;
/**
* 后台赠送道具券命令
*
* @author system
* @date 2025-11-05
*/
@Data
public class PropCouponGrantCmd {
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
@NotNull(message = "券类型不能为空")
private Integer couponType;
/**
* 道具ID
*/
@NotNull(message = "道具ID不能为空")
private Long propId;
/**
* 道具名称
*/
@NotBlank(message = "道具名称不能为空")
private String propName;
/**
* 有效天数
*/
@NotNull(message = "有效天数不能为空")
private Integer validDays;
/**
* 接收用户ID列表
*/
@NotNull(message = "接收用户ID列表不能为空")
private List<Long> userIds;
/**
* 备注
*/
private String remark;
}

View File

@ -0,0 +1,26 @@
package com.red.circle.other.app.dto.cmd;
import com.red.circle.common.business.dto.PageQueryCmd;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 查询道具券命令
*
* @author system
* @date 2025-11-05
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PropCouponQueryCmd extends PageQueryCmd {
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
private Integer couponType;
/**
* 券状态0未使用 1已使用 2已过期 3已赠送
*/
private Integer status;
}

View File

@ -0,0 +1,30 @@
package com.red.circle.other.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 赠送道具券命令
*
* @author system
* @date 2025-11-05
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PropCouponSendCmd extends AppExtCommand {
/**
* 券编号
*/
@NotBlank(message = "券编号不能为空")
private String couponNo;
/**
* 接收人ID
*/
@NotNull(message = "接收人ID不能为空")
private Long receiverId;
}

View File

@ -0,0 +1,23 @@
package com.red.circle.other.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 使用道具券命令
*
* @author system
* @date 2025-11-05
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PropCouponUseCmd extends AppExtCommand {
/**
* 券编号
*/
@NotBlank(message = "券编号不能为空")
private String couponNo;
}

View File

@ -0,0 +1,43 @@
package com.red.circle.other.app.service;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.other.app.dto.clientobject.PropCouponCO;
import com.red.circle.other.app.dto.clientobject.PropCouponRecordCO;
import com.red.circle.other.app.dto.cmd.PropCouponGrantCmd;
import com.red.circle.other.app.dto.cmd.PropCouponQueryCmd;
import com.red.circle.other.app.dto.cmd.PropCouponSendCmd;
import com.red.circle.other.app.dto.cmd.PropCouponUseCmd;
/**
* 道具券服务接口
*
* @author system
* @date 2025-11-05
*/
public interface PropCouponService {
/**
* 使用道具券
*/
void useCoupon(PropCouponUseCmd cmd);
/**
* 赠送道具券
*/
void sendCoupon(PropCouponSendCmd cmd);
/**
* 查询我的道具券列表
*/
PageResult<PropCouponCO> queryMyCoupons(PropCouponQueryCmd cmd);
/**
* 查询使用记录
*/
PageResult<PropCouponRecordCO> queryUseRecords(PropCouponQueryCmd cmd);
/**
* 后台赠送道具券
*/
void grantCoupons(PropCouponGrantCmd cmd);
}

View File

@ -0,0 +1,61 @@
package com.red.circle.other.domain.gateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import java.util.List;
/**
* 道具券网关接口
*
* @author system
* @date 2025-11-05
*/
public interface PropCouponGateway {
/**
* 保存道具券
*/
void save(PropCoupon propCoupon);
/**
* 更新道具券
*/
void update(PropCoupon propCoupon);
/**
* 根据券编号查询加锁
*/
PropCoupon selectForUpdate(String couponNo);
/**
* 根据券编号查询
*/
PropCoupon selectByCouponNo(String couponNo);
/**
* 根据ID查询
*/
PropCoupon selectById(Long id);
/**
* 查询用户的道具券列表
*/
List<PropCoupon> selectByUserId(Long userId, PropCouponType couponType, PropCouponStatus status, Integer pageNo, Integer pageSize);
/**
* 统计用户的道具券数量
*/
Long countByUserId(Long userId, PropCouponType couponType, PropCouponStatus status);
/**
* 批量过期券
*/
Integer expireOverdueCoupons();
/**
* 批量保存道具券
*/
void batchSave(List<PropCoupon> propCoupons);
}

View File

@ -0,0 +1,116 @@
package com.red.circle.other.domain.propcoupon;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 道具券领域对象
*
* @author system
* @date 2025-11-05
*/
@Data
public class PropCoupon {
/**
* 主键ID
*/
private Long id;
/**
* 券编号唯一标识
*/
private String couponNo;
/**
* 当前持有人ID
*/
private Long userId;
/**
* 券类型
*/
private PropCouponType couponType;
/**
* 对应道具ID
*/
private Long propId;
/**
* 道具名称
*/
private String propName;
/**
* 有效天数
*/
private Integer validDays;
/**
* 券状态
*/
private PropCouponStatus status;
/**
* 来源
*/
private PropCouponSource source;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 使用时间
*/
private LocalDateTime useTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 检查券是否有效
*/
public boolean isValid() {
// TODO: 实现逻辑
// 1. 状态是否为未使用
// 2. 是否未过期
return false;
}
/**
* 检查是否过期
*/
public boolean isExpired() {
// TODO: 实现逻辑
return false;
}
/**
* 标记为已使用
*/
public void markAsUsed() {
// TODO: 实现逻辑
// 1. 更新状态为已使用
// 2. 记录使用时间
}
/**
* 标记为已赠送
*/
public void markAsSent(Long receiverId) {
// TODO: 实现逻辑
// 1. 更新持有人ID
// 2. 更新状态
}
}

View File

@ -0,0 +1,48 @@
package com.red.circle.other.domain.propcoupon;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 道具券来源枚举
*
* @author system
* @date 2025-11-05
*/
@Getter
@AllArgsConstructor
public enum PropCouponSource {
/**
* 后台赠送
*/
ADMIN_GRANT(1, "后台赠送"),
/**
* 活动获得
*/
ACTIVITY(2, "活动获得"),
/**
* 用户赠送
*/
USER_GIFT(3, "用户赠送");
private final Integer code;
private final String desc;
/**
* 根据code获取枚举
*/
public static PropCouponSource getByCode(Integer code) {
if (code == null) {
return null;
}
for (PropCouponSource source : values()) {
if (source.getCode().equals(code)) {
return source;
}
}
return null;
}
}

View File

@ -0,0 +1,53 @@
package com.red.circle.other.domain.propcoupon;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 道具券状态枚举
*
* @author system
* @date 2025-11-05
*/
@Getter
@AllArgsConstructor
public enum PropCouponStatus {
/**
* 未使用
*/
UNUSED(0, "未使用"),
/**
* 已使用
*/
USED(1, "已使用"),
/**
* 已过期
*/
EXPIRED(2, "已过期"),
/**
* 已赠送
*/
SENT(3, "已赠送");
private final Integer code;
private final String desc;
/**
* 根据code获取枚举
*/
public static PropCouponStatus getByCode(Integer code) {
if (code == null) {
return null;
}
for (PropCouponStatus status : values()) {
if (status.getCode().equals(code)) {
return status;
}
}
return null;
}
}

View File

@ -0,0 +1,78 @@
package com.red.circle.other.domain.propcoupon;
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* 道具券类型枚举
*
* @author system
* @date 2025-11-05
*/
@Getter
@AllArgsConstructor
public enum PropCouponType {
/**
* 头像框
*/
AVATAR_FRAME(1, "头像框"),
/**
* 座驾
*/
RIDE(2, "座驾"),
/**
* VIP
*/
VIP(3, "VIP"),
/**
* 聊天气泡
*/
CHAT_BUBBLE(4, "聊天气泡"),
/**
* 主题
*/
THEME(5, "主题"),
/**
* 资料卡
*/
DATA_CARD(6, "资料卡"),
/**
* 飘屏
*/
FLOAT_PICTURE(7, "飘屏"),
/**
* 徽章
*/
BADGE(8, "徽章"),
/**
* 靓号
*/
SPECIAL_ID(9, "靓号");
private final Integer code;
private final String desc;
/**
* 根据code获取枚举
*/
public static PropCouponType getByCode(Integer code) {
if (code == null) {
return null;
}
for (PropCouponType type : values()) {
if (type.getCode().equals(code)) {
return type;
}
}
return null;
}
}

View File

@ -0,0 +1,31 @@
package com.red.circle.other.infra.database.rds.dao.props;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponDO;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* 道具券DAO
*
* @author system
* @date 2025-11-05
*/
public interface PropCouponDAO extends BaseDAO<PropCouponDO> {
/**
* 根据券编号查询加锁
*/
PropCouponDO selectForUpdate(@Param("couponNo") String couponNo);
/**
* 批量插入
*/
int batchInsert(@Param("list") List<PropCouponDO> list);
/**
* 批量过期券
*/
int expireOverdueCoupons();
}

View File

@ -0,0 +1,14 @@
package com.red.circle.other.infra.database.rds.dao.props;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponUseRecordDO;
/**
* 道具券使用记录DAO
*
* @author system
* @date 2025-11-05
*/
public interface PropCouponUseRecordDAO extends BaseDAO<PropCouponUseRecordDO> {
}

View File

@ -0,0 +1,90 @@
package com.red.circle.other.infra.database.rds.entity.props;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 道具券实体
*
* @author system
* @date 2025-11-05
*/
@Data
@TableName("prop_coupon")
public class PropCouponDO {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 券编号唯一标识
*/
private String couponNo;
/**
* 当前持有人ID
*/
private Long userId;
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
private Integer couponType;
/**
* 对应道具ID
*/
private Long propId;
/**
* 道具名称
*/
private String propName;
/**
* 道具图标
*/
private String propIcon;
/**
* 有效天数
*/
private Integer validDays;
/**
* 券状态0未使用 1已使用 2已过期 3已赠送
*/
private Integer status;
/**
* 来源1后台赠送 2活动获得 3用户赠送
*/
private Integer source;
/**
* 过期时间
*/
private LocalDateTime expireTime;
/**
* 使用时间
*/
private LocalDateTime useTime;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新时间
*/
private LocalDateTime updateTime;
}

View File

@ -0,0 +1,75 @@
package com.red.circle.other.infra.database.rds.entity.props;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 道具券使用记录实体
*
* @author system
* @date 2025-11-05
*/
@Data
@TableName("prop_coupon_use_record")
public class PropCouponUseRecordDO {
/**
* 主键ID
*/
@TableId(type = IdType.AUTO)
private Long id;
/**
* 券编号
*/
private String couponNo;
/**
* 使用人ID
*/
private Long userId;
/**
* 券类型1头像框 2座驾 3VIP 4聊天气泡 5主题 6资料卡 7飘屏 8徽章 9靓号
*/
private Integer couponType;
/**
* 道具ID
*/
private Long propId;
/**
* 道具名称
*/
private String propName;
/**
* 有效天数
*/
private Integer validDays;
/**
* 操作类型1使用 2赠送
*/
private Integer actionType;
/**
* 接收人ID赠送时有值
*/
private Long receiverId;
/**
* 备注
*/
private String remark;
/**
* 创建时间
*/
private LocalDateTime createTime;
}

View File

@ -0,0 +1,172 @@
package com.red.circle.other.infra.gateway;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.red.circle.other.domain.gateway.PropCouponGateway;
import com.red.circle.other.domain.propcoupon.PropCoupon;
import com.red.circle.other.domain.propcoupon.PropCouponSource;
import com.red.circle.other.domain.propcoupon.PropCouponStatus;
import com.red.circle.other.domain.propcoupon.PropCouponType;
import com.red.circle.other.infra.database.rds.dao.props.PropCouponDAO;
import com.red.circle.other.infra.database.rds.entity.props.PropCouponDO;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.stream.Collectors;
/**
* 道具券网关实现
*
* @author system
* @date 2025-11-05
*/
@Component
@RequiredArgsConstructor
public class PropCouponGatewayImpl implements PropCouponGateway {
private final PropCouponDAO propCouponDAO;
@Override
public void save(PropCoupon propCoupon) {
PropCouponDO propCouponDO = convertToDO(propCoupon);
propCouponDAO.insert(propCouponDO);
}
@Override
public void update(PropCoupon propCoupon) {
PropCouponDO propCouponDO = convertToDO(propCoupon);
propCouponDAO.updateById(propCouponDO);
}
@Override
public PropCoupon selectForUpdate(String couponNo) {
LambdaQueryWrapper<PropCouponDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PropCouponDO::getCouponNo, couponNo);
List<PropCouponDO> propCouponDOS = propCouponDAO.selectList(wrapper);
if (propCouponDOS == null || propCouponDOS.isEmpty()) {
return null;
}
return convertToDomain(propCouponDOS.get(0));
}
@Override
public PropCoupon selectByCouponNo(String couponNo) {
LambdaQueryWrapper<PropCouponDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PropCouponDO::getCouponNo, couponNo);
List<PropCouponDO> propCouponDOS = propCouponDAO.selectList(wrapper);
if (propCouponDOS == null || propCouponDOS.isEmpty()) {
return null;
}
return convertToDomain(propCouponDOS.get(0));
}
@Override
public PropCoupon selectById(Long id) {
// TODO: 实现根据ID查询逻辑
// PropCouponDO propCouponDO = propCouponDAO.selectById(id);
// return convertToDomain(propCouponDO);
return null;
}
@Override
public List<PropCoupon> selectByUserId(Long userId, PropCouponType couponType, PropCouponStatus status, Integer pageNo, Integer pageSize) {
LambdaQueryWrapper<PropCouponDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PropCouponDO::getUserId, userId);
if (couponType != null) {
wrapper.eq(PropCouponDO::getCouponType, couponType.getCode());
}
if (status != null) {
wrapper.eq(PropCouponDO::getStatus, status.getCode());
}
wrapper.orderByDesc(PropCouponDO::getCreateTime);
Page<PropCouponDO> page = new Page<>(pageNo, pageSize);
IPage<PropCouponDO> result = propCouponDAO.selectPage(page, wrapper);
return result.getRecords().stream()
.map(this::convertToDomain)
.collect(Collectors.toList());
}
@Override
public Long countByUserId(Long userId, PropCouponType couponType, PropCouponStatus status) {
LambdaQueryWrapper<PropCouponDO> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(PropCouponDO::getUserId, userId);
if (couponType != null) {
wrapper.eq(PropCouponDO::getCouponType, couponType.getCode());
}
if (status != null) {
wrapper.eq(PropCouponDO::getStatus, status.getCode());
}
return propCouponDAO.selectCount(wrapper);
}
@Override
public Integer expireOverdueCoupons() {
// TODO: 实现批量过期逻辑
// return propCouponDAO.expireOverdueCoupons();
return 0;
}
@Override
public void batchSave(List<PropCoupon> propCoupons) {
List<PropCouponDO> propCouponDOS = propCoupons.stream()
.map(this::convertToDO)
.collect(Collectors.toList());
propCouponDAO.batchInsert(propCouponDOS);
}
/**
* DO转Domain
*/
private PropCoupon convertToDomain(PropCouponDO propCouponDO) {
if (propCouponDO == null) {
return null;
}
PropCoupon propCoupon = new PropCoupon();
propCoupon.setId(propCouponDO.getId());
propCoupon.setCouponNo(propCouponDO.getCouponNo());
propCoupon.setUserId(propCouponDO.getUserId());
propCoupon.setCouponType(PropCouponType.getByCode(propCouponDO.getCouponType()));
propCoupon.setPropId(propCouponDO.getPropId());
propCoupon.setPropName(propCouponDO.getPropName());
propCoupon.setValidDays(propCouponDO.getValidDays());
propCoupon.setStatus(PropCouponStatus.getByCode(propCouponDO.getStatus()));
propCoupon.setSource(PropCouponSource.getByCode(propCouponDO.getSource()));
propCoupon.setExpireTime(propCouponDO.getExpireTime());
propCoupon.setUseTime(propCouponDO.getUseTime());
propCoupon.setCreateTime(propCouponDO.getCreateTime());
propCoupon.setUpdateTime(propCouponDO.getUpdateTime());
return propCoupon;
}
/**
* Domain转DO
*/
private PropCouponDO convertToDO(PropCoupon propCoupon) {
if (propCoupon == null) {
return null;
}
PropCouponDO propCouponDO = new PropCouponDO();
propCouponDO.setId(propCoupon.getId());
propCouponDO.setCouponNo(propCoupon.getCouponNo());
propCouponDO.setUserId(propCoupon.getUserId());
propCouponDO.setCouponType(propCoupon.getCouponType() != null ? propCoupon.getCouponType().getCode() : null);
propCouponDO.setPropId(propCoupon.getPropId());
propCouponDO.setPropName(propCoupon.getPropName());
propCouponDO.setValidDays(propCoupon.getValidDays());
propCouponDO.setStatus(propCoupon.getStatus() != null ? propCoupon.getStatus().getCode() : null);
propCouponDO.setSource(propCoupon.getSource() != null ? propCoupon.getSource().getCode() : null);
propCouponDO.setExpireTime(propCoupon.getExpireTime());
propCouponDO.setUseTime(propCoupon.getUseTime());
propCouponDO.setCreateTime(propCoupon.getCreateTime());
propCouponDO.setUpdateTime(propCoupon.getUpdateTime());
return propCouponDO;
}
}