OmgPaymentCreateService.java 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. package com.ruoyi.app.omgpay;
  2. import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
  3. import com.ruoyi.system.domain.PosStoreOmg;
  4. import com.ruoyi.system.omgpay.domain.OmgPaymentAttempt;
  5. import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
  6. import com.ruoyi.system.omgpay.service.IOmgPaymentAttemptService;
  7. import com.ruoyi.system.service.IPosStoreOmgService;
  8. import org.slf4j.Logger;
  9. import org.slf4j.LoggerFactory;
  10. import org.springframework.dao.DuplicateKeyException;
  11. import org.springframework.stereotype.Service;
  12. import org.springframework.transaction.annotation.Transactional;
  13. import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.*;
  14. @Service
  15. public class OmgPaymentCreateService {
  16. private static final Logger log = LoggerFactory.getLogger(OmgPaymentCreateService.class);
  17. private static final int MAX_TRADE_NUMBER_ATTEMPTS = 3;
  18. private final IOmgPaymentAttemptService attempts;
  19. private final IPosStoreOmgService credentials;
  20. private final OmgMerchantTradeNoGenerator generator;
  21. private final OmgPaymentFormFactory formFactory;
  22. public OmgPaymentCreateService(IOmgPaymentAttemptService attempts,
  23. IPosStoreOmgService credentials,
  24. OmgMerchantTradeNoGenerator generator,
  25. OmgPaymentFormFactory formFactory) {
  26. this.attempts = attempts;
  27. this.credentials = credentials;
  28. this.generator = generator;
  29. this.formFactory = formFactory;
  30. }
  31. @Transactional(rollbackFor = Exception.class)
  32. public OmgPaymentCreateOutcome create(Long userId, String orderId) {
  33. if (userId == null) {
  34. throw business(AUTH_REQUIRED);
  35. }
  36. String normalizedOrderId = normalizeOrderId(orderId);
  37. OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(normalizedOrderId);
  38. validateOrder(userId, normalizedOrderId, order);
  39. if (attempts.selectActiveCreatedByDdIdForUpdate(normalizedOrderId) != null) {
  40. throw business(PAYMENT_ATTEMPT_EXISTS, order.getStoreId());
  41. }
  42. PosStoreOmg credential = credentials.getEnabledCredential(order.getStoreId());
  43. validateCredential(credential, order.getStoreId());
  44. log.info("OMG payment validation passed orderId={}, userId={}, storeId={}",
  45. safeLogOrderId(order.getDdId()), userId, order.getStoreId());
  46. return createWithBoundedTradeNumberRetries(order, userId, credential);
  47. }
  48. /**
  49. * Replaces the exact attempt previously verified as unpaid by the gateway.
  50. * The order lock and MerchantTradeNo comparison prevent concurrent retries from both creating a new attempt.
  51. */
  52. @Transactional(rollbackFor = Exception.class)
  53. public OmgPaymentCreateOutcome replaceActiveForRetry(Long userId, String orderId,
  54. String expectedMerchantTradeNo) {
  55. if (userId == null) {
  56. throw business(AUTH_REQUIRED);
  57. }
  58. String normalizedOrderId = normalizeOrderId(orderId);
  59. OmgPaymentOrderSnapshot order = attempts.selectOrderForUpdate(normalizedOrderId);
  60. validateOrder(userId, normalizedOrderId, order);
  61. OmgPaymentAttempt active = attempts.selectActiveCreatedByDdIdForUpdate(normalizedOrderId);
  62. if (active == null || active.getId() == null || expectedMerchantTradeNo == null
  63. || !expectedMerchantTradeNo.equals(active.getMerchantTradeNo())) {
  64. throw business(PAYMENT_RETRY_NOT_AVAILABLE, order.getStoreId());
  65. }
  66. PosStoreOmg credential = credentials.getEnabledCredential(order.getStoreId());
  67. validateCredential(credential, order.getStoreId());
  68. if (attempts.supersedeCreated(active.getId()) != 1) {
  69. throw business(PAYMENT_RETRY_NOT_AVAILABLE, order.getStoreId());
  70. }
  71. log.info("OMG payment retry replacing attempt orderId={}, userId={}, storeId={}, oldMerchantTradeNo={}",
  72. safeLogOrderId(order.getDdId()), userId, order.getStoreId(),
  73. maskMerchantTradeNo(active.getMerchantTradeNo()));
  74. return createWithBoundedTradeNumberRetries(order, userId, credential);
  75. }
  76. private OmgPaymentCreateOutcome createWithBoundedTradeNumberRetries(
  77. OmgPaymentOrderSnapshot order, Long userId, PosStoreOmg credential) {
  78. for (int number = 1; number <= MAX_TRADE_NUMBER_ATTEMPTS; number++) {
  79. String merchantTradeNo = generator.generate();
  80. OmgPaymentForm form;
  81. try {
  82. form = formFactory.create(order.getDdId(), order.getAmount(), credential.getMerchantId(),
  83. credential.getHashKey(), credential.getHashIv(), merchantTradeNo);
  84. } catch (IllegalArgumentException error) {
  85. throw business(PAYMENT_CONFIGURATION_INVALID, order.getStoreId());
  86. }
  87. try {
  88. OmgPaymentAttempt attempt = attempts.createCreated(order.getDdId(), merchantTradeNo,
  89. order.getStoreId(), credential.getMerchantId(), order.getAmount(),
  90. credential.getHashKey(), credential.getHashIv());
  91. OmgCreatePaymentResponse response = new OmgCreatePaymentResponse(form.gatewayUrl(), form.fields());
  92. return new OmgPaymentCreateOutcome(response, attempt.getId(), order.getDdId(), userId,
  93. order.getStoreId(), order.getAmount(), maskMerchantTradeNo(merchantTradeNo));
  94. } catch (DuplicateKeyException error) {
  95. if (attempts.selectActiveCreatedByDdIdForUpdate(order.getDdId()) != null) {
  96. throw business(PAYMENT_ATTEMPT_EXISTS, order.getStoreId());
  97. }
  98. boolean tradeNumberCollision = attempts.selectByMerchantTradeNoForUpdate(merchantTradeNo) != null;
  99. if (!tradeNumberCollision || number == MAX_TRADE_NUMBER_ATTEMPTS) {
  100. throw business(PAYMENT_CREATION_FAILED, order.getStoreId());
  101. }
  102. }
  103. }
  104. throw business(PAYMENT_CREATION_FAILED, order.getStoreId());
  105. }
  106. private static String normalizeOrderId(String orderId) {
  107. if (orderId == null || orderId.isBlank()) {
  108. throw business(ORDER_REQUIRED);
  109. }
  110. String normalized = orderId.trim();
  111. if (normalized.length() > 64) {
  112. throw business(ORDER_REQUIRED);
  113. }
  114. return normalized;
  115. }
  116. private static void validateOrder(Long userId, String orderId, OmgPaymentOrderSnapshot order) {
  117. if (order == null || !userId.equals(order.getUserId())) {
  118. throw business(ORDER_NOT_AVAILABLE);
  119. }
  120. Long storeId = order.getStoreId();
  121. if (order.getParentDdId() == null || !orderId.equals(order.getParentDdId())) {
  122. throw business(MULTI_STORE_ORDER_NOT_SUPPORTED, storeId);
  123. }
  124. if (storeId == null) {
  125. throw business(ORDER_NOT_AVAILABLE);
  126. }
  127. if (order.getState() == null || order.getState() < 0 || order.getState() > 2) {
  128. throw business(ORDER_STATE_NOT_PAYABLE, storeId);
  129. }
  130. if (order.getPayStatus() == null || order.getPayStatus() != 0) {
  131. throw business(ORDER_ALREADY_PAID, storeId);
  132. }
  133. if (!"2".equals(order.getPayType())) {
  134. throw business(PAYMENT_TYPE_INVALID, storeId);
  135. }
  136. if (order.getAmount() == null || order.getAmount() <= 0) {
  137. throw business(ORDER_AMOUNT_INVALID, storeId);
  138. }
  139. }
  140. private static void validateCredential(PosStoreOmg credential, Long storeId) {
  141. if (credential == null || credential.getMerchantId() == null
  142. || !credential.getMerchantId().matches("[A-Za-z0-9]{1,10}")
  143. || credential.getHashKey() == null || credential.getHashKey().isBlank()
  144. || credential.getHashIv() == null || credential.getHashIv().isBlank()) {
  145. throw business(STORE_CREDENTIAL_UNAVAILABLE, storeId);
  146. }
  147. }
  148. static String maskMerchantTradeNo(String merchantTradeNo) {
  149. if (merchantTradeNo == null || merchantTradeNo.length() < 10) {
  150. return "***";
  151. }
  152. return merchantTradeNo.substring(0, 5) + "***"
  153. + merchantTradeNo.substring(merchantTradeNo.length() - 4);
  154. }
  155. private static String safeLogOrderId(String orderId) {
  156. if (orderId == null) {
  157. return "<empty>";
  158. }
  159. StringBuilder safe = new StringBuilder();
  160. for (int index = 0; index < orderId.length() && safe.length() < 64; index++) {
  161. char value = orderId.charAt(index);
  162. if ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')
  163. || (value >= '0' && value <= '9') || value == '-' || value == '_') {
  164. safe.append(value);
  165. }
  166. }
  167. return safe.isEmpty() ? "<empty>" : safe.toString();
  168. }
  169. private static OmgPaymentBusinessException business(OmgPaymentErrorCode code) {
  170. return new OmgPaymentBusinessException(code);
  171. }
  172. private static OmgPaymentBusinessException business(OmgPaymentErrorCode code, Long storeId) {
  173. return new OmgPaymentBusinessException(code, storeId);
  174. }
  175. }