package com.ruoyi.app.omgpay; import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse; import com.ruoyi.system.domain.PosStoreOmg; import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt; import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot; import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService; import com.ruoyi.system.service.IPosStoreOmgService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.dao.DuplicateKeyException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.*; @Service public class OmgPaymentCreateService { private static final Logger log = LoggerFactory.getLogger(OmgPaymentCreateService.class); private static final int MAX_TRADE_NUMBER_ATTEMPTS = 3; private final IOmgPaymentAttemptService attempts; private final IPosStoreOmgService credentials; private final OmgMerchantTradeNoGenerator generator; private final OmgPaymentFormFactory formFactory; public OmgPaymentCreateService(IOmgPaymentAttemptService attempts, IPosStoreOmgService credentials, OmgMerchantTradeNoGenerator generator, OmgPaymentFormFactory formFactory) { this.attempts = attempts; this.credentials = credentials; this.generator = generator; this.formFactory = formFactory; } @Transactional(rollbackFor = Exception.class) public OmgPaymentCreateOutcome create(Long userId, String orderId) { if (userId == null) { throw business(AUTH_REQUIRED); } String normalizedOrderId = normalizeOrderId(orderId); OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(normalizedOrderId); validateOrder(userId, normalizedOrderId, order); if (attempts.selectActiveCreatedByDdIdForUpdate(normalizedOrderId) != null) { throw business(PAYMENT_ATTEMPT_EXISTS, order.getStoreId()); } PosStoreOmg credential = credentials.getEnabledCredential(order.getStoreId()); validateCredential(credential, order.getStoreId()); log.info("OMG payment validation passed orderId={}, userId={}, storeId={}", safeLogOrderId(order.getDdId()), userId, order.getStoreId()); return createWithBoundedTradeNumberRetries(order, userId, credential); } /** * Replaces the exact attempt previously verified as unpaid by the gateway. * The order lock and MerchantTradeNo comparison prevent concurrent retries from both creating a new attempt. */ @Transactional(rollbackFor = Exception.class) public OmgPaymentCreateOutcome replaceActiveForRetry(Long userId, String orderId, String expectedMerchantTradeNo) { if (userId == null) { throw business(AUTH_REQUIRED); } String normalizedOrderId = normalizeOrderId(orderId); OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(normalizedOrderId); validateOrder(userId, normalizedOrderId, order); OmgPaymentAttempt active = attempts.selectActiveCreatedByDdIdForUpdate(normalizedOrderId); if (active == null || active.getId() == null || expectedMerchantTradeNo == null || !expectedMerchantTradeNo.equals(active.getMerchantTradeNo())) { throw business(PAYMENT_RETRY_NOT_AVAILABLE, order.getStoreId()); } PosStoreOmg credential = credentials.getEnabledCredential(order.getStoreId()); validateCredential(credential, order.getStoreId()); if (attempts.supersedeCreated(active.getId()) != 1) { throw business(PAYMENT_RETRY_NOT_AVAILABLE, order.getStoreId()); } log.info("OMG payment retry replacing attempt orderId={}, userId={}, storeId={}, oldMerchantTradeNo={}", safeLogOrderId(order.getDdId()), userId, order.getStoreId(), maskMerchantTradeNo(active.getMerchantTradeNo())); return createWithBoundedTradeNumberRetries(order, userId, credential); } private OmgPaymentCreateOutcome createWithBoundedTradeNumberRetries( OmgPaymentOrderSnapshot order, Long userId, PosStoreOmg credential) { for (int number = 1; number <= MAX_TRADE_NUMBER_ATTEMPTS; number++) { String merchantTradeNo = generator.generate(); OmgPaymentForm form; try { form = formFactory.create(order.getDdId(), order.getAmount(), credential.getMerchantId(), credential.getHashKey(), credential.getHashIv(), merchantTradeNo); } catch (IllegalArgumentException error) { throw business(PAYMENT_CONFIGURATION_INVALID, order.getStoreId()); } try { OmgPaymentAttempt attempt = attempts.createCreated(order.getDdId(), merchantTradeNo, order.getStoreId(), credential.getMerchantId(), order.getAmount(), credential.getHashKey(), credential.getHashIv()); OmgCreatePaymentResponse response = new OmgCreatePaymentResponse(form.gatewayUrl(), form.fields()); return new OmgPaymentCreateOutcome(response, attempt.getId(), order.getDdId(), userId, order.getStoreId(), order.getAmount(), maskMerchantTradeNo(merchantTradeNo)); } catch (DuplicateKeyException error) { if (attempts.selectActiveCreatedByDdIdForUpdate(order.getDdId()) != null) { throw business(PAYMENT_ATTEMPT_EXISTS, order.getStoreId()); } boolean tradeNumberCollision = attempts.selectByMerchantTradeNoForUpdate(merchantTradeNo) != null; if (!tradeNumberCollision || number == MAX_TRADE_NUMBER_ATTEMPTS) { throw business(PAYMENT_CREATION_FAILED, order.getStoreId()); } } } throw business(PAYMENT_CREATION_FAILED, order.getStoreId()); } private static String normalizeOrderId(String orderId) { if (orderId == null || orderId.isBlank()) { throw business(ORDER_REQUIRED); } String normalized = orderId.trim(); if (normalized.length() > 64) { throw business(ORDER_REQUIRED); } return normalized; } private static void validateOrder(Long userId, String orderId, OmgPaymentOrderSnapshot order) { if (order == null || !userId.equals(order.getUserId())) { throw business(ORDER_NOT_AVAILABLE); } Long storeId = order.getStoreId(); if (order.getParentDdId() == null || !orderId.equals(order.getParentDdId())) { throw business(MULTI_STORE_ORDER_NOT_SUPPORTED, storeId); } if (storeId == null) { throw business(ORDER_NOT_AVAILABLE); } if (order.getState() == null || order.getState() < 0 || order.getState() > 2) { throw business(ORDER_STATE_NOT_PAYABLE, storeId); } if (order.getPayStatus() == null || order.getPayStatus() != 0) { throw business(ORDER_ALREADY_PAID, storeId); } if (!"2".equals(order.getPayType())) { throw business(PAYMENT_TYPE_INVALID, storeId); } if (order.getAmount() == null || order.getAmount() <= 0) { throw business(ORDER_AMOUNT_INVALID, storeId); } } private static void validateCredential(PosStoreOmg credential, Long storeId) { if (credential == null || credential.getMerchantId() == null || !credential.getMerchantId().matches("[A-Za-z0-9]{1,10}") || credential.getHashKey() == null || credential.getHashKey().isBlank() || credential.getHashIv() == null || credential.getHashIv().isBlank()) { throw business(STORE_CREDENTIAL_UNAVAILABLE, storeId); } } static String maskMerchantTradeNo(String merchantTradeNo) { if (merchantTradeNo == null || merchantTradeNo.length() < 10) { return "***"; } return merchantTradeNo.substring(0, 5) + "***" + merchantTradeNo.substring(merchantTradeNo.length() - 4); } private static String safeLogOrderId(String orderId) { if (orderId == null) { return ""; } StringBuilder safe = new StringBuilder(); for (int index = 0; index < orderId.length() && safe.length() < 64; index++) { char value = orderId.charAt(index); if ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') || (value >= '0' && value <= '9') || value == '-' || value == '_') { safe.append(value); } } return safe.isEmpty() ? "" : safe.toString(); } private static OmgPaymentBusinessException business(OmgPaymentErrorCode code) { return new OmgPaymentBusinessException(code); } private static OmgPaymentBusinessException business(OmgPaymentErrorCode code, Long storeId) { return new OmgPaymentBusinessException(code, storeId); } }