diff --git a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java index 4d5a2bb2..68f61a78 100644 --- a/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java +++ b/rc-common-business/common-business-dto/src/main/java/com/red/circle/common/business/enums/SendPropsOrigin.java @@ -259,6 +259,11 @@ public enum SendPropsOrigin { */ LOTTERY_REWARD("Lottery Rewards"), + /** + * 道具券兑换 + */ + COUPON_EXCHANGE("Coupon Exchange"), + ; private final String desc; diff --git a/rc-service/rc-inner-api/external-inner/external-inner-model/src/main/java/com/red/circle/external/inner/model/enums/message/OfficialNoticeTypeEnum.java b/rc-service/rc-inner-api/external-inner/external-inner-model/src/main/java/com/red/circle/external/inner/model/enums/message/OfficialNoticeTypeEnum.java index 4f86474b..654f21e2 100644 --- a/rc-service/rc-inner-api/external-inner/external-inner-model/src/main/java/com/red/circle/external/inner/model/enums/message/OfficialNoticeTypeEnum.java +++ b/rc-service/rc-inner-api/external-inner/external-inner-model/src/main/java/com/red/circle/external/inner/model/enums/message/OfficialNoticeTypeEnum.java @@ -202,6 +202,11 @@ public enum OfficialNoticeTypeEnum { */ GAME_BAISHUN_WIN, + /** + * 收到道具券 + */ + RECEIVE_PROP_COUPON, + ; diff --git a/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java new file mode 100644 index 00000000..7b7b4528 --- /dev/null +++ b/rc-service/rc-inner-api/other-inner/other-inner-model/src/main/java/com/red/circle/other/inner/asserts/OtherErrorCode.java @@ -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(); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/props/PropCouponRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/props/PropCouponRestController.java new file mode 100644 index 00000000..6ae2b2d1 --- /dev/null +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/props/PropCouponRestController.java @@ -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 queryMyCoupons(@RequestBody @Validated PropCouponQueryCmd cmd) { + return propCouponService.queryMyCoupons(cmd); + } + + /** + * 查询使用记录 + */ + @PostMapping("/record/list") + public PageResult queryUseRecords(@RequestBody @Validated PropCouponQueryCmd cmd) { + return propCouponService.queryUseRecords(cmd); + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponGrantCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponGrantCmdExe.java new file mode 100644 index 00000000..b51b980f --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponGrantCmdExe.java @@ -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 userIds = Set.copyOf(cmd.getUserIds()); + Map userMap = validateUsers(userIds); + + // 3. 解析券类型 + PropCouponType couponType = PropCouponType.getByCode(cmd.getCouponType()); + ResponseAssert.notNull(OtherErrorCode.PROP_COUPON_TYPE_NOT_SUPPORT, couponType); + + // 4. 批量生成券 + List 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 validateUsers(Set userIds) { + Map userMap = ResponseAssert.requiredSuccess( + userProfileClient.mapByUserIds(userIds) + ); + + // 校验所有用户都存在 + ResponseAssert.isTrue(OtherErrorCode.PROP_COUPON_RECEIVER_NOT_FOUND, + userMap.size() == userIds.size()); + + return userMap; + } + + /** + * 批量生成券 + */ + private List generateCoupons(PropCouponGrantCmd cmd, PropCouponType couponType, Set userIds) { + List 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 propCoupons, Map 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()); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponSendCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponSendCmdExe.java new file mode 100644 index 00000000..685ac126 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponSendCmdExe.java @@ -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()); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponUseCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponUseCmdExe.java new file mode 100644 index 00000000..2e5a1ec4 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/PropCouponUseCmdExe.java @@ -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); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponListQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponListQryExe.java new file mode 100644 index 00000000..00ba1d8a --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponListQryExe.java @@ -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 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 propCoupons = propCouponGateway.selectByUserId( + userId, + couponType, + status, + cmd.getCurrent().intValue(), + cmd.getSize().intValue() + ); + + // 4. 统计总数 + Long total = propCouponGateway.countByUserId(userId, couponType, status); + + // 5. 转换为CO对象 + List couponCOList = propCoupons.stream() + .map(PropCouponConvertor::toClientObject) + .collect(Collectors.toList()); + + // 6. 返回分页结果 + PageResult pageResult = new PageResult<>(); + pageResult.setTotal(total); + pageResult.setRecords(couponCOList); + pageResult.setCurrent(cmd.getCurrent()); + pageResult.setSize(cmd.getSize()); + return pageResult; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponRecordQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponRecordQryExe.java new file mode 100644 index 00000000..0d50e71f --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/propcoupon/query/PropCouponRecordQryExe.java @@ -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 execute(PropCouponQueryCmd cmd) { + // 1. 获取当前用户ID + Long userId = cmd.requiredReqUserId(); + + // 2. 构建查询条件 + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PropCouponUseRecordDO::getUserId, userId); + + // 如果指定了券类型,添加过滤条件 + if (cmd.getCouponType() != null) { + wrapper.eq(PropCouponUseRecordDO::getCouponType, cmd.getCouponType()); + } + + // 按创建时间倒序 + wrapper.orderByDesc(PropCouponUseRecordDO::getCreateTime); + + // 3. 分页查询 + Page page = new Page<>(cmd.getCurrent(), cmd.getSize()); + IPage recordPage = propCouponUseRecordDAO.selectPage(page, wrapper); + + // 4. 获取接收人信息(用于赠送记录) + Set receiverIds = recordPage.getRecords().stream() + .filter(record -> record.getReceiverId() != null) + .map(PropCouponUseRecordDO::getReceiverId) + .collect(Collectors.toSet()); + + Map receiverMap = Map.of(); + if (!receiverIds.isEmpty()) { + receiverMap = userProfileClient.mapByUserIds(receiverIds).getBody(); + } + + // 5. 转换为CO对象 + Map finalReceiverMap = receiverMap; + List 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 pageResult = new PageResult<>(); + pageResult.setTotal(recordPage.getTotal()); + pageResult.setRecords(recordCOList); + pageResult.setCurrent(cmd.getCurrent()); + pageResult.setSize(cmd.getSize()); + return pageResult; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/PropCouponConvertor.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/PropCouponConvertor.java new file mode 100644 index 00000000..2ca28ba6 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/PropCouponConvertor.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/PropCouponExpireTask.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/PropCouponExpireTask.java new file mode 100644 index 00000000..76be2b64 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/PropCouponExpireTask.java @@ -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); + // } + } +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/PropCouponServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/PropCouponServiceImpl.java new file mode 100644 index 00000000..18c0b93d --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/PropCouponServiceImpl.java @@ -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 queryMyCoupons(PropCouponQueryCmd cmd) { + return propCouponListQryExe.execute(cmd); + } + + @Override + public PageResult queryUseRecords(PropCouponQueryCmd cmd) { + return propCouponRecordQryExe.execute(cmd); + } + + @Override + public void grantCoupons(PropCouponGrantCmd cmd) { + propCouponGrantCmdExe.execute(cmd); + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponCO.java new file mode 100644 index 00000000..e6702d17 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponCO.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponRecordCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponRecordCO.java new file mode 100644 index 00000000..818c8498 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/PropCouponRecordCO.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponGrantCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponGrantCmd.java new file mode 100644 index 00000000..8a733732 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponGrantCmd.java @@ -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 userIds; + + /** + * 备注 + */ + private String remark; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponQueryCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponQueryCmd.java new file mode 100644 index 00000000..aaf5794e --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponQueryCmd.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponSendCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponSendCmd.java new file mode 100644 index 00000000..56457f06 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponSendCmd.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponUseCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponUseCmd.java new file mode 100644 index 00000000..69bc06a2 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/PropCouponUseCmd.java @@ -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; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/PropCouponService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/PropCouponService.java new file mode 100644 index 00000000..ae927bf4 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/PropCouponService.java @@ -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 queryMyCoupons(PropCouponQueryCmd cmd); + + /** + * 查询使用记录 + */ + PageResult queryUseRecords(PropCouponQueryCmd cmd); + + /** + * 后台赠送道具券 + */ + void grantCoupons(PropCouponGrantCmd cmd); +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/PropCouponGateway.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/PropCouponGateway.java new file mode 100644 index 00000000..2119cf91 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/PropCouponGateway.java @@ -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 selectByUserId(Long userId, PropCouponType couponType, PropCouponStatus status, Integer pageNo, Integer pageSize); + + /** + * 统计用户的道具券数量 + */ + Long countByUserId(Long userId, PropCouponType couponType, PropCouponStatus status); + + /** + * 批量过期券 + */ + Integer expireOverdueCoupons(); + + /** + * 批量保存道具券 + */ + void batchSave(List propCoupons); +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCoupon.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCoupon.java new file mode 100644 index 00000000..9f4b1627 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCoupon.java @@ -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. 更新状态 + } +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponSource.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponSource.java new file mode 100644 index 00000000..356c294e --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponSource.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponStatus.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponStatus.java new file mode 100644 index 00000000..bb94d671 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponStatus.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponType.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponType.java new file mode 100644 index 00000000..1fe827e0 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/propcoupon/PropCouponType.java @@ -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; + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponDAO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponDAO.java new file mode 100644 index 00000000..3630eadc --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponDAO.java @@ -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 selectForUpdate(@Param("couponNo") String couponNo); + + /** + * 批量插入 + */ + int batchInsert(@Param("list") List list); + + /** + * 批量过期券 + */ + int expireOverdueCoupons(); +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponUseRecordDAO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponUseRecordDAO.java new file mode 100644 index 00000000..3411adab --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/props/PropCouponUseRecordDAO.java @@ -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 { + +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponDO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponDO.java new file mode 100644 index 00000000..ac103c54 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponDO.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponUseRecordDO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponUseRecordDO.java new file mode 100644 index 00000000..1a82850c --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/props/PropCouponUseRecordDO.java @@ -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; +} \ No newline at end of file diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/PropCouponGatewayImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/PropCouponGatewayImpl.java new file mode 100644 index 00000000..995ca05a --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/PropCouponGatewayImpl.java @@ -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 wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PropCouponDO::getCouponNo, couponNo); + List propCouponDOS = propCouponDAO.selectList(wrapper); + if (propCouponDOS == null || propCouponDOS.isEmpty()) { + return null; + } + + return convertToDomain(propCouponDOS.get(0)); + } + + @Override + public PropCoupon selectByCouponNo(String couponNo) { + LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>(); + wrapper.eq(PropCouponDO::getCouponNo, couponNo); + + List 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 selectByUserId(Long userId, PropCouponType couponType, PropCouponStatus status, Integer pageNo, Integer pageSize) { + LambdaQueryWrapper 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 page = new Page<>(pageNo, pageSize); + IPage 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 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 propCoupons) { + List 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; + } +} \ No newline at end of file