feat: update mifapay and region config flows

This commit is contained in:
hy001 2026-04-20 22:07:10 +08:00
parent f7d821ae37
commit 2fd0976d0d
57 changed files with 2198 additions and 770 deletions

View File

@ -34,8 +34,8 @@ red-circle:
access-url: ${LIKEI_TENCENT_COS_ACCESS_URL}
instant-message:
broadcast-group:
LOTFUN: ${LIKEI_IM_BROADCAST_GROUP_LOTFUN}
LIKEI: ${LIKEI_IM_BROADCAST_GROUP_LIKEI}
LOTFUN: "${LIKEI_IM_BROADCAST_GROUP_LOTFUN}"
LIKEI: "${LIKEI_IM_BROADCAST_GROUP_LIKEI}"
tencet-trtc:
secretId: ${LIKEI_TRTC_SECRET_ID}
secretKey: ${LIKEI_TRTC_SECRET_KEY}

View File

@ -52,6 +52,7 @@ red-circle:
gateway-base-url: ${LIKEI_MIFA_PAY_GATEWAY_BASE_URL}
rsa-private-key: ${LIKEI_MIFA_PAY_RSA_PRIVATE_KEY}
platform-rsa-public-key: ${LIKEI_MIFA_PAY_PLATFORM_RSA_PUBLIC_KEY}
notify-url: ${LIKEI_MIFA_PAY_NOTIFY_URL:https://jvapi.haiyihy.com/play-server-notice/mifa_pay/receive_payment}
airwallex:
client-id: ${LIKEI_AIRWALLEX_CLIENT_ID}

View File

@ -114,7 +114,7 @@ activity:
scheduler:
room-empty-clean: false
room-online-quantity-reconcile: true
room-red-packet-expire: false
game-king-reward: false
noble-daily-reward: false

View File

@ -0,0 +1,119 @@
package com.red.circle.common.business.core.util;
import com.fasterxml.jackson.core.type.TypeReference;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
/**
* 国家码别名工具.
*/
public final class CountryCodeAliasUtils {
private static final String SA = "SA";
private static final String KSA = "KSA";
private CountryCodeAliasUtils() {
}
/**
* 归一化国家码.
*/
public static String normalize(String code) {
return StringUtils.isBlank(code) ? null : code.trim().toUpperCase();
}
/**
* 沙特兼容归一化保留非沙特国家原有写法.
*/
public static String normalizeSaudiCode(String code) {
String normalized = normalize(code);
return isSaudiCode(normalized) ? SA : normalized;
}
/**
* 是否是沙特国家码.
*/
public static boolean isSaudiCode(String code) {
String normalized = normalize(code);
return SA.equals(normalized) || KSA.equals(normalized);
}
/**
* 获取国家码别名集合.
*/
public static Set<String> legacyAliases(String code) {
return buildMatchCodes(code, null, Collections.emptyList());
}
/**
* 解析存储中的别名数组.
*/
public static List<String> parseAliasCodes(String aliasCodes) {
if (StringUtils.isBlank(aliasCodes)) {
return Collections.emptyList();
}
try {
List<String> values = JacksonUtils.readValue(aliasCodes, new TypeReference<List<String>>() {});
return normalizeCodes(values);
} catch (Exception ignore) {
return normalizeCodes(List.of(aliasCodes.split(",")));
}
}
/**
* 序列化别名数组到存储值.
*/
public static String toAliasCodesJson(Collection<String> aliasCodes) {
List<String> values = normalizeCodes(aliasCodes);
if (values.isEmpty()) {
return "";
}
return JacksonUtils.toJson(values);
}
/**
* 构造完整匹配国家码集合.
*/
public static Set<String> buildMatchCodes(String alphaTwo, String alphaThree,
Collection<String> aliasCodes) {
LinkedHashSet<String> result = new LinkedHashSet<>();
appendCode(result, alphaTwo);
appendCode(result, alphaThree);
if (aliasCodes != null) {
aliasCodes.forEach(code -> appendCode(result, code));
}
if (result.stream().anyMatch(CountryCodeAliasUtils::isSaudiCode)) {
result.add(SA);
result.add(KSA);
}
return result;
}
/**
* 规范化国家码列表.
*/
public static List<String> normalizeCodes(Collection<String> codes) {
if (codes == null || codes.isEmpty()) {
return Collections.emptyList();
}
LinkedHashSet<String> result = new LinkedHashSet<>();
codes.stream().map(CountryCodeAliasUtils::normalize).filter(StringUtils::isNotBlank)
.forEach(result::add);
return new ArrayList<>(result);
}
private static void appendCode(Set<String> result, String code) {
String normalized = normalize(code);
if (StringUtils.isNotBlank(normalized)) {
result.add(normalized);
}
}
}

View File

@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.core.dto.CommonCommand;
import java.io.Serial;
import java.sql.Timestamp;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -56,6 +57,11 @@ public class SysCountryCodeCmd extends CommonCommand {
*/
private String aliasName;
/**
* 国家码别名集合.
*/
private List<String> countryCodeAliases;
/**
* 代码分配情况.
*/

View File

@ -5,6 +5,7 @@ import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.DTO;
import java.io.Serial;
import java.sql.Timestamp;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -56,6 +57,11 @@ public class SysCountryCodeDTO extends DTO {
*/
private String aliasName;
/**
* 国家码别名集合.
*/
private List<String> countryCodeAliases;
/**
* 代码分配情况.
*/

View File

@ -67,6 +67,8 @@ red-circle:
ignore-urls:
- /console/account/login
- /console/actuator/**
- /console/internal/resident-activity/invite/grant-gold
- /console/internal/resident-activity/invite/grant-props
- /console/datav/active/user-country-code
- /console/datav/online/user/count
- /console/datav/online/room/count

View File

@ -5,8 +5,10 @@ import com.red.circle.common.business.dto.cmd.IdStringCmd;
import com.red.circle.order.app.dto.clientobject.pay.ApplicationCommodityCardV2CO;
import com.red.circle.order.app.dto.clientobject.pay.PayApplicationCO;
import com.red.circle.order.app.dto.clientobject.pay.PayCountryCO;
import com.red.circle.order.app.dto.clientobject.pay.PayOrderStatusCO;
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
import com.red.circle.order.app.dto.clientobject.pay.UserProfileAndCountryCO;
import com.red.circle.order.app.dto.cmd.PayOrderStatusCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderReceiptCmd;
import com.red.circle.order.app.dto.cmd.PayWebApplicationCommodityCmd;
@ -104,6 +106,19 @@ public class WebPayRestController {
return inAppPurchaseProductService.placeAnOrder(cmd);
}
/**
* 查询支付单状态.
*
* @eo.name 查询支付单状态.
* @eo.url /order-status
* @eo.method get
* @eo.request-type formdata
*/
@GetMapping("/order-status")
public PayOrderStatusCO orderStatus(@Validated PayOrderStatusCmd cmd) {
return inAppPurchaseProductService.orderStatus(cmd);
}
/**
* 支付收据 (后续版本计划移除).
*

View File

@ -2,14 +2,14 @@ package com.red.circle.order.app.command.pay.web;
import com.red.circle.component.pay.paymax.PayMaxUtils;
import com.red.circle.order.app.common.InAppPurchaseCommon;
import com.red.circle.order.app.common.MiFaPayMysqlOrderSupport;
import com.red.circle.order.domain.order.MiFaPayReceivePaymentNotice;
import com.red.circle.order.infra.config.MiFaPayProperties;
import com.red.circle.order.infra.database.mongo.order.InAppPurchaseCollectionReceiptService;
import com.red.circle.order.infra.database.mongo.order.InAppPurchaseDetailsService;
import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetails;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseEventNotice;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayNoticeService;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayService;
import com.red.circle.tool.core.text.StringUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -33,8 +33,9 @@ public class MiFaPayServerNoticeReceivePaymentCmdExe {
private final MiFaPayProperties miFaPayProperties;
private final InAppPurchaseCommon inAppPurchaseCommon;
private final InAppPurchaseDetailsService inAppPurchaseDetailsService;
private final InAppPurchaseCollectionReceiptService inAppPurchaseCollectionReceiptService;
private final OrderUserPurchasePayNoticeService orderUserPurchasePayNoticeService;
private final OrderUserPurchasePayService orderUserPurchasePayService;
/**
* 处理MiFaPay支付回调
@ -78,68 +79,72 @@ public class MiFaPayServerNoticeReceivePaymentCmdExe {
MiFaPayReceivePaymentNotice.MiFaPayNoticeData noticeData = notice.getNoticeData();
// 查询订单
InAppPurchaseDetails order = inAppPurchaseDetailsService.getById(noticeData.getOrderId());
if (Objects.isNull(order)) {
log.error("MiFaPay回调未找到订单: orderId={}", noticeData.getOrderId());
OrderUserPurchasePay mysqlOrder = getMysqlOrder(noticeData.getOrderId());
if (Objects.isNull(mysqlOrder)) {
log.error("MiFaPay回调未找到MySQL订单: orderId={}", noticeData.getOrderId());
return FAIL;
}
// 检查订单状态避免重复处理
if (!Objects.equals(order.getStatus(), InAppPurchaseStatus.CREATE)) {
log.warn("MiFaPay订单已处理: orderId={}, status={}", order.getId(), order.getStatus());
return SUCCESS;
return handleMysqlOrder(notice, noticeData, mysqlOrder);
}
// 记录回调通知
inAppPurchaseDetailsService.addPayNotices(order.getId(),
new InAppPurchaseEventNotice()
.setEventId(notice.getSign())
.setNoticeType("mifapay-" + noticeData.getOrderStatus())
.setNoticeData(notice)
.setCreateTime(TimestampUtils.now())
private String handleMysqlOrder(MiFaPayReceivePaymentNotice notice,
MiFaPayReceivePaymentNotice.MiFaPayNoticeData noticeData,
OrderUserPurchasePay mysqlOrder) {
orderUserPurchasePayNoticeService.addNotice(
mysqlOrder.getId(),
notice.getSign(),
"mifapay-" + noticeData.getOrderStatus(),
notice,
mysqlOrder.getUpdateUser()
);
// 处理支付成功
if (!Objects.equals(mysqlOrder.getPayStatus(), MiFaPayMysqlOrderSupport.PAY_STATUS_PREPAYMENT)) {
log.warn("MiFaPay订单已处理(MySQL): orderId={}, status={}",
mysqlOrder.getId(), mysqlOrder.getPayStatus());
return SUCCESS;
}
InAppPurchaseDetails order = MiFaPayMysqlOrderSupport.toInAppPurchaseDetails(mysqlOrder);
if (noticeData.isSuccess()) {
if (!Objects.equals(order.statusEqCreate(), Boolean.TRUE)) {
log.warn("MiFaPay订单状态不是CREATE: orderId={}", order.getId());
return SUCCESS;
}
// 收款单
if (order.receiptTypeEqReceipt()) {
inAppPurchaseCommon.inAppPurchaseReceipt(order);
log.info("MiFaPay收款单处理成功: orderId={}", order.getId());
inAppPurchaseCommon.inAppPurchaseReceipt(order,
() -> orderUserPurchasePayService.updateSuccess(mysqlOrder.getId()));
log.info("MiFaPay收款单处理成功(MySQL): orderId={}", mysqlOrder.getId());
return SUCCESS;
}
// 付款单
if (order.receiptTypeEqPayment()) {
inAppPurchaseCommon.inAppPurchasePayment(order);
log.info("MiFaPay付款单处理成功: orderId={}", order.getId());
inAppPurchaseCommon.inAppPurchasePayment(order,
() -> orderUserPurchasePayService.updateSuccess(mysqlOrder.getId()));
log.info("MiFaPay付款单处理成功(MySQL): orderId={}", mysqlOrder.getId());
return SUCCESS;
}
log.error("MiFaPay订单类型错误: orderId={}", order.getId());
log.error("MiFaPay订单类型错误(MySQL): orderId={}", mysqlOrder.getId());
return FAIL;
}
// 处理支付失败
if (noticeData.isFailed()) {
if (order.receiptTypeEqReceipt()) {
inAppPurchaseCollectionReceiptService.updateStatusSuccessFail(order.getId());
if (order.receiptTypeEqReceipt() && StringUtils.isNotBlank(mysqlOrder.getReferenceId())) {
inAppPurchaseCollectionReceiptService.updateStatusSuccessFail(
mysqlOrder.getReferenceId());
}
inAppPurchaseDetailsService.updateStatusReason(
order.getId(),
InAppPurchaseStatus.FAIL,
orderUserPurchasePayService.updateFail(
mysqlOrder.getId(),
String.format("MiFaPay支付失败: %s", noticeData.getOrderStatus())
);
log.info("MiFaPay支付失败处理完成: orderId={}", order.getId());
log.info("MiFaPay支付失败处理完成(MySQL): orderId={}", mysqlOrder.getId());
return SUCCESS;
}
log.error("MiFaPay未知订单状态: orderId={}, status={}", order.getId(), noticeData.getOrderStatus());
log.error("MiFaPay未知订单状态(MySQL): orderId={}, status={}",
mysqlOrder.getId(), noticeData.getOrderStatus());
return SUCCESS;
}
private OrderUserPurchasePay getMysqlOrder(String orderId) {
Long mysqlOrderId = MiFaPayMysqlOrderSupport.parseOrderId(orderId);
return mysqlOrderId == null ? null : orderUserPurchasePayService.getById(mysqlOrderId);
}
}

View File

@ -0,0 +1,51 @@
package com.red.circle.order.app.command.pay.web;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.order.app.common.MiFaPayMysqlOrderSupport;
import com.red.circle.order.app.dto.clientobject.pay.PayOrderStatusCO;
import com.red.circle.order.app.dto.cmd.PayOrderStatusCmd;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayService;
import com.red.circle.order.inner.asserts.OrderErrorCode;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import java.util.Objects;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 支付单状态查询.
*
* @author system
*/
@Component
@RequiredArgsConstructor
public class PayWebOrderStatusQueryExe {
private final OrderUserPurchasePayService orderUserPurchasePayService;
public PayOrderStatusCO execute(PayOrderStatusCmd cmd) {
OrderUserPurchasePay mysqlOrder = getMysqlOrder(cmd.getOrderId());
ResponseAssert.notNull(OrderErrorCode.NO_PURCHASE_RECORD_FOUND, mysqlOrder);
ResponseAssert.isTrue(OrderErrorCode.NO_PURCHASE_RECORD_FOUND,
Objects.equals(mysqlOrder.getUserId(), cmd.getReqUserId()));
InAppPurchaseStatus status = MiFaPayMysqlOrderSupport
.toInAppPurchaseStatus(mysqlOrder.getPayStatus());
return new PayOrderStatusCO()
.setOrderId(Objects.toString(mysqlOrder.getId(), null))
.setTradeNo(mysqlOrder.getFactoryOrderId())
.setStatus(Objects.toString(status, null))
.setSuccess(MiFaPayMysqlOrderSupport.isSuccess(mysqlOrder.getPayStatus()))
.setFinished(MiFaPayMysqlOrderSupport.isFinished(mysqlOrder.getPayStatus()))
.setReason(mysqlOrder.getReason())
.setFactoryCode(mysqlOrder.getFactoryCode())
.setFactoryChannelCode(mysqlOrder.getPaymentChannel())
.setCurrency(mysqlOrder.getPaymentUnit())
.setAmount(mysqlOrder.getPaymentAmount())
.setUpdateTime(mysqlOrder.getUpdateTime());
}
private OrderUserPurchasePay getMysqlOrder(String orderId) {
Long mysqlOrderId = MiFaPayMysqlOrderSupport.parseOrderId(orderId);
return mysqlOrderId == null ? null : orderUserPurchasePayService.getById(mysqlOrderId);
}
}

View File

@ -1,6 +1,7 @@
package com.red.circle.order.app.command.pay.web.strategy;
import com.red.circle.component.pay.paymax.service.PlaceAnOrderParam;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.ResponseErrorCode;
import com.red.circle.order.app.common.MiFaPayPlaceAnOrderParam;
@ -104,7 +105,7 @@ public class MiFaPayWebPayPlaceAnOrderStrategy implements PayWebPlaceAnOrderStra
.payType(details.getFactoryChannel())
.userIp(details.getRequestIp())
.returnUrl(details.getSuccessRedirectUrl())
.notifyUrl(MiFaPayService.NOTIFY_URL)
.notifyUrl(miFaPayService.getNotifyUrl())
.language("en")
.email(memberInfo.getEmail())
.countryCode(replaceCountryCode(details.getCountryCode()))
@ -126,11 +127,7 @@ public class MiFaPayWebPayPlaceAnOrderStrategy implements PayWebPlaceAnOrderStra
}
private static String replaceCountryCode(String countryCode) {
if ("KSA".equalsIgnoreCase(countryCode)) {
return countryCode.replaceAll("K", "");
}
return countryCode;
return CountryCodeAliasUtils.normalizeSaudiCode(countryCode);
}

View File

@ -4,6 +4,7 @@ import com.google.api.client.util.Maps;
import com.red.circle.component.pay.paymax.PayMaxUtils;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.framework.web.props.EnvProperties;
import com.red.circle.order.app.common.MiFaPayMysqlOrderSupport;
import com.red.circle.order.app.common.PayerMaxProperties;
import com.red.circle.order.app.common.QuoteQueryParam;
import com.red.circle.order.app.common.QuoteQueryResponse;
@ -13,6 +14,8 @@ import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetai
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseEventNotice;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseFactory;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseStatusStep;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayNoticeService;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayService;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.http.RcHttpClient;
@ -34,6 +37,8 @@ public class PayPlaceAnOrderService {
private final EnvProperties envProperties;
private final InAppPurchaseDetailsService inAppPurchaseDetailsService;
private final OrderUserPurchasePayNoticeService orderUserPurchasePayNoticeService;
private final OrderUserPurchasePayService orderUserPurchasePayService;
private final FactoryPlaceAnOrderStrategyV2 factoryPlaceAnOrderStrategyV2;
private final PayerMaxProperties payerMaxProperties;
private final RcHttpClient rcHttpClient = RcHttpClient.builder().build();
@ -44,6 +49,19 @@ public class PayPlaceAnOrderService {
param.getStrategyCode())
.doOperation(param);
if (MiFaPayMysqlOrderSupport.isMiFaPay(param.getFactoryCode())) {
Long purchasePayId = MiFaPayMysqlOrderSupport.parseOrderId(param.getOrderId());
orderUserPurchasePayService.save(
MiFaPayMysqlOrderSupport.buildPrepaymentOrder(param, response, env()));
orderUserPurchasePayNoticeService.addNotice(
purchasePayId,
"custom-request-response-param",
"API-INTERNAL-PARAMETERS",
response.extra(),
param.getCreateUserId());
return response;
}
// 创建站内预支付订单.
inAppPurchaseDetailsService.create(
new InAppPurchaseDetails()

View File

@ -81,6 +81,13 @@ public class InAppPurchaseCommon {
private final UserProfileClient userProfileClient;
public void inAppPurchaseReceipt(InAppPurchaseDetails order) {
inAppPurchaseReceipt(order,
() -> inAppPurchaseDetailsService.updateStatus(order.getId(), InAppPurchaseStatus.SUCCESS));
}
public void inAppPurchaseReceipt(InAppPurchaseDetails order, Runnable successHandler) {
Runnable finalSuccessHandler = successHandler != null ? successHandler : () -> {
};
if (!order.receiptTypeEqReceipt()) {
return;
@ -92,7 +99,7 @@ public class InAppPurchaseCommon {
if (CollectionUtils.isEmpty(order.getProducts())) {
inAppPurchaseCollectionReceiptService.updateStatusSuccess(order.getTrackId());
inAppPurchaseDetailsService.updateStatus(order.getId(), InAppPurchaseStatus.SUCCESS);
finalSuccessHandler.run();
return;
}
@ -123,10 +130,17 @@ public class InAppPurchaseCommon {
);
inAppPurchaseCollectionReceiptService.updateStatusSuccess(order.getTrackId());
inAppPurchaseDetailsService.updateStatus(order.getId(), InAppPurchaseStatus.SUCCESS);
finalSuccessHandler.run();
}
public void inAppPurchasePayment(InAppPurchaseDetails order) {
inAppPurchasePayment(order,
() -> inAppPurchaseDetailsService.updateStatus(order.getId(), InAppPurchaseStatus.SUCCESS));
}
public void inAppPurchasePayment(InAppPurchaseDetails order, Runnable successHandler) {
Runnable finalSuccessHandler = successHandler != null ? successHandler : () -> {
};
InAppPurchaseProduct product = order.firstProduct();
ResponseAssert.notNull(CommonErrorCode.NOT_FOUND_MAPPING_INFO, product);
@ -141,7 +155,7 @@ public class InAppPurchaseCommon {
}
// 修改订单状态
inAppPurchaseDetailsService.updateStatus(order.getId(), InAppPurchaseStatus.SUCCESS);
finalSuccessHandler.run();
}

View File

@ -0,0 +1,168 @@
package com.red.circle.order.app.common;
import com.red.circle.framework.core.request.RequestClientEnum;
import com.red.circle.order.app.command.pay.web.strategy.PayPlaceAnOrderDetailsV2;
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
import com.red.circle.order.infra.database.mongo.order.entity.InAppPurchaseDetails;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseFactory;
import com.red.circle.order.infra.database.mongo.order.entity.assist.InAppPurchaseProduct;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseReceiptType;
import com.red.circle.order.inner.model.enums.inapp.InAppPurchaseStatus;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.sql.Timestamp;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
/**
* MiFaPay MySQL订单映射工具.
*/
public final class MiFaPayMysqlOrderSupport {
public static final String FACTORY_CODE = "MIFA_PAY";
public static final String PAY_STATUS_PREPAYMENT = "PREPAYMENT";
public static final String PAY_STATUS_SUCCESSFUL = "SUCCESSFUL";
public static final String PAY_STATUS_FAIL = "FAIL";
public static final String REFUND_STATUS_NONE = "NONE";
private MiFaPayMysqlOrderSupport() {
}
public static boolean isMiFaPay(String factoryCode) {
return Objects.equals(FACTORY_CODE, factoryCode);
}
public static Long parseOrderId(String orderId) {
if (StringUtils.isBlank(orderId)) {
return null;
}
try {
return Long.valueOf(orderId.trim());
} catch (NumberFormatException ignore) {
return null;
}
}
public static OrderUserPurchasePay buildPrepaymentOrder(PayPlaceAnOrderDetailsV2 param,
PlaceAnOrderResponseCO response, String env) {
Timestamp now = TimestampUtils.now();
InAppPurchaseProduct product = firstProduct(param.getProducts());
Long referenceId = parseOrderId(param.getApplicationId());
return new OrderUserPurchasePay()
.setId(parseOrderId(param.getOrderId()))
.setEvn(env)
.setSysOrigin(param.getSysOrigin())
.setUserId(param.getAcceptUserId())
.setFactoryCode(param.getFactoryCode())
.setFactoryOrderId(Objects.toString(response.getTradeNo(), ""))
.setReferenceId(Objects.toString(param.getApplicationId(), ""))
.setReceiptType(Objects.toString(param.getReceiptType(),
InAppPurchaseReceiptType.PAYMENT.name()))
.setPaymentChannel(Objects.toString(param.getChannelCode(), ""))
.setFactoryChannel(Objects.toString(param.getFactoryChannel(), ""))
.setProductCode(product != null ? Objects.toString(product.getCode(), "") : "")
.setProductContent(product != null ? Objects.toString(product.getContent(), "") : "")
.setProductDescriptor(Objects.toString(param.getProductDescriptor(), ""))
.setPaymentUnit(Objects.toString(param.getCurrency(), ""))
.setPaymentAmount(defaultDecimal(param.getAmount()))
.setComputeUsdAmount(defaultDecimal(param.getAmountUsd()))
.setComputeUsdRate(computeUsdRate(param.getAmount(), param.getAmountUsd()))
.setGiveAwayContent("")
.setCountryId(param.getCountry() != null ? param.getCountry().getId() : 0L)
.setCountryCode(Objects.toString(response.getCountryCode(), param.getCountryCode()))
.setPayCountryId(param.getPayCountry() != null ? param.getPayCountry().getId() : 0L)
.setPayStatus(PAY_STATUS_PREPAYMENT)
.setReason("")
.setRefundStatus(REFUND_STATUS_NONE)
.setReceiptDetailsId(referenceId != null ? referenceId : 0L)
.setCreateTime(now)
.setUpdateTime(now)
.setCreateUser(param.getCreateUserId())
.setUpdateUser(param.getCreateUserId());
}
public static InAppPurchaseDetails toInAppPurchaseDetails(OrderUserPurchasePay order) {
List<InAppPurchaseProduct> products = StringUtils.isBlank(order.getProductCode())
? Collections.emptyList()
: List.of(new InAppPurchaseProduct()
.setId(order.getReceiptDetailsId())
.setName(order.getProductCode())
.setCode(order.getProductCode())
.setDescribe(order.getProductDescriptor())
.setContent(order.getProductContent())
.setAmountUsd(defaultDecimal(order.getComputeUsdAmount()))
.setQuantity(1));
return new InAppPurchaseDetails()
.setId(Objects.toString(order.getId(), null))
.setReceiptType(parseReceiptType(order.getReceiptType()))
.setTrackId(order.getReferenceId())
.setSysOrigin(order.getSysOrigin())
.setAcceptUserId(order.getUserId())
.setOrderId(order.getFactoryOrderId())
.setEnv(order.getEvn())
.setFactory(new InAppPurchaseFactory()
.setPlatform(RequestClientEnum.H5.getClientName())
.setFactoryCode(order.getFactoryCode())
.setFactoryChannelCode(order.getPaymentChannel()))
.setProducts(products)
.setProductDescriptor(order.getProductDescriptor())
.setCountryCode(order.getCountryCode())
.setCurrency(order.getPaymentUnit())
.setAmount(defaultDecimal(order.getPaymentAmount()))
.setAmountUsd(defaultDecimal(order.getComputeUsdAmount()))
.setTrialPeriod(Boolean.FALSE)
.setStatus(toInAppPurchaseStatus(order.getPayStatus()))
.setReason(order.getReason())
.setCreateTime(order.getCreateTime())
.setUpdateTime(order.getUpdateTime())
.setCreateUser(order.getCreateUser());
}
public static InAppPurchaseStatus toInAppPurchaseStatus(String payStatus) {
if (Objects.equals(PAY_STATUS_SUCCESSFUL, payStatus)) {
return InAppPurchaseStatus.SUCCESS;
}
if (Objects.equals(PAY_STATUS_FAIL, payStatus)) {
return InAppPurchaseStatus.FAIL;
}
return InAppPurchaseStatus.CREATE;
}
public static boolean isSuccess(String payStatus) {
return Objects.equals(PAY_STATUS_SUCCESSFUL, payStatus);
}
public static boolean isFinished(String payStatus) {
return isSuccess(payStatus) || Objects.equals(PAY_STATUS_FAIL, payStatus);
}
private static String computeUsdRate(BigDecimal paymentAmount, BigDecimal usdAmount) {
if (paymentAmount == null || usdAmount == null || BigDecimal.ZERO.compareTo(usdAmount) == 0) {
return "";
}
return paymentAmount.divide(usdAmount, 8, RoundingMode.HALF_UP)
.stripTrailingZeros()
.toPlainString();
}
private static InAppPurchaseProduct firstProduct(List<InAppPurchaseProduct> products) {
return products == null || products.isEmpty() ? null : products.get(0);
}
private static InAppPurchaseReceiptType parseReceiptType(String receiptType) {
if (Objects.equals(InAppPurchaseReceiptType.RECEIPT.name(), receiptType)) {
return InAppPurchaseReceiptType.RECEIPT;
}
return InAppPurchaseReceiptType.PAYMENT;
}
private static BigDecimal defaultDecimal(BigDecimal value) {
return value != null ? value : BigDecimal.ZERO;
}
}

View File

@ -24,6 +24,7 @@ import com.red.circle.order.app.command.pay.web.AirwallexServerNoticeCmdExe;
import com.red.circle.order.app.command.pay.web.ClipspayServerNoticeCmdExe;
import com.red.circle.order.app.command.pay.web.CollectionReceiptQueryExe;
import com.red.circle.order.app.command.pay.web.PayPalServerNoticeCmdExe;
import com.red.circle.order.app.command.pay.web.PayWebOrderStatusQueryExe;
import com.red.circle.order.app.command.pay.web.PayWebPlaceAnOrderCmdExe;
import com.red.circle.order.app.command.pay.web.PayerMaxServerNoticeReceivePaymentCmdExe;
import com.red.circle.order.app.command.pay.web.PayerMaxServerNoticeRefundNoticeCmdExe;
@ -33,8 +34,10 @@ import com.red.circle.order.app.command.pay.web.ReceiptPayWebPlaceAnOrderReceipt
import com.red.circle.order.app.dto.clientobject.PayMaxResponseCO;
import com.red.circle.order.app.dto.clientobject.PayerMaxResponseV2CO;
import com.red.circle.order.app.dto.clientobject.PurchaseReceiptCO;
import com.red.circle.order.app.dto.clientobject.pay.PayOrderStatusCO;
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
import com.red.circle.order.app.dto.cmd.AbstractPurchaseCmd;
import com.red.circle.order.app.dto.cmd.PayOrderStatusCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderReceiptCmd;
import com.red.circle.tool.core.date.LocalDateTimeUtils;
@ -60,6 +63,7 @@ public class InAppPurchaseProductServiceImpl implements InAppPurchaseProductServ
private final AppInAppPurchaseCmdExe appInAppPurchaseCmdExe;
private final PayWebPlaceAnOrderCmdExe payWebPlaceAnOrderCmdExe;
private final PayWebOrderStatusQueryExe payWebOrderStatusQueryExe;
private final PayPalServerNoticeCmdExe payPalServerNoticeCmdExe;
private final CollectionReceiptQueryExe collectionReceiptQueryExe;
private final ClipspayServerNoticeCmdExe clipspayServerNoticeCmdExe;
@ -149,6 +153,11 @@ public class InAppPurchaseProductServiceImpl implements InAppPurchaseProductServ
return payWebPlaceAnOrderCmdExe.execute(cmd);
}
@Override
public PayOrderStatusCO orderStatus(PayOrderStatusCmd cmd) {
return payWebOrderStatusQueryExe.execute(cmd);
}
@Override
public PlaceAnOrderResponseCO payReceipt(PayPlaceAnOrderReceiptCmd cmd) {
return receiptPayWebPlaceAnOrderReceiptCmdExe.execute(cmd);

View File

@ -6,6 +6,7 @@ import com.red.circle.order.app.dto.clientobject.MiFaPayResponseCO;
import com.red.circle.order.infra.config.MiFaPayProperties;
import com.red.circle.tool.core.http.RcHttpClient;
import com.red.circle.tool.core.json.JacksonUtils;
import com.red.circle.tool.core.text.StringUtils;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
@ -25,7 +26,6 @@ public class MiFaPayService {
/**
* 下单接口路径
*/
public static final String NOTIFY_URL = "";
private static final String PLACE_ORDER_PATH = "/paygateway/mbpay/order/en_v2";
private final String active;
@ -45,6 +45,8 @@ public class MiFaPayService {
* @return 支付响应
*/
public MiFaPayResponseCO placeAnOrder(MiFaPayPlaceAnOrderParam param) {
validateRequiredConfig();
// 设置商户编号
param.setMerNo(miFaPayProperties.getMerNo());
@ -82,21 +84,54 @@ public class MiFaPayService {
log.warn("MiFaPay Response: {}", JacksonUtils.toJson(response));
}
// 验证响应签名
if (response != null && response.isSuccess() && response.getData() != null) {
// boolean validSign = PayMaxUtils.validSignRsa(
// response.getData(),
// response.getSign(),
// miFaPayProperties.getPlatformRsaPublicKey()
// );
// if (!validSign) {
// log.error("MiFaPay response sign verification failed!");
// }
}
verifySuccessfulResponse(response);
return response;
}
public String getNotifyUrl() {
validateRequiredValue("notifyUrl", miFaPayProperties.getNotifyUrl());
return miFaPayProperties.getNotifyUrl();
}
private void verifySuccessfulResponse(MiFaPayResponseCO response) {
if (response == null || !response.isSuccess()) {
return;
}
validateRequiredValue("response.data", response.getData());
validateRequiredValue("response.sign", response.getSign());
if (StringUtils.isNotBlank(response.getMerAccount())
&& !Objects.equals(response.getMerAccount(), miFaPayProperties.getMerAccount())) {
throw new IllegalStateException("MiFaPay response merAccount mismatch");
}
boolean validSign = PayMaxUtils.validSignRsa(
response.getData(),
response.getSign(),
miFaPayProperties.getPlatformRsaPublicKey()
);
if (!validSign) {
throw new IllegalStateException("MiFaPay response signature verification failed");
}
}
private void validateRequiredConfig() {
validateRequiredValue("merAccount", miFaPayProperties.getMerAccount());
validateRequiredValue("merNo", miFaPayProperties.getMerNo());
validateRequiredValue("gatewayBaseUrl", miFaPayProperties.getGatewayBaseUrl());
validateRequiredValue("rsaPrivateKey", miFaPayProperties.getRsaPrivateKey());
validateRequiredValue("platformRsaPublicKey", miFaPayProperties.getPlatformRsaPublicKey());
validateRequiredValue("notifyUrl", miFaPayProperties.getNotifyUrl());
}
private void validateRequiredValue(String fieldName, String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalStateException("MiFaPay config missing: " + fieldName);
}
}
/**
* MiFaPay请求体结构
*/

View File

@ -0,0 +1,76 @@
package com.red.circle.order.app.dto.clientobject.pay;
import com.red.circle.framework.dto.DTO;
import java.math.BigDecimal;
import java.sql.Timestamp;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* 支付单状态.
*
* @author system
*/
@Data
@Accessors(chain = true)
@EqualsAndHashCode(callSuper = true)
public class PayOrderStatusCO extends DTO {
private static final long serialVersionUID = 1L;
/**
* 站内支付单号.
*/
private String orderId;
/**
* 第三方平台订单号.
*/
private String tradeNo;
/**
* 当前支付状态.
*/
private String status;
/**
* 是否支付成功.
*/
private Boolean success;
/**
* 是否已进入终态.
*/
private Boolean finished;
/**
* 失败或挂起原因.
*/
private String reason;
/**
* 厂商编码.
*/
private String factoryCode;
/**
* 厂商渠道编码.
*/
private String factoryChannelCode;
/**
* 币种.
*/
private String currency;
/**
* 金额.
*/
private BigDecimal amount;
/**
* 最后更新时间.
*/
private Timestamp updateTime;
}

View File

@ -0,0 +1,26 @@
package com.red.circle.order.app.dto.cmd;
import com.red.circle.common.business.dto.cmd.AppExtCommand;
import jakarta.validation.constraints.NotBlank;
import java.io.Serial;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 支付单状态查询.
*
* @author system
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class PayOrderStatusCmd extends AppExtCommand {
@Serial
private static final long serialVersionUID = 1L;
/**
* 站内支付单号.
*/
@NotBlank(message = "orderId required.")
private String orderId;
}

View File

@ -5,8 +5,10 @@ import com.red.circle.common.business.dto.cmd.IdStringCmd;
import com.red.circle.order.app.dto.clientobject.PayMaxResponseCO;
import com.red.circle.order.app.dto.clientobject.PayerMaxResponseV2CO;
import com.red.circle.order.app.dto.clientobject.PurchaseReceiptCO;
import com.red.circle.order.app.dto.clientobject.pay.PayOrderStatusCO;
import com.red.circle.order.app.dto.clientobject.pay.PlaceAnOrderResponseCO;
import com.red.circle.order.app.dto.cmd.AbstractPurchaseCmd;
import com.red.circle.order.app.dto.cmd.PayOrderStatusCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderCmd;
import com.red.circle.order.app.dto.cmd.PayPlaceAnOrderReceiptCmd;
import java.util.Map;
@ -32,6 +34,11 @@ public interface InAppPurchaseProductService {
PlaceAnOrderResponseCO placeAnOrder(PayPlaceAnOrderCmd cmd);
/**
* 查询支付单状态.
*/
PayOrderStatusCO orderStatus(PayOrderStatusCmd cmd);
@Deprecated
PlaceAnOrderResponseCO payReceipt(PayPlaceAnOrderReceiptCmd cmd);

View File

@ -40,4 +40,9 @@ public class MiFaPayProperties {
* 平台公钥用于验签
*/
private String platformRsaPublicKey;
/**
* 支付结果异步通知地址
*/
private String notifyUrl;
}

View File

@ -25,6 +25,8 @@ import org.springframework.stereotype.Service;
public class InAppPurchaseCollectionReceiptServiceImpl implements
InAppPurchaseCollectionReceiptService {
private static final String ID_FIELD = "_id";
private final MongoTemplate mongoTemplate;
@Override
@ -34,7 +36,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
@Override
public void update(InAppPurchaseCollectionReceipt receipt) {
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(receipt.getId())),
mongoTemplate.updateFirst(Query.query(Criteria.where(ID_FIELD).is(receipt.getId())),
new Update()
.set("acceptUserId", receipt.getAcceptUserId())
.set("amount", receipt.getAmount())
@ -52,7 +54,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
@Override
public void close(String id, Long operationUserId) {
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id)),
mongoTemplate.updateFirst(Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("updateTime", TimestampUtils.now())
.set("status", InAppPurchaseCollectionReceiptStatus.CLOSE)
@ -62,7 +64,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
@Override
public void updateStatusSuccess(String id) {
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id)),
mongoTemplate.updateFirst(Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("updateTime", TimestampUtils.now())
.set("status", InAppPurchaseCollectionReceiptStatus.SUCCESS)
@ -72,7 +74,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
@Override
public void updateStatusSuccessFail(String id) {
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id)),
mongoTemplate.updateFirst(Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("updateTime", TimestampUtils.now())
.set("status", InAppPurchaseCollectionReceiptStatus.FAIL)
@ -90,7 +92,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
if (StringUtils.isNotBlank(query.getId())) {
query.setLastId(null);
criteria.and("id").is(query.getId());
criteria.and(ID_FIELD).is(query.getId());
}
if (StringUtils.isNotBlank(query.getStatus())) {
@ -113,7 +115,7 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
}
if (Objects.nonNull(query.getLastId())) {
criteria.and("id").lt(query.getLastId());
criteria.and(ID_FIELD).lt(query.getLastId());
}
if (Objects.isNull(query.getLimit())) {
@ -121,20 +123,20 @@ public class InAppPurchaseCollectionReceiptServiceImpl implements
}
return mongoTemplate.find(Query.query(criteria)
.with(Sort.by(Sort.Order.desc("id")))
.with(Sort.by(Sort.Order.desc(ID_FIELD)))
.limit(query.getLimit()),
InAppPurchaseCollectionReceipt.class);
}
@Override
public InAppPurchaseCollectionReceipt getById(String id) {
return mongoTemplate.findOne(Query.query(Criteria.where("id").is(id)),
return mongoTemplate.findOne(Query.query(Criteria.where(ID_FIELD).is(id)),
InAppPurchaseCollectionReceipt.class);
}
@Override
public void lockTime(String id, long minute) {
mongoTemplate.updateFirst(Query.query(Criteria.where("id").is(id)),
mongoTemplate.updateFirst(Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("updateTime", TimestampUtils.now())
.set("lockTime", TimestampUtils.nowPlusMinutes(minute)),

View File

@ -35,6 +35,8 @@ import org.springframework.stereotype.Service;
@RequiredArgsConstructor
public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsService {
private static final String ID_FIELD = "_id";
private final MongoTemplate mongoTemplate;
@Override
@ -44,7 +46,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public InAppPurchaseDetails getById(String id) {
return mongoTemplate.findOne(Query.query(Criteria.where("id").is(id)),
return mongoTemplate.findOne(Query.query(Criteria.where(ID_FIELD).is(id)),
InAppPurchaseDetails.class);
}
@ -58,7 +60,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public boolean existsPayNotice(String id, String eventId, String noticeType) {
return mongoTemplate.exists(Query.query(
Criteria.where("id").is(id)
Criteria.where(ID_FIELD).is(id)
.elemMatch(Criteria.where("eventId").is(eventId).is("noticeType").is(noticeType))
), InAppPurchaseDetails.class);
}
@ -66,21 +68,21 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public boolean existsPayNoticeByEventId(String id, String eventId) {
return mongoTemplate.exists(Query.query(
Criteria.where("id").is(id).elemMatch(Criteria.where("eventId").is(eventId))
Criteria.where(ID_FIELD).is(id).elemMatch(Criteria.where("eventId").is(eventId))
), InAppPurchaseDetails.class);
}
@Override
public boolean existsPayNoticeByNoticeType(String id, String noticeType) {
return mongoTemplate.exists(Query.query(
Criteria.where("id").is(id).elemMatch(Criteria.where("noticeType").is(noticeType))
Criteria.where(ID_FIELD).is(id).elemMatch(Criteria.where("noticeType").is(noticeType))
), InAppPurchaseDetails.class);
}
@Override
public void addPayNotices(String id, InAppPurchaseEventNotice payNotice) {
mongoTemplate.updateFirst(
Query.query(Criteria.where("id").is(id)),
Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.push("payNotices", payNotice)
.set("updateTime", TimestampUtils.now()),
@ -102,7 +104,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public void updateStatus(String id, InAppPurchaseStatus status) {
mongoTemplate.updateFirst(
Query.query(Criteria.where("id").is(id)),
Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("status", status)
.push("statusSteps", new InAppPurchaseStatusStep()
@ -117,7 +119,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public void updateStatusReason(String id, InAppPurchaseStatus status, String reason) {
mongoTemplate.updateFirst(
Query.query(Criteria.where("id").is(id)),
Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("status", status)
.set("reason", reason)
@ -141,7 +143,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
String newKey = key.replaceAll("\\.", "_").replaceAll("\\$", "_");
details.getMetadata().put(newKey, val);
return mongoTemplate.updateFirst(
Query.query(Criteria.where("id").is(details.getId())
Query.query(Criteria.where(ID_FIELD).is(details.getId())
.and("version").is(details.getVersion())),
new Update()
.set("metadata", details.getMetadata())
@ -158,7 +160,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
if (StringUtils.isNotBlank(query.getId())) {
query.setLastId(null);
criteria.and("id").is(query.getId());
criteria.and(ID_FIELD).is(query.getId());
}
if (Objects.nonNull(query.getReceiptType())) {
criteria.and("receiptType").is(query.getReceiptType());
@ -204,7 +206,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
}
if (StringUtils.isNotBlank(query.getLastId())) {
criteria.and("id").lt(query.getLastId());
criteria.and(ID_FIELD).lt(query.getLastId());
}
if (Objects.isNull(query.getLimit()) || query.getLimit() <= 0) {
@ -213,7 +215,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
return mongoTemplate
.find(Query.query(criteria)
.with(Sort.by(Sort.Order.desc("id")))
.with(Sort.by(Sort.Order.desc(ID_FIELD)))
.limit(query.getLimit()), InAppPurchaseDetails.class);
}
@ -222,7 +224,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
Criteria criteria = new Criteria();
if (StringUtils.isNotBlank(query.getId())) {
criteria.and("id").is(query.getId());
criteria.and(ID_FIELD).is(query.getId());
}
if (Objects.nonNull(query.getReceiptType())) {
criteria.and("receiptType").is(query.getReceiptType());
@ -338,7 +340,7 @@ public class InAppPurchaseDetailsServiceImpl implements InAppPurchaseDetailsServ
@Override
public void updateMetadata(String id, Map<String, String> metadata) {
mongoTemplate.updateFirst(
Query.query(Criteria.where("id").is(id)),
Query.query(Criteria.where(ID_FIELD).is(id)),
new Update()
.set("metadata", metadata),
InAppPurchaseDetails.class

View File

@ -0,0 +1,10 @@
package com.red.circle.order.infra.database.rds.dao.order;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
/**
* Web支付预下单 Mapper.
*/
public interface OrderUserPurchasePayDAO extends BaseDAO<OrderUserPurchasePay> {
}

View File

@ -0,0 +1,10 @@
package com.red.circle.order.infra.database.rds.dao.order;
import com.red.circle.framework.mybatis.dao.BaseDAO;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePayNotice;
/**
* Web支付通知明细 Mapper.
*/
public interface OrderUserPurchasePayNoticeDAO extends BaseDAO<OrderUserPurchasePayNotice> {
}

View File

@ -0,0 +1,113 @@
package com.red.circle.order.infra.database.rds.entity.order;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serial;
import java.io.Serializable;
import java.math.BigDecimal;
import java.sql.Timestamp;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* Web支付预下单记录.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("order_user_purchase_pay")
public class OrderUserPurchasePay implements Serializable {
@Serial
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.INPUT)
private Long id;
@TableField("evn")
private String evn;
@TableField("sys_origin")
private String sysOrigin;
@TableField("user_id")
private Long userId;
@TableField("factory_code")
private String factoryCode;
@TableField("factory_order_id")
private String factoryOrderId;
@TableField("reference_id")
private String referenceId;
@TableField("receipt_type")
private String receiptType;
@TableField("payment_channel")
private String paymentChannel;
@TableField("factory_channel")
private String factoryChannel;
@TableField("product_code")
private String productCode;
@TableField("product_content")
private String productContent;
@TableField("product_descriptor")
private String productDescriptor;
@TableField("payment_unit")
private String paymentUnit;
@TableField("payment_amount")
private BigDecimal paymentAmount;
@TableField("compute_usd_amount")
private BigDecimal computeUsdAmount;
@TableField("compute_usd_rate")
private String computeUsdRate;
@TableField("give_away_content")
private String giveAwayContent;
@TableField("country_id")
private Long countryId;
@TableField("country_code")
private String countryCode;
@TableField("pay_country_id")
private Long payCountryId;
@TableField("pay_status")
private String payStatus;
@TableField("reason")
private String reason;
@TableField("refund_status")
private String refundStatus;
@TableField("receipt_details_id")
private Long receiptDetailsId;
@TableField("create_time")
private Timestamp createTime;
@TableField("update_time")
private Timestamp updateTime;
@TableField("create_user")
private Long createUser;
@TableField("update_user")
private Long updateUser;
}

View File

@ -0,0 +1,37 @@
package com.red.circle.order.infra.database.rds.entity.order;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.red.circle.framework.mybatis.entity.TimestampBaseEntity;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
/**
* Web支付通知明细.
*/
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("order_user_purchase_pay_notice")
public class OrderUserPurchasePayNotice extends TimestampBaseEntity {
private static final long serialVersionUID = 1L;
@TableId(value = "id", type = IdType.ASSIGN_ID)
private Long id;
@TableField("purchase_pay_id")
private Long purchasePayId;
@TableField("event_id")
private String eventId;
@TableField("notice_type")
private String noticeType;
@TableField("notice_data")
private String noticeData;
}

View File

@ -0,0 +1,16 @@
package com.red.circle.order.infra.database.rds.service.order;
import com.red.circle.framework.mybatis.service.BaseService;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePayNotice;
/**
* Web支付通知明细服务.
*/
public interface OrderUserPurchasePayNoticeService extends BaseService<OrderUserPurchasePayNotice> {
/**
* 保存通知.
*/
void addNotice(Long purchasePayId, String eventId, String noticeType, Object noticeData,
Long operationUserId);
}

View File

@ -0,0 +1,20 @@
package com.red.circle.order.infra.database.rds.service.order;
import com.red.circle.framework.mybatis.service.BaseService;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
/**
* Web支付预下单服务.
*/
public interface OrderUserPurchasePayService extends BaseService<OrderUserPurchasePay> {
/**
* 标记支付成功.
*/
void updateSuccess(Long id);
/**
* 标记支付失败.
*/
void updateFail(Long id, String reason);
}

View File

@ -0,0 +1,31 @@
package com.red.circle.order.infra.database.rds.service.order.impl;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import com.red.circle.order.infra.database.rds.dao.order.OrderUserPurchasePayNoticeDAO;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePayNotice;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayNoticeService;
import com.red.circle.tool.core.json.JacksonUtils;
import java.util.Objects;
import org.springframework.stereotype.Service;
/**
* Web支付通知明细服务实现.
*/
@Service
public class OrderUserPurchasePayNoticeServiceImpl
extends BaseServiceImpl<OrderUserPurchasePayNoticeDAO, OrderUserPurchasePayNotice>
implements OrderUserPurchasePayNoticeService {
@Override
public void addNotice(Long purchasePayId, String eventId, String noticeType, Object noticeData,
Long operationUserId) {
OrderUserPurchasePayNotice notice = new OrderUserPurchasePayNotice()
.setPurchasePayId(purchasePayId)
.setEventId(Objects.toString(eventId, ""))
.setNoticeType(Objects.toString(noticeType, ""))
.setNoticeData(JacksonUtils.toJson(noticeData));
notice.setCreateUser(operationUserId);
notice.setUpdateUser(operationUserId);
save(notice);
}
}

View File

@ -0,0 +1,40 @@
package com.red.circle.order.infra.database.rds.service.order.impl;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import com.red.circle.order.infra.database.rds.dao.order.OrderUserPurchasePayDAO;
import com.red.circle.order.infra.database.rds.entity.order.OrderUserPurchasePay;
import com.red.circle.order.infra.database.rds.service.order.OrderUserPurchasePayService;
import com.red.circle.tool.core.date.TimestampUtils;
import org.springframework.stereotype.Service;
/**
* Web支付预下单服务实现.
*/
@Service
public class OrderUserPurchasePayServiceImpl
extends BaseServiceImpl<OrderUserPurchasePayDAO, OrderUserPurchasePay>
implements OrderUserPurchasePayService {
private static final String PAY_STATUS_SUCCESSFUL = "SUCCESSFUL";
private static final String PAY_STATUS_FAIL = "FAIL";
@Override
public void updateSuccess(Long id) {
update()
.set(OrderUserPurchasePay::getPayStatus, PAY_STATUS_SUCCESSFUL)
.set(OrderUserPurchasePay::getReason, "")
.set(OrderUserPurchasePay::getUpdateTime, TimestampUtils.now())
.eq(OrderUserPurchasePay::getId, id)
.execute();
}
@Override
public void updateFail(Long id, String reason) {
update()
.set(OrderUserPurchasePay::getPayStatus, PAY_STATUS_FAIL)
.set(OrderUserPurchasePay::getReason, reason)
.set(OrderUserPurchasePay::getUpdateTime, TimestampUtils.now())
.eq(OrderUserPurchasePay::getId, id)
.execute();
}
}

View File

@ -7,6 +7,7 @@ import com.red.circle.other.app.dto.clientobject.gift.ActivityGiftCO;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.cache.service.other.BannerCacheService;
import com.red.circle.other.infra.database.mongo.entity.sys.ActivityConfig;
import com.red.circle.other.infra.database.mongo.service.sys.ActivityConfigService;
@ -39,6 +40,7 @@ public class ActivityGiftListQryExe {
private final BannerConfigService bannerConfigService;
private final UserProfileGateway userProfileGateway;
private final UserRegionGateway userRegionGateway;
private final CountryCodeAliasSupport countryCodeAliasSupport;
public List<ActivityGiftCO> execute(AppExtCommand cmd) {
// 获取进行中的活动
@ -82,7 +84,8 @@ public class ActivityGiftListQryExe {
.in(BannerConfig::getId, bannerIds);
bannerUrlMap = bannerConfigService.list(query).stream()
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getRegions()) || bannerCache.getRegions().contains(regionId))
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getCountryCode()) || bannerCache.getCountryCode().contains(countryCode))
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getCountryCode())
|| countryCodeAliasSupport.containsCode(bannerCache.getCountryCode(), countryCode))
.collect(Collectors.toMap(BannerConfig::getId, e -> e, (u1, u2) -> u1));
} else {
bannerUrlMap = new HashMap<>();

View File

@ -11,6 +11,7 @@ import com.red.circle.other.app.dto.clientobject.room.RoomBrowseRecordsV2CO;
import com.red.circle.other.app.dto.clientobject.room.RoomVoiceProfileCO;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.gateway.user.ability.UserSVipGateway;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.mongo.entity.activity.RankQueenCount;
import com.red.circle.other.infra.database.mongo.entity.activity.RankQueenType;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
@ -40,6 +41,7 @@ public class RoomGiftListQueryExe {
private final UserSVipGateway userSVipGateway;
private final RankCountService rankCountService;
private final UserRegionGateway userRegionGateway;
private final CountryCodeAliasSupport countryCodeAliasSupport;
private final RoomProfileAppConvertor roomProfileAppConvertor;
private final ActiveVoiceRoomService activeVoiceRoomService;
private final RoomVoiceProfileCommon roomVoiceProfileCommon;
@ -183,7 +185,8 @@ public class RoomGiftListQueryExe {
return assemblyRoomVoiceProfile(activeVoiceRooms, cmd);
}
List<String> excludeCountryCodes = List.of("SA", "AE", "EG", "MA", "US");
List<String> excludeCountryCodes = new ArrayList<>(
countryCodeAliasSupport.expandCodes(List.of("SA", "AE", "EG", "MA", "US")));
List<RoomBrowseRecordsV2CO> roomVoiceProfiles = Lists.newArrayList();
if (CollectionUtils.isEmpty(activeVoiceRooms)) {
List<RoomVoiceProfileCO> roomVoiceProfileCos = roomVoiceProfileCommon.listRoomVoiceProfilesV2(

View File

@ -15,6 +15,7 @@ import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.gateway.user.ability.UserSVipGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.domain.rocket.RocketStatus;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.cache.service.user.RegionRoomCacheService;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
@ -63,6 +64,7 @@ public class RoomVoiceDiscoverQryExe {
private final RocketStatusCacheService rocketStatusCacheService;
private final GameLudoService gameLudoService;
private final UserProfileGateway userProfileGateway;
private final CountryCodeAliasSupport countryCodeAliasSupport;
private static final String EMPTY_ROOM_CACHE_KEY = "empty_room_cache:*";
@ -261,7 +263,8 @@ public class RoomVoiceDiscoverQryExe {
return emptyRooms.stream()
.filter(room -> Objects.equals(sysOrigin, room.getSysOrigin()) &&
(isAllRegion || shouldShowRoom(region, room.getRegion())) &&
(!SA_REGIONS.contains(region) || userCountryCode == null || userCountryCode.equals(room.getCountryCode())))
(!SA_REGIONS.contains(region) || userCountryCode == null
|| countryCodeAliasSupport.matches(userCountryCode, room.getCountryCode())))
.collect(Collectors.toList());
}

View File

@ -10,6 +10,7 @@ import com.red.circle.other.app.dto.clientobject.sys.BannerCO;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.cache.entity.sys.BannerCache;
import com.red.circle.other.infra.database.cache.service.other.BannerCacheService;
import com.red.circle.other.infra.database.rds.entity.sys.BannerConfig;
@ -39,6 +40,7 @@ public class BannerQryExe {
private final SysConfigAppConvertor sysConfigAppConvertor;
private final OrderPurchaseHistoryClient orderPurchaseHistoryClient;
private final UserProfileGateway userProfileGateway;
private final CountryCodeAliasSupport countryCodeAliasSupport;
public List<BannerCO> execute(AppExtCommand cmd, List<String> typeList) {
UserProfile userProfile = userProfileGateway.getByUserId(cmd.requiredReqUserId());
@ -57,7 +59,8 @@ public class BannerQryExe {
return sysConfigAppConvertor.toListBannerCache(configs);
}).stream()
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getRegions()) || bannerCache.getRegions().contains(regionId))
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getCountryCode()) || bannerCache.getCountryCode().contains(countryCode))
.filter(bannerCache -> StringUtils.isBlank(bannerCache.getCountryCode())
|| countryCodeAliasSupport.containsCode(bannerCache.getCountryCode(), countryCode))
.toList();
return sysConfigAppConvertor.toListBannerCO(list);

View File

@ -2,6 +2,7 @@ package com.red.circle.other.app.command.sys.query;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.Lists;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.other.app.dto.clientobject.sys.CountryCodeCO;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
@ -43,6 +44,10 @@ public class SysCountryOpenTopQryExe {
.setAlphaThree(sysCountryCode.getAlphaThree())
.setCountryName(sysCountryCode.getCountryName())
.setAliasName(sysCountryCode.getAliasName())
.setCountryCodeAliases(CountryCodeAliasUtils.normalizeCodes(
CountryCodeAliasUtils.buildMatchCodes(sysCountryCode.getAlphaTwo(),
sysCountryCode.getAlphaThree(),
CountryCodeAliasUtils.parseAliasCodes(sysCountryCode.getAliasCodes()))))
.setNationalFlag(sysCountryCode.getNationalFlag())
.setPhonePrefix(sysCountryCode.getPhonePrefix())
.setOpen(sysCountryCode.getOpen())

View File

@ -372,8 +372,7 @@ public class GameLuckyGiftCommon {
private static int getRewardMultiple(GameLuckyGiftParam param, Long rewardAmount) {
long payAmount = param.getQuantity() * param.getGift().getGiftCandy().longValue();
return (int) (rewardAmount / payAmount);
return LuckyGiftMultipleCalculator.calculate(rewardAmount, param.getGift().getGiftCandy());
}
/**
@ -461,7 +460,7 @@ public class GameLuckyGiftCommon {
private void saveLuckGiftCount(GameLuckyGiftParam param, Long awardAmount) {
long payAmount = param.getQuantity() * param.getGift().getGiftCandy().longValue();
int multiple = (int) (awardAmount / payAmount);
int multiple = LuckyGiftMultipleCalculator.calculate(awardAmount, param.getGift().getGiftCandy());
gameLuckyGiftCountService.save(new GameLuckyGiftCount()
.setId(param.getId())

View File

@ -0,0 +1,22 @@
package com.red.circle.other.app.common.gift;
import java.math.BigDecimal;
final class LuckyGiftMultipleCalculator {
private LuckyGiftMultipleCalculator() {
}
static int calculate(Long rewardAmount, BigDecimal giftUnitPrice) {
if (rewardAmount == null || rewardAmount <= 0 || giftUnitPrice == null) {
return 0;
}
long unitPrice = giftUnitPrice.longValue();
if (unitPrice <= 0) {
return 0;
}
return (int) (rewardAmount / unitPrice);
}
}

View File

@ -81,7 +81,8 @@ public class LuckyGiftResultProcessor {
}
UserPropsResourcesDTO avatarFrame = getUserAvatarFrameProps(sendUserProfile);
long payAmount = resolvePayAmount(event, giftConfig);
BigDecimal giftUnitPrice = resolveGiftUnitPrice(event, giftConfig);
long payAmount = resolvePayAmount(event, giftUnitPrice);
BigDecimal balanceAfter = BigDecimal.valueOf(Objects.requireNonNullElse(response.getBalanceAfter(), 0L));
for (LuckyGiftGoClient.LuckyGiftGoDrawResult result : response.getResults()) {
@ -91,7 +92,7 @@ public class LuckyGiftResultProcessor {
}
long rewardNum = Objects.requireNonNullElse(result.getRewardNum(), 0L);
int multiple = payAmount > 0 && rewardNum > 0 ? (int) (rewardNum / payAmount) : 0;
int multiple = LuckyGiftMultipleCalculator.calculate(rewardNum, giftUnitPrice);
gameLuckyGiftCountService.saveOrUpdate(new GameLuckyGiftCount()
.setId(Long.valueOf(result.getId()))
@ -121,9 +122,15 @@ public class LuckyGiftResultProcessor {
}
}
private long resolvePayAmount(GameLuckyGiftBusinessEvent event, GiftConfigDTO giftConfig) {
BigDecimal giftPrice = Objects.nonNull(event.getGiftPrice()) ? event.getGiftPrice() : giftConfig.getGiftCandy();
return giftPrice.multiply(BigDecimal.valueOf(Objects.requireNonNullElse(event.getQuantity(), 0))).longValue();
private BigDecimal resolveGiftUnitPrice(GameLuckyGiftBusinessEvent event, GiftConfigDTO giftConfig) {
return Objects.nonNull(event.getGiftPrice()) ? event.getGiftPrice() : giftConfig.getGiftCandy();
}
private long resolvePayAmount(GameLuckyGiftBusinessEvent event, BigDecimal giftUnitPrice) {
if (giftUnitPrice == null) {
return 0L;
}
return giftUnitPrice.multiply(BigDecimal.valueOf(Objects.requireNonNullElse(event.getQuantity(), 0))).longValue();
}
private void sendLuckyGiftMessage(

View File

@ -151,6 +151,7 @@ public class RoomVoiceProfileCommon {
if (Objects.isNull(roomVoiceProfile)) {
return null;
}
roomVoiceProfile.putRoomMemberQuantity(toRoomMemberQuantity(activeVoiceRoom.getOnlineQuantity()));
roomVoiceProfile.setHotRoom(activeVoiceRoom.getHot());
roomVoiceProfile.setRoomGameIcon("");
roomVoiceProfile.setUserSVipLevel(SVIPLevelEnum.valueOf(activeVoiceRoom.getSuperVipLevel()));
@ -185,5 +186,12 @@ public class RoomVoiceProfileCommon {
&& !Objects.equals(manager.getEvent(), RoomEventEnum.CLOSE.name());
}
private Integer toRoomMemberQuantity(Long onlineQuantity) {
if (Objects.isNull(onlineQuantity) || onlineQuantity < 0) {
return 0;
}
return onlineQuantity > Integer.MAX_VALUE ? Integer.MAX_VALUE : onlineQuantity.intValue();
}
}

View File

@ -1,10 +1,12 @@
package com.red.circle.other.app.convertor.sys;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.convertor.ConvertorModel;
import com.red.circle.other.app.dto.clientobject.sys.CountryCodeCO;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @author pengliang on 2021/3/10
@ -14,5 +16,15 @@ public interface SysCountryAppConvertor {
List<CountryCodeCO> toListCountryCodeCO(List<SysCountryCode> countryCodes);
@Mapping(target = "countryCodeAliases", source = ".")
CountryCodeCO toCountryCodeCO(SysCountryCode countryCode);
default List<String> mapCountryCodeAliases(SysCountryCode countryCode) {
if (countryCode == null) {
return List.of();
}
return CountryCodeAliasUtils.normalizeCodes(
CountryCodeAliasUtils.buildMatchCodes(countryCode.getAlphaTwo(), countryCode.getAlphaThree(),
CountryCodeAliasUtils.parseAliasCodes(countryCode.getAliasCodes())));
}
}

View File

@ -1,75 +0,0 @@
// 路径: rc-service-other/other-application/src/main/java/com/red/circle/other/app/scheduler/EmptyRoomCleanTask.java
package com.red.circle.other.app.scheduler;
import com.alibaba.nacos.client.naming.utils.CollectionUtils;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.live.inner.endpoint.LiveMicClient;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService;
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 java.util.ArrayList;
import java.util.List;
/**
* 清理空房间定时任务
* 每2分钟清理一次DB中没人的房间
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "scheduler.room-empty-clean", havingValue = "true", matchIfMissing = true)
public class EmptyRoomCleanTask {
private final ActiveVoiceRoomService activeVoiceRoomService;
private final LiveMicClient liveMicClient;
/**
* 每2分钟清理一次空房间
*/
@Scheduled(cron = "0 */2 * * * ?")
@TaskCacheLock(key = "EMPTY_ROOM_CLEAN_TASK", expireSecond = 59 * 2)
public void cleanEmptyRooms() {
try {
// 获取所有活跃房间
List<ActiveVoiceRoom> allRooms = activeVoiceRoomService.listDiscover(SysOriginPlatformEnum.LIKEI.name(),true,"",null,200);
if (CollectionUtils.isEmpty(allRooms)) {
return;
}
List<Long> emptyRoomIds = new ArrayList<>();
// 检查每个房间的在线人数
for (ActiveVoiceRoom room : allRooms) {
// 跳过固定权重的房间这些是固定展示的房间
if (room.getFixedWeights() != 0) {
continue;
}
try {
Long roomUserCount = liveMicClient.getLiveRoomUserSize(room.getId()).getBody();
if (roomUserCount != null && roomUserCount == 0) {
emptyRoomIds.add(room.getId());
}
} catch (Exception e) {
}
}
// 批量删除空房间
if (!CollectionUtils.isEmpty(emptyRoomIds)) {
activeVoiceRoomService.removeByIds(emptyRoomIds);
log.info("清理空房间完成,删除房间数量: {}, 房间ID: {}", emptyRoomIds.size(), emptyRoomIds);
} else {
log.warn("清理空房间失败, 无法获取房间在线人数");
}
} catch (Exception e) {
log.error("清理空房间任务执行失败", e);
}
}
}

View File

@ -0,0 +1,92 @@
package com.red.circle.other.app.scheduler;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.component.redis.annotation.TaskCacheLock;
import com.red.circle.live.inner.endpoint.LiveMicClient;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService;
import com.red.circle.tool.core.collection.CollectionUtils;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
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 java.util.List;
/**
* 定时对账在线房间人数修正 ActiveVoiceRoom.onlineQuantity 漂移
*/
@Slf4j
@Component
@RequiredArgsConstructor
@ConditionalOnProperty(name = "scheduler.room-online-quantity-reconcile", havingValue = "true", matchIfMissing = true)
public class RoomOnlineQuantityReconcileTask {
private static final int PER_SYS_ORIGIN_LIMIT = 200;
private final ActiveVoiceRoomService activeVoiceRoomService;
private final LiveMicClient liveMicClient;
@Scheduled(cron = "0 */2 * * * ?")
@TaskCacheLock(key = "ROOM_ONLINE_QUANTITY_RECONCILE_TASK", expireSecond = 59 * 2)
public void reconcileOnlineQuantity() {
try {
List<ActiveVoiceRoom> activeRooms = listActiveRooms();
if (CollectionUtils.isEmpty(activeRooms)) {
return;
}
int correctedCount = 0;
int failedCount = 0;
for (ActiveVoiceRoom room : activeRooms) {
if (Objects.isNull(room.getId()) || Objects.isNull(room.getRoomAccount())) {
continue;
}
try {
Long actualQuantity = liveMicClient.getLiveRoomUserSize(room.getId()).getBody();
actualQuantity = Objects.nonNull(actualQuantity) ? actualQuantity : 0L;
Long currentQuantity = Objects.nonNull(room.getOnlineQuantity()) ? room.getOnlineQuantity() : 0L;
if (Objects.equals(currentQuantity, actualQuantity)) {
continue;
}
activeVoiceRoomService.updateQuantityByRoomAccount(room.getRoomAccount(), actualQuantity);
correctedCount++;
log.info("reconcile room online quantity, roomId={}, roomAccount={}, currentQuantity={}, actualQuantity={}",
room.getId(), room.getRoomAccount(), currentQuantity, actualQuantity);
} catch (Exception e) {
failedCount++;
log.warn("reconcile room online quantity failed, roomId={}, roomAccount={}",
room.getId(), room.getRoomAccount(), e);
}
}
log.info("room online quantity reconcile finished, scanCount={}, correctedCount={}, failedCount={}",
activeRooms.size(), correctedCount, failedCount);
} catch (Exception e) {
log.error("room online quantity reconcile task failed", e);
}
}
private List<ActiveVoiceRoom> listActiveRooms() {
Map<Long, ActiveVoiceRoom> rooms = new LinkedHashMap<>();
for (SysOriginPlatformEnum sysOrigin : SysOriginPlatformEnum.getVoiceSystems()) {
List<ActiveVoiceRoom> currentRooms = activeVoiceRoomService.listDiscover(
sysOrigin.name(),
true,
"",
null,
PER_SYS_ORIGIN_LIMIT
);
if (CollectionUtils.isEmpty(currentRooms)) {
continue;
}
currentRooms.forEach(room -> rooms.putIfAbsent(room.getId(), room));
}
return List.copyOf(rooms.values());
}
}

View File

@ -0,0 +1,43 @@
package com.red.circle.other.app.command.party3rd.callback.trtc;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.external.inner.model.callback.trtc.EventInfo;
import com.red.circle.external.inner.model.callback.trtc.TrtcCallbackEvent;
import com.red.circle.other.app.common.live.OnlineRoomCommon;
import com.red.circle.other.infra.database.cache.service.other.RoomManagerCacheService;
import com.red.circle.other.infra.database.mongo.service.live.ActiveVoiceRoomService;
import org.junit.jupiter.api.Test;
class EnterRoomCallbackStrategyTest {
@Test
void doOperation_shouldUpdateOnlineQuantityWithIncrementedRoomUserSize() {
OnlineRoomCommon onlineRoomCommon = mock(OnlineRoomCommon.class);
ActiveVoiceRoomService activeVoiceRoomService = mock(ActiveVoiceRoomService.class);
RoomManagerCacheService roomManagerCacheService = mock(RoomManagerCacheService.class);
EnterRoomCallbackStrategy strategy = new EnterRoomCallbackStrategy(
onlineRoomCommon,
activeVoiceRoomService,
roomManagerCacheService
);
TrtcCallbackEvent event = new TrtcCallbackEvent();
EventInfo eventInfo = new EventInfo();
eventInfo.setRoomId("123456");
eventInfo.setUserId("9527");
eventInfo.setEventTs("1710000000");
event.setEventInfo(eventInfo);
when(activeVoiceRoomService.existsByRoomAccount("123456")).thenReturn(true);
when(roomManagerCacheService.incrementNumberPeople("123456")).thenReturn(6L);
strategy.doOperation(event);
verify(activeVoiceRoomService).updateQuantityByRoomAccount("123456", 6L);
verify(onlineRoomCommon, never()).addOnlineRoomByAccount("123456");
}
}

View File

@ -0,0 +1,126 @@
package com.red.circle.other.app.common.gift;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.red.circle.component.redis.service.RedisService;
import com.red.circle.external.inner.endpoint.message.ImGroupClient;
import com.red.circle.external.inner.model.cmd.message.BroadcastGroupMsgBodyCmd;
import com.red.circle.mq.business.model.event.gift.GameLuckyGiftBusinessEvent;
import com.red.circle.mq.business.model.event.gift.GameLuckyGiftUser;
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
import com.red.circle.other.app.dto.clientobject.game.GameLuckyGiftMsgCO;
import com.red.circle.other.app.service.task.RoomDailyTaskProgressService;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.infra.database.cache.service.other.GiftCacheService;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.infra.database.rds.entity.game.GameLuckyGiftCount;
import com.red.circle.other.infra.database.rds.service.game.GameLuckyGiftCountService;
import com.red.circle.other.infra.database.rds.service.live.RoomMemberService;
import com.red.circle.other.inner.model.dto.material.GiftConfigDTO;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import java.math.BigDecimal;
import java.util.List;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
class LuckyGiftResultProcessorTest {
@Test
void process_shouldUseGiftUnitPriceToCalculateMultiple() {
GameLuckyGiftCountService gameLuckyGiftCountService = mock(GameLuckyGiftCountService.class);
GiftCacheService giftCacheService = mock(GiftCacheService.class);
UserProfileGateway userProfileGateway = mock(UserProfileGateway.class);
UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
ImGroupClient imGroupClient = mock(ImGroupClient.class);
RoomDailyTaskProgressService roomDailyTaskProgressService = mock(RoomDailyTaskProgressService.class);
RoomMemberService roomMemberService = mock(RoomMemberService.class);
RedisService redisService = mock(RedisService.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
GameLuckyGiftCommon gameLuckyGiftCommon = mock(GameLuckyGiftCommon.class);
LuckyGiftResultProcessor processor = new LuckyGiftResultProcessor(
gameLuckyGiftCountService,
giftCacheService,
userProfileGateway,
userProfileAppConvertor,
imGroupClient,
roomDailyTaskProgressService,
roomMemberService,
redisService,
roomProfileManagerService,
gameLuckyGiftCommon
);
GiftConfigDTO giftConfig = new GiftConfigDTO()
.setId(11L)
.setGiftCandy(BigDecimal.valueOf(173))
.setGiftPhoto("gift.png");
UserProfile senderProfile = new UserProfile();
senderProfile.setId(1001L);
senderProfile.setAccount("sender001");
senderProfile.setUserNickname("sender");
UserProfile acceptProfile = new UserProfile();
acceptProfile.setId(2002L);
acceptProfile.setUserNickname("receiver");
UserProfileDTO senderProfileDTO = new UserProfileDTO();
senderProfileDTO.setId(1001L);
senderProfileDTO.setAccount("sender001");
senderProfileDTO.setUserNickname("sender");
senderProfileDTO.setUserAvatar("sender.png");
when(giftCacheService.getById(11L)).thenReturn(giftConfig);
when(userProfileGateway.getByUserId(1001L)).thenReturn(senderProfile);
when(userProfileGateway.getByUserId(2002L)).thenReturn(acceptProfile);
when(userProfileAppConvertor.toUserProfileDTO(senderProfile)).thenReturn(senderProfileDTO);
when(gameLuckyGiftCommon.getRewardMultipleType(20)).thenReturn("SUPER");
when(roomMemberService.getRoomMember(3003L, 1001L)).thenReturn(null);
GameLuckyGiftBusinessEvent event = new GameLuckyGiftBusinessEvent()
.setBusinessId("biz-001")
.setUserId(1001L)
.setRoomId(3003L)
.setRoomAccount("room-account")
.setSysOrigin("LIKEI")
.setGiftId(11L)
.setGiftPrice(BigDecimal.valueOf(173))
.setQuantity(4)
.setGiftCombos(0)
.setRegionCode("SA")
.setUsers(List.of(
new GameLuckyGiftUser()
.setId(90001L)
.setAcceptUserId(2002L)
));
LuckyGiftGoClient.LuckyGiftGoDrawResponse response = new LuckyGiftGoClient.LuckyGiftGoDrawResponse()
.setBusinessId("biz-001")
.setBalanceAfter(999999L)
.setResults(List.of(
new LuckyGiftGoClient.LuckyGiftGoDrawResult()
.setId("90001")
.setAcceptUserId(2002L)
.setIsWin(true)
.setRewardNum(3460L)
));
processor.process(event, response);
ArgumentCaptor<GameLuckyGiftCount> countCaptor = ArgumentCaptor.forClass(GameLuckyGiftCount.class);
verify(gameLuckyGiftCountService).saveOrUpdate(countCaptor.capture());
GameLuckyGiftCount saved = countCaptor.getValue();
assertEquals(Long.valueOf(692L), saved.getPayAmount());
assertEquals(Integer.valueOf(20), saved.getMultiple());
assertEquals(Long.valueOf(3460L), saved.getAwardAmount());
ArgumentCaptor<BroadcastGroupMsgBodyCmd> messageCaptor = ArgumentCaptor.forClass(BroadcastGroupMsgBodyCmd.class);
verify(imGroupClient).sendMessageBroadcast(messageCaptor.capture());
GameLuckyGiftMsgCO message = (GameLuckyGiftMsgCO) messageCaptor.getValue().getData();
assertEquals(Integer.valueOf(20), message.getMultiple());
assertEquals(Long.valueOf(3460L), message.getAwardAmount());
}
}

View File

@ -0,0 +1,105 @@
package com.red.circle.other.app.common.room;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import com.red.circle.other.app.convertor.live.RoomProfileAppConvertor;
import com.red.circle.other.app.convertor.user.UserProfileAppConvertor;
import com.red.circle.other.app.dto.clientobject.family.RoomSettingCO;
import com.red.circle.other.app.dto.clientobject.room.RoomVoiceProfileCO;
import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
import com.red.circle.other.infra.database.mongo.entity.live.RoomCounter;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfileManager;
import com.red.circle.other.infra.database.mongo.entity.live.RoomSetting;
import com.red.circle.other.infra.database.mongo.service.live.RoomProfileManagerService;
import com.red.circle.other.inner.enums.room.RoomEventEnum;
import com.red.circle.other.inner.model.dto.user.UserProfileDTO;
import java.lang.reflect.Field;
import java.util.List;
import java.util.Map;
import org.junit.jupiter.api.Test;
class RoomVoiceProfileCommonTest {
@Test
void toListRoomVoiceProfileCO_shouldUseActiveVoiceRoomOnlineQuantityAsMemberQuantity() {
UserProfileGateway userProfileGateway = mock(UserProfileGateway.class);
UserProfileAppConvertor userProfileAppConvertor = mock(UserProfileAppConvertor.class);
RoomProfileAppConvertor roomProfileAppConvertor = mock(RoomProfileAppConvertor.class);
RoomProfileManagerService roomProfileManagerService = mock(RoomProfileManagerService.class);
RoomVoiceProfileCommon roomVoiceProfileCommon = new RoomVoiceProfileCommon(
userProfileGateway,
userProfileAppConvertor,
roomProfileAppConvertor,
roomProfileManagerService
);
ActiveVoiceRoom activeVoiceRoom = new ActiveVoiceRoom()
.setId(10001L)
.setUserId(20001L)
.setOnlineQuantity(6L)
.setSuperVipLevel("NONE");
RoomProfileManager roomProfileManager = new RoomProfileManager();
roomProfileManager.setId(10001L);
roomProfileManager.setUserId(20001L);
roomProfileManager.setDel(Boolean.FALSE);
roomProfileManager.setEvent(RoomEventEnum.AVAILABLE.name());
roomProfileManager.setSetting(RoomSetting.createDefaultRoomSetting());
roomProfileManager.setCounter(new RoomCounter().setMemberCount(1).setAdminCount(0));
UserProfileDTO userProfileDTO = new UserProfileDTO();
userProfileDTO.setId(20001L);
RoomVoiceProfileCO convertedProfile = new RoomVoiceProfileCO()
.setId(10001L)
.setUserId(20001L);
when(roomProfileManagerService.mapByRoomIds(java.util.Set.of(10001L)))
.thenReturn(Map.of(10001L, roomProfileManager));
when(userProfileGateway.mapByUserIds(java.util.Set.of(20001L)))
.thenReturn(Map.of());
when(userProfileAppConvertor.toMapUserProfileDTO(anyMap()))
.thenReturn(Map.of(20001L, userProfileDTO));
when(roomProfileAppConvertor.toRoomVoiceProfileCO(roomProfileManager))
.thenReturn(convertedProfile);
when(roomProfileAppConvertor.toRoomSettingCO(roomProfileManager.getSetting()))
.thenReturn(new RoomSettingCO());
List<RoomVoiceProfileCO> roomProfiles = roomVoiceProfileCommon.toListRoomVoiceProfileCO(
List.of(activeVoiceRoom));
assertEquals(1, roomProfiles.size());
assertEquals("6", getExtValue(roomProfiles.get(0), "memberQuantity"));
}
private Object getExtValue(RoomVoiceProfileCO roomVoiceProfileCO, String key) {
Map<?, ?> extValues = findExtValues(roomVoiceProfileCO);
assertNotNull(extValues, "extValues should exist on RoomVoiceProfileCO");
return extValues.get(key);
}
private Map<?, ?> findExtValues(Object target) {
Class<?> current = target.getClass();
while (current != null) {
try {
Field field = current.getDeclaredField("extValues");
field.setAccessible(true);
Object value = field.get(target);
if (value instanceof Map<?, ?> extValues) {
return extValues;
}
return null;
} catch (NoSuchFieldException ignored) {
current = current.getSuperclass();
} catch (IllegalAccessException e) {
throw new IllegalStateException("read extValues failed", e);
}
}
return null;
}
}

View File

@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.red.circle.framework.dto.ClientObject;
import java.util.List;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
@ -45,6 +46,11 @@ public class CountryCodeCO extends ClientObject {
*/
private String aliasName;
/**
* 国家码别名集合.
*/
private List<String> countryCodeAliases;
/**
* 国旗.
*/

View File

@ -0,0 +1,82 @@
package com.red.circle.other.infra.common.sys;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.infra.database.rds.service.sys.SysCountryCodeService;
import com.red.circle.tool.core.text.StringUtils;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
/**
* 国家码别名支持.
*/
@Component
@RequiredArgsConstructor
public class CountryCodeAliasSupport {
private final SysCountryCodeService sysCountryCodeService;
/**
* 获取某个国家码的全部可匹配代码.
*/
public Set<String> getMatchCodes(String code) {
String normalized = CountryCodeAliasUtils.normalize(code);
if (StringUtils.isBlank(normalized)) {
return Collections.emptySet();
}
SysCountryCode sysCountryCode = sysCountryCodeService.getByCode(normalized);
if (sysCountryCode == null) {
return CountryCodeAliasUtils.legacyAliases(normalized);
}
return CountryCodeAliasUtils.buildMatchCodes(sysCountryCode.getAlphaTwo(),
sysCountryCode.getAlphaThree(),
CountryCodeAliasUtils.parseAliasCodes(sysCountryCode.getAliasCodes()));
}
/**
* 扩展多组国家码.
*/
public Set<String> expandCodes(Collection<String> codes) {
if (codes == null || codes.isEmpty()) {
return Collections.emptySet();
}
LinkedHashSet<String> result = new LinkedHashSet<>();
for (String code : codes) {
result.addAll(getMatchCodes(code));
}
return result;
}
/**
* 两个国家码是否匹配.
*/
public boolean matches(String leftCode, String rightCode) {
Set<String> leftCodes = getMatchCodes(leftCode);
Set<String> rightCodes = getMatchCodes(rightCode);
if (leftCodes.isEmpty() || rightCodes.isEmpty()) {
return false;
}
return leftCodes.stream().anyMatch(rightCodes::contains);
}
/**
* 逗号分隔国家列表是否包含指定国家码.
*/
public boolean containsCode(String csvCodes, String countryCode) {
if (StringUtils.isBlank(csvCodes) || StringUtils.isBlank(countryCode)) {
return false;
}
return Arrays.stream(csvCodes.split(","))
.map(CountryCodeAliasUtils::normalize)
.filter(StringUtils::isNotBlank)
.anyMatch(code -> matches(code, countryCode));
}
}

View File

@ -1,5 +1,6 @@
package com.red.circle.other.infra.convertor.sys;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.convertor.ConvertorModel;
import com.red.circle.other.infra.database.rds.entity.sys.SysCountryCode;
import com.red.circle.other.inner.model.cmd.sys.SysCountryCodeCmd;
@ -7,6 +8,7 @@ import com.red.circle.other.inner.model.dto.sys.SysCountryCodeDTO;
import java.util.List;
import java.util.Map;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
/**
* @author pengliang on 2023/5/24
@ -14,6 +16,7 @@ import org.mapstruct.Mapper;
@Mapper(componentModel = ConvertorModel.SPRING)
public interface CountryCodeInnerConvertor {
@Mapping(target = "countryCodeAliases", source = ".")
SysCountryCodeDTO toSysCountryCodeDTO(SysCountryCode sysCountryCode);
List<SysCountryCodeDTO> toListSysCountryCodeDTO(List<SysCountryCode> sysCountryCodes);
@ -21,6 +24,20 @@ public interface CountryCodeInnerConvertor {
Map<Long, SysCountryCodeDTO> toMapLongSysCountryCodeDTO(
Map<Long, SysCountryCode> sysCountryCodeMap);
@Mapping(target = "aliasCodes", source = "countryCodeAliases")
SysCountryCode toSysCountryCode(SysCountryCodeCmd cmd);
default List<String> mapCountryCodeAliases(SysCountryCode sysCountryCode) {
if (sysCountryCode == null) {
return List.of();
}
return CountryCodeAliasUtils.normalizeCodes(
CountryCodeAliasUtils.buildMatchCodes(sysCountryCode.getAlphaTwo(), sysCountryCode.getAlphaThree(),
CountryCodeAliasUtils.parseAliasCodes(sysCountryCode.getAliasCodes())));
}
default String mapCountryCodeAliases(List<String> countryCodeAliases) {
return CountryCodeAliasUtils.toAliasCodesJson(countryCodeAliases);
}
}

View File

@ -3,6 +3,7 @@ package com.red.circle.other.infra.database.mongo.service.live.impl;
import com.alibaba.fastjson.JSON;
import com.red.circle.common.business.core.enums.SysOriginPlatformEnum;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.mongo.dto.query.live.ActiveVoiceRoomQuery;
import com.red.circle.other.infra.database.mongo.dto.query.live.FamilyOnlineRoomQuery;
import com.red.circle.other.infra.database.mongo.entity.live.ActiveVoiceRoom;
@ -35,6 +36,7 @@ import org.springframework.stereotype.Service;
public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
private final MongoTemplate mongoTemplate;
private final CountryCodeAliasSupport countryCodeAliasSupport;
private static final Set<String> TR_REGIONS = Set.of("TR");
private static final Set<String> SA_REGIONS = Set.of("BD","SA", "IN", "PK");
@ -117,7 +119,7 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
}
if (StringUtils.isNotBlank(qryCmd.getCountryCode())) {
criteria.and("countryCode").is(qryCmd.getCountryCode());
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(qryCmd.getCountryCode()));
}
Query query = Query.query(criteria);
@ -150,7 +152,7 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
}
// SA_REGIONS 用户只能看到相同国家的房间
if (SA_REGIONS.contains(region) && !isAllRegion && StringUtils.isNotBlank(countryCode)) {
criteria.and("countryCode").is(countryCode);
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(countryCode));
}
Aggregation aggregation = Aggregation.newAggregation(
// 1. 匹配国家可选条件
@ -238,7 +240,8 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
Integer limit) {
Query query = Query.query(
Criteria.where("sysOrigin").is(sysOrigin).and("countryCode").is(countryCode)
Criteria.where("sysOrigin").is(sysOrigin)
.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(countryCode))
);
return mongoTemplate.find(query.limit(limit), ActiveVoiceRoom.class);
@ -261,7 +264,7 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
public List<ActiveVoiceRoom> listExcludeCountry(AppActiveVoiceRoomQryCmd query) {
Criteria criteria = Criteria.where("sysOrigin").is(query.getSysOrigin());
if (StringUtils.isNotBlank(query.getCountryCode())) {
criteria.and("countryCode").nin(query.getCountryCode());
criteria.and("countryCode").nin(countryCodeAliasSupport.getMatchCodes(query.getCountryCode()));
criteria.and("region").nin("AR", "TR");
}
Query mQuery = Query.query(criteria);
@ -279,7 +282,7 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
}
if (StringUtils.isNotBlank(query.getCountryCode())) {
criteria.and("countryCode").is(query.getCountryCode());
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(query.getCountryCode()));
}
if (StringUtils.isNotBlank(query.getRoomTag())) {
@ -318,7 +321,8 @@ public class ActiveVoiceRoomServiceImpl implements ActiveVoiceRoomService {
if (Objects.nonNull(query.getRoomId())) {
criteria.and("roomId").is(query.getRoomId());
}
criteria.and("countryCode").nin("SA", "AE", "EG", "MA", "US");
criteria.and("countryCode").nin(
countryCodeAliasSupport.expandCodes(List.of("SA", "AE", "EG", "MA", "US")));
if (StringUtils.isNotBlank(query.getRoomTag())) {
criteria.and("roomTag").is(query.getRoomTag());

View File

@ -6,6 +6,7 @@ import com.mongodb.BasicDBObject;
import com.red.circle.common.business.core.ReplaceString;
import com.red.circle.common.business.core.SensitiveWordFilter;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.cache.service.other.RoomManagerCacheService;
import com.red.circle.other.infra.database.mongo.entity.live.RoomCounter;
import com.red.circle.other.infra.database.mongo.entity.live.RoomProfile;
@ -57,6 +58,7 @@ public class RoomProfileManagerServiceImpl implements RoomProfileManagerService
private final RedisTemplate<String, String> redisTemplate;
private final ActiveVoiceRoomService activeVoiceRoomService;
private final RoomManagerCacheService roomManagerCacheService;
private final CountryCodeAliasSupport countryCodeAliasSupport;
/**
* Redis缓存Key前缀
@ -265,7 +267,7 @@ public class RoomProfileManagerServiceImpl implements RoomProfileManagerService
Criteria criteria = Criteria.where("sysOrigin").in(sysOrigin)
.and("del").is(Boolean.FALSE);
if (StringUtils.isNotBlank(countryCode)) {
criteria.and("countryCode").is(countryCode);
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(countryCode));
}
if (CollectionUtils.isNotEmpty(excludeIds)) {
@ -284,7 +286,7 @@ public class RoomProfileManagerServiceImpl implements RoomProfileManagerService
Criteria criteria = Criteria.where("sysOrigin").in(sysOrigin);
if (CollectionUtils.isNotEmpty(countryCodes)) {
criteria.and("countryCode").nin(countryCodes);
criteria.and("countryCode").nin(countryCodeAliasSupport.expandCodes(countryCodes));
}
if (CollectionUtils.isNotEmpty(excludeIds)) {

View File

@ -1,6 +1,7 @@
package com.red.circle.other.infra.database.mongo.service.team.team.impl;
import com.google.common.collect.Maps;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.mongo.dto.team.TeamPolicy;
import com.red.circle.other.infra.database.mongo.entity.team.team.TeamPolicyManager;
import com.red.circle.other.infra.database.mongo.service.team.team.TeamPolicyManagerService;
@ -28,6 +29,7 @@ import java.util.stream.Collectors;
public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
private final MongoTemplate mongoTemplate;
private final CountryCodeAliasSupport countryCodeAliasSupport;
@Override
public List<TeamPolicyManager> listSysOriginRegion(String sysOrigin, String region) {
@ -58,7 +60,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
criteria.and("policyType").ne(TeamPolicyTypeEnum.SALARY_DIAMOND.name());
}
if (StringUtils.isNotBlank(countryCode)) {
criteria.and("countryCode").is(countryCode);
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(countryCode));
}
return mongoTemplate.find( Query.query(criteria).with(Sort.by(Sort.Order.desc("createTime"))), TeamPolicyManager.class);
@ -97,7 +99,7 @@ public class TeamPolicyManagerServiceImpl implements TeamPolicyManagerService {
criteria.and("policyType").ne(TeamPolicyTypeEnum.SALARY_DIAMOND.name());
}
if (StringUtils.isNotBlank(countryCode)) {
criteria.and("countryCode").is(countryCode);
criteria.and("countryCode").in(countryCodeAliasSupport.getMatchCodes(countryCode));
}
return mongoTemplate.findOne(Query.query(criteria), TeamPolicyManager.class);

View File

@ -64,6 +64,12 @@ public class SysCountryCode extends TimestampBaseEntity {
@TableField("alias_name")
private String aliasName;
/**
* 国家码别名集合(JSON数组).
*/
@TableField("alias_codes")
private String aliasCodes;
/**
* 代码分配情况.
*/

View File

@ -1,8 +1,10 @@
package com.red.circle.other.infra.database.rds.service.sys.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.red.circle.framework.dto.PageResult;
import com.red.circle.framework.mybatis.constant.PageConstant;
import com.red.circle.framework.mybatis.service.impl.BaseServiceImpl;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.database.rds.dao.sys.BannerConfigDAO;
import com.red.circle.other.infra.database.rds.entity.sys.BannerConfig;
import com.red.circle.other.infra.database.rds.service.sys.BannerConfigService;
@ -11,9 +13,11 @@ import com.red.circle.tool.core.collection.CollectionUtils;
import com.red.circle.tool.core.date.TimestampUtils;
import com.red.circle.tool.core.text.StringUtils;
import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
/**
@ -25,9 +29,12 @@ import org.springframework.stereotype.Service;
* @since 2021-01-26
*/
@Service
@RequiredArgsConstructor
public class BannerConfigServiceImpl extends
BaseServiceImpl<BannerConfigDAO, BannerConfig> implements BannerConfigService {
private final CountryCodeAliasSupport countryCodeAliasSupport;
@Override
public List<BannerConfig> listEffective(String sysOrigin, String platform, String countryCode, List<String> types) {
return query()
@ -37,7 +44,7 @@ public class BannerConfigServiceImpl extends
wrapper.in(BannerConfig::getPlatform, "", platform)
)
.and(StringUtils.isNotBlank(countryCode), wrapper ->
wrapper.apply("(country_code = '' OR FIND_IN_SET({0}, country_code))", countryCode)
applyCountryCodeFilter(wrapper, countryCode)
)
.gt(BannerConfig::getExpiredTime, LocalDateTime.now())
.eq(BannerConfig::getShowcase, Boolean.TRUE)
@ -53,7 +60,7 @@ public class BannerConfigServiceImpl extends
wrapper.in(BannerConfig::getPlatform, "", query.getPlatform())
)
.and(StringUtils.isNotBlank(query.getCountryCode()), wrapper ->
wrapper.apply("(country_code = '' OR FIND_IN_SET({0}, country_code))", query.getCountryCode())
applyCountryCodeFilter(wrapper, query.getCountryCode())
)
.eq(Objects.equals(query.getShowcase(), 1), BannerConfig::getShowcase, Boolean.TRUE)
.gt(Objects.equals(query.getShowcase(), 1), BannerConfig::getExpiredTime, TimestampUtils.now())
@ -87,4 +94,17 @@ public class BannerConfigServiceImpl extends
.execute();
}
private void applyCountryCodeFilter(LambdaQueryWrapper<BannerConfig> wrapper, String countryCode) {
Set<String> countryCodes = countryCodeAliasSupport.getMatchCodes(countryCode);
List<String> aliasList = new ArrayList<>(countryCodes);
StringBuilder sql = new StringBuilder("(country_code = ''");
Object[] params = new Object[aliasList.size()];
for (int i = 0; i < aliasList.size(); i++) {
sql.append(" OR FIND_IN_SET({").append(i).append("}, country_code)");
params[i] = aliasList.get(i);
}
sql.append(")");
wrapper.apply(sql.toString(), params);
}
}

View File

@ -1,5 +1,6 @@
package com.red.circle.other.infra.database.rds.service.sys.impl;
import com.red.circle.common.business.core.util.CountryCodeAliasUtils;
import com.red.circle.framework.core.asserts.ResponseAssert;
import com.red.circle.framework.core.response.CommonErrorCode;
import com.red.circle.framework.dto.PageResult;
@ -40,12 +41,31 @@ public class SysCountryCodeServiceImpl extends
return null;
}
String normalizedCode = CountryCodeAliasUtils.normalize(code);
SysCountryCode countryCode = getByExactCode(normalizedCode);
if (Objects.nonNull(countryCode)) {
return countryCode;
}
return query().list().stream()
.filter(item -> CountryCodeAliasUtils
.buildMatchCodes(item.getAlphaTwo(), item.getAlphaThree(),
CountryCodeAliasUtils.parseAliasCodes(item.getAliasCodes()))
.contains(normalizedCode))
.findFirst()
.orElse(null);
}
private SysCountryCode getByExactCode(String code) {
if (StringUtils.isBlank(code)) {
return null;
}
int len = code.length();
boolean isTwo = Objects.equals(len, NumConstant.TWO);
String upperCaseCode = code.toUpperCase();
return query()
.eq(isTwo, SysCountryCode::getAlphaTwo, upperCaseCode)
.eq(!isTwo, SysCountryCode::getAlphaThree, upperCaseCode)
.eq(isTwo, SysCountryCode::getAlphaTwo, code)
.eq(!isTwo, SysCountryCode::getAlphaThree, code)
.last(PageConstant.LIMIT_ONE)
.getOne();
}

View File

@ -7,6 +7,7 @@ import com.red.circle.other.domain.gateway.user.UserProfileGateway;
import com.red.circle.other.domain.gateway.user.ability.UserRegionGateway;
import com.red.circle.other.domain.model.user.UserProfile;
import com.red.circle.other.domain.model.user.ability.RegionConfig;
import com.red.circle.other.infra.common.sys.CountryCodeAliasSupport;
import com.red.circle.other.infra.convertor.user.UserRegionInfraConvertor;
import com.red.circle.other.infra.database.cache.service.user.SysRegionCacheService;
import com.red.circle.other.infra.database.cache.service.user.UserRegionCacheService;
@ -54,6 +55,7 @@ public class UserRegionGatewayImpl implements UserRegionGateway {
private final UserRegionInfraConvertor userRegionInfraConvertor;
private final SysRegionAssistConfigService sysRegionAssistConfigService;
private final UserRegionCacheService userRegionCacheService;
private final CountryCodeAliasSupport countryCodeAliasSupport;
/**
* key=用户id, value=区域code 获取一组区域code.
@ -185,9 +187,9 @@ public class UserRegionGatewayImpl implements UserRegionGateway {
// 2.1 国家匹配
if (StringUtils.isNotBlank(userProfile.getCountryCode())) {
SysRegionConfig regionConfig = configs.stream()
.filter(region -> StringUtils.containsIgnoreCase(region.getCountryCodes(),
userProfile.getCountryCode())
).findFirst().orElse(null);
.filter(region -> countryCodeAliasSupport.containsCode(region.getCountryCodes(),
userProfile.getCountryCode()))
.findFirst().orElse(null);
UserRegionDTO userRegion = createUserRegionDTO(userProfile, regionConfig);
if (Objects.nonNull(userRegion)) {
userRegions.add(userRegion);
@ -907,8 +909,8 @@ public class UserRegionGatewayImpl implements UserRegionGateway {
SysRegionConfig sysRegionConfig = configs.stream()
.filter(region -> StringUtils.isNotBlank(region.getCountryCodes())
&& StringUtils.isNotBlank(userProfile.getCountryCode())
&& region.getCountryCodes().toLowerCase()
.contains(userProfile.getCountryCode().toLowerCase())).findFirst()
&& countryCodeAliasSupport.containsCode(region.getCountryCodes(),
userProfile.getCountryCode())).findFirst()
.orElse(null);
if (Objects.nonNull(sysRegionConfig)) {

View File

@ -0,0 +1,64 @@
SET @table_schema = DATABASE();
SET @ddl = IF(
EXISTS(
SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @table_schema
AND TABLE_NAME = 'order_user_purchase_pay'
AND COLUMN_NAME = 'receipt_type'
),
'SELECT 1',
"ALTER TABLE `order_user_purchase_pay` ADD COLUMN `receipt_type` varchar(32) NOT NULL DEFAULT 'PAYMENT' COMMENT '单据类型 PAYMENT/RECEIPT' AFTER `reference_id`"
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
EXISTS(
SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @table_schema
AND TABLE_NAME = 'order_user_purchase_pay'
AND COLUMN_NAME = 'product_descriptor'
),
'SELECT 1',
"ALTER TABLE `order_user_purchase_pay` ADD COLUMN `product_descriptor` varchar(255) NOT NULL DEFAULT '' COMMENT '商品描述' AFTER `product_content`"
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @ddl = IF(
EXISTS(
SELECT 1
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = @table_schema
AND TABLE_NAME = 'order_user_purchase_pay'
AND COLUMN_NAME = 'reason'
),
'SELECT 1',
"ALTER TABLE `order_user_purchase_pay` ADD COLUMN `reason` varchar(255) NOT NULL DEFAULT '' COMMENT '失败或挂起原因' AFTER `pay_status`"
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
UPDATE `order_user_purchase_pay`
SET `receipt_type` = 'PAYMENT'
WHERE `receipt_type` = '';
CREATE TABLE IF NOT EXISTS `order_user_purchase_pay_notice` (
`id` bigint NOT NULL COMMENT '主键ID',
`purchase_pay_id` bigint NOT NULL COMMENT '支付预订单ID',
`event_id` varchar(128) NOT NULL DEFAULT '' COMMENT '事件标识',
`notice_type` varchar(64) NOT NULL DEFAULT '' COMMENT '通知类型',
`notice_data` longtext COMMENT '通知内容(JSON)',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`create_user` bigint DEFAULT NULL COMMENT '创建人',
`update_user` bigint DEFAULT NULL COMMENT '更新人',
PRIMARY KEY (`id`),
KEY `idx_purchase_pay_notice_order` (`purchase_pay_id`, `id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Web支付通知明细';

View File

@ -0,0 +1,6 @@
ALTER TABLE `sys_country_code`
ADD COLUMN `alias_codes` varchar(255) NOT NULL DEFAULT '' COMMENT '国家code别名集合(JSON数组)' AFTER `alias_name`;
UPDATE `sys_country_code`
SET `alias_codes` = '["SA","KSA"]'
WHERE `alpha_two` = 'SA' OR `alpha_three` = 'KSA';