|
@@ -0,0 +1,344 @@
|
|
|
+package com.webchat.ugc.service.redpacket;
|
|
|
+
|
|
|
+import com.webchat.common.constants.RedPacketConstants;
|
|
|
+import com.webchat.common.enums.ChatMessageTypeEnum;
|
|
|
+import com.webchat.common.enums.RedisKeyEnum;
|
|
|
+import com.webchat.common.enums.messagequeue.MessageBroadChannelEnum;
|
|
|
+import com.webchat.common.enums.payment.PaymentTransEventEnum;
|
|
|
+import com.webchat.common.enums.payment.PaymentTransTypeEnum;
|
|
|
+import com.webchat.common.exception.BusinessException;
|
|
|
+import com.webchat.common.service.RedisService;
|
|
|
+import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
|
|
|
+import com.webchat.common.util.DateUtils;
|
|
|
+import com.webchat.common.util.JsonUtil;
|
|
|
+import com.webchat.domain.dto.messagecard.MessageCardSendDTO;
|
|
|
+import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
|
|
|
+import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
|
|
|
+import com.webchat.domain.vo.request.SendRedPacketRequestVO;
|
|
|
+import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
|
|
|
+import com.webchat.domain.vo.request.mess.MessageBaseVO;
|
|
|
+import com.webchat.domain.vo.response.RedPacketBaseVO;
|
|
|
+import com.webchat.domain.vo.response.RedPacketDetailVO;
|
|
|
+import com.webchat.ugc.config.MessageCardAccountConfig;
|
|
|
+import com.webchat.ugc.config.MessageCardTemplateConfig;
|
|
|
+import com.webchat.ugc.repository.dao.IRedPacketDAO;
|
|
|
+import com.webchat.ugc.repository.dao.IRedPacketRecordDAO;
|
|
|
+import com.webchat.ugc.repository.entity.RedPacketEntity;
|
|
|
+import com.webchat.ugc.service.PaymentService;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.redisson.api.RLock;
|
|
|
+import org.redisson.api.RedissonClient;
|
|
|
+import org.springframework.beans.BeanUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.cloud.context.config.annotation.RefreshScope;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import org.springframework.transaction.annotation.Transactional;
|
|
|
+import org.springframework.util.Assert;
|
|
|
+
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+@RefreshScope
|
|
|
+@Service
|
|
|
+public class RedPacketService {
|
|
|
+
|
|
|
+ @Autowired
|
|
|
+ private IRedPacketDAO redPacketDAO;
|
|
|
+ @Autowired
|
|
|
+ private IRedPacketRecordDAO redPacketRecordDAO;
|
|
|
+ @Autowired
|
|
|
+ private RedisService redisService;
|
|
|
+ @Autowired
|
|
|
+ private PaymentService paymentService;
|
|
|
+ @Autowired
|
|
|
+ private MessageQueueProducer<Object, Long> messageQueueProducer;
|
|
|
+ @Autowired
|
|
|
+ private MessageCardAccountConfig messageCardAccountConfig;
|
|
|
+ @Autowired
|
|
|
+ private MessageCardTemplateConfig messageCardTemplateConfig;
|
|
|
+ @Autowired
|
|
|
+ private RedissonClient redissonClient;
|
|
|
+
|
|
|
+ @Value("${red-packet.open-with}")
|
|
|
+ private String openWith;
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ * 发送包
|
|
|
+ *
|
|
|
+ * @param sendRedPacketRequest
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public Long send(SendRedPacketRequestVO sendRedPacketRequest) {
|
|
|
+
|
|
|
+ sendRedPacketRequest.setExpireDate(this.getRedPacketExpireDate());
|
|
|
+ String sendUserId = sendRedPacketRequest.getSender();
|
|
|
+ BigDecimal redPacketAmount = sendRedPacketRequest.getTotalMoney();
|
|
|
+
|
|
|
+ * 1. 校验发包人账户余额
|
|
|
+ */
|
|
|
+ paymentService.validateAccountBalance(sendUserId, redPacketAmount);
|
|
|
+
|
|
|
+ * 2. 申请分布式交易事务id, 申请创建一条交易订单
|
|
|
+ */
|
|
|
+ String transId = paymentService.transId();
|
|
|
+
|
|
|
+ PaymentOrderCreateDTO paymentOrderCreateDTO = this.buildPaymentOrderCreateDTO(sendRedPacketRequest);
|
|
|
+ String orderId = paymentService.createOrder(transId, paymentOrderCreateDTO);
|
|
|
+
|
|
|
+ * 3. 扣除发包人账户余额(实际交易)
|
|
|
+ */
|
|
|
+
|
|
|
+ PaymentTransRequestDTO paymentTransRequest = this.buildPaymentTransRequestDTO(orderId, sendRedPacketRequest);
|
|
|
+ boolean transResult = paymentService.doTrans(paymentTransRequest);
|
|
|
+ if (!transResult) {
|
|
|
+
|
|
|
+ paymentService.doRollBack(transId);
|
|
|
+ throw new BusinessException("发包发送失败,稍后重试!");
|
|
|
+ }
|
|
|
+ try {
|
|
|
+
|
|
|
+ * 4. 持久化红包发送记录
|
|
|
+ */
|
|
|
+ RedPacketEntity redPacketEntity = this.buildRedPacketEntity(orderId, sendRedPacketRequest);
|
|
|
+ redPacketEntity = redPacketDAO.save(redPacketEntity);
|
|
|
+
|
|
|
+ * 5. 刷新红包详情redis缓存
|
|
|
+ * 后续红包的信息要在IM客户端展示,为了保证后续请求性能,我们现将红包详情信息做一层redis缓存
|
|
|
+ */
|
|
|
+ this.refreshRedPacketDetailCache(redPacketEntity);
|
|
|
+ this.initOpenRedPacketContextInfo(redPacketEntity);
|
|
|
+
|
|
|
+ * 6. MQ广播通知websocket推送红包消息给接收人
|
|
|
+ */
|
|
|
+ MessageBaseVO<RedPacketBaseVO> chatMessage = this.buildMessageVOForSendRedPacket(redPacketEntity);
|
|
|
+ messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_RED_PACKET_NOTIFY, chatMessage);
|
|
|
+
|
|
|
+ * 7. 走“WebChat支付这个服务号推送用户消息认证”
|
|
|
+ */
|
|
|
+ this.doPushRedPacketSendPaymentOrderMessage(sendRedPacketRequest);
|
|
|
+ return redPacketEntity.getId();
|
|
|
+ } catch (Exception e) {
|
|
|
+
|
|
|
+
|
|
|
+ paymentService.doRollBack(transId);
|
|
|
+
|
|
|
+
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private void initOpenRedPacketContextInfo(RedPacketEntity redPacketEntity) {
|
|
|
+
|
|
|
+ Long redPacketId = redPacketEntity.getId();
|
|
|
+ int gradCount = redPacketEntity.getCount();
|
|
|
+
|
|
|
+ BigDecimal totalMoneyYun = redPacketEntity.getTotalMoney();
|
|
|
+
|
|
|
+ int totalMoneyFen = totalMoneyYun.multiply(new BigDecimal(100)).intValue();
|
|
|
+ if (RedPacketConstants.OpenRedPacketWithEnum.LOCK.name().equals(openWith) ||
|
|
|
+ RedPacketConstants.OpenRedPacketWithEnum.LUA.name().equals(openWith)) {
|
|
|
+
|
|
|
+ OpenRedPacketWithRedissonLockService openRedPacketService =
|
|
|
+ (OpenRedPacketWithRedissonLockService) OpenRedPacketWithFactory.getService(
|
|
|
+ RedPacketConstants.OpenRedPacketWithEnum.LOCK.name());
|
|
|
+ openRedPacketService.initBalanceCache(redPacketId, totalMoneyFen);
|
|
|
+ openRedPacketService.initGradCountCache(redPacketId, gradCount);
|
|
|
+ } else if (RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name().equals(openWith)) {
|
|
|
+
|
|
|
+ OpenRedPacketWithQueueService openRedPacketService =
|
|
|
+ (OpenRedPacketWithQueueService) OpenRedPacketWithFactory.getService(
|
|
|
+ RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name());
|
|
|
+ openRedPacketService.preGeneratedRedPackets(redPacketId, totalMoneyFen, gradCount);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 更新红包的状态
|
|
|
+ *
|
|
|
+ * @param redPacketId
|
|
|
+ * @param redPacketStatus
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ @Transactional
|
|
|
+ public boolean updateRedPacketStatus(Long redPacketId, RedPacketConstants.RedPacketStatus redPacketStatus) {
|
|
|
+
|
|
|
+ RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
|
|
|
+ Assert.notNull(entity, "红包状态更新失败:红包不存在!");
|
|
|
+ entity.setStatus(redPacketStatus.getStatus());
|
|
|
+ try {
|
|
|
+ redPacketDAO.save(entity);
|
|
|
+ this.refreshRedPacketDetailCache(entity);
|
|
|
+ } catch (Exception e) {
|
|
|
+ throw new BusinessException("红包状态更新失败:redis缓存数据刷新异常!");
|
|
|
+ }
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 红包拆分
|
|
|
+ *
|
|
|
+ * @param redPacketId
|
|
|
+ * @param userId
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ public String open(Long redPacketId, String userId) {
|
|
|
+ int amount = OpenRedPacketWithFactory.getService(openWith)
|
|
|
+ .open(redPacketId, userId);
|
|
|
+ return new BigDecimal(amount).movePointLeft(2).setScale(2, RoundingMode.HALF_UP).toString();
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 红包发送成功,走webchtaPay服务号推送交易凭证消息卡片
|
|
|
+ *------------------
|
|
|
+ * @param requestVO
|
|
|
+ */
|
|
|
+ private void doPushRedPacketSendPaymentOrderMessage(SendRedPacketRequestVO requestVO) {
|
|
|
+
|
|
|
+ MessageCardSendDTO messageCardSendDTO = new MessageCardSendDTO();
|
|
|
+ messageCardSendDTO.setSender(messageCardAccountConfig.getPayment());
|
|
|
+ messageCardSendDTO.setTemplateId(messageCardTemplateConfig.getRedPacketSend());
|
|
|
+ messageCardSendDTO.setReceiver(requestVO.getSender());
|
|
|
+ messageCardSendDTO.setTitle("扣费凭证");
|
|
|
+ messageCardSendDTO.setLogo("https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_c3a791ab87c5419b980bc27d9791bab6.png");
|
|
|
+ messageCardSendDTO.setRedirectName("查看交易明细");
|
|
|
+ messageCardSendDTO.setRedirectUrl("https://www.coderutil.com");
|
|
|
+ Map<String, Object> vars = new HashMap<>();
|
|
|
+ vars.put("amountTitle", "扣费金额");
|
|
|
+ vars.put("amount", requestVO.getTotalMoney().toString());
|
|
|
+ vars.put("tip", "红包发送,账户变更");
|
|
|
+ vars.put("k1", "扣费服务");
|
|
|
+ vars.put("k2", "支付方式");
|
|
|
+ vars.put("v1", "红包服务");
|
|
|
+ vars.put("v2", "WebChatPay线上支付");
|
|
|
+ messageCardSendDTO.setVars(vars);
|
|
|
+
|
|
|
+ * MQ广播通知connect服务,完成服务号消息卡内容websocket主动推送
|
|
|
+ */
|
|
|
+ messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_MESSAGE_CARD_PUSH, messageCardSendDTO);
|
|
|
+ }
|
|
|
+
|
|
|
+ private MessageBaseVO<RedPacketBaseVO> buildMessageVOForSendRedPacket(RedPacketEntity redPacketEntity) {
|
|
|
+ MessageBaseVO<RedPacketBaseVO> chatMessage = new ChatMessageRequestVO();
|
|
|
+ RedPacketBaseVO redPacketBaseVO = new RedPacketBaseVO();
|
|
|
+ chatMessage.setSenderId(redPacketEntity.getSender());
|
|
|
+ chatMessage.setReceiverId(redPacketEntity.getReceiver());
|
|
|
+ chatMessage.setType(ChatMessageTypeEnum.RED_PACKET.getType());
|
|
|
+ chatMessage.setMessageExt(redPacketBaseVO);
|
|
|
+ redPacketBaseVO.setId(redPacketEntity.getId());
|
|
|
+ redPacketBaseVO.setCover(redPacketEntity.getCover());
|
|
|
+ redPacketBaseVO.setBlessing(redPacketEntity.getBlessing());
|
|
|
+ return chatMessage;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ private RedPacketDetailVO refreshRedPacketDetailCache(Long redPacketId) {
|
|
|
+ RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
|
|
|
+ return this.refreshRedPacketDetailCache(entity);
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 刷新红包缓存
|
|
|
+ *
|
|
|
+ * @param redPacketEntity
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private RedPacketDetailVO refreshRedPacketDetailCache(RedPacketEntity redPacketEntity) {
|
|
|
+ if (redPacketEntity == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ RedPacketDetailVO redPacketDetailVO = new RedPacketDetailVO();
|
|
|
+ BeanUtils.copyProperties(redPacketEntity, redPacketDetailVO);
|
|
|
+ redPacketDetailVO.setSendTime(redPacketEntity.getCreateDate().getTime());
|
|
|
+ redPacketDetailVO.setExpireTime(redPacketEntity.getExpireDate().getTime());
|
|
|
+
|
|
|
+ String cacheKey = this.redPacketCacheKey(redPacketEntity.getId());
|
|
|
+ redisService.set(cacheKey, JsonUtil.toJsonString(redPacketDetailVO),
|
|
|
+ RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getExpireTime());
|
|
|
+ return redPacketDetailVO;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ public RedPacketBaseVO getRedPacket(Long redPacketId) {
|
|
|
+
|
|
|
+ String cacheKey = this.redPacketCacheKey(redPacketId);
|
|
|
+ String cache = redisService.get(cacheKey);
|
|
|
+ if (StringUtils.isNotBlank(cache)) {
|
|
|
+ return JsonUtil.fromJson(cache, RedPacketBaseVO.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ * 虽然红包缓存设置的2 * 24小时,且红包24小时内有效,但是不能完全依赖redis
|
|
|
+ * 如果缓存失效,需要主动刷新
|
|
|
+ */
|
|
|
+ String refreshRedPacketCacheKey = RedisKeyEnum.LOCK_REFRESH_RED_PACKET_CACHE.getKey(String.valueOf(redPacketId));
|
|
|
+ RLock lock = redissonClient.getLock(refreshRedPacketCacheKey);
|
|
|
+ try {
|
|
|
+ lock.lock();
|
|
|
+ cache = redisService.get(cacheKey);
|
|
|
+ if (StringUtils.isNotBlank(cache)) {
|
|
|
+ return JsonUtil.fromJson(cache, RedPacketBaseVO.class);
|
|
|
+ }
|
|
|
+ return this.refreshRedPacketDetailCache(redPacketId);
|
|
|
+ } catch (Exception e) {
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ if (lock.isLocked() && lock.isHeldByCurrentThread()) {
|
|
|
+ lock.unlock();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String redPacketCacheKey(Long id) {
|
|
|
+
|
|
|
+ return RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getKey(String.valueOf(id));
|
|
|
+ }
|
|
|
+
|
|
|
+ private RedPacketEntity buildRedPacketEntity(String orderId, SendRedPacketRequestVO sendRedPacketRequestVO) {
|
|
|
+
|
|
|
+ RedPacketEntity entity = new RedPacketEntity();
|
|
|
+ BeanUtils.copyProperties(sendRedPacketRequestVO, entity);
|
|
|
+ entity.setOrderId(orderId);
|
|
|
+ entity.setStatus(RedPacketConstants.RedPacketStatus.RUNNING.getStatus());
|
|
|
+ return entity;
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ * 构造创建交易订单业务参数
|
|
|
+ *
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+ private PaymentOrderCreateDTO buildPaymentOrderCreateDTO(SendRedPacketRequestVO sendRedPacketRequest) {
|
|
|
+ PaymentOrderCreateDTO dto = new PaymentOrderCreateDTO();
|
|
|
+ dto.setAmount(sendRedPacketRequest.getTotalMoney());
|
|
|
+ dto.setSourceAccount(sendRedPacketRequest.getSender());
|
|
|
+ dto.setTargetAccount(sendRedPacketRequest.getReceiver());
|
|
|
+ dto.setEventType(PaymentTransEventEnum.RED_PACKET.getTransEvent());
|
|
|
+ dto.setExpireDate(sendRedPacketRequest.getExpireDate());
|
|
|
+ dto.setBillType(PaymentTransTypeEnum.EXPENSES.getTransType());
|
|
|
+ dto.setDescription("红包发送订单");
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Date getRedPacketExpireDate() {
|
|
|
+ return DateUtils.addDays(new Date(), 1);
|
|
|
+ }
|
|
|
+
|
|
|
+ private PaymentTransRequestDTO buildPaymentTransRequestDTO(String orderId, SendRedPacketRequestVO sendRedPacketRequest) {
|
|
|
+ PaymentTransRequestDTO dto = new PaymentTransRequestDTO();
|
|
|
+ dto.setOrderId(orderId);
|
|
|
+ dto.setAmount(sendRedPacketRequest.getTotalMoney());
|
|
|
+ dto.setTransType(PaymentTransTypeEnum.EXPENSES.getTransType());
|
|
|
+ dto.setTransDetail("红包发送");
|
|
|
+ dto.setTransEvent(PaymentTransEventEnum.RED_PACKET.getTransEvent());
|
|
|
+ dto.setSourceUserId(sendRedPacketRequest.getSender());
|
|
|
+ dto.setTargetUserId(sendRedPacketRequest.getReceiver());
|
|
|
+ return dto;
|
|
|
+ }
|
|
|
+}
|