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 index 748fd911..1965992d 100644 --- 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 @@ -164,6 +164,10 @@ public enum OtherErrorCode implements IResponseErrorCode { */ DEVICE_ALREADY_USED(3274, "Device has been used by another account"), + CANNOT_SEND_TO_SELF(40301, "You can't give yourself red envelopes"), + + NOT_RED_PACKET_RECEIVER(40302, "You are not a red envelope recipient"), + ; private final int code; diff --git a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java index c95291ec..77d6cbfb 100644 --- a/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java +++ b/rc-service/rc-inner-api/wallet-inner/wallet-inner-model/src/main/java/com/red/circle/wallet/inner/model/enums/GoldOrigin.java @@ -732,6 +732,11 @@ public enum GoldOrigin { * 家族宝箱奖励 */ FAMILY_BOX_REWARD("Family box reward"), + + /** + * 房间红包 + */ + USER_RED_PACKET("User red packet"), ; private final String desc; diff --git a/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/UserRedPacketRestController.java b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/UserRedPacketRestController.java new file mode 100644 index 00000000..64dec183 --- /dev/null +++ b/rc-service/rc-service-other/other-adapter/src/main/java/com/red/circle/other/adapter/app/UserRedPacketRestController.java @@ -0,0 +1,46 @@ +package com.red.circle.other.adapter.app; + +import com.red.circle.other.app.dto.clientobject.GrabUserRedPacketResultCO; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.app.dto.cmd.GrabUserRedPacketCmd; +import com.red.circle.other.app.dto.cmd.QueryUserRedPacketDetailCmd; +import com.red.circle.other.app.dto.cmd.SendUserRedPacketCmd; +import com.red.circle.other.app.service.UserRedPacketService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +/** + * 用户红包REST控制器 + */ +@RestController +@RequestMapping("/user/red-packet") +@RequiredArgsConstructor +public class UserRedPacketRestController { + + private final UserRedPacketService userRedPacketService; + + /** + * 发送用户红包 + */ + @PostMapping("/send") + public UserRedPacketCO sendRedPacket(@Valid @RequestBody SendUserRedPacketCmd cmd) { + return userRedPacketService.sendRedPacket(cmd); + } + + /** + * 领取用户红包 + */ + @PostMapping("/grab") + public GrabUserRedPacketResultCO grabRedPacket(@Valid @RequestBody GrabUserRedPacketCmd cmd) { + return userRedPacketService.grabRedPacket(cmd); + } + + /** + * 查询红包详情 + */ + @GetMapping("/detail") + public UserRedPacketCO queryRedPacketDetail(@Valid QueryUserRedPacketDetailCmd cmd) { + return userRedPacketService.queryRedPacketDetail(cmd); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/GrabUserRedPacketCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/GrabUserRedPacketCmdExe.java new file mode 100644 index 00000000..4d90a041 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/GrabUserRedPacketCmdExe.java @@ -0,0 +1,153 @@ +package com.red.circle.other.app.command.redpacket; + +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.other.app.convertor.UserRedPacketAppConvertor; +import com.red.circle.other.app.dto.clientobject.GrabUserRedPacketResultCO; +import com.red.circle.other.app.dto.cmd.GrabUserRedPacketCmd; +import com.red.circle.other.domain.gateway.UserRedPacketGateway; +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.domain.redpacket.UserRedPacketStatus; +import com.red.circle.other.infra.database.cache.service.other.UserRedPacketCacheService; +import com.red.circle.other.inner.asserts.OtherErrorCode; +import com.red.circle.tool.core.tuple.PennyAmount; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.Executor; + +/** + * 领取用户红包命令执行器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class GrabUserRedPacketCmdExe { + + private final UserRedPacketGateway userRedPacketGateway; + private final UserRedPacketCacheService userRedPacketCacheService; + private final WalletGoldClient walletGoldClient; + private final Executor redPacketAsyncExecutor; + + /** + * 执行领取红包 + */ + public GrabUserRedPacketResultCO execute(GrabUserRedPacketCmd cmd) { + Long userId = cmd.getReqUserId(); + String packetId = cmd.getPacketId(); + + boolean grabbed = userRedPacketCacheService.hasUserGrabbed(packetId, userId); + ResponseAssert.isFalse(OtherErrorCode.RED_PACKET_ALREADY_GRABBED, grabbed); + + Map packetData = userRedPacketCacheService.getRedPacket(packetId); + if (packetData == null || packetData.isEmpty()) { + UserRedPacket redPacket = userRedPacketGateway.findByPacketId(packetId); + ResponseAssert.notNull(OtherErrorCode.RED_PACKET_NOT_FOUND, redPacket); + + checkRedPacketStatus(redPacket, userId); + + cacheRedPacketData(redPacket); + packetData = userRedPacketCacheService.getRedPacket(packetId); + } + + String expireTimeStr = String.valueOf(packetData.get("expireTime")); + LocalDateTime expireTime = LocalDateTime.parse(expireTimeStr); + ResponseAssert.isFalse(OtherErrorCode.RED_PACKET_EXPIRED, LocalDateTime.now().isAfter(expireTime)); + + Long receiverUserId = Long.valueOf(String.valueOf(packetData.get("receiverUserId"))); + ResponseAssert.isTrue(OtherErrorCode.NOT_RED_PACKET_RECEIVER, userId.equals(receiverUserId)); + + Integer status = Integer.valueOf(String.valueOf(packetData.get("status"))); + ResponseAssert.isTrue(OtherErrorCode.RED_PACKET_ALREADY_GRABBED, UserRedPacketStatus.PENDING.getCode().equals(status)); + + Long amount = Long.valueOf(String.valueOf(packetData.get("totalAmount"))); + Long senderUserId = Long.valueOf(String.valueOf(packetData.get("senderUserId"))); + + long expireSeconds = Math.max(60, Duration.between(LocalDateTime.now(), expireTime).getSeconds()); + boolean setSuccess = userRedPacketCacheService.setUserGrabbedFlag(packetId, userId, expireSeconds); + ResponseAssert.isTrue(OtherErrorCode.RED_PACKET_ALREADY_GRABBED, setSuccess); + + CompletableFuture.runAsync(() -> { + asyncProcessGrab(packetId, cmd.getReqSysOrigin().getOrigin(), userId, amount); + }, redPacketAsyncExecutor); + + log.info("领取用户红包成功 userId={} packetId={} amount={}", userId, packetId, amount); + + return UserRedPacketAppConvertor.toGrabResultCO(packetId, amount, senderUserId); + } + + /** + * 检查红包状态 + */ + private void checkRedPacketStatus(UserRedPacket redPacket, Long userId) { + ResponseAssert.isFalse(OtherErrorCode.RED_PACKET_ALREADY_GRABBED, + UserRedPacketStatus.GRABBED.getCode().equals(redPacket.getStatus())); + ResponseAssert.isFalse(OtherErrorCode.RED_PACKET_EXPIRED, + UserRedPacketStatus.EXPIRED.getCode().equals(redPacket.getStatus())); + ResponseAssert.isFalse(OtherErrorCode.RED_PACKET_EXPIRED, + LocalDateTime.now().isAfter(redPacket.getExpireTime())); + ResponseAssert.isTrue(OtherErrorCode.NOT_RED_PACKET_RECEIVER, + userId.equals(redPacket.getReceiverUserId())); + } + + /** + * 异步处理领取 + */ + private void asyncProcessGrab(String packetId, String sysOrigin, Long userId, Long amount) { + try { + LocalDateTime grabbedAt = LocalDateTime.now(); + boolean updated = userRedPacketGateway.updateToGrabbed(packetId, grabbedAt); + if (!updated) { + log.warn("更新红包状态失败 packetId={} userId={}", packetId, userId); + } + + addUserGold(userId, sysOrigin, amount, packetId); + } catch (Exception e) { + log.error("异步处理领取失败 userId={} packetId={} amount={}", userId, packetId, amount, e); + } + } + + /** + * 增加用户金币 + */ + private void addUserGold(Long userId, String sysOrigin, Long amount, String packetId) { + GoldReceiptCmd build = GoldReceiptCmd.builder() + .appIncome() + .userId(userId) + .amount(PennyAmount.ofDollar(amount)) + .eventId(packetId + "_grab") + .sysOrigin(sysOrigin) + .origin(GoldOrigin.USER_RED_PACKET) + .build(); + + try { + walletGoldClient.changeBalance(build); + } catch (Exception e) { + log.error("增加金币失败 userId={} amount={} packetId={}", userId, amount, packetId, e); + throw new RuntimeException("Failed to increase gold coins"); + } + } + + /** + * 缓存红包数据 + */ + private void cacheRedPacketData(UserRedPacket redPacket) { + Map packetData = Map.of( + "senderUserId", redPacket.getSenderUserId(), + "receiverUserId", redPacket.getReceiverUserId(), + "totalAmount", redPacket.getTotalAmount(), + "expireTime", redPacket.getExpireTime().toString(), + "status", redPacket.getStatus() + ); + + long expireSeconds = Duration.between(LocalDateTime.now(), redPacket.getExpireTime()).getSeconds(); + userRedPacketCacheService.setRedPacket(redPacket.getPacketId(), packetData, expireSeconds); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/SendUserRedPacketCmdExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/SendUserRedPacketCmdExe.java new file mode 100644 index 00000000..8379e1a7 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/SendUserRedPacketCmdExe.java @@ -0,0 +1,135 @@ +package com.red.circle.other.app.command.redpacket; + +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.other.app.convertor.UserRedPacketAppConvertor; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.app.dto.cmd.SendUserRedPacketCmd; +import com.red.circle.other.domain.gateway.UserRedPacketGateway; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.redpacket.RoomRedPacketType; +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.domain.redpacket.UserRedPacketStatus; +import com.red.circle.other.infra.database.cache.service.other.UserRedPacketCacheService; +import com.red.circle.other.inner.asserts.OtherErrorCode; +import com.red.circle.tool.core.sequence.IdWorkerUtils; +import com.red.circle.tool.core.tuple.PennyAmount; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import com.red.circle.wallet.inner.error.WalletErrorCode; +import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.time.LocalDateTime; +import java.util.HashMap; +import java.util.Map; + +/** + * 发送用户红包命令执行器 + */ +@Slf4j +@Component +@RequiredArgsConstructor +public class SendUserRedPacketCmdExe { + + private final UserRedPacketGateway userRedPacketGateway; + private final UserRedPacketCacheService userRedPacketCacheService; + private final WalletGoldClient walletGoldClient; + + /** + * 执行发用户红包 + */ + @Transactional(rollbackFor = Exception.class) + public UserRedPacketCO execute(SendUserRedPacketCmd cmd) { + Long senderUserId = cmd.getReqUserId(); + Long receiverUserId = cmd.getReceiverUserId(); + + validateParams(cmd); + + ResponseAssert.isFalse(OtherErrorCode.CANNOT_SEND_TO_SELF, senderUserId.equals(receiverUserId)); + + String packetId = IdWorkerUtils.getIdStr(); + + LocalDateTime expireTime = LocalDateTime.now().plusHours(24); + + UserRedPacket redPacket = buildRedPacket(cmd, senderUserId, receiverUserId, packetId, expireTime); + userRedPacketGateway.save(redPacket); + + deductUserGold(senderUserId, cmd, packetId); + + cacheRedPacketData(redPacket); + + log.info("发用户红包成功 senderUserId={} receiverUserId={} packetId={} amount={}", + senderUserId, receiverUserId, packetId, cmd.getTotalAmount()); + + return UserRedPacketAppConvertor.toCO(redPacket); + } + + /** + * 参数校验 + */ + private void validateParams(SendUserRedPacketCmd cmd) { + if (cmd.getTotalAmount() < 100 || cmd.getTotalAmount() > 50000) { + throw new RuntimeException(OtherErrorCode.RED_PACKET_AMOUNT_ERROR.getMessage()); + } + } + + /** + * 扣除用户金币 + */ + private void deductUserGold(Long userId, SendUserRedPacketCmd cmd, String packetId) { + GoldReceiptCmd build = GoldReceiptCmd.builder() + .appExpenditure() + .userId(userId) + .amount(PennyAmount.ofDollar(cmd.getTotalAmount())) + .eventId(packetId) + .sysOrigin(cmd.getReqSysOrigin().getOrigin()) + .origin(GoldOrigin.USER_RED_PACKET) + .build(); + + try { + walletGoldClient.changeBalance(build); + } catch (Exception e) { + log.error("扣除金币失败 userId={} amount={} packetId={}", userId, cmd.getTotalAmount(), packetId, e); + ResponseAssert.isTrue(WalletErrorCode.INSUFFICIENT_BALANCE, false); + } + } + + /** + * 构建红包对象 + */ + private UserRedPacket buildRedPacket(SendUserRedPacketCmd cmd, Long senderUserId, + Long receiverUserId, String packetId, LocalDateTime expireTime) { + return UserRedPacket.builder() + .packetId(packetId) + .senderUserId(senderUserId) + .receiverUserId(receiverUserId) + .packetType(RoomRedPacketType.FIXED.getCode()) + .totalAmount(cmd.getTotalAmount()) + .expireTime(expireTime) + .status(UserRedPacketStatus.PENDING.getCode()) + .refundStatus(0) + .createTime(LocalDateTime.now()) + .updateTime(LocalDateTime.now()) + .build(); + } + + /** + * 缓存红包数据到Redis + */ + private void cacheRedPacketData(UserRedPacket redPacket) { + Map packetData = new HashMap<>(); + packetData.put("senderUserId", redPacket.getSenderUserId()); + packetData.put("receiverUserId", redPacket.getReceiverUserId()); + packetData.put("totalAmount", redPacket.getTotalAmount()); + packetData.put("expireTime", redPacket.getExpireTime().toString()); + packetData.put("status", redPacket.getStatus()); + + long expireSeconds = Duration.between(LocalDateTime.now(), redPacket.getExpireTime()).getSeconds(); + userRedPacketCacheService.setRedPacket(redPacket.getPacketId(), packetData, expireSeconds); + } + +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/query/QueryUserRedPacketDetailQryExe.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/query/QueryUserRedPacketDetailQryExe.java new file mode 100644 index 00000000..4f952bcf --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/command/redpacket/query/QueryUserRedPacketDetailQryExe.java @@ -0,0 +1,50 @@ +package com.red.circle.other.app.command.redpacket.query; + +import com.red.circle.framework.core.asserts.ResponseAssert; +import com.red.circle.other.app.convertor.UserRedPacketAppConvertor; +import com.red.circle.other.app.dto.clientobject.SimpleUserInfoCO; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.app.dto.cmd.QueryUserRedPacketDetailCmd; +import com.red.circle.other.domain.gateway.UserRedPacketGateway; +import com.red.circle.other.domain.gateway.user.UserProfileGateway; +import com.red.circle.other.domain.model.user.UserProfile; +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.inner.asserts.OtherErrorCode; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +/** + * 查询用户红包详情执行器 + */ +@Component +@RequiredArgsConstructor +public class QueryUserRedPacketDetailQryExe { + + private final UserRedPacketGateway userRedPacketGateway; + private final UserProfileGateway userProfileGateway; + + public UserRedPacketCO execute(QueryUserRedPacketDetailCmd cmd) { + UserRedPacket redPacket = userRedPacketGateway.findByPacketId(cmd.getPacketId()); + ResponseAssert.notNull(OtherErrorCode.RED_PACKET_NOT_FOUND, redPacket); + + UserProfile senderProfile = userProfileGateway.getByUserId(redPacket.getSenderUserId()); + UserProfile receiverProfile = userProfileGateway.getByUserId(redPacket.getReceiverUserId()); + + SimpleUserInfoCO senderInfo = buildSimpleUserInfo(senderProfile); + SimpleUserInfoCO receiverInfo = buildSimpleUserInfo(receiverProfile); + + return UserRedPacketAppConvertor.toDetailCO(redPacket, senderInfo, receiverInfo); + } + + private SimpleUserInfoCO buildSimpleUserInfo(UserProfile userProfile) { + if (userProfile == null) { + return null; + } + return SimpleUserInfoCO.builder() + .userId(userProfile.getId()) + .account(userProfile.getAccount()) + .nickName(userProfile.getUserNickname()) + .avatar(userProfile.getUserAvatar()) + .build(); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/UserRedPacketAppConvertor.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/UserRedPacketAppConvertor.java new file mode 100644 index 00000000..1b305057 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/convertor/UserRedPacketAppConvertor.java @@ -0,0 +1,57 @@ +package com.red.circle.other.app.convertor; + +import com.red.circle.other.app.dto.clientobject.GrabUserRedPacketResultCO; +import com.red.circle.other.app.dto.clientobject.SimpleUserInfoCO; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.domain.redpacket.UserRedPacket; + +import java.sql.Time; +import java.sql.Timestamp; + +/** + * 用户红包应用层转换器 + */ +public class UserRedPacketAppConvertor { + + public static UserRedPacketCO toCO(UserRedPacket domain) { + if (domain == null) { + return null; + } + return UserRedPacketCO.builder() + .packetId(domain.getPacketId()) + .senderUserId(domain.getSenderUserId()) + .receiverUserId(domain.getReceiverUserId()) + .totalAmount(domain.getTotalAmount()) + .status(domain.getStatus()) + .expireTime(domain.getExpireTime() != null ? Timestamp.valueOf(domain.getExpireTime()) : null) + .grabbedAt(domain.getGrabbedAt() != null ? Timestamp.valueOf(domain.getGrabbedAt()) : null) + .createTime(domain.getCreateTime() != null ? Timestamp.valueOf(domain.getCreateTime()) : null) + .build(); + } + + public static UserRedPacketCO toDetailCO(UserRedPacket domain, SimpleUserInfoCO senderInfo, SimpleUserInfoCO receiverInfo) { + if (domain == null) { + return null; + } + return UserRedPacketCO.builder() + .packetId(domain.getPacketId()) + .senderUserId(domain.getSenderUserId()) + .receiverUserId(domain.getReceiverUserId()) + .senderInfo(senderInfo) + .receiverInfo(receiverInfo) + .totalAmount(domain.getTotalAmount()) + .status(domain.getStatus()) + .expireTime(domain.getExpireTime() != null ? Timestamp.valueOf(domain.getExpireTime()) : null) + .grabbedAt(domain.getGrabbedAt() != null ? Timestamp.valueOf(domain.getGrabbedAt()) : null) + .createTime(domain.getCreateTime() != null ? Timestamp.valueOf(domain.getCreateTime()) : null) + .build(); + } + + public static GrabUserRedPacketResultCO toGrabResultCO(String packetId, Long amount, Long senderUserId) { + return GrabUserRedPacketResultCO.builder() + .packetId(packetId) + .amount(amount) + .senderUserId(senderUserId) + .build(); + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/UserRedPacketExpireTask.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/UserRedPacketExpireTask.java new file mode 100644 index 00000000..d33aa8a1 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/UserRedPacketExpireTask.java @@ -0,0 +1,124 @@ +package com.red.circle.other.app.scheduler; + +import com.red.circle.component.redis.annotation.TaskCacheLock; +import com.red.circle.other.domain.gateway.UserRedPacketGateway; +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.domain.redpacket.UserRedPacketStatus; +import com.red.circle.other.infra.database.cache.service.other.UserRedPacketCacheService; +import com.red.circle.tool.core.tuple.PennyAmount; +import com.red.circle.wallet.inner.endpoint.wallet.WalletGoldClient; +import com.red.circle.wallet.inner.model.cmd.GoldReceiptCmd; +import com.red.circle.wallet.inner.model.enums.GoldOrigin; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户红包过期退款定时任务 + */ +@Slf4j +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(name = "scheduler.user-red-packet-expire", havingValue = "true", matchIfMissing = true) +public class UserRedPacketExpireTask { + + private final UserRedPacketGateway userRedPacketGateway; + private final UserRedPacketCacheService userRedPacketCacheService; + private final WalletGoldClient walletGoldClient; + + /** + * 每30秒执行一次 + */ + @Scheduled(cron = "0/30 * * * * ?") + @TaskCacheLock(key = "USER_RED_PACKET_TASK", expireSecond = 20) + public void refundExpiredPackets() { + try { + LocalDateTime now = LocalDateTime.now(); + + List expiredPackets = userRedPacketGateway.findExpiredAndNotRefunded(now); + + if (expiredPackets.isEmpty()) { + return; + } + + log.info("开始处理过期用户红包退款 数量={}", expiredPackets.size()); + + int successCount = 0; + int failCount = 0; + + for (UserRedPacket packet : expiredPackets) { + try { + refundSinglePacket(packet); + successCount++; + } catch (Exception e) { + log.error("用户红包退款失败 packetId={}", packet.getPacketId(), e); + failCount++; + } + } + + log.info("过期用户红包退款定时任务执行完成 总数={} 成功={} 失败={}", + expiredPackets.size(), successCount, failCount); + + } catch (Exception e) { + log.error("过期用户红包退款定时任务执行失败", e); + } + } + + /** + * 退款单个红包 + */ + @Transactional(rollbackFor = Exception.class) + public void refundSinglePacket(UserRedPacket packet) { + String packetId = packet.getPacketId(); + + if (packet.getRefundStatus() != 0) { + log.warn("用户红包已退款 packetId={} refundStatus={}", + packetId, packet.getRefundStatus()); + return; + } + + refundToUser(packet); + + userRedPacketGateway.updateStatus(packetId, UserRedPacketStatus.EXPIRED); + userRedPacketGateway.updateRefundStatus(packetId, 1); + + userRedPacketCacheService.deleteRedPacket(packetId); + + log.info("用户红包过期处理完成 packetId={} senderUserId={} amount={}", + packetId, packet.getSenderUserId(), packet.getTotalAmount()); + } + + /** + * 退款给用户 + */ + private void refundToUser(UserRedPacket packet) { + Long amount = packet.getTotalAmount(); + Long userId = packet.getSenderUserId(); + String packetId = packet.getPacketId(); + + GoldReceiptCmd build = GoldReceiptCmd.builder() + .appIncome() + .userId(userId) + .amount(PennyAmount.ofDollar(amount)) + .eventId(packetId + "_refund") + .sysOrigin("LIKEI") + .origin(GoldOrigin.USER_RED_PACKET) + .build(); + + try { + walletGoldClient.changeBalance(build); + log.info("用户红包退款成功 packetId={} userId={} amount={}", + packetId, userId, amount); + } catch (Exception e) { + log.error("用户红包退款失败 packetId={} userId={} amount={}", + packetId, userId, amount, e); + throw new RuntimeException("用户红包退款失败", e); + } + } +} diff --git a/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/UserRedPacketServiceImpl.java b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/UserRedPacketServiceImpl.java new file mode 100644 index 00000000..8efa2887 --- /dev/null +++ b/rc-service/rc-service-other/other-application/src/main/java/com/red/circle/other/app/service/UserRedPacketServiceImpl.java @@ -0,0 +1,43 @@ +package com.red.circle.other.app.service; + +import com.red.circle.other.app.command.redpacket.GrabUserRedPacketCmdExe; +import com.red.circle.other.app.command.redpacket.SendUserRedPacketCmdExe; +import com.red.circle.other.app.command.redpacket.query.QueryUserRedPacketDetailQryExe; +import com.red.circle.other.app.dto.clientobject.GrabUserRedPacketResultCO; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.app.dto.cmd.GrabUserRedPacketCmd; +import com.red.circle.other.app.dto.cmd.QueryUserRedPacketDetailCmd; +import com.red.circle.other.app.dto.cmd.SendUserRedPacketCmd; +import com.red.circle.other.app.util.DistributedLockUtil; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +/** + * 用户红包服务实现 + */ +@Service +@RequiredArgsConstructor +public class UserRedPacketServiceImpl implements UserRedPacketService { + + private final SendUserRedPacketCmdExe sendUserRedPacketCmdExe; + private final GrabUserRedPacketCmdExe grabUserRedPacketCmdExe; + private final QueryUserRedPacketDetailQryExe queryUserRedPacketDetailQryExe; + private final DistributedLockUtil distributedLockUtil; + + @Override + public UserRedPacketCO sendRedPacket(SendUserRedPacketCmd cmd) { + String key = "user:red_packet:send:" + cmd.getReqUserId(); + return distributedLockUtil.executeWithLock(key, () -> sendUserRedPacketCmdExe.execute(cmd)); + } + + @Override + public GrabUserRedPacketResultCO grabRedPacket(GrabUserRedPacketCmd cmd) { + String key = "user:red_packet:grab:" + cmd.getPacketId() + ":" + cmd.getReqUserId(); + return distributedLockUtil.executeWithLock(key, () -> grabUserRedPacketCmdExe.execute(cmd)); + } + + @Override + public UserRedPacketCO queryRedPacketDetail(QueryUserRedPacketDetailCmd cmd) { + return queryUserRedPacketDetailQryExe.execute(cmd); + } +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/GrabUserRedPacketResultCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/GrabUserRedPacketResultCO.java new file mode 100644 index 00000000..36338a96 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/GrabUserRedPacketResultCO.java @@ -0,0 +1,31 @@ +package com.red.circle.other.app.dto.clientobject; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 领取用户红包结果CO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class GrabUserRedPacketResultCO { + + /** + * 红包ID + */ + private String packetId; + + /** + * 领取到的金额(金币) + */ + private Long amount; + + /** + * 发送者用户ID + */ + private Long senderUserId; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/SimpleUserInfoCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/SimpleUserInfoCO.java new file mode 100644 index 00000000..b5d359ef --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/SimpleUserInfoCO.java @@ -0,0 +1,36 @@ +package com.red.circle.other.app.dto.clientobject; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * 用户简要信息CO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class SimpleUserInfoCO { + + /** + * 用户ID + */ + private Long userId; + + /** + * 账号 + */ + private String account; + + /** + * 昵称 + */ + private String nickName; + + /** + * 头像 + */ + private String avatar; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/UserRedPacketCO.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/UserRedPacketCO.java new file mode 100644 index 00000000..c3509540 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/clientobject/UserRedPacketCO.java @@ -0,0 +1,69 @@ +package com.red.circle.other.app.dto.clientobject; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.sql.Timestamp; +import java.time.LocalDateTime; + +/** + * 用户红包CO + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserRedPacketCO { + + /** + * 红包ID + */ + private String packetId; + + /** + * 发送者用户ID + */ + private Long senderUserId; + + /** + * 接收者用户ID + */ + private Long receiverUserId; + + /** + * 发送者信息 + */ + private SimpleUserInfoCO senderInfo; + + /** + * 接收者信息 + */ + private SimpleUserInfoCO receiverInfo; + + /** + * 总金额(金币) + */ + private Long totalAmount; + + /** + * 状态:1-待领取 2-已领取 3-已过期 + */ + private Integer status; + + /** + * 过期时间 + */ + private Timestamp expireTime; + + /** + * 领取时间 + */ + private Timestamp grabbedAt; + + /** + * 创建时间 + */ + private Timestamp createTime; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/GrabUserRedPacketCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/GrabUserRedPacketCmd.java new file mode 100644 index 00000000..750d304f --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/GrabUserRedPacketCmd.java @@ -0,0 +1,20 @@ +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; + +/** + * 领取用户红包命令 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class GrabUserRedPacketCmd extends AppExtCommand { + + /** + * 红包ID + */ + @NotBlank(message = "红包ID不能为空") + private String packetId; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/QueryUserRedPacketDetailCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/QueryUserRedPacketDetailCmd.java new file mode 100644 index 00000000..a3afc71f --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/QueryUserRedPacketDetailCmd.java @@ -0,0 +1,20 @@ +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; + +/** + * 查询用户红包详情命令 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class QueryUserRedPacketDetailCmd extends AppExtCommand { + + /** + * 红包ID + */ + @NotBlank(message = "红包ID不能为空") + private String packetId; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/SendUserRedPacketCmd.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/SendUserRedPacketCmd.java new file mode 100644 index 00000000..4711d4fa --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/dto/cmd/SendUserRedPacketCmd.java @@ -0,0 +1,31 @@ +package com.red.circle.other.app.dto.cmd; + +import com.red.circle.common.business.dto.cmd.AppExtCommand; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.EqualsAndHashCode; + +/** + * 发送用户红包命令 + */ +@Data +@EqualsAndHashCode(callSuper = true) +public class SendUserRedPacketCmd extends AppExtCommand { + + /** + * 接收者用户ID + */ + @NotNull(message = "接收者用户ID不能为空") + private Long receiverUserId; + + /** + * 总金额(金币) + */ + @NotNull(message = "红包金额不能为空") + private Long totalAmount; + + /** + * 红包类型:1-固定金额 + */ + private Integer packetType = 1; +} diff --git a/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/UserRedPacketService.java b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/UserRedPacketService.java new file mode 100644 index 00000000..8cee6b39 --- /dev/null +++ b/rc-service/rc-service-other/other-client/src/main/java/com/red/circle/other/app/service/UserRedPacketService.java @@ -0,0 +1,28 @@ +package com.red.circle.other.app.service; + +import com.red.circle.other.app.dto.clientobject.GrabUserRedPacketResultCO; +import com.red.circle.other.app.dto.clientobject.UserRedPacketCO; +import com.red.circle.other.app.dto.cmd.GrabUserRedPacketCmd; +import com.red.circle.other.app.dto.cmd.QueryUserRedPacketDetailCmd; +import com.red.circle.other.app.dto.cmd.SendUserRedPacketCmd; + +/** + * 用户红包服务 + */ +public interface UserRedPacketService { + + /** + * 发送用户红包 + */ + UserRedPacketCO sendRedPacket(SendUserRedPacketCmd cmd); + + /** + * 领取用户红包 + */ + GrabUserRedPacketResultCO grabRedPacket(GrabUserRedPacketCmd cmd); + + /** + * 查询红包详情 + */ + UserRedPacketCO queryRedPacketDetail(QueryUserRedPacketDetailCmd cmd); +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/UserRedPacketGateway.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/UserRedPacketGateway.java new file mode 100644 index 00000000..9116592d --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/gateway/UserRedPacketGateway.java @@ -0,0 +1,53 @@ +package com.red.circle.other.domain.gateway; + +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.domain.redpacket.UserRedPacketStatus; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户红包Gateway + */ +public interface UserRedPacketGateway { + + /** + * 保存红包 + */ + void save(UserRedPacket userRedPacket); + + /** + * 根据红包ID查询 + */ + UserRedPacket findByPacketId(String packetId); + + /** + * 更新红包状态 + */ + boolean updateStatus(String packetId, UserRedPacketStatus status); + + /** + * 更新退款状态 + */ + boolean updateRefundStatus(String packetId, Integer refundStatus); + + /** + * 更新为已领取 + */ + boolean updateToGrabbed(String packetId, LocalDateTime grabbedAt); + + /** + * 查询过期且未退款的红包 + */ + List findExpiredAndNotRefunded(LocalDateTime now); + + /** + * 查询用户发送的红包列表 + */ + List findBySenderUserId(Long senderUserId, Integer limit); + + /** + * 查询用户接收的红包列表 + */ + List findByReceiverUserId(Long receiverUserId, Integer limit); +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacket.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacket.java new file mode 100644 index 00000000..ece7f335 --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacket.java @@ -0,0 +1,78 @@ +package com.red.circle.other.domain.redpacket; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +import java.time.LocalDateTime; + +/** + * 用户红包领域对象 + */ +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class UserRedPacket { + + /** + * 主键ID + */ + private Long id; + + /** + * 红包ID(UUID) + */ + private String packetId; + + /** + * 发送者用户ID + */ + private Long senderUserId; + + /** + * 接收者用户ID + */ + private Long receiverUserId; + + /** + * 红包类型:1-固定金额 2-拼手气 + */ + private Integer packetType; + + /** + * 总金额(分) + */ + private Long totalAmount; + + /** + * 过期时间 + */ + private LocalDateTime expireTime; + + /** + * 状态:1-待领取 2-已领取 3-已过期 + */ + private Integer status; + + /** + * 退款状态:0-未退 1-已退 + */ + private Integer refundStatus; + + /** + * 领取时间 + */ + private LocalDateTime grabbedAt; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacketStatus.java b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacketStatus.java new file mode 100644 index 00000000..acd2f4ba --- /dev/null +++ b/rc-service/rc-service-other/other-domain/src/main/java/com/red/circle/other/domain/redpacket/UserRedPacketStatus.java @@ -0,0 +1,39 @@ +package com.red.circle.other.domain.redpacket; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +/** + * 用户红包状态 + */ +@Getter +@AllArgsConstructor +public enum UserRedPacketStatus { + + /** + * 待领取 + */ + PENDING(1, "Pending"), + + /** + * 已领取 + */ + GRABBED(2, "Grabbed"), + + /** + * 已过期 + */ + EXPIRED(3, "Expired"); + + private final Integer code; + private final String desc; + + public static UserRedPacketStatus of(Integer code) { + for (UserRedPacketStatus status : values()) { + if (status.getCode().equals(code)) { + return status; + } + } + return null; + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/convertor/UserRedPacketConvertor.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/convertor/UserRedPacketConvertor.java new file mode 100644 index 00000000..ac0e0ad9 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/convertor/UserRedPacketConvertor.java @@ -0,0 +1,50 @@ +package com.red.circle.other.infra.convertor; + +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.infra.database.rds.entity.UserRedPacketEntity; + +/** + * 用户红包转换器 + */ +public class UserRedPacketConvertor { + + public static UserRedPacket toDomain(UserRedPacketEntity entity) { + if (entity == null) { + return null; + } + return UserRedPacket.builder() + .id(entity.getId()) + .packetId(entity.getPacketId()) + .senderUserId(entity.getSenderUserId()) + .receiverUserId(entity.getReceiverUserId()) + .packetType(entity.getPacketType()) + .totalAmount(entity.getTotalAmount()) + .expireTime(entity.getExpireTime()) + .status(entity.getStatus()) + .refundStatus(entity.getRefundStatus()) + .grabbedAt(entity.getGrabbedAt()) + .createTime(entity.getCreateTime()) + .updateTime(entity.getUpdateTime()) + .build(); + } + + public static UserRedPacketEntity toEntity(UserRedPacket domain) { + if (domain == null) { + return null; + } + UserRedPacketEntity entity = new UserRedPacketEntity(); + entity.setId(domain.getId()); + entity.setPacketId(domain.getPacketId()); + entity.setSenderUserId(domain.getSenderUserId()); + entity.setReceiverUserId(domain.getReceiverUserId()); + entity.setPacketType(domain.getPacketType()); + entity.setTotalAmount(domain.getTotalAmount()); + entity.setExpireTime(domain.getExpireTime()); + entity.setStatus(domain.getStatus()); + entity.setRefundStatus(domain.getRefundStatus()); + entity.setGrabbedAt(domain.getGrabbedAt()); + entity.setCreateTime(domain.getCreateTime()); + entity.setUpdateTime(domain.getUpdateTime()); + return entity; + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/UserRedPacketCacheService.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/UserRedPacketCacheService.java new file mode 100644 index 00000000..63bee0d0 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/UserRedPacketCacheService.java @@ -0,0 +1,39 @@ +package com.red.circle.other.infra.database.cache.service.other; + +import java.util.Map; + +/** + * 用户红包缓存服务 + */ +public interface UserRedPacketCacheService { + + /** + * 设置红包数据 + */ + void setRedPacket(String packetId, Map data, long expireSeconds); + + /** + * 获取红包数据 + */ + Map getRedPacket(String packetId); + + /** + * 检查用户是否已领取 + */ + boolean hasUserGrabbed(String packetId, Long userId); + + /** + * 设置用户已领取标记 + */ + boolean setUserGrabbedFlag(String packetId, Long userId, long expireSeconds); + + /** + * 删除红包缓存 + */ + void deleteRedPacket(String packetId); + + /** + * 删除用户领取标记 + */ + void deleteUserGrabbedFlag(String packetId, Long userId); +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/impl/UserRedPacketCacheServiceImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/impl/UserRedPacketCacheServiceImpl.java new file mode 100644 index 00000000..5ab7ebf2 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/cache/service/other/impl/UserRedPacketCacheServiceImpl.java @@ -0,0 +1,59 @@ +package com.red.circle.other.infra.database.cache.service.other.impl; + +import com.red.circle.other.infra.database.cache.service.other.UserRedPacketCacheService; +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import java.util.Map; +import java.util.concurrent.TimeUnit; + +/** + * 用户红包缓存服务实现 + */ +@Service +@RequiredArgsConstructor +public class UserRedPacketCacheServiceImpl implements UserRedPacketCacheService { + + private final RedisTemplate redisTemplate; + + private static final String PACKET_KEY_PREFIX = "user:red_packet:"; + private static final String GRABBED_KEY_PREFIX = "user:red_packet:grabbed:"; + + @Override + public void setRedPacket(String packetId, Map data, long expireSeconds) { + String key = PACKET_KEY_PREFIX + packetId; + redisTemplate.opsForHash().putAll(key, data); + redisTemplate.expire(key, expireSeconds, TimeUnit.SECONDS); + } + + @Override + public Map getRedPacket(String packetId) { + String key = PACKET_KEY_PREFIX + packetId; + return redisTemplate.opsForHash().entries(key); + } + + @Override + public boolean hasUserGrabbed(String packetId, Long userId) { + String key = GRABBED_KEY_PREFIX + packetId + ":" + userId; + return Boolean.TRUE.equals(redisTemplate.hasKey(key)); + } + + @Override + public boolean setUserGrabbedFlag(String packetId, Long userId, long expireSeconds) { + String key = GRABBED_KEY_PREFIX + packetId + ":" + userId; + return Boolean.TRUE.equals(redisTemplate.opsForValue().setIfAbsent(key, "1", expireSeconds, TimeUnit.SECONDS)); + } + + @Override + public void deleteRedPacket(String packetId) { + String key = PACKET_KEY_PREFIX + packetId; + redisTemplate.delete(key); + } + + @Override + public void deleteUserGrabbedFlag(String packetId, Long userId) { + String key = GRABBED_KEY_PREFIX + packetId + ":" + userId; + redisTemplate.delete(key); + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/redpacket/UserRedPacketDAO.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/redpacket/UserRedPacketDAO.java new file mode 100644 index 00000000..96acad0b --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/dao/redpacket/UserRedPacketDAO.java @@ -0,0 +1,53 @@ +package com.red.circle.other.infra.database.rds.dao.redpacket; + +import com.red.circle.framework.mybatis.dao.BaseDAO; +import com.red.circle.other.infra.database.rds.entity.UserRedPacketEntity; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Param; + +import java.time.LocalDateTime; +import java.util.List; + +/** + * 用户红包DAO + */ +@Mapper +public interface UserRedPacketDAO extends BaseDAO { + + /** + * 根据红包ID查询 + */ + UserRedPacketEntity selectByPacketId(@Param("packetId") String packetId); + + /** + * 更新红包状态 + */ + int updateStatus(@Param("packetId") String packetId, @Param("status") Integer status); + + /** + * 更新退款状态 + */ + int updateRefundStatus(@Param("packetId") String packetId, @Param("refundStatus") Integer refundStatus); + + /** + * 更新为已领取 + */ + int updateToGrabbed(@Param("packetId") String packetId, @Param("grabbedAt") LocalDateTime grabbedAt); + + /** + * 查询过期且未退款的红包 + */ + List selectExpiredAndNotRefunded(@Param("now") LocalDateTime now); + + /** + * 查询用户发送的红包列表 + */ + List selectBySenderUserId(@Param("senderUserId") Long senderUserId, + @Param("limit") Integer limit); + + /** + * 查询用户接收的红包列表 + */ + List selectByReceiverUserId(@Param("receiverUserId") Long receiverUserId, + @Param("limit") Integer limit); +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserRedPacketEntity.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserRedPacketEntity.java new file mode 100644 index 00000000..609e52f6 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/database/rds/entity/UserRedPacketEntity.java @@ -0,0 +1,77 @@ +package com.red.circle.other.infra.database.rds.entity; + +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; + +/** + * 用户红包实体 + */ +@Data +@TableName("user_red_packet") +public class UserRedPacketEntity { + + /** + * 主键ID + */ + @TableId(type = IdType.AUTO) + private Long id; + + /** + * 红包ID(UUID) + */ + private String packetId; + + /** + * 发送者用户ID + */ + private Long senderUserId; + + /** + * 接收者用户ID + */ + private Long receiverUserId; + + /** + * 红包类型:1-固定金额 2-拼手气 + */ + private Integer packetType; + + /** + * 总金额(分) + */ + private Long totalAmount; + + /** + * 过期时间 + */ + private LocalDateTime expireTime; + + /** + * 状态:1-待领取 2-已领取 3-已过期 + */ + private Integer status; + + /** + * 退款状态:0-未退 1-已退 + */ + private Integer refundStatus; + + /** + * 领取时间 + */ + private LocalDateTime grabbedAt; + + /** + * 创建时间 + */ + private LocalDateTime createTime; + + /** + * 更新时间 + */ + private LocalDateTime updateTime; +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/UserRedPacketGatewayImpl.java b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/UserRedPacketGatewayImpl.java new file mode 100644 index 00000000..1dec8067 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/java/com/red/circle/other/infra/gateway/UserRedPacketGatewayImpl.java @@ -0,0 +1,75 @@ +package com.red.circle.other.infra.gateway; + +import com.red.circle.other.domain.gateway.UserRedPacketGateway; +import com.red.circle.other.domain.redpacket.UserRedPacket; +import com.red.circle.other.domain.redpacket.UserRedPacketStatus; +import com.red.circle.other.infra.convertor.UserRedPacketConvertor; +import com.red.circle.other.infra.database.rds.dao.redpacket.UserRedPacketDAO; +import com.red.circle.other.infra.database.rds.entity.UserRedPacketEntity; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 用户红包Gateway实现 + */ +@Component +@RequiredArgsConstructor +public class UserRedPacketGatewayImpl implements UserRedPacketGateway { + + private final UserRedPacketDAO userRedPacketDAO; + + @Override + public void save(UserRedPacket userRedPacket) { + UserRedPacketEntity entity = UserRedPacketConvertor.toEntity(userRedPacket); + userRedPacketDAO.insert(entity); + } + + @Override + public UserRedPacket findByPacketId(String packetId) { + UserRedPacketEntity entity = userRedPacketDAO.selectByPacketId(packetId); + return UserRedPacketConvertor.toDomain(entity); + } + + @Override + public boolean updateStatus(String packetId, UserRedPacketStatus status) { + return userRedPacketDAO.updateStatus(packetId, status.getCode()) > 0; + } + + @Override + public boolean updateRefundStatus(String packetId, Integer refundStatus) { + return userRedPacketDAO.updateRefundStatus(packetId, refundStatus) > 0; + } + + @Override + public boolean updateToGrabbed(String packetId, LocalDateTime grabbedAt) { + return userRedPacketDAO.updateToGrabbed(packetId, grabbedAt) > 0; + } + + @Override + public List findExpiredAndNotRefunded(LocalDateTime now) { + List entities = userRedPacketDAO.selectExpiredAndNotRefunded(now); + return entities.stream() + .map(UserRedPacketConvertor::toDomain) + .collect(Collectors.toList()); + } + + @Override + public List findBySenderUserId(Long senderUserId, Integer limit) { + List entities = userRedPacketDAO.selectBySenderUserId(senderUserId, limit); + return entities.stream() + .map(UserRedPacketConvertor::toDomain) + .collect(Collectors.toList()); + } + + @Override + public List findByReceiverUserId(Long receiverUserId, Integer limit) { + List entities = userRedPacketDAO.selectByReceiverUserId(receiverUserId, limit); + return entities.stream() + .map(UserRedPacketConvertor::toDomain) + .collect(Collectors.toList()); + } +} diff --git a/rc-service/rc-service-other/other-infrastructure/src/main/resources/dao/redpacket/UserRedPacketDAO.xml b/rc-service/rc-service-other/other-infrastructure/src/main/resources/dao/redpacket/UserRedPacketDAO.xml new file mode 100644 index 00000000..e8825248 --- /dev/null +++ b/rc-service/rc-service-other/other-infrastructure/src/main/resources/dao/redpacket/UserRedPacketDAO.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + UPDATE user_red_packet + SET status = #{status}, update_time = NOW() + WHERE packet_id = #{packetId} + + + + UPDATE user_red_packet + SET refund_status = #{refundStatus}, update_time = NOW() + WHERE packet_id = #{packetId} + + + + UPDATE user_red_packet + SET status = 2, grabbed_at = #{grabbedAt}, update_time = NOW() + WHERE packet_id = #{packetId} AND status = 1 + + + + + + + + +