新增用户红包功能
This commit is contained in:
parent
a77872c681
commit
d2a3c8e6e1
@ -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;
|
||||
|
||||
@ -732,6 +732,11 @@ public enum GoldOrigin {
|
||||
* 家族宝箱奖励
|
||||
*/
|
||||
FAMILY_BOX_REWARD("Family box reward"),
|
||||
|
||||
/**
|
||||
* 房间红包
|
||||
*/
|
||||
USER_RED_PACKET("User red packet"),
|
||||
;
|
||||
|
||||
private final String desc;
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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<Object, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> 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);
|
||||
}
|
||||
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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<UserRedPacket> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
@ -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<UserRedPacket> findExpiredAndNotRefunded(LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询用户发送的红包列表
|
||||
*/
|
||||
List<UserRedPacket> findBySenderUserId(Long senderUserId, Integer limit);
|
||||
|
||||
/**
|
||||
* 查询用户接收的红包列表
|
||||
*/
|
||||
List<UserRedPacket> findByReceiverUserId(Long receiverUserId, Integer limit);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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<String, Object> data, long expireSeconds);
|
||||
|
||||
/**
|
||||
* 获取红包数据
|
||||
*/
|
||||
Map<Object, Object> 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);
|
||||
}
|
||||
@ -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<String, Object> 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<String, Object> data, long expireSeconds) {
|
||||
String key = PACKET_KEY_PREFIX + packetId;
|
||||
redisTemplate.opsForHash().putAll(key, data);
|
||||
redisTemplate.expire(key, expireSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<Object, Object> 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);
|
||||
}
|
||||
}
|
||||
@ -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<UserRedPacketEntity> {
|
||||
|
||||
/**
|
||||
* 根据红包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<UserRedPacketEntity> selectExpiredAndNotRefunded(@Param("now") LocalDateTime now);
|
||||
|
||||
/**
|
||||
* 查询用户发送的红包列表
|
||||
*/
|
||||
List<UserRedPacketEntity> selectBySenderUserId(@Param("senderUserId") Long senderUserId,
|
||||
@Param("limit") Integer limit);
|
||||
|
||||
/**
|
||||
* 查询用户接收的红包列表
|
||||
*/
|
||||
List<UserRedPacketEntity> selectByReceiverUserId(@Param("receiverUserId") Long receiverUserId,
|
||||
@Param("limit") Integer limit);
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
@ -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<UserRedPacket> findExpiredAndNotRefunded(LocalDateTime now) {
|
||||
List<UserRedPacketEntity> entities = userRedPacketDAO.selectExpiredAndNotRefunded(now);
|
||||
return entities.stream()
|
||||
.map(UserRedPacketConvertor::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserRedPacket> findBySenderUserId(Long senderUserId, Integer limit) {
|
||||
List<UserRedPacketEntity> entities = userRedPacketDAO.selectBySenderUserId(senderUserId, limit);
|
||||
return entities.stream()
|
||||
.map(UserRedPacketConvertor::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<UserRedPacket> findByReceiverUserId(Long receiverUserId, Integer limit) {
|
||||
List<UserRedPacketEntity> entities = userRedPacketDAO.selectByReceiverUserId(receiverUserId, limit);
|
||||
return entities.stream()
|
||||
.map(UserRedPacketConvertor::toDomain)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,65 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
<mapper namespace="com.red.circle.other.infra.database.rds.dao.redpacket.UserRedPacketDAO">
|
||||
|
||||
<resultMap id="BaseResultMap" type="com.red.circle.other.infra.database.rds.entity.UserRedPacketEntity">
|
||||
<id column="id" property="id"/>
|
||||
<result column="packet_id" property="packetId"/>
|
||||
<result column="sender_user_id" property="senderUserId"/>
|
||||
<result column="receiver_user_id" property="receiverUserId"/>
|
||||
<result column="packet_type" property="packetType"/>
|
||||
<result column="total_amount" property="totalAmount"/>
|
||||
<result column="expire_time" property="expireTime"/>
|
||||
<result column="status" property="status"/>
|
||||
<result column="refund_status" property="refundStatus"/>
|
||||
<result column="grabbed_at" property="grabbedAt"/>
|
||||
<result column="create_time" property="createTime"/>
|
||||
<result column="update_time" property="updateTime"/>
|
||||
</resultMap>
|
||||
|
||||
<select id="selectByPacketId" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_red_packet
|
||||
WHERE packet_id = #{packetId}
|
||||
</select>
|
||||
|
||||
<update id="updateStatus">
|
||||
UPDATE user_red_packet
|
||||
SET status = #{status}, update_time = NOW()
|
||||
WHERE packet_id = #{packetId}
|
||||
</update>
|
||||
|
||||
<update id="updateRefundStatus">
|
||||
UPDATE user_red_packet
|
||||
SET refund_status = #{refundStatus}, update_time = NOW()
|
||||
WHERE packet_id = #{packetId}
|
||||
</update>
|
||||
|
||||
<update id="updateToGrabbed">
|
||||
UPDATE user_red_packet
|
||||
SET status = 2, grabbed_at = #{grabbedAt}, update_time = NOW()
|
||||
WHERE packet_id = #{packetId} AND status = 1
|
||||
</update>
|
||||
|
||||
<select id="selectExpiredAndNotRefunded" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_red_packet
|
||||
WHERE expire_time < #{now}
|
||||
AND status = 1
|
||||
AND refund_status = 0
|
||||
LIMIT 100
|
||||
</select>
|
||||
|
||||
<select id="selectBySenderUserId" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_red_packet
|
||||
WHERE sender_user_id = #{senderUserId}
|
||||
ORDER BY create_time DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
<select id="selectByReceiverUserId" resultMap="BaseResultMap">
|
||||
SELECT * FROM user_red_packet
|
||||
WHERE receiver_user_id = #{receiverUserId}
|
||||
ORDER BY create_time DESC
|
||||
LIMIT #{limit}
|
||||
</select>
|
||||
|
||||
</mapper>
|
||||
Loading…
x
Reference in New Issue
Block a user