新增道具券功能
This commit is contained in:
parent
2bbf29ad0a
commit
c74a4b11d1
@ -259,6 +259,11 @@ public enum SendPropsOrigin {
|
||||
*/
|
||||
LOTTERY_REWARD("Lottery Rewards"),
|
||||
|
||||
/**
|
||||
* 道具券兑换
|
||||
*/
|
||||
COUPON_EXCHANGE("Coupon Exchange"),
|
||||
|
||||
;
|
||||
|
||||
private final String desc;
|
||||
|
||||
@ -202,6 +202,11 @@ public enum OfficialNoticeTypeEnum {
|
||||
*/
|
||||
GAME_BAISHUN_WIN,
|
||||
|
||||
/**
|
||||
* 收到道具券
|
||||
*/
|
||||
RECEIVE_PROP_COUPON,
|
||||
|
||||
;
|
||||
|
||||
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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());
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
// }
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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. 更新状态
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
@ -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> {
|
||||
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user