diff --git a/rc-common-business/common-business-mq/business-mq-model/src/main/java/com/red/circle/mq/business/model/event/pay/RechargeSuccessEvent.java b/rc-common-business/common-business-mq/business-mq-model/src/main/java/com/red/circle/mq/business/model/event/pay/RechargeSuccessEvent.java new file mode 100644 index 00000000..c81e22b2 --- /dev/null +++ b/rc-common-business/common-business-mq/business-mq-model/src/main/java/com/red/circle/mq/business/model/event/pay/RechargeSuccessEvent.java @@ -0,0 +1,36 @@ +package com.red.circle.mq.business.model.event.pay; + +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import java.io.Serializable; +import java.util.Map; +import lombok.Data; +import lombok.experimental.Accessors; + +/** + * 通用充值成功事件。 + */ +@Data +@Accessors(chain = true) +public class RechargeSuccessEvent implements Serializable { + + private static final long serialVersionUID = 1L; + + private String eventId; + private String sysOrigin; + + @JsonSerialize(using = ToStringSerializer.class) + private Long userId; + + @JsonSerialize(using = ToStringSerializer.class) + private Long amountCents; + + private String amountUsd; + private String currency; + private String payPlatform; + private String paymentMethod; + private String sourceOrderId; + private String orderId; + private String occurredAt; + private Map payload; +} diff --git a/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/constant/QueueNameConstant.java b/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/constant/QueueNameConstant.java index 76127de6..67e10d33 100644 --- a/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/constant/QueueNameConstant.java +++ b/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/constant/QueueNameConstant.java @@ -17,13 +17,18 @@ public interface QueueNameConstant { */ String LOGIN_REGISTER = "LOGIN_REGISTER"; - /** - * 购买成功 - */ - String BUY_SUCCESS = "BUY_SUCCESS"; - - /** - * 视频语音房间会话 + /** + * 购买成功 + */ + String BUY_SUCCESS = "BUY_SUCCESS"; + + /** + * 通用充值成功。 + */ + String RECHARGE_SUCCESS = "RECHARGE_SUCCESS"; + + /** + * 视频语音房间会话 */ String LIVE_VOICE_SESSION = "LIVE_VOICE_SESSION"; diff --git a/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/streams/RechargeSuccessSink.java b/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/streams/RechargeSuccessSink.java new file mode 100644 index 00000000..41cb9129 --- /dev/null +++ b/rc-common-business/common-business-mq/business-rocketmq/src/main/java/com/red/circle/mq/rocket/business/streams/RechargeSuccessSink.java @@ -0,0 +1,12 @@ +package com.red.circle.mq.rocket.business.streams; + +import com.red.circle.mq.rocket.business.constant.QueueNameConstant; + +/** + * 通用充值成功事件。 + */ +public interface RechargeSuccessSink { + + String INPUT = QueueNameConstant.RECHARGE_SUCCESS; + String TAG = "recharge_success_v1"; +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/InAppPurchaseCommon.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/InAppPurchaseCommon.java index aacbead0..8fdc2edb 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/InAppPurchaseCommon.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/InAppPurchaseCommon.java @@ -8,8 +8,9 @@ import com.red.circle.framework.core.asserts.ResponseAssert; import com.red.circle.framework.core.response.CommonErrorCode; import com.red.circle.framework.dto.ResultResponse; import com.red.circle.framework.web.props.EnvProperties; -import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; -import com.red.circle.order.app.command.pay.web.strategy.PayEnv; +import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; +import com.red.circle.order.app.common.task.RechargeSuccessEventPublisher; +import com.red.circle.order.app.command.pay.web.strategy.PayEnv; import com.red.circle.order.domain.gateway.UserRechargeCountGateway; import com.red.circle.order.infra.database.mongo.order.InAppPurchaseCollectionReceiptService; import com.red.circle.order.infra.database.mongo.order.InAppPurchaseDetailsService; @@ -76,9 +77,10 @@ public class InAppPurchaseCommon { private final OrderPurchaseHistoryService orderPurchaseHistoryService; private final InAppPurchaseCollectionReceiptService inAppPurchaseCollectionReceiptService; private final UserLevelClient userLevelClient; - private final PropsActivityClient propsActivityClient; + private final PropsActivityClient propsActivityClient; private final UserOneTimeTaskClient userOneTimeTaskClient; private final UserProfileClient userProfileClient; + private final RechargeSuccessEventPublisher rechargeSuccessEventPublisher; public void inAppPurchaseReceipt(InAppPurchaseDetails order) { inAppPurchaseReceipt(order, @@ -192,13 +194,15 @@ public class InAppPurchaseCommon { userSvipClient.incrIntegral(order.getAcceptUserId(), amount.longValue()); - // 被邀请人充值邀请人获得佣金 - inviteUserClient.processRechargeCommission(amount, - order.getSysOrigin(), order.getAcceptUserId()); - - // 发送首充奖励 - completedFirstCharge(order.getAcceptUserId(), order.getSysOrigin()); - } + // 被邀请人充值邀请人获得佣金 + inviteUserClient.processRechargeCommission(amount, + order.getSysOrigin(), order.getAcceptUserId()); + + rechargeSuccessEventPublisher.reportPaymentRecharge(order); + + // 发送首充奖励 + completedFirstCharge(order.getAcceptUserId(), order.getSysOrigin()); + } private void completedFirstCharge(Long userId, String sysOrigin) { UserProfileDTO userProfileDTO = userProfileClient.getByUserId(userId).getBody(); diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/task/RechargeSuccessEventPublisher.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/task/RechargeSuccessEventPublisher.java new file mode 100644 index 00000000..264b8758 --- /dev/null +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/common/task/RechargeSuccessEventPublisher.java @@ -0,0 +1,214 @@ +package com.red.circle.order.app.common.task; + +import com.red.circle.component.mq.MessageEvent; +import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; +import com.red.circle.mq.business.model.event.pay.RechargeSuccessEvent; +import com.red.circle.mq.rocket.business.service.MessageSenderService; +import com.red.circle.mq.rocket.business.streams.RechargeSuccessSink; +import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetails; +import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseProduct; +import com.red.circle.order.inner.model.enums.PayApplicationCommodityType; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.sql.Timestamp; +import java.time.Instant; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Slf4j +@Component +public class RechargeSuccessEventPublisher { + + private static final String GOOGLE = "GOOGLE"; + private static final String APPLE = "APPLE"; + private static final String THIRD_PARTY = "THIRD_PARTY"; + private static final String UNKNOWN = "UNKNOWN"; + + private final ObjectProvider messageSenderServiceProvider; + + @Value("${recharge.success.mq.enabled:true}") + private boolean mqEnabled; + + @Value("${recharge.success.mq.topic:RC_DEFAULT_APP_ORDINARY}") + private String mqTopic; + + @Value("${recharge.success.mq.tag:" + RechargeSuccessSink.TAG + "}") + private String mqTag; + + public RechargeSuccessEventPublisher(ObjectProvider messageSenderServiceProvider) { + this.messageSenderServiceProvider = messageSenderServiceProvider; + } + + public void reportAppPurchaseRecharge(BuySuccessEvent event) { + if (Objects.isNull(event)) { + return; + } + Long userId = event.getAcceptUserId(); + Long purchaseHistoryId = event.getPurchaseHistoryId(); + BigDecimal amountUsd = Objects.nonNull(event.getProductConfig()) + ? event.getProductConfig().getUnitPrice() + : null; + Long amountCents = toCents(amountUsd); + if (Objects.isNull(userId) || Objects.isNull(purchaseHistoryId) || !positive(amountCents)) { + return; + } + + Map payload = new HashMap<>(); + payload.put("payerUserId", event.getUserId()); + payload.put("acceptUserId", userId); + payload.put("purchaseHistoryId", purchaseHistoryId); + payload.put("candyQuantity", event.getCandyQuantity()); + payload.put("platform", event.getPlatform()); + payload.put("payPlatform", event.getPayPlatform()); + + String sourceOrderId = String.valueOf(purchaseHistoryId); + String payPlatform = normalizePayPlatform(defaultIfBlank(event.getPayPlatform(), event.getPlatform())); + String paymentMethod = classifyPaymentMethod(payPlatform); + publish(new RechargeSuccessEvent() + .setEventId("RECHARGE_SUCCESS:" + paymentMethod + ":" + sourceOrderId) + .setSysOrigin(event.getSysOrigin()) + .setUserId(userId) + .setAmountCents(amountCents) + .setAmountUsd(formatAmount(amountUsd)) + .setCurrency("USD") + .setPayPlatform(payPlatform) + .setPaymentMethod(paymentMethod) + .setSourceOrderId(sourceOrderId) + .setOrderId(sourceOrderId) + .setOccurredAt(toInstantString(event.getBuyDateTime())) + .setPayload(payload)); + } + + public void reportPaymentRecharge(InAppPurchaseDetails order) { + if (Objects.isNull(order) || !order.receiptTypeEqPayment()) { + return; + } + InAppPurchaseProduct product = order.firstProduct(); + if (Objects.isNull(product) || !PayApplicationCommodityType.GOLD.eq(product.getCode())) { + return; + } + String payPlatform = normalizePayPlatform(Objects.nonNull(order.getFactory()) ? order.getFactory().getFactoryCode() : ""); + String paymentMethod = classifyPaymentMethod(payPlatform); + + BigDecimal amountUsd = firstPositive(order.getAmountUsd(), product.getAmountUsd()); + Long amountCents = toCents(amountUsd); + if (Objects.isNull(order.getAcceptUserId()) || isBlank(order.getId()) || !positive(amountCents)) { + return; + } + + Map payload = new HashMap<>(); + payload.put("trackId", order.getTrackId()); + payload.put("sourceOrderId", order.getId()); + payload.put("orderId", order.getOrderId()); + payload.put("factoryCode", payPlatform); + payload.put("factoryChannelCode", Objects.nonNull(order.getFactory()) ? order.getFactory().getFactoryChannelCode() : null); + payload.put("platform", Objects.nonNull(order.getFactory()) ? order.getFactory().getPlatform() : null); + payload.put("productCode", product.getCode()); + payload.put("productContent", product.getContent()); + + publish(new RechargeSuccessEvent() + .setEventId("RECHARGE_SUCCESS:" + paymentMethod + ":" + payPlatform + ":" + order.getId()) + .setSysOrigin(order.getSysOrigin()) + .setUserId(order.getAcceptUserId()) + .setAmountCents(amountCents) + .setAmountUsd(formatAmount(amountUsd)) + .setCurrency(defaultIfBlank(order.getCurrency(), "USD")) + .setPayPlatform(payPlatform) + .setPaymentMethod(paymentMethod) + .setSourceOrderId(order.getId()) + .setOrderId(defaultIfBlank(order.getOrderId(), order.getId())) + .setOccurredAt(firstTime(order.getPurchaseDateMs(), order.getUpdateTime(), order.getCreateTime())) + .setPayload(payload)); + } + + private void publish(RechargeSuccessEvent event) { + if (!mqEnabled) { + return; + } + try { + MessageSenderService senderService = messageSenderServiceProvider.getIfAvailable(); + if (Objects.isNull(senderService)) { + log.warn("Recharge success mq sender missing, skip event report. eventId={}", event.getEventId()); + return; + } + senderService.sendEventMessage(MessageEvent.builder() + .consumeId(event.getEventId()) + .topicString(defaultIfBlank(mqTopic, "RC_DEFAULT_APP_ORDINARY")) + .tag(defaultIfBlank(mqTag, RechargeSuccessSink.TAG)) + .body(event) + .build()); + } catch (Exception e) { + log.warn("Recharge success mq report error. eventId={}", event.getEventId(), e); + } + } + + private String normalizePayPlatform(String value) { + String normalized = Objects.toString(value, "").trim().toUpperCase(); + return normalized.isEmpty() ? UNKNOWN : normalized; + } + + private String classifyPaymentMethod(String payPlatform) { + if (Objects.equals(GOOGLE, payPlatform)) { + return GOOGLE; + } + if (Objects.equals(APPLE, payPlatform)) { + return APPLE; + } + if (Objects.equals(UNKNOWN, payPlatform)) { + return UNKNOWN; + } + return THIRD_PARTY; + } + + private Long toCents(BigDecimal amount) { + if (Objects.isNull(amount) || amount.compareTo(BigDecimal.ZERO) <= 0) { + return null; + } + return amount.multiply(BigDecimal.valueOf(100)).setScale(0, RoundingMode.HALF_UP).longValue(); + } + + private BigDecimal firstPositive(BigDecimal first, BigDecimal second) { + if (Objects.nonNull(first) && first.compareTo(BigDecimal.ZERO) > 0) { + return first; + } + return second; + } + + private boolean positive(Long value) { + return Objects.nonNull(value) && value > 0; + } + + private String toInstantString(Timestamp timestamp) { + return Objects.nonNull(timestamp) ? timestamp.toInstant().toString() : Instant.now().toString(); + } + + private String firstTime(Timestamp first, Timestamp second, Timestamp third) { + if (Objects.nonNull(first)) { + return first.toInstant().toString(); + } + if (Objects.nonNull(second)) { + return second.toInstant().toString(); + } + if (Objects.nonNull(third)) { + return third.toInstant().toString(); + } + return Instant.now().toString(); + } + + private String formatAmount(BigDecimal amount) { + return Objects.isNull(amount) ? null : amount.stripTrailingZeros().toPlainString(); + } + + private String defaultIfBlank(String value, String fallback) { + return isBlank(value) ? fallback : value.trim(); + } + + private boolean isBlank(String value) { + return Objects.toString(value, "").trim().isEmpty(); + } +} diff --git a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/listener/BuySuccessListener.java b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/listener/BuySuccessListener.java index 9dce5258..fba190d9 100644 --- a/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/listener/BuySuccessListener.java +++ b/rc-service/rc-service-order/order-application/src/main/java/com/red/circle/order/app/listener/BuySuccessListener.java @@ -17,6 +17,7 @@ import com.red.circle.framework.dto.ResultResponse; import com.red.circle.mq.business.model.event.pay.BuySuccessEvent; import com.red.circle.mq.business.model.event.pay.PurchaseProductEvent; import com.red.circle.mq.rocket.business.streams.BuySuccessSink; +import com.red.circle.order.app.common.task.RechargeSuccessEventPublisher; import com.red.circle.order.app.common.task.TaskCenterGoClient; import com.red.circle.other.inner.endpoint.activity.CumulativeRechargeClient; import com.red.circle.other.inner.endpoint.activity.PropsActivityClient; @@ -71,6 +72,7 @@ public class BuySuccessListener implements MessageListener { private final CumulativeRechargeClient cumulativeRechargeClient; private final RedisService redisService; private final TaskCenterGoClient taskCenterGoClient; + private final RechargeSuccessEventPublisher rechargeSuccessEventPublisher; @Override public Action consume(ConsumerMessage message) { @@ -133,6 +135,7 @@ public class BuySuccessListener implements MessageListener { eventBody.getPlatform(), eventBody.getPayPlatform(), Objects.nonNull(eventBody.getBuyDateTime()) ? eventBody.getBuyDateTime().toInstant() : null); + rechargeSuccessEventPublisher.reportAppPurchaseRecharge(eventBody); userExpandClient.updatePurchasingToTrue(eventBody.getUserId());