RedPacketService.java 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344
  1. package com.webchat.ugc.service.redpacket;
  2. import com.webchat.common.constants.RedPacketConstants;
  3. import com.webchat.common.enums.ChatMessageTypeEnum;
  4. import com.webchat.common.enums.RedisKeyEnum;
  5. import com.webchat.common.enums.messagequeue.MessageBroadChannelEnum;
  6. import com.webchat.common.enums.payment.PaymentTransEventEnum;
  7. import com.webchat.common.enums.payment.PaymentTransTypeEnum;
  8. import com.webchat.common.exception.BusinessException;
  9. import com.webchat.common.service.RedisService;
  10. import com.webchat.common.service.messagequeue.producer.MessageQueueProducer;
  11. import com.webchat.common.util.DateUtils;
  12. import com.webchat.common.util.JsonUtil;
  13. import com.webchat.domain.dto.messagecard.MessageCardSendDTO;
  14. import com.webchat.domain.dto.payment.PaymentOrderCreateDTO;
  15. import com.webchat.domain.dto.payment.PaymentTransRequestDTO;
  16. import com.webchat.domain.vo.request.SendRedPacketRequestVO;
  17. import com.webchat.domain.vo.request.mess.ChatMessageRequestVO;
  18. import com.webchat.domain.vo.request.mess.MessageBaseVO;
  19. import com.webchat.domain.vo.response.RedPacketBaseVO;
  20. import com.webchat.domain.vo.response.RedPacketDetailVO;
  21. import com.webchat.ugc.config.MessageCardAccountConfig;
  22. import com.webchat.ugc.config.MessageCardTemplateConfig;
  23. import com.webchat.ugc.repository.dao.IRedPacketDAO;
  24. import com.webchat.ugc.repository.dao.IRedPacketRecordDAO;
  25. import com.webchat.ugc.repository.entity.RedPacketEntity;
  26. import com.webchat.ugc.service.PaymentService;
  27. import org.apache.commons.lang3.StringUtils;
  28. import org.redisson.api.RLock;
  29. import org.redisson.api.RedissonClient;
  30. import org.springframework.beans.BeanUtils;
  31. import org.springframework.beans.factory.annotation.Autowired;
  32. import org.springframework.beans.factory.annotation.Value;
  33. import org.springframework.cloud.context.config.annotation.RefreshScope;
  34. import org.springframework.stereotype.Service;
  35. import org.springframework.transaction.annotation.Transactional;
  36. import org.springframework.util.Assert;
  37. import java.math.BigDecimal;
  38. import java.math.RoundingMode;
  39. import java.util.Date;
  40. import java.util.HashMap;
  41. import java.util.Map;
  42. @RefreshScope
  43. @Service
  44. public class RedPacketService {
  45. @Autowired
  46. private IRedPacketDAO redPacketDAO;
  47. @Autowired
  48. private IRedPacketRecordDAO redPacketRecordDAO;
  49. @Autowired
  50. private RedisService redisService;
  51. @Autowired
  52. private PaymentService paymentService;
  53. @Autowired
  54. private MessageQueueProducer<Object, Long> messageQueueProducer;
  55. @Autowired
  56. private MessageCardAccountConfig messageCardAccountConfig;
  57. @Autowired
  58. private MessageCardTemplateConfig messageCardTemplateConfig;
  59. @Autowired
  60. private RedissonClient redissonClient;
  61. @Value("${red-packet.open-with}")
  62. private String openWith;
  63. /**
  64. * 发送包
  65. *
  66. * @param sendRedPacketRequest
  67. * @return
  68. */
  69. @Transactional
  70. public Long send(SendRedPacketRequestVO sendRedPacketRequest) {
  71. sendRedPacketRequest.setExpireDate(this.getRedPacketExpireDate());
  72. String sendUserId = sendRedPacketRequest.getSender();
  73. BigDecimal redPacketAmount = sendRedPacketRequest.getTotalMoney();
  74. /**
  75. * 1. 校验发包人账户余额
  76. */
  77. paymentService.validateAccountBalance(sendUserId, redPacketAmount);
  78. /**
  79. * 2. 申请分布式交易事务id, 申请创建一条交易订单
  80. */
  81. String transId = paymentService.transId();
  82. // 构造支付平台创建订单所需业务参数
  83. PaymentOrderCreateDTO paymentOrderCreateDTO = this.buildPaymentOrderCreateDTO(sendRedPacketRequest);
  84. String orderId = paymentService.createOrder(transId, paymentOrderCreateDTO);
  85. /**
  86. * 3. 扣除发包人账户余额(实际交易)
  87. */
  88. // 构造实际交易请求参数
  89. PaymentTransRequestDTO paymentTransRequest = this.buildPaymentTransRequestDTO(orderId, sendRedPacketRequest);
  90. boolean transResult = paymentService.doTrans(paymentTransRequest);
  91. if (!transResult) {
  92. // 回滚交易订单
  93. paymentService.doRollBack(transId);
  94. throw new BusinessException("发包发送失败,稍后重试!");
  95. }
  96. try {
  97. /**
  98. * 4. 持久化红包发送记录
  99. */
  100. RedPacketEntity redPacketEntity = this.buildRedPacketEntity(orderId, sendRedPacketRequest);
  101. redPacketEntity = redPacketDAO.save(redPacketEntity);
  102. /**
  103. * 5. 刷新红包详情redis缓存
  104. * 后续红包的信息要在IM客户端展示,为了保证后续请求性能,我们现将红包详情信息做一层redis缓存
  105. */
  106. this.refreshRedPacketDetailCache(redPacketEntity);
  107. this.initOpenRedPacketContextInfo(redPacketEntity);
  108. /**
  109. * 6. MQ广播通知websocket推送红包消息给接收人
  110. */
  111. MessageBaseVO<RedPacketBaseVO> chatMessage = this.buildMessageVOForSendRedPacket(redPacketEntity);
  112. messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_RED_PACKET_NOTIFY, chatMessage);
  113. /**
  114. * 7. 走“WebChat支付这个服务号推送用户消息认证”
  115. */
  116. this.doPushRedPacketSendPaymentOrderMessage(sendRedPacketRequest);
  117. return redPacketEntity.getId();
  118. } catch (Exception e) {
  119. // 交易成功,订单创建成功,但是业务侧处理失败,我们需要回滚用户交易订单
  120. // 回滚交易订单
  121. paymentService.doRollBack(transId);
  122. // 回滚redis数据
  123. // TODO
  124. }
  125. return null;
  126. }
  127. private void initOpenRedPacketContextInfo(RedPacketEntity redPacketEntity) {
  128. Long redPacketId = redPacketEntity.getId();
  129. int gradCount = redPacketEntity.getCount();
  130. // 设置红包金额(元)
  131. BigDecimal totalMoneyYun = redPacketEntity.getTotalMoney();
  132. // 设置红包金额(分)
  133. int totalMoneyFen = totalMoneyYun.multiply(new BigDecimal(100)).intValue();
  134. if (RedPacketConstants.OpenRedPacketWithEnum.LOCK.name().equals(openWith) ||
  135. RedPacketConstants.OpenRedPacketWithEnum.LUA.name().equals(openWith)) {
  136. // 初始化红包金额
  137. OpenRedPacketWithRedissonLockService openRedPacketService =
  138. (OpenRedPacketWithRedissonLockService) OpenRedPacketWithFactory.getService(
  139. RedPacketConstants.OpenRedPacketWithEnum.LOCK.name());
  140. openRedPacketService.initBalanceCache(redPacketId, totalMoneyFen);
  141. openRedPacketService.initGradCountCache(redPacketId, gradCount);
  142. } else if (RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name().equals(openWith)) {
  143. // 预先生成好红包份额对应的金额
  144. OpenRedPacketWithQueueService openRedPacketService =
  145. (OpenRedPacketWithQueueService) OpenRedPacketWithFactory.getService(
  146. RedPacketConstants.OpenRedPacketWithEnum.QUEUE.name());
  147. openRedPacketService.preGeneratedRedPackets(redPacketId, totalMoneyFen, gradCount);
  148. }
  149. }
  150. /**
  151. * 更新红包的状态
  152. *
  153. * @param redPacketId
  154. * @param redPacketStatus
  155. * @return
  156. */
  157. @Transactional
  158. public boolean updateRedPacketStatus(Long redPacketId, RedPacketConstants.RedPacketStatus redPacketStatus) {
  159. RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
  160. Assert.notNull(entity, "红包状态更新失败:红包不存在!");
  161. entity.setStatus(redPacketStatus.getStatus());
  162. try {
  163. redPacketDAO.save(entity);
  164. this.refreshRedPacketDetailCache(entity);
  165. } catch (Exception e) {
  166. throw new BusinessException("红包状态更新失败:redis缓存数据刷新异常!");
  167. }
  168. return true;
  169. }
  170. /**
  171. * 红包拆分
  172. *
  173. * @param redPacketId
  174. * @param userId
  175. * @return
  176. */
  177. public String open(Long redPacketId, String userId) {
  178. int amount = OpenRedPacketWithFactory.getService(openWith)
  179. .open(redPacketId, userId);
  180. return new BigDecimal(amount).movePointLeft(2).setScale(2, RoundingMode.HALF_UP).toString();
  181. }
  182. /**
  183. * 红包发送成功,走webchtaPay服务号推送交易凭证消息卡片
  184. *------------------
  185. * @param requestVO
  186. */
  187. private void doPushRedPacketSendPaymentOrderMessage(SendRedPacketRequestVO requestVO) {
  188. MessageCardSendDTO messageCardSendDTO = new MessageCardSendDTO();
  189. messageCardSendDTO.setSender(messageCardAccountConfig.getPayment());
  190. messageCardSendDTO.setTemplateId(messageCardTemplateConfig.getRedPacketSend());
  191. messageCardSendDTO.setReceiver(requestVO.getSender());
  192. messageCardSendDTO.setTitle("扣费凭证");
  193. messageCardSendDTO.setLogo("https://coderutil.oss-cn-beijing.aliyuncs.com/bbs-image/file_c3a791ab87c5419b980bc27d9791bab6.png");
  194. messageCardSendDTO.setRedirectName("查看交易明细");
  195. messageCardSendDTO.setRedirectUrl("https://www.coderutil.com");
  196. Map<String, Object> vars = new HashMap<>();
  197. vars.put("amountTitle", "扣费金额");
  198. vars.put("amount", requestVO.getTotalMoney().toString());
  199. vars.put("tip", "红包发送,账户变更");
  200. vars.put("k1", "扣费服务");
  201. vars.put("k2", "支付方式");
  202. vars.put("v1", "红包服务");
  203. vars.put("v2", "WebChatPay线上支付");
  204. messageCardSendDTO.setVars(vars);
  205. /**
  206. * MQ广播通知connect服务,完成服务号消息卡内容websocket主动推送
  207. */
  208. messageQueueProducer.broadSend(MessageBroadChannelEnum.QUEUE_MESSAGE_CARD_PUSH, messageCardSendDTO);
  209. }
  210. private MessageBaseVO<RedPacketBaseVO> buildMessageVOForSendRedPacket(RedPacketEntity redPacketEntity) {
  211. MessageBaseVO<RedPacketBaseVO> chatMessage = new ChatMessageRequestVO();
  212. RedPacketBaseVO redPacketBaseVO = new RedPacketBaseVO();
  213. chatMessage.setSenderId(redPacketEntity.getSender());
  214. chatMessage.setReceiverId(redPacketEntity.getReceiver());
  215. chatMessage.setType(ChatMessageTypeEnum.RED_PACKET.getType());
  216. chatMessage.setMessageExt(redPacketBaseVO);
  217. redPacketBaseVO.setId(redPacketEntity.getId());
  218. redPacketBaseVO.setCover(redPacketEntity.getCover());
  219. redPacketBaseVO.setBlessing(redPacketEntity.getBlessing());
  220. return chatMessage;
  221. }
  222. private RedPacketDetailVO refreshRedPacketDetailCache(Long redPacketId) {
  223. RedPacketEntity entity = redPacketDAO.findById(redPacketId).orElse(null);
  224. return this.refreshRedPacketDetailCache(entity);
  225. }
  226. /**
  227. * 刷新红包缓存
  228. *
  229. * @param redPacketEntity
  230. * @return
  231. */
  232. private RedPacketDetailVO refreshRedPacketDetailCache(RedPacketEntity redPacketEntity) {
  233. if (redPacketEntity == null) {
  234. return null;
  235. }
  236. RedPacketDetailVO redPacketDetailVO = new RedPacketDetailVO();
  237. BeanUtils.copyProperties(redPacketEntity, redPacketDetailVO);
  238. redPacketDetailVO.setSendTime(redPacketEntity.getCreateDate().getTime());
  239. redPacketDetailVO.setExpireTime(redPacketEntity.getExpireDate().getTime());
  240. // 缓存红包详情
  241. String cacheKey = this.redPacketCacheKey(redPacketEntity.getId());
  242. redisService.set(cacheKey, JsonUtil.toJsonString(redPacketDetailVO),
  243. RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getExpireTime());
  244. return redPacketDetailVO;
  245. }
  246. public RedPacketDetailVO getRedPacket(Long redPacketId) {
  247. String cacheKey = this.redPacketCacheKey(redPacketId);
  248. String cache = redisService.get(cacheKey);
  249. if (StringUtils.isNotBlank(cache)) {
  250. return JsonUtil.fromJson(cache, RedPacketDetailVO.class);
  251. }
  252. /**
  253. * 虽然红包缓存设置的2 * 24小时,且红包24小时内有效,但是不能完全依赖redis
  254. * 如果缓存失效,需要主动刷新
  255. */
  256. String refreshRedPacketCacheKey = RedisKeyEnum.LOCK_REFRESH_RED_PACKET_CACHE.getKey(String.valueOf(redPacketId));
  257. RLock lock = redissonClient.getLock(refreshRedPacketCacheKey);
  258. try {
  259. lock.lock();
  260. cache = redisService.get(cacheKey);
  261. if (StringUtils.isNotBlank(cache)) {
  262. return JsonUtil.fromJson(cache, RedPacketDetailVO.class);
  263. }
  264. return this.refreshRedPacketDetailCache(redPacketId);
  265. } catch (Exception e) {
  266. // TODO
  267. } finally {
  268. if (lock.isLocked() && lock.isHeldByCurrentThread()) {
  269. lock.unlock();
  270. }
  271. }
  272. return null;
  273. }
  274. private String redPacketCacheKey(Long id) {
  275. return RedisKeyEnum.RED_PACKET_DETAIL_CACHE.getKey(String.valueOf(id));
  276. }
  277. private RedPacketEntity buildRedPacketEntity(String orderId, SendRedPacketRequestVO sendRedPacketRequestVO) {
  278. RedPacketEntity entity = new RedPacketEntity();
  279. BeanUtils.copyProperties(sendRedPacketRequestVO, entity);
  280. entity.setOrderId(orderId);
  281. entity.setStatus(RedPacketConstants.RedPacketStatus.RUNNING.getStatus());
  282. return entity;
  283. }
  284. /**
  285. * 构造创建交易订单业务参数
  286. *
  287. * @return
  288. */
  289. private PaymentOrderCreateDTO buildPaymentOrderCreateDTO(SendRedPacketRequestVO sendRedPacketRequest) {
  290. PaymentOrderCreateDTO dto = new PaymentOrderCreateDTO();
  291. dto.setAmount(sendRedPacketRequest.getTotalMoney());
  292. dto.setSourceAccount(sendRedPacketRequest.getSender());
  293. dto.setTargetAccount(sendRedPacketRequest.getReceiver());
  294. dto.setEventType(PaymentTransEventEnum.RED_PACKET.getTransEvent());
  295. dto.setExpireDate(sendRedPacketRequest.getExpireDate());
  296. dto.setBillType(PaymentTransTypeEnum.EXPENSES.getTransType());
  297. dto.setDescription("红包发送订单");
  298. return dto;
  299. }
  300. private Date getRedPacketExpireDate() {
  301. return DateUtils.addDays(new Date(), 1);
  302. }
  303. private PaymentTransRequestDTO buildPaymentTransRequestDTO(String orderId, SendRedPacketRequestVO sendRedPacketRequest) {
  304. PaymentTransRequestDTO dto = new PaymentTransRequestDTO();
  305. dto.setOrderId(orderId);
  306. dto.setAmount(sendRedPacketRequest.getTotalMoney());
  307. dto.setTransType(PaymentTransTypeEnum.EXPENSES.getTransType());
  308. dto.setTransDetail("红包发送");
  309. dto.setTransEvent(PaymentTransEventEnum.RED_PACKET.getTransEvent());
  310. dto.setSourceUserId(sendRedPacketRequest.getSender());
  311. dto.setTargetUserId(sendRedPacketRequest.getReceiver());
  312. return dto;
  313. }
  314. }