Explorar el Código

feat: complete safe OMG payment creation

qmj hace 2 semanas
padre
commit
39a1b78f4e
Se han modificado 53 ficheros con 1016 adiciones y 3062 borrados
  1. 3 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgCheckMacSigner.java
  2. 19 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGenerator.java
  3. 28 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentBusinessException.java
  4. 74 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java
  5. 13 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateOutcome.java
  6. 145 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java
  7. 26 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentErrorCode.java
  8. 6 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentForm.java
  9. 70 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java
  10. 39 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentProperties.java
  11. 21 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentTokenUserResolver.java
  12. 13 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java
  13. 9 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentResponse.java
  14. 4 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgPaymentErrorResponse.java
  15. 3 126
      ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java
  16. 0 97
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java
  17. 0 9
      ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java
  18. 0 9
      ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java
  19. 0 1109
      ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java
  20. 0 153
      ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgCallbackRequest.java
  21. 0 15
      ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgOrderRequest.java
  22. 0 24
      ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgRefundOutcome.java
  23. 0 111
      ruoyi-admin/src/main/java/com/ruoyi/app/task/OmgReconcileTask.java
  24. 0 62
      ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgQueryThrottle.java
  25. 2 27
      ruoyi-admin/src/main/resources/application.yml
  26. 12 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  27. 12 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  28. 12 0
      ruoyi-admin/src/main/resources/i18n/messages_vi.properties
  29. 12 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  30. 12 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties
  31. 30 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgLegacyRetirementTest.java
  32. 24 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGeneratorTest.java
  33. 139 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java
  34. 197 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java
  35. 63 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java
  36. 13 70
      ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java
  37. 0 382
      ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java
  38. 0 90
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java
  39. 0 57
      ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java
  40. 0 73
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java
  41. 0 22
      ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java
  42. 1 0
      ruoyi-system/src/main/java/com/ruoyi/system/omgpay/mapper/OmgPaymentAttemptMapper.java
  43. 1 0
      ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/IOmgPaymentAttemptService.java
  44. 5 0
      ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/impl/OmgPaymentAttemptServiceImpl.java
  45. 0 82
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgPaymentService.java
  46. 0 24
      ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgRefundService.java
  47. 0 154
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImpl.java
  48. 0 64
      ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImpl.java
  49. 0 163
      ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgPaymentMapper.xml
  50. 0 29
      ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgRefundMapper.xml
  51. 8 0
      ruoyi-system/src/main/resources/mapper/omgpay/OmgPaymentAttemptMapper.xml
  52. 0 72
      ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImplTest.java
  53. 0 38
      ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImplTest.java

+ 3 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgCheckMacSigner.java

@@ -11,6 +11,9 @@ import java.util.Objects;
 import java.util.TreeMap;
 import java.util.stream.Collectors;
 
+import org.springframework.stereotype.Component;
+
+@Component
 public class OmgCheckMacSigner {
 
     public String sign(Map<String, String> fields, String hashKey, String hashIv) {

+ 19 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGenerator.java

@@ -0,0 +1,19 @@
+package com.ruoyi.app.omgpay;
+
+import org.springframework.stereotype.Component;
+
+import java.security.SecureRandom;
+
+@Component
+public class OmgMerchantTradeNoGenerator {
+    private static final String ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
+    private final SecureRandom random = new SecureRandom();
+
+    public String generate() {
+        StringBuilder value = new StringBuilder("OMG");
+        while (value.length() < 20) {
+            value.append(ALPHABET.charAt(random.nextInt(ALPHABET.length())));
+        }
+        return value.toString();
+    }
+}

+ 28 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentBusinessException.java

@@ -0,0 +1,28 @@
+package com.ruoyi.app.omgpay;
+
+public class OmgPaymentBusinessException extends RuntimeException {
+    private final OmgPaymentErrorCode code;
+    private final Long storeId;
+
+    public OmgPaymentBusinessException(OmgPaymentErrorCode code) {
+        this(code, null);
+    }
+
+    public OmgPaymentBusinessException(OmgPaymentErrorCode code, Long storeId) {
+        super(code.name());
+        this.code = code;
+        this.storeId = storeId;
+    }
+
+    public OmgPaymentErrorCode getCode() {
+        return code;
+    }
+
+    public String getMessageKey() {
+        return code.getMessageKey();
+    }
+
+    public Long getStoreId() {
+        return storeId;
+    }
+}

+ 74 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java

@@ -0,0 +1,74 @@
+package com.ruoyi.app.omgpay;
+
+import com.ruoyi.app.omgpay.dto.OmgCreatePaymentRequest;
+import com.ruoyi.app.omgpay.dto.OmgPaymentErrorResponse;
+import com.ruoyi.common.annotation.Anonymous;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
+import com.ruoyi.system.utils.Auth;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+@RequestMapping("/pay/omg")
+public class OmgPaymentController {
+    private static final Logger log = LoggerFactory.getLogger(OmgPaymentController.class);
+
+    private final OmgPaymentTokenUserResolver tokenUserResolver;
+    private final OmgPaymentCreateService createService;
+
+    public OmgPaymentController(OmgPaymentTokenUserResolver tokenUserResolver,
+                                OmgPaymentCreateService createService) {
+        this.tokenUserResolver = tokenUserResolver;
+        this.createService = createService;
+    }
+
+    @Anonymous
+    @Auth
+    @PostMapping("/create")
+    public AjaxResult create(@RequestHeader(name = "token") String token,
+                             @RequestBody(required = false) OmgCreatePaymentRequest request) {
+        String orderId = request == null ? null : request.getOrderId();
+        String safeOrderId = safeLogOrderId(orderId);
+        Long safeUserId = null;
+        try {
+            safeUserId = tokenUserResolver.requireUserId(token);
+            log.info("OMG payment create started orderId={}, userId={}", safeOrderId, safeUserId);
+            OmgPaymentCreateOutcome outcome = createService.create(safeUserId, orderId);
+            log.info("OMG payment create succeeded orderId={}, userId={}, storeId={}, "
+                            + "attemptId={}, amount={}, status=CREATED, merchantTradeNo={}",
+                    outcome.orderId(), outcome.userId(), outcome.storeId(), outcome.attemptId(),
+                    outcome.amount(), outcome.maskedMerchantTradeNo());
+            return AjaxResult.success(outcome.response());
+        } catch (OmgPaymentBusinessException error) {
+            log.warn("OMG payment create rejected orderId={}, userId={}, storeId={}, code={}",
+                    safeOrderId, safeUserId, error.getStoreId(), error.getCode());
+            return AjaxResult.error(MessageUtils.message(error.getMessageKey()),
+                    new OmgPaymentErrorResponse(error.getCode().name()));
+        } catch (Exception error) {
+            log.error("OMG payment create failed orderId={}, userId={}", safeOrderId, safeUserId, error);
+            return AjaxResult.error(MessageUtils.message("omg.pay.creation.failed"),
+                    new OmgPaymentErrorResponse("PAYMENT_CREATION_FAILED"));
+        }
+    }
+
+    static String safeLogOrderId(String orderId) {
+        if (orderId == null) {
+            return "<empty>";
+        }
+        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() ? "<empty>" : safe.toString();
+    }
+}

+ 13 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateOutcome.java

@@ -0,0 +1,13 @@
+package com.ruoyi.app.omgpay;
+
+import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
+
+public record OmgPaymentCreateOutcome(
+        OmgCreatePaymentResponse response,
+        Long attemptId,
+        String orderId,
+        Long userId,
+        Long storeId,
+        Integer amount,
+        String maskedMerchantTradeNo) {
+}

+ 145 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java

@@ -0,0 +1,145 @@
+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.selectActiveCreatedByDdId(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={}",
+                order.getDdId(), userId, order.getStoreId());
+        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());
+                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.selectActiveCreatedByDdId(order.getDdId()) != null) {
+                    throw business(PAYMENT_ATTEMPT_EXISTS, order.getStoreId());
+                }
+                boolean tradeNumberCollision = attempts.selectByMerchantTradeNo(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 OmgPaymentBusinessException business(OmgPaymentErrorCode code) {
+        return new OmgPaymentBusinessException(code);
+    }
+
+    private static OmgPaymentBusinessException business(OmgPaymentErrorCode code, Long storeId) {
+        return new OmgPaymentBusinessException(code, storeId);
+    }
+}

+ 26 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentErrorCode.java

@@ -0,0 +1,26 @@
+package com.ruoyi.app.omgpay;
+
+public enum OmgPaymentErrorCode {
+    AUTH_REQUIRED("omg.pay.auth.required"),
+    ORDER_REQUIRED("omg.pay.order.required"),
+    ORDER_NOT_AVAILABLE("omg.pay.order.not.available"),
+    MULTI_STORE_ORDER_NOT_SUPPORTED("omg.pay.multi.store.unsupported"),
+    ORDER_STATE_NOT_PAYABLE("omg.pay.order.state.not.payable"),
+    ORDER_ALREADY_PAID("omg.pay.order.already.paid"),
+    ORDER_AMOUNT_INVALID("omg.pay.order.amount.invalid"),
+    PAYMENT_TYPE_INVALID("omg.pay.payment.type.invalid"),
+    STORE_CREDENTIAL_UNAVAILABLE("omg.pay.credential.unavailable"),
+    PAYMENT_ATTEMPT_EXISTS("omg.pay.attempt.exists"),
+    PAYMENT_CONFIGURATION_INVALID("omg.pay.configuration.invalid"),
+    PAYMENT_CREATION_FAILED("omg.pay.creation.failed");
+
+    private final String messageKey;
+
+    OmgPaymentErrorCode(String messageKey) {
+        this.messageKey = messageKey;
+    }
+
+    public String getMessageKey() {
+        return messageKey;
+    }
+}

+ 6 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentForm.java

@@ -0,0 +1,6 @@
+package com.ruoyi.app.omgpay;
+
+import java.util.Map;
+
+public record OmgPaymentForm(String gatewayUrl, Map<String, String> fields) {
+}

+ 70 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java

@@ -0,0 +1,70 @@
+package com.ruoyi.app.omgpay;
+
+import org.springframework.stereotype.Component;
+
+import java.time.Clock;
+import java.time.ZoneId;
+import java.time.ZonedDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.Map;
+
+@Component
+public class OmgPaymentFormFactory {
+    public static final String STAGE_GATEWAY_URL =
+            "https://payment-stage.funpoint.com.tw/Cashier/AioCheckOut/V5";
+    private static final DateTimeFormatter TRADE_DATE = DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss");
+    private static final ZoneId TAIPEI = ZoneId.of("Asia/Taipei");
+
+    private final OmgPaymentProperties properties;
+    private final OmgCheckMacSigner signer;
+    private final Clock clock;
+
+    public OmgPaymentFormFactory(OmgPaymentProperties properties, OmgCheckMacSigner signer) {
+        this(properties, signer, Clock.systemUTC());
+    }
+
+    OmgPaymentFormFactory(OmgPaymentProperties properties, OmgCheckMacSigner signer, Clock clock) {
+        this.properties = properties;
+        this.signer = signer;
+        this.clock = clock;
+    }
+
+    public OmgPaymentForm create(String orderId, Integer amount, String merchantId,
+                                 String hashKey, String hashIv, String merchantTradeNo) {
+        String safeOrderId = safeOrderReference(orderId);
+        LinkedHashMap<String, String> fields = new LinkedHashMap<>();
+        fields.put("MerchantID", merchantId);
+        fields.put("MerchantTradeNo", merchantTradeNo);
+        fields.put("MerchantTradeDate", ZonedDateTime.now(clock).withZoneSameInstant(TAIPEI).format(TRADE_DATE));
+        fields.put("PaymentType", "aio");
+        fields.put("TotalAmount", String.valueOf(amount));
+        fields.put("TradeDesc", "Foodie order " + safeOrderId);
+        fields.put("ItemName", "Order " + safeOrderId);
+        fields.put("ReturnURL", properties.requireSafeReturnUrl());
+        fields.put("ChoosePayment", "ALL");
+        fields.put("EncryptType", "1");
+        fields.put("InvoiceMark", "N");
+        fields.put("NeedExtraPaidInfo", "Y");
+        fields.put("ExpireDate", "1");
+        fields.put("StoreExpireDate", "30");
+        fields.put("BarcodeATMExpireDate", "1");
+        fields.put("CheckMacValue", signer.sign(fields, hashKey, hashIv));
+        Map<String, String> responseFields = Collections.unmodifiableMap(new LinkedHashMap<>(fields));
+        return new OmgPaymentForm(STAGE_GATEWAY_URL, responseFields);
+    }
+
+    private static String safeOrderReference(String orderId) {
+        String source = orderId == null ? "" : orderId;
+        StringBuilder safe = new StringBuilder();
+        for (int i = 0; i < source.length() && safe.length() < 64; i++) {
+            char value = source.charAt(i);
+            if ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')
+                    || (value >= '0' && value <= '9')) {
+                safe.append(value);
+            }
+        }
+        return safe.isEmpty() ? "ORDER" : safe.toString();
+    }
+}

+ 39 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentProperties.java

@@ -0,0 +1,39 @@
+package com.ruoyi.app.omgpay;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.stereotype.Component;
+
+import java.net.URI;
+
+@Component
+@ConfigurationProperties(prefix = "omgpay")
+public class OmgPaymentProperties {
+    private String returnUrl;
+
+    public String getReturnUrl() {
+        return returnUrl;
+    }
+
+    public void setReturnUrl(String returnUrl) {
+        this.returnUrl = returnUrl;
+    }
+
+    public String requireSafeReturnUrl() {
+        if (returnUrl == null || returnUrl.isBlank()) {
+            throw new IllegalArgumentException("OMG ReturnURL is required");
+        }
+        URI uri;
+        try {
+            uri = URI.create(returnUrl.trim());
+        } catch (IllegalArgumentException error) {
+            throw new IllegalArgumentException("OMG ReturnURL is invalid", error);
+        }
+        if (!uri.isAbsolute() || !"https".equalsIgnoreCase(uri.getScheme())
+                || uri.getHost() == null || uri.getHost().isBlank()
+                || uri.getUserInfo() != null || uri.getQuery() != null || uri.getFragment() != null
+                || !"/pay/omg/notify".equals(uri.getPath())) {
+            throw new IllegalArgumentException("OMG ReturnURL is unsafe");
+        }
+        return uri.toASCIIString();
+    }
+}

+ 21 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentTokenUserResolver.java

@@ -0,0 +1,21 @@
+package com.ruoyi.app.omgpay;
+
+import com.ruoyi.system.utils.JwtUtil;
+import org.springframework.stereotype.Component;
+
+@Component
+public class OmgPaymentTokenUserResolver {
+    public Long requireUserId(String token) {
+        try {
+            String userId = new JwtUtil().getusid(token);
+            if (userId == null || userId.isBlank()) {
+                throw new OmgPaymentBusinessException(OmgPaymentErrorCode.AUTH_REQUIRED);
+            }
+            return Long.valueOf(userId);
+        } catch (OmgPaymentBusinessException error) {
+            throw error;
+        } catch (Exception error) {
+            throw new OmgPaymentBusinessException(OmgPaymentErrorCode.AUTH_REQUIRED);
+        }
+    }
+}

+ 13 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java

@@ -0,0 +1,13 @@
+package com.ruoyi.app.omgpay.dto;
+
+public class OmgCreatePaymentRequest {
+    private String orderId;
+
+    public String getOrderId() {
+        return orderId;
+    }
+
+    public void setOrderId(String orderId) {
+        this.orderId = orderId;
+    }
+}

+ 9 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentResponse.java

@@ -0,0 +1,9 @@
+package com.ruoyi.app.omgpay.dto;
+
+import java.util.Map;
+
+public record OmgCreatePaymentResponse(String status, String gatewayUrl, Map<String, String> formFields) {
+    public OmgCreatePaymentResponse(String gatewayUrl, Map<String, String> formFields) {
+        this("CREATED", gatewayUrl, formFields);
+    }
+}

+ 4 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgPaymentErrorResponse.java

@@ -0,0 +1,4 @@
+package com.ruoyi.app.omgpay.dto;
+
+public record OmgPaymentErrorResponse(String status) {
+}

+ 3 - 126
ruoyi-admin/src/main/java/com/ruoyi/app/order/OrderLifecycleService.java

@@ -9,13 +9,9 @@ import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.MessageUtils;
 import com.ruoyi.system.domain.PointsTransaction;
 import com.ruoyi.system.domain.PosOrder;
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import com.ruoyi.system.domain.PosOrderOmgRefund;
 import com.ruoyi.system.domain.PosOrderLinePayment;
 import com.ruoyi.system.domain.PosOrderLineRefund;
 import com.ruoyi.system.domain.UserWallet;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IPosOrderLinePaymentService;
 import com.ruoyi.system.service.IPosOrderLineRefundService;
@@ -35,15 +31,13 @@ import java.util.Objects;
 public class OrderLifecycleService {
 
     public static final String PAY_TYPE_OFFLINE = "1";
-    public static final String PAY_TYPE_OMG = "7";
+    public static final String PAY_TYPE_OMG = "2";
 
     private final IPosOrderService posOrderService;
     private final OrderService billingService;
     private final OrderLogHelper orderLogHelper;
     private final IUserWalletService userWalletService;
     private final IPointsTransactionService pointsTransactionService;
-    private final IPosOrderOmgPaymentService omgPaymentService;
-    private final IPosOrderOmgRefundService omgRefundService;
     private final IPosOrderLinePaymentService linePaymentService;
     private final IPosOrderLineRefundService lineRefundService;
 
@@ -51,11 +45,9 @@ public class OrderLifecycleService {
                                  OrderService billingService,
                                  OrderLogHelper orderLogHelper,
                                  IUserWalletService userWalletService,
-                                 IPointsTransactionService pointsTransactionService,
-                                 IPosOrderOmgPaymentService omgPaymentService,
-                                 IPosOrderOmgRefundService omgRefundService) {
+                                 IPointsTransactionService pointsTransactionService) {
         this(posOrderService, billingService, orderLogHelper, userWalletService,
-                pointsTransactionService, omgPaymentService, omgRefundService, null, null);
+                pointsTransactionService, null, null);
     }
 
     @Autowired
@@ -64,8 +56,6 @@ public class OrderLifecycleService {
                                  OrderLogHelper orderLogHelper,
                                  IUserWalletService userWalletService,
                                  IPointsTransactionService pointsTransactionService,
-                                 IPosOrderOmgPaymentService omgPaymentService,
-                                 IPosOrderOmgRefundService omgRefundService,
                                  IPosOrderLinePaymentService linePaymentService,
                                  IPosOrderLineRefundService lineRefundService) {
         this.posOrderService = posOrderService;
@@ -73,8 +63,6 @@ public class OrderLifecycleService {
         this.orderLogHelper = orderLogHelper;
         this.userWalletService = userWalletService;
         this.pointsTransactionService = pointsTransactionService;
-        this.omgPaymentService = omgPaymentService;
-        this.omgRefundService = omgRefundService;
         this.linePaymentService = linePaymentService;
         this.lineRefundService = lineRefundService;
     }
@@ -169,39 +157,6 @@ public class OrderLifecycleService {
         applyCas(order, update);
     }
 
-    public AdminOrderStatusContext validateOmgReconcile(Long id, AdminOrderActionRequest request) {
-        requireReason(request);
-        PosOrder order = requireOrder(id);
-        validateSnapshot(order, request);
-        if (!PAY_TYPE_OMG.equals(order.getPayType()) || order.getPayStatus() != 0L
-                || order.getState() == 4L || order.getAfterSaleStatus() != 0L) {
-            throw new ServiceException("当前订单状态不允许 OMG 支付补单");
-        }
-        String ddId = String.valueOf(order.getDdId());
-        boolean hasPaid = omgPaymentService.existsPaidByDdId(ddId);
-        boolean hasUnpaid = !omgPaymentService.listUnpaidByDdId(ddId).isEmpty();
-        if (!hasPaid && !hasUnpaid) {
-            throw new ServiceException("没有可查询的 OMG 支付流水");
-        }
-        return buildContext(order);
-    }
-
-    public AdminOrderStatusContext validateOmgRefund(Long id, AdminOrderActionRequest request) {
-        requireReason(request);
-        PosOrder order = requireOrder(id);
-        validateSnapshot(order, request);
-        if (!PAY_TYPE_OMG.equals(order.getPayType())) {
-            throw new ServiceException("该订单非 OMG 支付");
-        }
-        validateRefundableOrder(order);
-        PosOrderOmgPayment payment = omgPaymentService.getLatestRefundableByDdId(String.valueOf(order.getDdId()));
-        if (payment == null || (!Integer.valueOf(1).equals(payment.getPayStatus())
-                && !Integer.valueOf(3).equals(payment.getPayStatus()))) {
-            throw new ServiceException("OMG 支付流水状态不允许退款");
-        }
-        return buildContext(order);
-    }
-
     public void auditExternalAction(Long id, AdminOrderActionRequest request,
                                     Long operatorId, String operatorName, String action) {
         PosOrder order = requireOrder(id);
@@ -324,52 +279,6 @@ public class OrderLifecycleService {
         return buildContext(order);
     }
 
-    @Transactional(rollbackFor = Exception.class)
-    public AdminOrderStatusContext finalizeOmgRefund(Long id, AdminOrderActionRequest request,
-                                                     Long operatorId, String operatorName,
-                                                     String action) {
-        requireReason(request);
-        PosOrder order = requireOrder(id);
-        validateSnapshot(order, request);
-        if (!PAY_TYPE_OMG.equals(order.getPayType())) {
-            throw new ServiceException("该订单非 OMG 支付");
-        }
-        validateRefundableOrder(order);
-
-        PosOrder update = refundedOrderUpdate();
-        applyCas(order, update);
-        order.setState(4L);
-        order.setPayStatus(2L);
-        order.setAfterSaleStatus(3L);
-        refundPoints(order);
-        logStatusChange(order, request, operatorId, operatorName,
-                request.getExpectedState(), request.getExpectedDeliveryStatus(),
-                request.getExpectedPayStatus(), request.getExpectedAfterSaleStatus(), action);
-        return buildContext(order);
-    }
-
-    @Transactional(rollbackFor = Exception.class)
-    public AdminOrderStatusContext finalizeSystemOmgRefund(Long id) {
-        PosOrder order = requireOrder(id);
-        if (order.getState() == 4L && order.getPayStatus() == 2L && order.getAfterSaleStatus() == 3L) {
-            return buildContext(order);
-        }
-        if (!PAY_TYPE_OMG.equals(order.getPayType()) || order.getPayStatus() != 1L
-                || order.getAfterSaleStatus() != 0L || order.getState() == 3L) {
-            throw new ServiceException("OMG 退款后的订单状态不允许同步");
-        }
-        PosOrder update = refundedOrderUpdate();
-        applyCas(order, update);
-        Long beforeState = order.getState();
-        order.setState(4L);
-        order.setPayStatus(2L);
-        order.setAfterSaleStatus(3L);
-        refundPoints(order);
-        orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统",
-                "OMG退款同步:state " + beforeState + "→4,pay 1→2,afterSale 0→3");
-        return buildContext(order);
-    }
-
     @Transactional(rollbackFor = Exception.class)
     public AdminOrderStatusContext finalizeSystemLineRefund(Long id) {
         PosOrder order = requireOrder(id);
@@ -645,30 +554,6 @@ public class OrderLifecycleService {
         context.setCanConfirmOfflineRefund(active && PAY_TYPE_OFFLINE.equals(order.getPayType())
                 && order.getPayStatus() == 1L);
 
-        if (PAY_TYPE_OMG.equals(order.getPayType())) {
-            String ddId = String.valueOf(order.getDdId());
-            // 退款/已付相关读可退款行(pay_status IN(1,3,4));未付款订单无此行 → payment=null
-            PosOrderOmgPayment payment = omgPaymentService.getLatestRefundableByDdId(ddId);
-            if (payment != null) {
-                context.setOmgPaymentStatus(payment.getPayStatus());
-                List<PosOrderOmgRefund> refunds = omgRefundService.listByPayment(payment.getId());
-                boolean manualDone = refunds.stream().anyMatch(this::isManualRefundDone);
-                boolean manualPending = !manualDone && refunds.stream().anyMatch(this::isManualRefundPending);
-                context.setManualRefundPending(manualPending);
-                context.setRefundUnknown(Integer.valueOf(4).equals(payment.getPayStatus()));
-                context.setCanRefundOmg(active && order.getPayStatus() == 1L
-                        && (Integer.valueOf(1).equals(payment.getPayStatus())
-                        || Integer.valueOf(3).equals(payment.getPayStatus()))
-                        && !manualPending && !manualDone);
-                context.setCanConfirmManualOmgRefund(active && order.getPayStatus() == 1L
-                        && (manualPending && (Integer.valueOf(1).equals(payment.getPayStatus())
-                        || Integer.valueOf(3).equals(payment.getPayStatus()))
-                        || manualDone && Integer.valueOf(3).equals(payment.getPayStatus())));
-            }
-            // canReconcileOmg:订单未核销且仍有 OMG 流水(未付行可 queryTrade / 已付未核销可自愈),不依赖可退款行
-            boolean hasOmgActivity = payment != null || !omgPaymentService.listUnpaidByDdId(ddId).isEmpty();
-            context.setCanReconcileOmg(active && order.getPayStatus() == 0L && hasOmgActivity);
-        }
         if (linePaymentService != null) {
             List<PosOrderLinePayment> attempts = linePaymentService.getByDdId(String.valueOf(order.getDdId()));
             PosOrderLinePayment line = selectLinePaymentForAdmin(attempts);
@@ -770,14 +655,6 @@ public class OrderLifecycleService {
         return allowed;
     }
 
-    private boolean isManualRefundPending(PosOrderOmgRefund refund) {
-        return refund.getAction() == null && refund.getRtnCode() == null;
-    }
-
-    private boolean isManualRefundDone(PosOrderOmgRefund refund) {
-        return refund.getAction() == null && Integer.valueOf(1).equals(refund.getRtnCode());
-    }
-
     private PosOrder requireOrder(Long id) {
         PosOrder order = id == null ? null : posOrderService.getById(id);
         if (order == null) {

+ 0 - 97
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderController.java

@@ -19,8 +19,6 @@ import com.ruoyi.app.order.dto.AdminOrderStatusContext;
 import com.ruoyi.app.pay.LinePayService;
 import com.ruoyi.app.pay.LinePayRefundService;
 import com.ruoyi.app.order.dto.AdminOrderStatusUpdateRequest;
-import com.ruoyi.app.pay.OmgPayController;
-import com.ruoyi.app.pay.dto.OmgRefundOutcome;
 import com.ruoyi.app.service.UserService;
 import com.ruoyi.app.service.WalletService;
 import com.ruoyi.app.user.dto.StoreOutput;
@@ -156,8 +154,6 @@ public class PosOrderController extends BaseController {
     @Autowired
     private OrderLifecycleService orderLifecycleService;
     @Autowired
-    private OmgPayController omgPayController;
-    @Autowired
     private LinePayService linePayService;
     @Autowired
     private LinePayRefundService linePayRefundService;
@@ -1569,86 +1565,6 @@ public class PosOrderController extends BaseController {
                 operator == null ? null : operator.getUserId(), operatorName(operator)));
     }
 
-    @PreAuthorize("@ss.hasPermi('system:order:edit')")
-    @RepeatSubmit(interval = 2000, message = "查询过于频繁")
-    @PostMapping("/{id}/omg-payment/reconcile")
-    public AjaxResult adminReconcileOmgPayment(@PathVariable Long id,
-                                               @Valid @RequestBody AdminOrderActionRequest request) {
-        AdminOrderStatusContext before = orderLifecycleService.validateOmgReconcile(id, request);
-        int[] result = omgPayController.reconcileByQuery(before.getDdId(), "admin");
-        AdminOrderStatusContext after = orderLifecycleService.getStatusContext(id);
-        if (!Objects.equals(before.getPayStatus(), after.getPayStatus())) {
-            SysUser operator = currentAdmin();
-            orderLifecycleService.auditExternalAction(id, request,
-                    operator == null ? null : operator.getUserId(), operatorName(operator), "平台发起OMG补单");
-        }
-        Map<String, Object> data = new LinkedHashMap<>();
-        data.put("payStatus", after.getPayStatus());
-        data.put("reconciled", result[1] == 1 && after.getPayStatus() == 1L);
-        data.put("context", after);
-        return success(after.getPayStatus() == 1L ? "OMG 支付核验成功" : "OMG 尚未确认支付", data);
-    }
-
-    @PreAuthorize("@ss.hasPermi('system:order:edit')")
-    @RepeatSubmit(interval = 2000, message = "请求过于频繁")
-    @PostMapping("/{id}/omg-refund")
-    public AjaxResult adminRefundOmgPayment(@PathVariable Long id,
-                                            @Valid @RequestBody AdminOrderActionRequest request) {
-        AdminOrderStatusContext before = orderLifecycleService.validateOmgRefund(id, request);
-        OmgRefundOutcome outcome = omgPayController.refundOrderOutcome(posOrderService.getById(id));
-        if (outcome.getStatus() == OmgRefundOutcome.Status.REFUNDED
-                || outcome.getStatus() == OmgRefundOutcome.Status.IDEMPOTENT) {
-            SysUser operator = currentAdmin();
-            try {
-                AdminOrderStatusContext context = orderLifecycleService.finalizeOmgRefund(id, request,
-                        operator == null ? null : operator.getUserId(), operatorName(operator), "平台OMG退款成功");
-                return success(outcome.getMessage(), refundData(outcome, context));
-            } catch (Exception e) {
-                orderLogHelper.logSync(String.valueOf(before.getDdId()), 0,
-                        operator == null ? null : operator.getUserId(), operatorName(operator),
-                        "高优先级:OMG退款成功,但订单状态同步失败,请人工核对");
-                throw new ServiceException("OMG退款成功,本地订单状态待同步");
-            }
-        }
-        if (outcome.getStatus() == OmgRefundOutcome.Status.MANUAL_PENDING) {
-            if (!Boolean.TRUE.equals(before.getManualRefundPending())) {
-                SysUser operator = currentAdmin();
-                orderLifecycleService.auditExternalAction(id, request,
-                        operator == null ? null : operator.getUserId(), operatorName(operator),
-                        "平台发起OMG人工退款待办");
-            }
-            return success(outcome.getMessage(), refundData(outcome, orderLifecycleService.getStatusContext(id)));
-        }
-        return error(outcome.getMessage(), refundData(outcome, orderLifecycleService.getStatusContext(id)));
-    }
-
-    @PreAuthorize("@ss.hasPermi('system:order:edit')")
-    @RepeatSubmit(interval = 2000, message = "请求过于频繁")
-    @PostMapping("/{id}/omg-refund/manual-confirm")
-    public AjaxResult adminConfirmManualOmgRefund(@PathVariable Long id,
-                                                  @Valid @RequestBody AdminOrderActionRequest request) {
-        AdminOrderStatusContext before = orderLifecycleService.validateOmgRefund(id, request);
-        if (!Boolean.TRUE.equals(before.getCanConfirmManualOmgRefund())) {
-            throw new ServiceException("无可确认的 OMG 人工退款待办");
-        }
-        OmgRefundOutcome outcome = omgPayController.confirmManualRefundOutcome(posOrderService.getById(id));
-        if (outcome.getStatus() != OmgRefundOutcome.Status.REFUNDED
-                && outcome.getStatus() != OmgRefundOutcome.Status.IDEMPOTENT) {
-            return error(outcome.getMessage(), refundData(outcome, orderLifecycleService.getStatusContext(id)));
-        }
-        SysUser operator = currentAdmin();
-        try {
-            AdminOrderStatusContext context = orderLifecycleService.finalizeOmgRefund(id, request,
-                    operator == null ? null : operator.getUserId(), operatorName(operator), "平台确认OMG人工退款完成");
-            return success(outcome.getMessage(), refundData(outcome, context));
-        } catch (Exception e) {
-            orderLogHelper.logSync(String.valueOf(before.getDdId()), 0,
-                    operator == null ? null : operator.getUserId(), operatorName(operator),
-                    "高优先级:OMG人工退款已确认,但订单状态同步失败,请人工核对");
-            throw new ServiceException("OMG退款已确认,本地订单状态待同步");
-        }
-    }
-
     /**
      * 新增posorder
      */
@@ -1686,19 +1602,6 @@ public class PosOrderController extends BaseController {
         return operator == null || operator.getNickName() == null ? "" : operator.getNickName();
     }
 
-    private Map<String, Object> refundData(OmgRefundOutcome outcome, AdminOrderStatusContext context) {
-        Map<String, Object> data = new LinkedHashMap<>();
-        data.put("outcome", outcome.getStatus().name());
-        data.put("state", context.getState());
-        data.put("payStatus", context.getPayStatus());
-        data.put("afterSaleStatus", context.getAfterSaleStatus());
-        data.put("manualRefundPending", context.getManualRefundPending());
-        data.put("refundUnknown", context.getRefundUnknown());
-        data.put("context", context);
-        return data;
-    }
-
-
     /**
      * 订单取消、退款成功返回积分
      *

+ 0 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/order/PosOrderShOprateController.java

@@ -368,15 +368,6 @@ public class PosOrderShOprateController extends BaseController {
 
         // 取消落库后按最新支付状态退款,避免漏掉与取消并发到达的成功回调。
         PosOrder latest = posOrderService.getById(order.getId());
-        if (latest != null && com.ruoyi.app.pay.OmgPayController.PAY_TYPE_OMG.equals(latest.getPayType())
-                && Long.valueOf(1L).equals(latest.getPayStatus())) {
-            try {
-                com.ruoyi.common.utils.spring.SpringUtils.getBean(com.ruoyi.app.pay.OmgPayController.class)
-                        .refundOrder(latest);
-            } catch (Exception e) {
-                logger.warn("OMG商家取消订单退款异常: ddId={}", latest.getDdId(), e);
-            }
-        }
         requestLineRefundIfPaid(latest, "STORE_CANCEL");
         InfoUser shUser = infoUserService.getOne(new LambdaQueryWrapper<InfoUser>().eq(InfoUser::getUserId, Long.valueOf(new JwtUtil().getusid(token))));
         String shName = shUser != null ? shUser.getNickName() : "";

+ 0 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/order/UserOrderController.java

@@ -724,15 +724,6 @@ public class UserOrderController extends BaseController {
 
         // 取消落库后按最新支付状态退款,避免旧订单对象覆盖退款状态或漏掉并发成功回调。
         PosOrder latest = posOrderService.getById(order.getId());
-        if (latest != null && com.ruoyi.app.pay.OmgPayController.PAY_TYPE_OMG.equals(latest.getPayType())
-                && Long.valueOf(1L).equals(latest.getPayStatus())) {
-            try {
-                com.ruoyi.common.utils.spring.SpringUtils.getBean(com.ruoyi.app.pay.OmgPayController.class)
-                        .refundOrder(latest);
-            } catch (Exception e) {
-                logger.warn("OMG取消订单退款异常: ddId={}", latest.getDdId(), e);
-            }
-        }
         if (latest != null && Long.valueOf(1L).equals(latest.getPayStatus())) {
             linePaymentService.getByDdId(String.valueOf(latest.getDdId())).stream()
                     .filter(payment -> "PAID".equals(payment.getStatus()))

+ 0 - 1109
ruoyi-admin/src/main/java/com/ruoyi/app/pay/OmgPayController.java

@@ -1,1109 +0,0 @@
-package com.ruoyi.app.pay;
-
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
-import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
-import com.alibaba.fastjson2.JSON;
-import com.ruoyi.app.order.dto.OrderPushBodyDto;
-import com.ruoyi.app.order.OrderLifecycleService;
-import com.ruoyi.app.pay.dto.OmgCallbackRequest;
-import com.ruoyi.app.pay.dto.OmgOrderRequest;
-import com.ruoyi.app.pay.dto.OmgRefundOutcome;
-import com.ruoyi.app.utils.PayPush;
-import com.ruoyi.app.utils.event.PushEventService;
-import com.ruoyi.app.utils.omg.OmgCheckMacValue;
-import com.ruoyi.app.utils.omg.OmgPay;
-import com.ruoyi.app.utils.omg.OmgPayConfig;
-import com.ruoyi.app.utils.omg.OmgQueryThrottle;
-import com.ruoyi.common.annotation.Anonymous;
-import com.ruoyi.common.annotation.RepeatSubmit;
-import com.ruoyi.common.core.controller.BaseController;
-import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.utils.MessageUtils;
-import com.ruoyi.system.domain.InfoUser;
-import com.ruoyi.system.domain.IpnLog;
-import com.ruoyi.system.domain.PosOrder;
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import com.ruoyi.system.domain.PosStoreOmg;
-import com.ruoyi.system.service.IInfoUserService;
-import com.ruoyi.system.service.IIpnLogService;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
-import com.ruoyi.system.service.IPosOrderService;
-import com.ruoyi.system.service.IPosStoreOmgService;
-import com.ruoyi.system.utils.Auth;
-import com.ruoyi.system.utils.JwtUtil;
-import com.ruoyi.system.utils.OrderLogHelper;
-import jakarta.servlet.http.HttpServletResponse;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.dao.DuplicateKeyException;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.transaction.support.TransactionSynchronization;
-import org.springframework.transaction.support.TransactionSynchronizationManager;
-import org.springframework.web.bind.annotation.*;
-
-import java.io.IOException;
-import java.net.URLEncoder;
-import java.nio.charset.StandardCharsets;
-import java.text.SimpleDateFormat;
-import java.util.Date;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
-import java.util.TimeZone;
-import java.util.UUID;
-
-/**
- * OMG(歐買尬/FunPoint) AIO 线上支付 Controller(独立于 newebpay,零蓝新依赖)。
- *
- * <p>本期实现:
- * <ul>
- *   <li>{@code POST /pay/omg/create} — 发起 AIO 幕前支付(US1,@Auth)。返回含 gatewayUrl 的 form 字段,
- *       前端构建隐藏 form submit 到 OMG 收银台(ChoosePayment=ALL)。</li>
- *   <li>{@code POST /pay/omg/notify} — OMG 服务端回调(US2,@Anonymous)。验签 CheckMacValue + 幂等 + 金额 +
- *       RtnCode/SimulatePaid 校验 → markSuccess → 更新订单 payStatus=1 + 推送用户/商家 → 回纯串 {@code 1|OK}。</li>
- *   <li>{@code GET|POST /pay/omg/return} — 完成页引导(US2,@Anonymous)。仅 302 回前端结果页,不改订单状态。</li>
- * </ul>
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-@RestController
-@RequestMapping("/pay/omg")
-public class OmgPayController extends BaseController {
-
-    private static final Logger log = LoggerFactory.getLogger(OmgPayController.class);
-
-    /** payType 取值:OMG 在线支付(发起时写入 pos_order.pay_type;具体方式如 Credit_CreditCard、ATM 系列、CVS 系列等由回调写入 pos_order_omg_payment.pay_type)。 */
-    public static final String PAY_TYPE_OMG = "2";
-
-    @Autowired
-    private IPosOrderService posOrderService;
-    @Autowired
-    private IPosStoreOmgService storeOmgService;
-    @Autowired
-    private IPosOrderOmgPaymentService paymentService;
-    @Autowired
-    private OmgPay omgPay;
-    @Autowired
-    private IIpnLogService ipnLogService;
-    @Autowired
-    private IInfoUserService infoUserService;
-    @Autowired
-    private PushEventService pushEventService;
-    @Autowired
-    private OrderLogHelper orderLogHelper;
-    @Autowired
-    private IPosOrderOmgRefundService refundService;
-    @Autowired
-    private OrderLifecycleService orderLifecycleService;
-    @Autowired
-    private PaymentCreateGuardService paymentCreateGuardService;
-    @Autowired
-    private OmgQueryThrottle omgQueryThrottle;
-
-    @Value("${omg.base-url}")
-    private String baseUrl;
-    @Value("${omg.return-url}")
-    private String returnUrl;
-    @Value("${omg.order-result-url}")
-    private String orderResultUrl;
-    @Value("${omg.payment-info-url}")
-    private String paymentInfoUrl;
-
-    /** create 复用新鲜期(分钟):窗口内且 trade_no 为空的活跃行复用 MTN,超期则轮换新建(T058 防堆积,stage 实测调整)。 */
-    @Value("${omg.create.reuse-fresh-minutes:3}")
-    private int reuseFreshMinutes;
-
-    // ============================ US1:发起 AIO 幕前支付 ============================
-
-    /**
-     * 发起 OMG AIO 幕前支付。校验订单归属/未支付/金额 → 查门店启用凭证 → 生成 MerchantTradeNo →
-     * 组参 + CheckMacValue → 落流水(pay_status=0) + 更新订单 payType=PAY_TYPE_OMG/payUrl → 返回 form 字段供前端 Form Post。
-     */
-    @Anonymous
-    @Auth
-    @RepeatSubmit(interval = 1000, message = "请求过于频繁")
-    @PostMapping("/create")
-    @Transactional(rollbackFor = Exception.class)
-    public AjaxResult create(@RequestHeader String token,
-                             @RequestBody(required = false) OmgOrderRequest request) {
-        if (invalidOrderRequest(request)) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        // 按 ddId 串行化发起(T058 防堆积):同一订单并发 create 在分布式锁内排队,锁释放在事务提交后,
-        // 保证后一个请求能看到前一个已提交的活跃流水 → 命中复用而非新建(详见 PaymentCreateGuardService)。
-        return paymentCreateGuardService == null ? createUnderLock(token, request)
-                : paymentCreateGuardService.withLock(request.getOrderid(),
-                () -> createUnderLock(token, request));
-    }
-
-    /**
-     * 发起支付的实际业务逻辑,在 {@link #create} 的 {@code @Transactional} 与 {@link PaymentCreateGuardService}
-     * 分布式锁内执行。锁释放在事务 afterCommit,确保并发请求看不到未提交的活跃流水 INSERT,避免重复活跃行。
-     *
-     * <p>流程:校验登录/订单归属/未支付/金额/门店 → 查门店 OMG 凭证 → 复用或新建 MerchantTradeNo →
-     * 组 AIO 参 + CheckMacValue → 落流水(pay_status=0) + 更新订单 payType/payUrl → 返回 form 字段。
-     */
-    private AjaxResult createUnderLock(String token, OmgOrderRequest request) {
-        if (invalidOrderRequest(request)) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        String orderid = request.getOrderid();
-        String userId;
-        try {
-            userId = new JwtUtil().getusid(token);
-        } catch (Exception e) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (userId == null || userId.isEmpty()) {
-            return error("请先登录");
-        }
-
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
-        if (order == null) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
-            return error("无权操作该订单");
-        }
-        if (!PAY_TYPE_OMG.equals(order.getPayType())) {
-            return error("订单支付方式不是 OMG");
-        }
-        if (order.getState() != null && order.getState() == 4L) {
-            return error("订单已取消,不可重新支付");
-        }
-        if (order.getPayStatus() != null && order.getPayStatus() == 1L) {
-            return error("订单已支付");
-        }
-        if (order.getAmount() == null || order.getAmount() <= 0) {
-            return error("订单金额异常");
-        }
-        if (order.getMdId() == null) {
-            return error("订单门店缺失");
-        }
-
-        // 门店 OMG 凭证(已开通且启用)→ 转 OmgPayConfig(Controller 层完成,避免 system→admin 反向依赖)
-        PosStoreOmg cred = storeOmgService.getEnabledCredential(order.getMdId());
-        if (cred == null) {
-            return error("该门店暂不支持线上支付");
-        }
-        OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
-
-        String merchantTradeNo = resolveMerchantTradeNo(order, cred);
-
-        // OMG AIO 参数(contracts/api.md §A1/B1);InvoiceMark=N 固定(发票走 ezPay);EncryptType=1 固定(SHA256)
-        SimpleDateFormat fmt = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
-        fmt.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
-        Map<String, String> params = new LinkedHashMap<>();
-        params.put("MerchantID", cred.getMerchantId());
-        params.put("MerchantTradeNo", merchantTradeNo);
-        params.put("MerchantTradeDate", fmt.format(new Date()));
-        params.put("PaymentType", "aio");
-        params.put("TotalAmount", String.valueOf(order.getAmount()));
-        params.put("TradeDesc", "food order " + orderid);
-        params.put("ItemName", "order " + orderid);
-        params.put("ReturnURL", returnUrl);
-        params.put("ChoosePayment", "ALL");
-        params.put("EncryptType", "1");
-        params.put("InvoiceMark", "N");
-        params.put("NeedExtraPaidInfo", "Y");
-        if (paymentInfoUrl != null && !paymentInfoUrl.isEmpty()) {
-            params.put("PaymentInfoURL", paymentInfoUrl);
-        }
-        if (orderResultUrl != null && !orderResultUrl.isEmpty()) {
-            params.put("OrderResultURL", orderResultUrl);
-        }
-
-        // 组参 + CheckMacValue,返回含 gatewayUrl 的 form 字段
-        Map<String, String> form = omgPay.createAioForm(baseUrl, cfg, params);
-
-        // 更新订单 payType=OMG(PAY_TYPE_OMG) / payUrl=gatewayUrl(仅更这两个字段)
-        PosOrder upd = new PosOrder();
-        upd.setId(order.getId());
-        upd.setPayType(PAY_TYPE_OMG);
-        upd.setPayUrl(form.get("gatewayUrl"));
-        posOrderService.saveOrUpdate(upd);
-
-        log.info("[OMG] create orderid={}, merchantTradeNo={}, amount={}, gatewayUrl={}",
-                orderid, merchantTradeNo, order.getAmount(), form.get("gatewayUrl"));
-        return success(form);
-    }
-
-    /**
-     * 解析本次发起用的 MerchantTradeNo(T058 防堆积核心)。
-     *
-     * <p>优先复用新鲜期内活跃未付且未取号(trade_no IS NULL)的旧 MTN(连点/重发起命中 → 不新建流水);
-     * 未命中则先把旧行轮换为历史(is_active 1→0,pay_status 不动,保留接迟到 notify 与审计),再新建活跃行。
-     * 必须在 create 的 @Transactional 与 PaymentCreateGuardService 分布式锁内调用。
-     */
-    private String resolveMerchantTradeNo(PosOrder order, PosStoreOmg cred) {
-        String ddId = String.valueOf(order.getDdId());
-        PosOrderOmgPayment reusable = paymentService.getActiveForReuse(ddId, reuseFreshMinutes);
-        if (reusable != null) {
-            log.info("[OMG] create reuse orderid={}, merchantTradeNo={} (fresh within {}min, trade_no null)",
-                    order.getDdId(), reusable.getMerchantTradeNo(), reuseFreshMinutes);
-            return reusable.getMerchantTradeNo();
-        }
-        paymentService.markActiveHistorical(ddId);
-        return createPaymentAttempt(order, cred);
-    }
-
-    /**
-     * MerchantTradeNo 生成:OMG 要求全平台永久唯一、≤20 字元、英数大小写混合,且不可复用。
-     * 使用 "OMG" + UUID 随机片段,并依靠数据库唯一索引与冲突重试兜底;ddId 由支付流水反查。
-     */
-    private String createPaymentAttempt(PosOrder order, PosStoreOmg cred) {
-        for (int attempt = 0; attempt < 3; attempt++) {
-            String merchantTradeNo = genMerchantTradeNo();
-            try {
-                paymentService.createPayment(String.valueOf(order.getDdId()), merchantTradeNo, order.getMdId(),
-                        cred.getMerchantId(), order.getAmount(), "ALL");
-                return merchantTradeNo;
-            } catch (DuplicateKeyException e) {
-                if (attempt == 2) {
-                    throw e;
-                }
-            }
-        }
-        throw new IllegalStateException("Unable to allocate OMG MerchantTradeNo");
-    }
-
-    private String genMerchantTradeNo() {
-        String random = UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT);
-        return "OMG" + random.substring(0, 17);
-    }
-
-    // ============================ US2:ReturnURL 支付结果回调 ============================
-
-    /**
-     * OMG 支付结果回调(@Anonymous,OMG 服务端 Form Post)。OMG 明文参数 + 单 CheckMacValue(无 AES 解密)。
-     *
-     * <p>记 IPN → 按 MerchantID 查凭证 → 验签 → trade_no 幂等 → 金额校验 → RtnCode==1 且 SimulatePaid!=1
-     * → markSuccess + 推送 → 回纯串 {@code 1|OK}(注意:非 JSON,与蓝新不同)。任何异常/校验失败都仍回 1|OK
-     * (OMG 收到非成功会重试),但绝不错误更新订单。
-     */
-    @Anonymous
-    @PostMapping(value = "/notify", produces = "text/plain;charset=UTF-8")
-    @Transactional(rollbackFor = Exception.class)
-    public String notify(@ModelAttribute OmgCallbackRequest callback,
-                         @RequestHeader(value = "X-Forwarded-For", required = false) String forwardedFor) {
-        Map<String, String> form;
-        try {
-            form = callback.toParameterMap();
-        } catch (IllegalArgumentException e) {
-            log.warn("OMG callback rejected: {}", e.getMessage());
-            return "1|OK";
-        }
-
-        // 记录 IPN 日志
-        try {
-            IpnLog ipnLog = new IpnLog();
-            ipnLog.setIp(callbackIp(forwardedFor));
-            ipnLog.setIpnLog(auditJson(form));
-            ipnLog.setType("omg");
-            ipnLogService.insertIpnLog(ipnLog);
-        } catch (Exception e) {
-            log.warn("记 OMG IPN 日志失败", e);
-        }
-
-        String merchantId = form.get("MerchantID");
-        if (merchantId == null || merchantId.isEmpty()) {
-            log.warn("OMG callback missing MerchantID");
-            return "1|OK";
-        }
-        PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(merchantId);
-        if (cred == null) {
-            log.warn("OMG 回调无匹配凭证: merchantId={}", merchantId);
-            return "1|OK";
-        }
-
-        // 验签(OMG 无解密,直接对全部回调参 + CheckMacValue 重算比对)
-        if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
-            log.warn("OMG 回调验签失败: merchantId={}", merchantId);
-            return "1|OK";
-        }
-
-        String tradeNo = form.get("TradeNo");
-        String merchantTradeNo = form.get("MerchantTradeNo");
-        if (tradeNo == null || tradeNo.isEmpty() || merchantTradeNo == null || merchantTradeNo.isEmpty()) {
-            log.warn("OMG callback missing trade identifiers: merchantId={}", merchantId);
-            return "1|OK";
-        }
-        int rtnCode = toInt(form.get("RtnCode"), -1);
-        int tradeAmt = toInt(form.get("TradeAmt"), -1);
-        String simulatePaid = form.get("SimulatePaid");
-        String paymentType = form.get("PaymentType");
-        String paymentDate = form.get("PaymentDate");
-
-        // 订单关联:由 MerchantTradeNo 反查流水与订单
-        PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
-        if (payment == null) {
-            log.warn("OMG 回调无对应发起记录: merchantTradeNo={}", merchantTradeNo);
-            return "1|OK";
-        }
-        if (!merchantId.equals(payment.getMerchantId()) || payment.getStoreId() == null
-                || cred.getStoreId() == null || !payment.getStoreId().equals(cred.getStoreId())) {
-            log.warn("OMG callback credential/ledger mismatch: merchantId={}, merchantTradeNo={}", merchantId, merchantTradeNo);
-            return "1|OK";
-        }
-        PosOrderOmgPayment exist = paymentService.getByTradeNo(tradeNo);
-        if (exist != null && !payment.getId().equals(exist.getId())) {
-            log.warn("OMG TradeNo already belongs to another payment: tradeNo={}", tradeNo);
-            return "1|OK";
-        }
-        String ddId = payment.getDdId();
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
-        if (order == null) {
-            log.warn("OMG 回调订单不存在: ddId={}", ddId);
-            return "1|OK";
-        }
-        if (order.getMdId() == null || !order.getMdId().equals(payment.getStoreId())
-                || !order.getMdId().equals(cred.getStoreId())) {
-            log.warn("OMG callback order/store mismatch: ddId={}", ddId);
-            return "1|OK";
-        }
-        // 金额校验(以平台实际应收金额为准)
-        if (payment.getAmount() == null || order.getAmount() == null
-                || tradeAmt != payment.getAmount() || order.getAmount().intValue() != payment.getAmount()) {
-            log.error("OMG callback amount mismatch: ddId={}, paymentAmt={}, orderAmt={}, callbackAmt={}",
-                    ddId, payment.getAmount(), order.getAmount(), tradeAmt);
-            return "1|OK";
-        }
-
-        boolean terminalOrder = (order.getState() != null && order.getState() == 4L)
-                || (order.getPayStatus() != null && order.getPayStatus() == 2L);
-
-        // 流水已成功但订单未核销时,重复回调用于补偿上一次订单更新失败。
-        if (Integer.valueOf(1).equals(payment.getPayStatus())) {
-            if (!tradeNo.equals(payment.getTradeNo())) {
-                log.warn("OMG paid callback TradeNo mismatch: merchantTradeNo={}", merchantTradeNo);
-                return "1|OK";
-            }
-            if (payment.getPayType() != null && paymentType != null && !paymentType.equals(payment.getPayType())) {
-                log.warn("OMG paid callback PaymentType mismatch: merchantTradeNo={}", merchantTradeNo);
-                return "1|OK";
-            }
-            if (rtnCode == 1 && !"1".equals(simulatePaid)
-                    && !Long.valueOf(1L).equals(order.getPayStatus())) {
-                recordPaidOrderWithoutFulfillment(order, terminalOrder
-                        ? "OMG支付成功回调晚于订单取消/退款,需立即退款或人工核对"
-                        : "系统补偿OMG已支付流水与订单支付状态不一致");
-            }
-            return "1|OK";
-        }
-
-        if (rtnCode == 1 && !"1".equals(simulatePaid)) {
-            Date payTime = parsePayTime(paymentDate);
-            Date tradeDate = parsePayTime(form.get("TradeDate"));
-            if (payTime == null || tradeDate == null) {
-                log.warn("OMG callback contains invalid date: merchantTradeNo={}", merchantTradeNo);
-                return "1|OK";
-            }
-            String authCode = form.get("auth_code");
-            if (authCode == null) {
-                authCode = form.get("AuthCode");
-            }
-            // 核销成功(notify/补单共用:幂等 markSuccess + 订单状态流转 + 推送)
-            applyPaidResult(payment, order, tradeNo, paymentType, rtnCode, form.get("RtnMsg"),
-                    authCode, payTime, tradeDate, auditJson(form));
-        } else if (rtnCode != 1) {
-            paymentService.markFail(payment.getId(), rtnCode, form.get("RtnMsg"), auditJson(form));
-            log.warn("OMG 回调交易失败: ddId={}, rtnCode={}, rtnMsg={}", ddId, rtnCode, form.get("RtnMsg"));
-        } else {
-            // SimulatePaid=1 模拟支付:不发货,仅记录
-            log.warn("OMG 回调为模拟支付(SimulatePaid=1),不发货: ddId={}", ddId);
-        }
-        return "1|OK";
-    }
-
-    /**
-     * 支付完成返回页(@Anonymous)。仅 302 引导回前端结果页(带 ddId),<b>不</b>改订单状态(以 notify 为准)。
-     */
-    @Anonymous
-    @RequestMapping(value = "/return", method = {RequestMethod.GET, RequestMethod.POST})
-    public void returnCallback(@ModelAttribute OmgCallbackRequest callback,
-                               HttpServletResponse response) throws IOException {
-        String ddId = "";
-        try {
-            Map<String, String> form = callback.toParameterMap();
-            String mtn = form.get("MerchantTradeNo");
-            // 由 MerchantTradeNo 反查流水拿 ddId(MTN 不再编码 ddId)
-            if (mtn != null && !mtn.isEmpty()) {
-                PosOrderOmgPayment p = paymentService.getByMerchantTradeNo(mtn);
-                if (p != null && p.getDdId() != null) {
-                    ddId = p.getDdId();
-                }
-            }
-        } catch (Exception e) {
-            log.warn("OMG ReturnURL 解析失败", e);
-        }
-        if (orderResultUrl == null || orderResultUrl.isEmpty()) {
-            log.warn("OMG ReturnURL 未配置 omg.order-result-url,无法重定向");
-            response.setStatus(204);
-            return;
-        }
-        String sep = orderResultUrl.contains("?") ? "&" : "?";
-        response.sendRedirect(orderResultUrl + sep + "ddId=" + URLEncoder.encode(ddId == null ? "" : ddId, StandardCharsets.UTF_8));
-    }
-
-    // ============================ US3:ATM/超商 取号回调 + 取号查询 ============================
-
-    /**
-     * OMG ATM/超商取号回调(@Anonymous,PaymentInfoURL)。验签后把虚帐/缴费码原始报文
-     * (BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate 等)落到流水的 callbackRaw,不改 pay_status;
-     * 实际付款后 OMG 再回调 /pay/omg/notify(RtnCode=1)走 US2 核销。回纯串 {@code 1|OK}。
-     */
-    @Anonymous
-    @PostMapping(value = "/paymentInfo", produces = "text/plain;charset=UTF-8")
-    public String paymentInfoCallback(@ModelAttribute OmgCallbackRequest callback,
-                                      @RequestHeader(value = "X-Forwarded-For", required = false) String forwardedFor) {
-        Map<String, String> form;
-        try {
-            form = callback.toParameterMap();
-        } catch (IllegalArgumentException e) {
-            log.warn("OMG paymentInfo rejected: {}", e.getMessage());
-            return "1|OK";
-        }
-        try {
-            IpnLog ipnLog = new IpnLog();
-            ipnLog.setIp(callbackIp(forwardedFor));
-            ipnLog.setIpnLog(auditJson(form));
-            ipnLog.setType("omg");
-            ipnLogService.insertIpnLog(ipnLog);
-        } catch (Exception e) {
-            log.warn("记 OMG paymentInfo IPN 日志失败", e);
-        }
-
-        String merchantId = form.get("MerchantID");
-        if (merchantId == null || merchantId.isEmpty()) {
-            log.warn("OMG paymentInfo missing MerchantID");
-            return "1|OK";
-        }
-        PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(merchantId);
-        if (cred == null) {
-            log.warn("OMG paymentInfo 无匹配凭证: merchantId={}", merchantId);
-            return "1|OK";
-        }
-        if (!OmgCheckMacValue.verify(form, cred.getHashKey(), cred.getHashIv())) {
-            log.warn("OMG paymentInfo 验签失败: merchantId={}", merchantId);
-            return "1|OK";
-        }
-
-        String merchantTradeNo = form.get("MerchantTradeNo");
-        PosOrderOmgPayment payment = paymentService.getByMerchantTradeNo(merchantTradeNo);
-        if (payment == null) {
-            log.warn("OMG paymentInfo 无对应发起记录: merchantTradeNo={}", merchantTradeNo);
-            return "1|OK";
-        }
-        if (!merchantId.equals(payment.getMerchantId()) || payment.getStoreId() == null
-                || cred.getStoreId() == null || !payment.getStoreId().equals(cred.getStoreId())) {
-            log.warn("OMG paymentInfo credential/ledger mismatch: merchantTradeNo={}", merchantTradeNo);
-            return "1|OK";
-        }
-        // 取号成功 RtnCode:ATM=2,CVS/BarcodeATM=10100073(取号本身非付款;付款成功另走 /notify 的 RtnCode=1)
-        int pickupRtnCode = toInt(form.get("RtnCode"), -1);
-        if (pickupRtnCode != 2 && pickupRtnCode != 10100073) {
-            log.warn("OMG paymentInfo 取号非成功 rtnCode={}, merchantTradeNo={}", pickupRtnCode, merchantTradeNo);
-            return "1|OK";
-        }
-        // 落取号信息(BankCode/vAccount/ExpireDate 或 PaymentNo/ExpireDate)为 JSON,不改 pay_status
-        paymentService.markPaymentInfo(payment.getId(), form.get("TradeNo"), auditJson(form));
-        log.info("[OMG] paymentInfo 已记录取号信息: ddId={}, merchantTradeNo={}", payment.getDdId(), merchantTradeNo);
-        return "1|OK";
-    }
-
-    /**
-     * 取号信息查询(前端 ATM/超商结果页展示虚帐/缴费码 + 期限用,@Auth)。
-     * 返回 payType/amount/payStatus + 解析后的取号字段 info。
-     */
-    @Anonymous
-    @Auth
-    @GetMapping("/paymentInfo/{orderid}")
-    public AjaxResult getPaymentInfo(@RequestHeader String token, @PathVariable String orderid) {
-        String userId;
-        try {
-            userId = new JwtUtil().getusid(token);
-        } catch (Exception e) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (userId == null || userId.isEmpty()) {
-            return error("请先登录");
-        }
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
-        if (order == null || order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
-            return error("无权操作该订单");
-        }
-        // 优先取已付/退款行(展示回执);无则取最新行(未付款场景展示 ATM/超商虚帐)
-        PosOrderOmgPayment p = paymentService.getLatestRefundableByDdId(orderid);
-        if (p == null) {
-            p = paymentService.getLatestByDdId(orderid);
-        }
-        if (p == null) {
-            return error("无支付记录");
-        }
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("payType", p.getPayType());
-        result.put("amount", p.getAmount());
-        result.put("payStatus", p.getPayStatus());
-        result.put("info", parsePaymentInfo(p.getCallbackRaw()));
-        return success(result);
-    }
-
-    // ============================ US4:订单取消与退款 ============================
-
-    /**
-     * OMG 退款(@Auth,供取消链路/管理员调用)。信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);
-     * ATM/超商/BarcodeATM 无退款 API → 记录待人工在 OMG 后台处理。仅正式端点可用(stage DoAction 不可用)。
-     */
-    @Anonymous
-    @Auth
-    @PostMapping("/refund")
-    public AjaxResult refund(@RequestHeader String token,
-                             @RequestBody(required = false) OmgOrderRequest request) {
-        if (invalidOrderRequest(request)) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        String orderid = request.getOrderid();
-        String userId;
-        try {
-            userId = new JwtUtil().getusid(token);
-        } catch (Exception e) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (userId == null || userId.isEmpty()) {
-            return error("请先登录");
-        }
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
-        if (order == null) {
-            return error("订单不存在");
-        }
-        if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
-            return error("无权操作该订单");
-        }
-        return refundOrder(order);
-    }
-
-    /**
-     * OMG 退款核心(public,供取消链路 {@code cancelOrder} 直接调用,免重复实现)。
-     * 信用卡(含 Apple Pay) 调 DoAction(Action=R 退刷);ATM/超商/BarcodeATM 记人工。仅正式端点可用。
-     * 返回 success=已退款;error=需人工或失败(refund 已落记录可重试)。
-     */
-    public AjaxResult refundOrder(PosOrder order) {
-        OmgRefundOutcome outcome = refundOrderOutcome(order);
-        if (outcome.getStatus() == OmgRefundOutcome.Status.REFUNDED
-                || outcome.getStatus() == OmgRefundOutcome.Status.IDEMPOTENT) {
-            try {
-                orderLifecycleService.finalizeSystemOmgRefund(order.getId());
-                return success(outcome.getMessage());
-            } catch (Exception e) {
-                log.error("OMG refund succeeded but order state synchronization failed: orderId={}, errorType={}",
-                        order == null ? null : order.getId(), e.getClass().getSimpleName());
-                return error("OMG 退款成功,本地订单状态待同步");
-            }
-        }
-        return error(outcome.getMessage());
-    }
-
-    /**
-     * 返回明确退款结果,供管理端根据资金事实同步订单状态。
-     */
-    public OmgRefundOutcome refundOrderOutcome(PosOrder order) {
-        if (order == null) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "订单不存在");
-        }
-        if (!PAY_TYPE_OMG.equals(order.getPayType())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "该订单非 OMG 支付");
-        }
-        if (order.getPayStatus() == null || order.getPayStatus() != 1L) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "订单未支付,无需退款");
-        }
-        String ddId = String.valueOf(order.getDdId());
-        PosOrderOmgPayment payment = paymentService.getLatestRefundableByDdId(ddId);
-        if (payment == null) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无 OMG 支付流水");
-        }
-        if (Integer.valueOf(3).equals(payment.getPayStatus())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 已完成退款");
-        }
-        if (Integer.valueOf(4).equals(payment.getPayStatus())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN,
-                    "OMG 退款结果待确认,请勿重复发起");
-        }
-        if (!Integer.valueOf(1).equals(payment.getPayStatus())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水不是已支付状态");
-        }
-        int amount = payment.getAmount() == null ? 0 : payment.getAmount();
-        String payType = payment.getPayType();
-        if (amount <= 0 || payment.getTradeNo() == null || payment.getTradeNo().isEmpty()) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水不完整");
-        }
-        if (order.getAmount() == null || order.getAmount().intValue() != amount) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 支付流水金额与订单不一致");
-        }
-
-        // ATM/超商/BarcodeATM 无退款 API → 记录待人工
-        if (!"Credit_CreditCard".equals(payType)) {
-            List<com.ruoyi.system.domain.PosOrderOmgRefund> records = refundService.listByPayment(payment.getId());
-            boolean pending = records != null && records.stream()
-                    .anyMatch(row -> row.getAction() == null && row.getRtnCode() == null);
-            boolean completed = records != null && records.stream()
-                    .anyMatch(row -> row.getAction() == null && Integer.valueOf(1).equals(row.getRtnCode()));
-            if (completed) {
-                return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 人工退款已确认完成");
-            }
-            if (!pending) {
-                refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, amount, null,
-                        "延期支付方式无退款API,待人工在 OMG 后台处理", "");
-            }
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.MANUAL_PENDING,
-                    "该支付方式需在 OMG 后台人工退款,订单暂保持已支付");
-        }
-
-        if (paymentService.markRefunding(payment.getId()) == 0) {
-            PosOrderOmgPayment latest = paymentService.getLatestRefundableByDdId(ddId);
-            if (latest != null && Integer.valueOf(3).equals(latest.getPayStatus())) {
-                return OmgRefundOutcome.of(OmgRefundOutcome.Status.IDEMPOTENT, "OMG 已完成退款");
-            }
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "退款已处理或正在处理中");
-        }
-
-        PosStoreOmg cred = storeOmgService.getCredentialByMerchantId(payment.getMerchantId());
-        if (cred == null || cred.getStoreId() == null || !cred.getStoreId().equals(payment.getStoreId())) {
-            paymentService.restorePaidFromRefunding(payment.getId());
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "门店 OMG 凭证不可用");
-        }
-        OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
-
-        // 信用卡(含 Apple Pay)→ DoAction(Action=R 退刷);MVP 统一 R,失败再按状态分支(D7)
-        refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, null,
-                "退款请求处理中", "");
-        Map<String, String> resp;
-        try {
-            resp = omgPay.doAction(baseUrl, cfg, payment.getMerchantTradeNo(), payment.getTradeNo(), "R", amount);
-        } catch (Exception e) {
-            refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, null,
-                    "退款结果未知,需对账", "");
-            log.error("OMG refund outcome unknown: ddId={}, paymentId={}, errorType={}",
-                    ddId, payment.getId(), e.getClass().getSimpleName());
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN,
-                    "OMG 退款结果待确认,请勿重复发起");
-        }
-        int rtnCode = toInt(resp == null ? null : resp.get("RtnCode"), -1);
-        String rtnMsg = resp == null ? "" : resp.get("RtnMsg");
-        refundService.record(payment.getId(), ddId, payment.getTradeNo(), "R", amount, rtnCode, rtnMsg,
-                resp == null ? "" : JSON.toJSONString(resp));
-
-        if (rtnCode == 1) {
-            if (paymentService.markRefunded(payment.getId()) == 0) {
-                log.error("OMG refund succeeded but ledger transition failed: paymentId={}", payment.getId());
-                return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 退款成功,账务状态待核对");
-            }
-            orderLogHelper.logSync(ddId, 0, null, "系统", "OMG 信用卡退款成功");
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.REFUNDED, "退款成功");
-        }
-        paymentService.restorePaidFromRefunding(payment.getId());
-        log.warn("OMG refund failed: ddId={}, paymentId={}, rtnCode={}", ddId, payment.getId(), rtnCode);
-        return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "OMG 退款失败,请核对后重试");
-    }
-
-    /**
-     * 管理员已在 OMG 后台核实完成延期支付退款后,收口支付流水。
-     */
-    public OmgRefundOutcome confirmManualRefundOutcome(PosOrder order) {
-        if (order == null || !PAY_TYPE_OMG.equals(order.getPayType())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "该订单非 OMG 支付");
-        }
-        String ddId = String.valueOf(order.getDdId());
-        PosOrderOmgPayment payment = paymentService.getLatestRefundableByDdId(ddId);
-        if (payment == null || "Credit_CreditCard".equals(payment.getPayType())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无可确认的 OMG 人工退款待办");
-        }
-        List<com.ruoyi.system.domain.PosOrderOmgRefund> records = refundService.listByPayment(payment.getId());
-        boolean pending = records != null && records.stream()
-                .anyMatch(row -> row.getAction() == null && row.getRtnCode() == null);
-        boolean completed = records != null && records.stream()
-                .anyMatch(row -> row.getAction() == null && Integer.valueOf(1).equals(row.getRtnCode()));
-        if (!pending && !completed) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.FAILED, "无可确认的 OMG 人工退款待办");
-        }
-        if (Integer.valueOf(4).equals(payment.getPayStatus())) {
-            return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 退款状态正在处理中");
-        }
-        if (!Integer.valueOf(3).equals(payment.getPayStatus())) {
-            if (!Integer.valueOf(1).equals(payment.getPayStatus())
-                    || paymentService.markRefunding(payment.getId()) == 0
-                    || paymentService.markRefunded(payment.getId()) == 0) {
-                return OmgRefundOutcome.of(OmgRefundOutcome.Status.UNKNOWN, "OMG 人工退款状态发生变化,请刷新");
-            }
-        }
-        if (!completed) {
-            refundService.record(payment.getId(), ddId, payment.getTradeNo(), null, payment.getAmount(), 1,
-                    "管理员确认已在 OMG 后台完成人工退款", "");
-        }
-        return OmgRefundOutcome.of(OmgRefundOutcome.Status.REFUNDED, "已确认 OMG 人工退款完成");
-    }
-
-    // ============================ US6:漏单补单(回调可靠性 / callback-reconcile.md) ============================
-
-    /**
-     * 方案A 被动补单(@Auth)。前端结果页轮询仍 {@code payStatus=0} 时调用:后端查 OMG 真实状态并补单。
-     * 返回 {@code {payStatus:0未付/1已付/2失败, reconciled:true=本次触发补单}}。严格幂等(见 reconcileByQuery)。
-     */
-    @Anonymous
-    @Auth
-    @RepeatSubmit(interval = 2000, message = "查询过于频繁")
-    @PostMapping("/query")
-    public AjaxResult query(@RequestHeader String token,
-                            @RequestBody(required = false) OmgOrderRequest request) {
-        if (invalidOrderRequest(request)) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        String orderid = request.getOrderid();
-        String userId;
-        try {
-            userId = new JwtUtil().getusid(token);
-        } catch (Exception e) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (userId == null || userId.isEmpty()) {
-            return error("请先登录");
-        }
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", orderid));
-        if (order == null) {
-            return error(MessageUtils.message("no.order.id.error"));
-        }
-        if (order.getUserId() == null || !userId.equals(String.valueOf(order.getUserId()))) {
-            return error("无权操作该订单");
-        }
-        if (!PAY_TYPE_OMG.equals(order.getPayType())) {
-            return error("该订单非 OMG 支付");
-        }
-        if (order.getState() != null && order.getState() == 4L) {
-            return error("订单已取消");
-        }
-        Map<String, Object> result = new LinkedHashMap<>();
-        Long payStatus = order.getPayStatus();
-        // 订单已核销/已退款 → 直接返回,不重复查 OMG
-        if (payStatus != null && (payStatus == 1L || payStatus == 2L)) {
-            result.put("payStatus", payStatus == 1L ? 1 : 2);
-            result.put("reconciled", false);
-            return success(result);
-        }
-        int[] res = reconcileByQuery(orderid, "query");
-        result.put("payStatus", res[0]);
-        result.put("reconciled", res[1] == 1);
-        return success(result);
-    }
-
-    /**
-     * 查询 OMG 真实交易状态并按结果补单/标失败。{@code /query}(被动补单/方案A) 与定时任务(方案B)共用。
-     *
-     * <p>幂等与自愈:流水已 {@code pay_status=1} 时,若订单未核销(跨事务中断残留)则补推订单状态,
-     * 否则直接返回不重复处理;流水处于失败/退款终态不再查询。
-     *
-     * @param ddId   订单号
-     * @param source 调用来源标记("query" 被动补单 / "scheduled" 定时补单),仅用于日志
-     * @return {@code int[2]} = {payStatus, reconciled}:payStatus 0未付/1已付/2失败;reconciled 1=本次触发补单核销
-     */
-        public int[] reconcileByQuery(String ddId, String source) {
-        PosOrder order = posOrderService.getOne(new QueryWrapper<PosOrder>().eq("dd_id", ddId));
-        if (order == null) {
-            return new int[]{0, 0};
-        }
-        if (order.getState() != null && order.getState() == 4L) {
-            return new int[]{0, 0};
-        }
-
-        // ① 自愈:流水已付(pay_status IN(1,3,4))但订单未核销(跨事务中断残留)→ 补推订单状态
-        PosOrderOmgPayment paid = paymentService.getLatestPaidByDdId(ddId);
-        if (paid != null) {
-            boolean isPaid = Integer.valueOf(1).equals(paid.getPayStatus());
-            if (isPaid && (order.getPayStatus() == null || order.getPayStatus() == 0L)) {
-                handlePaymentSuccess(order);
-            }
-            return new int[]{isPaid ? 1 : 0, 0};
-        }
-
-        // 闸3:per-ddId 扫描间隔节流(/query 60s / 定时 180s),挡前端轮询放大、防 OMG 403 自残(T055)
-        if (!omgQueryThrottle.acquireDdIdSlot(ddId, source)) {
-            log.debug("[OMG-{}] per-ddId 节流,跳过本轮 OMG 查询: ddId={}", source, ddId);
-            return new int[]{order.getPayStatus() == null ? 0 : order.getPayStatus().intValue(), 0};
-        }
-
-        // ② 遍历全量未付(pay_status=0,含历史行)queryTrade,任一已付即补单核销(补较早 MTN 如 id=26)
-        List<PosOrderOmgPayment> unpaid = paymentService.listUnpaidByDdId(ddId);
-        if (unpaid == null || unpaid.isEmpty()) {
-            return new int[]{0, 0};
-        }
-        int lastStatus = 0;
-        for (PosOrderOmgPayment p : unpaid) {
-            PosStoreOmg cred = storeOmgService.getCredentialByMerchantIdAndStoreId(p.getMerchantId(), p.getStoreId());
-            if (cred == null) {
-                log.error("[OMG-{}] 门店凭证不可用: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
-                continue;
-            }
-            // per-MerchantID 令牌桶节流(1token/3s,burst1):拿不到令牌跳过本行本轮,不阻塞(防 OMG 403)
-            if (!omgQueryThrottle.tryAcquireMerchantToken(p.getMerchantId())) {
-                log.debug("[OMG-{}] MerchantID 令牌桶限流,跳过本行本轮: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
-                continue;
-            }
-            OmgPayConfig cfg = new OmgPayConfig(cred.getMerchantId(), cred.getHashKey(), cred.getHashIv());
-            Map<String, String> resp;
-            try {
-                resp = omgPay.queryTrade(baseUrl, cfg, p.getMerchantTradeNo());
-            } catch (Exception e) {
-                log.error("[OMG-{}] queryTrade 失败(下轮重试): ddId={}, mtcn={}, err={}: {}",
-                        source, ddId, p.getMerchantTradeNo(), e.getClass().getName(), e.getMessage());
-                Map<String, String> errLog = new LinkedHashMap<>();
-                errLog.put("ddId", ddId);
-                errLog.put("mtcn", p.getMerchantTradeNo());
-                errLog.put("source", source);
-                errLog.put("error", e.getClass().getName() + ": " + e.getMessage());
-                IpnLog ipnLog = new IpnLog();
-                ipnLog.setType("omg_query_error");
-                ipnLog.setIpnLog(JSON.toJSONString(errLog));
-                ipnLogService.insertIpnLog(ipnLog);
-                // OMG 按 MerchantID 限流(HTTP 403)时停止遍历,避免连发罚(T055 令牌桶节流完整前的过渡)
-                if (e.getMessage() != null && e.getMessage().contains("HTTP 403")) {
-                    log.warn("[OMG-{}] queryTrade 触发 OMG 403 限流,停止本单遍历(下轮重试): ddId={}", source, ddId);
-                    return new int[]{lastStatus, 0};
-                }
-                continue;
-            }
-            String tradeStatus = resp.get("TradeStatus");
-            if ("1".equals(tradeStatus)) {
-                String tradeNo = resp.get("TradeNo");
-                String paymentType = resp.get("PaymentType");
-                int respAmt = toInt(resp.get("TradeAmt"), -1);
-                if (tradeNo == null || tradeNo.isEmpty()
-                        || p.getAmount() == null || respAmt != p.getAmount()
-                        || order.getAmount() == null || order.getAmount().intValue() != p.getAmount()) {
-                    log.error("[OMG-{}] 补单金额/字段校验失败: ddId={}, mtcn={}, paymentAmt={}, orderAmt={}, queryAmt={}, tradeNo={}",
-                            source, ddId, p.getMerchantTradeNo(), p.getAmount(), order.getAmount(), respAmt, tradeNo);
-                    orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单金额/字段不符,需人工核对");
-                    continue;
-                }
-                Date payTime = parsePayTime(resp.get("PaymentDate"));
-                Date tradeDate = parsePayTime(resp.get("TradeDate"));
-                if (payTime == null || tradeDate == null) {
-                    log.warn("[OMG-{}] 补单响应日期非法: ddId={}, mtcn={}", source, ddId, p.getMerchantTradeNo());
-                    continue;
-                }
-                applyPaidResult(p, order, tradeNo, paymentType, 1, resp.get("RtnMsg"),
-                        null, payTime, tradeDate, auditJson(resp));
-                orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单成功");
-                log.info("[OMG-{}] 补单成功: ddId={}, mtcn={}, tradeNo={}", source, ddId, p.getMerchantTradeNo(), tradeNo);
-                return new int[]{1, 1};
-            } else if ("10200095".equals(tradeStatus)) {
-                String tradeNo = resp.get("TradeNo");
-                if (tradeNo == null || tradeNo.isEmpty()) {
-                    // 从未与 OMG 建立交易(未取号)→ markFail(0→2,CAS 允许 2→1 复活)
-                    paymentService.markFail(p.getId(), null, "OMG查询返回失败(10200095)", auditJson(resp));
-                    orderLogHelper.logSync(ddId, 0, null, "系统", "OMG查询补单:交易失败(10200095),mtcn=" + p.getMerchantTradeNo());
-                    lastStatus = 2;
-                } else {
-                    // 已取号(可能 OMG 延迟建案/结算滞后)→ 不标失败,下轮再查
-                    log.warn("[OMG-{}] queryTrade 返回 10200095 但已取号(tradeNo={}),不标失败: ddId={}, mtcn={}",
-                            source, tradeNo, ddId, p.getMerchantTradeNo());
-                }
-            } else {
-                // TradeStatus=0 未付(延期支付付款前常态)或未知 → no-op,定时任务下轮再查
-                log.warn("[OMG-{}] queryTrade 返回未付/未知: ddId={}, mtcn={}, TradeStatus={}",
-                        source, ddId, p.getMerchantTradeNo(), tradeStatus);
-            }
-        }
-        return new int[]{lastStatus, 0};
-    }
-
-    /**
-     * 应用「已支付」结果(notify 回调成功分支、/query 被动补单、定时补单 三处共用)。
-     * 幂等:markSuccess 按 trade_no CAS,已 pay_status=1 返回 0 → 不重复改单/推送。
-     * 若订单已进终态(取消/退款),只记录资金事实不触发发货推送(需人工退款/核对)。
-     */
-    private void applyPaidResult(PosOrderOmgPayment payment, PosOrder order, String tradeNo,
-                                 String payType, int rtnCode, String rtnMsg, String authCode,
-                                 Date payTime, Date tradeDate, String callbackRaw) {
-        boolean terminalOrder = (order.getState() != null && order.getState() == 4L)
-                || (order.getPayStatus() != null && order.getPayStatus() == 2L);
-        int n = paymentService.markSuccess(payment.getId(), tradeNo, payType, rtnCode, rtnMsg,
-                authCode, payTime, tradeDate, callbackRaw);
-        if (n <= 0) {
-            return;
-        }
-        if (terminalOrder) {
-            recordPaidOrderWithoutFulfillment(order,
-                    "OMG支付成功晚于订单取消/退款,需立即退款或人工核对");
-            log.error("OMG late successful payment requires refund/reconciliation: ddId={}, tradeNo={}",
-                    order.getDdId(), tradeNo);
-        } else {
-            // 首次成功:原子核销订单后,在事务提交后推送用户/商家
-            handlePaymentSuccess(order);
-        }
-    }
-
-    // ============================ 支付成功业务链路(参照 PosOrderController.sendHdfkMessage 货到付款同款) ============================
-
-    /** 支付成功:原子更新订单支付状态并写日志;事务提交后再推送,避免外部调用干扰账务事务。 */
-    private void handlePaymentSuccess(PosOrder order) {
-        PosOrder upd = new PosOrder();
-        upd.setPayStatus(1L);
-        boolean updated = posOrderService.update(upd, new UpdateWrapper<PosOrder>()
-                .eq("id", order.getId())
-                .eq("state", 0)
-                .eq("pay_status", 0));
-        if (!updated) {
-            PosOrder latest = posOrderService.getById(order.getId());
-            if (latest != null && (Long.valueOf(1L).equals(latest.getPayStatus())
-                    || Long.valueOf(2L).equals(latest.getPayStatus())
-                    || Long.valueOf(4L).equals(latest.getState()))) {
-                recordPaidOrderWithoutFulfillment(latest,
-                        "OMG支付核销遇到订单状态并发变化,需退款或人工核对");
-                return;
-            }
-            throw new IllegalStateException("原子更新OMG订单支付状态失败");
-        }
-
-        orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", "系统收到OMG支付成功回调");
-        runAfterCommit(() -> pushPaymentSuccess(order));
-    }
-
-    /** 只记录资金事实,不改变订单业务状态、不触发发货推送。 */
-    private void recordPaidOrderWithoutFulfillment(PosOrder order, String logContent) {
-        PosOrder upd = new PosOrder();
-        upd.setId(order.getId());
-        upd.setPayStatus(1L);
-        if (!posOrderService.saveOrUpdate(upd)) {
-            throw new IllegalStateException("补偿OMG订单支付状态失败");
-        }
-        orderLogHelper.logSync(String.valueOf(order.getDdId()), 0, null, "系统", logContent);
-    }
-
-    private void runAfterCommit(Runnable action) {
-        if (!TransactionSynchronizationManager.isSynchronizationActive()) {
-            action.run();
-            return;
-        }
-        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
-            @Override
-            public void afterCommit() {
-                action.run();
-            }
-        });
-    }
-
-    private void pushPaymentSuccess(PosOrder order) {
-        try {
-            String ddId = String.valueOf(order.getDdId());
-            String title = MessageUtils.message("no.message.push.message");
-            String body = OrderPushBodyDto.getJson(ddId, "0", 0);
-
-            InfoUser user = order.getUserId() == null ? null : infoUserService.getById(order.getUserId());
-            if (user != null) {
-                PayPush push = new PayPush();
-                push.apppush(user.getCid(), title, MessageUtils.message("no.message.push.payment.success"), body);
-                pushEventService.PublisherEvent(user.getUserId(), title,
-                        MessageUtils.message("no.message.push.payment.success"), body);
-            }
-
-            InfoUser sh = order.getShId() == null ? null : infoUserService.getById(order.getShId());
-            if (sh != null) {
-                PayPush push = new PayPush();
-                push.shpush(sh.getCid(), title, MessageUtils.message("no.message.push.new.order"), body);
-                pushEventService.PublisherEvent(sh.getUserId(), title,
-                        MessageUtils.message("no.message.push.new.order"), body);
-            }
-        } catch (Exception e) {
-            log.error("OMG 支付成功推送异常: ddId={}", order.getDdId(), e);
-        }
-    }
-
-    // ============================ 辅助 ============================
-
-    private String callbackIp(String forwardedFor) {
-        if (forwardedFor == null || forwardedFor.isBlank()) {
-            return "unknown";
-        }
-        String firstIp = forwardedFor.split(",", 2)[0].trim();
-        return firstIp.isEmpty() || firstIp.length() > 64 ? "unknown" : firstIp;
-    }
-
-    private boolean invalidOrderRequest(OmgOrderRequest request) {
-        if (request == null || request.getOrderid() == null) {
-            return true;
-        }
-        String orderid = request.getOrderid();
-        return orderid.trim().isEmpty();
-    }
-
-    private int toInt(String s, int def) {
-        if (s == null || s.isEmpty()) {
-            return def;
-        }
-        try {
-            return Integer.parseInt(s);
-        } catch (Exception e) {
-            return def;
-        }
-    }
-
-    /** 解析 callbackRaw(JSON)为 Map,供前端读 ATM/超商取号信息。 */
-    private Map<String, String> parseJsonObject(String json) {
-        Map<String, String> map = new LinkedHashMap<>();
-        if (json == null || json.isEmpty()) {
-            return map;
-        }
-        try {
-            com.alibaba.fastjson2.JSONObject obj = JSON.parseObject(json);
-            for (String k : obj.keySet()) {
-                Object v = obj.get(k);
-                map.put(k, v == null ? "" : v.toString());
-            }
-        } catch (Exception e) {
-            log.warn("解析 callbackRaw JSON 失败", e);
-        }
-        return map;
-    }
-
-    /** 取号页只返回支付凭证字段,避免把信用卡额外回传参数暴露给客户端。 */
-    private Map<String, String> parsePaymentInfo(String json) {
-        Set<String> allowed = Set.of("BankCode", "vAccount", "PaymentNo", "ExpireDate",
-                "PaymentType", "TradeNo", "MerchantTradeNo", "RtnCode", "RtnMsg",
-                "CVSStoreID", "CVSStoreName", "PayFrom", "Barcode1", "Barcode2", "Barcode3");
-        Map<String, String> raw = parseJsonObject(json);
-        raw.keySet().removeIf(key -> !allowed.contains(key));
-        return raw;
-    }
-
-    private String auditJson(Map<String, String> form) {
-        Map<String, String> audit = new LinkedHashMap<>(form);
-//        audit.remove("CheckMacValue");
-//        audit.remove("AuthCode");
-//        audit.remove("auth_code");
-//        audit.remove("card4no");
-//        audit.remove("card6no");
-        return JSON.toJSONString(audit);
-    }
-
-    /** 严格解析 OMG 回调日期;非法日期返回 null,避免把坏数据伪装成当前时间。 */
-    private Date parsePayTime(String s) {
-        if (s == null || s.isEmpty()) {
-            return null;
-        }
-        String[] fmts = {"yyyy/MM/dd HH:mm:ss", "yyyy-MM-dd HH:mm:ss"};
-        for (String f : fmts) {
-            try {
-                SimpleDateFormat format = new SimpleDateFormat(f);
-                format.setLenient(false);
-                format.setTimeZone(TimeZone.getTimeZone("Asia/Taipei"));
-                return format.parse(s);
-            } catch (Exception ignore) {
-            }
-        }
-        return null;
-    }
-}

+ 0 - 153
ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgCallbackRequest.java

@@ -1,153 +0,0 @@
-package com.ruoyi.app.pay.dto;
-
-import lombok.Data;
-
-import java.util.LinkedHashMap;
-import java.util.Map;
-
-/**
- * OMG AIO 支付、取号及结果页回调参数。
- *
- * <p>OMG 使用 PascalCase/form-urlencoded 字段并基于原字段名计算 CheckMacValue,
- * 因此属性名与 OMG 协议保持一致。未列入协议白名单的字段不会进入验签。</p>
- */
-@Data
-public class OmgCallbackRequest {
-
-    private static final int MAX_VALUE_LENGTH = 4096;
-
-    private String[] MerchantID;
-    private String[] MerchantTradeNo;
-    private String[] StoreID;
-    private String[] RtnCode;
-    private String[] RtnMsg;
-    private String[] TradeNo;
-    private String[] TradeAmt;
-    private String[] PaymentDate;
-    private String[] PaymentType;
-    private String[] PaymentTypeChargeFee;
-    private String[] TradeDate;
-    private String[] SimulatePaid;
-    private String[] CustomField1;
-    private String[] CustomField2;
-    private String[] CustomField3;
-    private String[] CustomField4;
-    private String[] CheckMacValue;
-
-    private String[] BankCode;
-    private String[] vAccount;
-    private String[] ExpireDate;
-    private String[] PaymentNo;
-    private String[] Barcode1;
-    private String[] Barcode2;
-    private String[] Barcode3;
-    private String[] CVSStoreID;
-    private String[] CVSStoreName;
-    private String[] PayFrom;
-
-    private String[] AlipayID;
-    private String[] AlipayTradeNo;
-    private String[] ATMAccBank;
-    private String[] ATMAccNo;
-    private String[] WebATMAccBank;
-    private String[] WebATMAccNo;
-    private String[] WebATMBankName;
-    private String[] auth_code;
-    private String[] AuthCode;
-    private String[] card4no;
-    private String[] card6no;
-    private String[] eci;
-    private String[] gwsr;
-    private String[] process_date;
-    private String[] stage;
-    private String[] stast;
-    private String[] staed;
-    private String[] red_dan;
-    private String[] red_de_amt;
-    private String[] red_ok_amt;
-    private String[] red_yet;
-    private String[] PeriodType;
-    private String[] Frequency;
-    private String[] ExecTimes;
-    private String[] PeriodAmount;
-    private String[] amount;
-    private String[] TotalSuccessTimes;
-    private String[] TotalSuccessAmount;
-
-    /**
-     * 转为 OMG 验签所需的原始字段名集合,并在进入验签前限制单字段长度。
-     */
-    public Map<String, String> toParameterMap() {
-        Map<String, String> parameters = new LinkedHashMap<>();
-        put(parameters, "MerchantID", MerchantID);
-        put(parameters, "MerchantTradeNo", MerchantTradeNo);
-        put(parameters, "StoreID", StoreID);
-        put(parameters, "RtnCode", RtnCode);
-        put(parameters, "RtnMsg", RtnMsg);
-        put(parameters, "TradeNo", TradeNo);
-        put(parameters, "TradeAmt", TradeAmt);
-        put(parameters, "PaymentDate", PaymentDate);
-        put(parameters, "PaymentType", PaymentType);
-        put(parameters, "PaymentTypeChargeFee", PaymentTypeChargeFee);
-        put(parameters, "TradeDate", TradeDate);
-        put(parameters, "SimulatePaid", SimulatePaid);
-        put(parameters, "CustomField1", CustomField1);
-        put(parameters, "CustomField2", CustomField2);
-        put(parameters, "CustomField3", CustomField3);
-        put(parameters, "CustomField4", CustomField4);
-        put(parameters, "CheckMacValue", CheckMacValue);
-        put(parameters, "BankCode", BankCode);
-        put(parameters, "vAccount", vAccount);
-        put(parameters, "ExpireDate", ExpireDate);
-        put(parameters, "PaymentNo", PaymentNo);
-        put(parameters, "Barcode1", Barcode1);
-        put(parameters, "Barcode2", Barcode2);
-        put(parameters, "Barcode3", Barcode3);
-        put(parameters, "CVSStoreID", CVSStoreID);
-        put(parameters, "CVSStoreName", CVSStoreName);
-        put(parameters, "PayFrom", PayFrom);
-        put(parameters, "AlipayID", AlipayID);
-        put(parameters, "AlipayTradeNo", AlipayTradeNo);
-        put(parameters, "ATMAccBank", ATMAccBank);
-        put(parameters, "ATMAccNo", ATMAccNo);
-        put(parameters, "WebATMAccBank", WebATMAccBank);
-        put(parameters, "WebATMAccNo", WebATMAccNo);
-        put(parameters, "WebATMBankName", WebATMBankName);
-        put(parameters, "auth_code", auth_code);
-        put(parameters, "AuthCode", AuthCode);
-        put(parameters, "card4no", card4no);
-        put(parameters, "card6no", card6no);
-        put(parameters, "eci", eci);
-        put(parameters, "gwsr", gwsr);
-        put(parameters, "process_date", process_date);
-        put(parameters, "stage", stage);
-        put(parameters, "stast", stast);
-        put(parameters, "staed", staed);
-        put(parameters, "red_dan", red_dan);
-        put(parameters, "red_de_amt", red_de_amt);
-        put(parameters, "red_ok_amt", red_ok_amt);
-        put(parameters, "red_yet", red_yet);
-        put(parameters, "PeriodType", PeriodType);
-        put(parameters, "Frequency", Frequency);
-        put(parameters, "ExecTimes", ExecTimes);
-        put(parameters, "PeriodAmount", PeriodAmount);
-        put(parameters, "amount", amount);
-        put(parameters, "TotalSuccessTimes", TotalSuccessTimes);
-        put(parameters, "TotalSuccessAmount", TotalSuccessAmount);
-        return parameters;
-    }
-
-    private void put(Map<String, String> parameters, String name, String[] values) {
-        if (values == null) {
-            return;
-        }
-        if (values.length != 1 || values[0] == null) {
-            throw new IllegalArgumentException("duplicate or invalid callback parameter: " + name);
-        }
-        String value = values[0];
-        if (value.length() > MAX_VALUE_LENGTH) {
-            throw new IllegalArgumentException("callback parameter is too long: " + name);
-        }
-        parameters.put(name, value);
-    }
-}

+ 0 - 15
ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgOrderRequest.java

@@ -1,15 +0,0 @@
-package com.ruoyi.app.pay.dto;
-
-import lombok.Data;
-
-/**
- * OMG 订单操作请求。
- *
- * <p>字段名保留 {@code orderid},兼容现有 API 请求契约。业务校验由 Controller 负责,
- * 以便错误信息统一走项目国际化机制。</p>
- */
-@Data
-public class OmgOrderRequest {
-
-    private String orderid;
-}

+ 0 - 24
ruoyi-admin/src/main/java/com/ruoyi/app/pay/dto/OmgRefundOutcome.java

@@ -1,24 +0,0 @@
-package com.ruoyi.app.pay.dto;
-
-import lombok.AllArgsConstructor;
-import lombok.Data;
-
-@Data
-@AllArgsConstructor
-public class OmgRefundOutcome {
-
-    public enum Status {
-        REFUNDED,
-        MANUAL_PENDING,
-        UNKNOWN,
-        FAILED,
-        IDEMPOTENT
-    }
-
-    private Status status;
-    private String message;
-
-    public static OmgRefundOutcome of(Status status, String message) {
-        return new OmgRefundOutcome(status, message);
-    }
-}

+ 0 - 111
ruoyi-admin/src/main/java/com/ruoyi/app/task/OmgReconcileTask.java

@@ -1,111 +0,0 @@
-package com.ruoyi.app.task;
-
-import com.ruoyi.app.pay.OmgPayController;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import org.redisson.api.RLock;
-import org.redisson.api.RedissonClient;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.scheduling.annotation.Scheduled;
-import org.springframework.stereotype.Component;
-
-import java.util.Date;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/**
- * OMG 漏单补单定时兜底(方案B,见 specs/016-omg-payment/callback-reconcile.md §5)。
- *
- * <p>防的是「用户付完即关 App、回调又丢失」的极端情况——用户不会再回来触发被动补单(方案A)。
- * 扫描补单窗口内 {@code pay_status=0} 的 OMG 流水,查 OMG 真实交易状态并补单/标失败,
- * 逐笔复用 {@link OmgPayController#reconcileByQuery(String, String)}。
- *
- * <p>多实例部署靠 Redisson 分布式锁({@code lock:omg:reconcile})保证同一轮只一个节点执行。
- * 严禁依赖/修改已废弃的 {@code TestTask.java}(见 CLAUDE.md「已废弃代码清单」)。
- *
- * @author ruoyi
- * @date 2026-08-07
- */
-@Component
-public class OmgReconcileTask {
-
-    private static final Logger log = LoggerFactory.getLogger(OmgReconcileTask.class);
-    private static final String LOCK_KEY = "lock:omg:reconcile";
-
-    @Autowired
-    private IPosOrderOmgPaymentService paymentService;
-    @Autowired
-    private OmgPayController omgPayController;
-    @Autowired
-    private RedissonClient redissonClient;
-
-    /** 补单窗口(小时):默认 168h=7 天,需覆盖延期支付(ATM/超商)的 ExpireDate(1~3 天) */
-    @Value("${omg.reconcile.window-hours:168}")
-    private int windowHours;
-    /** 宽限期(分钟):默认 2,跳过最近创建的流水,给 OMG 回调/重试留送达时间 */
-    @Value("${omg.reconcile.grace-minutes:2}")
-    private int graceMinutes;
-    /** 单轮最多处理笔数 */
-    @Value("${omg.reconcile.batch-size:50}")
-    private int batchSize;
-    /** 分布式锁获取等待(秒):0=拿不到立即让出,由持锁节点执行 */
-    @Value("${omg.reconcile.lock-wait-seconds:0}")
-    private int lockWaitSeconds;
-    /** 分布式锁租期(秒):需大于单轮最坏耗时(batchSize × queryTrade ~1s) */
-    @Value("${omg.reconcile.lock-lease-seconds:600}")
-    private int lockLeaseSeconds;
-
-    /**
-     * 每 3 分钟一轮(固定延迟:上一轮跑完才开始计时,避免重叠)。
-     * initialDelay 60s:避开应用刚启动时的并发。
-     */
-    @Scheduled(fixedDelayString = "${omg.reconcile.fixed-delay-ms:180000}",
-            initialDelayString = "${omg.reconcile.initial-delay-ms:60000}")
-    public void reconcile() {
-        RLock lock = redissonClient.getLock(LOCK_KEY);
-        boolean acquired;
-        try {
-            acquired = lock.tryLock(lockWaitSeconds, lockLeaseSeconds, TimeUnit.SECONDS);
-        } catch (InterruptedException e) {
-            Thread.currentThread().interrupt();
-            return;
-        }
-        if (!acquired) {
-            return; // 其他节点正在执行,跳过本轮
-        }
-        try {
-            doReconcile();
-        } catch (Exception e) {
-            log.error("[OMG-reconcile] 定时补单异常", e);
-        } finally {
-            if (lock.isHeldByCurrentThread()) {
-                lock.unlock();
-            }
-        }
-    }
-
-    private void doReconcile() {
-        long now = System.currentTimeMillis();
-        Date graceCutoff = new Date(now - graceMinutes * 60_000L);
-        Date windowStart = new Date(now - windowHours * 3_600_000L);
-        List<String> ddIds = paymentService.scanLeakOrders(windowStart, graceCutoff, batchSize);
-        if (ddIds == null || ddIds.isEmpty()) {
-            return;
-        }
-        log.info("[OMG-reconcile] 扫描到 {} 笔漏单待核对 (window={}h, grace={}min)", ddIds.size(), windowHours, graceMinutes);
-        int reconciled = 0;
-        for (String ddId : ddIds) {
-            try {
-                int[] res = omgPayController.reconcileByQuery(ddId, "scheduled");
-                if (res[0] == 1 && res[1] == 1) {
-                    reconciled++;
-                }
-            } catch (Exception e) {
-                log.warn("[OMG-reconcile] 补单失败 ddId={}, err={}", ddId, e.getMessage());
-            }
-        }
-        log.info("[OMG-reconcile] 本轮完成,补单 {} 笔 / 共 {} 笔", reconciled, ddIds.size());
-    }
-}

+ 0 - 62
ruoyi-admin/src/main/java/com/ruoyi/app/utils/omg/OmgQueryThrottle.java

@@ -1,62 +0,0 @@
-package com.ruoyi.app.utils.omg;
-
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.data.redis.core.StringRedisTemplate;
-import org.springframework.stereotype.Component;
-
-import java.util.concurrent.TimeUnit;
-
-/**
- * OMG queryTrade 节流(防 OMG 按 MerchantID HTTP 403 自残)。
- *
- * <p>OMG 的 403 是"短时间内反复查同一笔"触发(频率),不是"下单后 N 分钟内不能查"。故只做 <b>频率</b> 节流、
- * 不做时间黑名单——已付款订单可立即查状态(notify 丢失时 /query 立刻补单)。原"40min 首查延迟"是对 OMG 节流的
- * 误读(会让已付客户等 40min 才看到状态变更,不可接受),已移除。两道闸,均基于 Redis {@code SET NX EX}(原子,无锁竞争):
- * <ol>
- *   <li>per-MerchantID 令牌桶:同一商店代号每 interval(默认 3s)只放行一次 queryTrade(burst 1),
- *       非阻塞——拿不到令牌跳过本行本轮。</li>
- *   <li>per-ddId 扫描间隔:{@code /query} 默认 60s、定时默认 180s 内不重复查同一订单(挡前端 2s 轮询放大)。</li>
- * </ol>
- * 参数 §9 待 OMG 官方确认后据实调整。
- */
-@Component
-public class OmgQueryThrottle {
-
-    @Value("${omg.query-throttle.merchant-token-seconds:3}")
-    private int merchantTokenSeconds;
-
-    @Value("${omg.query-throttle.query-dd-seconds:60}")
-    private int queryDdSeconds;
-
-    @Value("${omg.query-throttle.reconcile-dd-seconds:180}")
-    private int reconcileDdSeconds;
-
-    private final StringRedisTemplate redis;
-
-    public OmgQueryThrottle(StringRedisTemplate redis) {
-        this.redis = redis;
-    }
-
-    /** per-MerchantID 令牌桶(1 token / merchantTokenSeconds,burst 1)。true=已占位可查;false=限流跳过本行本轮。 */
-    public boolean tryAcquireMerchantToken(String merchantId) {
-        if (merchantId == null || merchantId.isEmpty()) {
-            return true;
-        }
-        String key = "omg:qt:token:" + merchantId;
-        Boolean ok = redis.opsForValue().setIfAbsent(key, "1", merchantTokenSeconds, TimeUnit.SECONDS);
-        return Boolean.TRUE.equals(ok);
-    }
-
-    /** per-ddId 扫描间隔。source="query"→/query(queryDdSeconds);其他→定时(reconcileDdSeconds)。
-     *  true=可处理;false=窗口内重复,跳过本轮。 */
-    public boolean acquireDdIdSlot(String ddId, String source) {
-        if (ddId == null || ddId.isEmpty()) {
-            return true;
-        }
-        boolean passive = "query".equals(source);
-        String key = (passive ? "omg:query:dd:" : "omg:reconcile:dd:") + ddId;
-        long ttl = passive ? queryDdSeconds : reconcileDdSeconds;
-        Boolean ok = redis.opsForValue().setIfAbsent(key, "1", ttl, TimeUnit.SECONDS);
-        return Boolean.TRUE.equals(ok);
-    }
-}

+ 2 - 27
ruoyi-admin/src/main/resources/application.yml

@@ -41,34 +41,9 @@ newebpay:
 omg:
   # 幕前支付根地址(测试 payment-stage.funpoint.com.tw;正式 payment.funpoint.com.tw)
   base-url: https://payment-stage.funpoint.com.tw
-  # 支付结果服务端回调(须公网,OMG POST 回调 /pay/omg/notify,平台回纯串 1|OK)
+
+omgpay:
   return-url: https://foodieapi.waimai-paotui.com/pay/omg/notify
-  # 方案B:留空=不向 OMG 传 OrderResultURL,支付后由 App 端轮询 /pay/omg/query 驱动回跳(见 return-to-app-flow.md)
-  order-result-url:
-  # ATM/超商取号服务端回调(OMG POST 回调 /pay/omg/paymentInfo)
-  payment-info-url: https://foodieapi.waimai-paotui.com/pay/omg/paymentInfo
-  # ATM/超商取号前端展示页(可选)
-  client-redirect-url: https://foodieapi.waimai-paotui.com/#/pages/payInfo
-  # create 复用预检(T058 防堆积):连点/重发起时复用新鲜期内活跃未付未取号的旧 MTN,避免每次新建流水
-  create:
-    # 复用新鲜期(分钟):窗口内且 trade_no 仍为空的活跃行复用 MTN,超期则轮换旧行新建;stage 实测同 MTN 重 POST 行为后调整
-    reuse-fresh-minutes: 3
-  # 漏单补单定时兜底(方案B,OmgReconcileTask)—— 回调丢失不丢单
-  reconcile:
-    # 调度固定延迟(毫秒,上一轮跑完才开始计时),默认 3 分钟
-    fixed-delay-ms: 180000
-    # 启动后首次执行延迟(毫秒),避开启动并发
-    initial-delay-ms: 60000
-    # 补单窗口(小时):默认 168=7 天,需覆盖延期支付(ATM/超商) ExpireDate(1~3 天);过期停止扫描
-    window-hours: 168
-    # 宽限期(分钟):跳过最近创建的流水,给 OMG 回调/重试留送达时间
-    grace-minutes: 2
-    # 单轮最多处理笔数
-    batch-size: 50
-    # Redisson 分布式锁等待(秒):0=拿不到立即让出,由持锁节点执行
-    lock-wait-seconds: 0
-    # Redisson 分布式锁租期(秒):需大于单轮最坏耗时
-    lock-lease-seconds: 600
 
 # IM 即时沟通配置
 im:

+ 12 - 0
ruoyi-admin/src/main/resources/i18n/messages.properties

@@ -223,3 +223,15 @@ omg.payment.merchantTradeNo.required=OMG MerchantTradeNo 不能为空
 omg.payment.storeId.required=OMG 门店 ID 不能为空
 omg.payment.merchantId.required=OMG 商户号不能为空
 omg.payment.amount.invalid=OMG 支付金额必须大于 0
+omg.pay.auth.required=请先登录
+omg.pay.order.required=订单号不能为空
+omg.pay.order.not.available=订单不存在或无权操作
+omg.pay.multi.store.unsupported=多门店订单暂不支持 OMG 支付
+omg.pay.order.state.not.payable=当前订单状态不可支付
+omg.pay.order.already.paid=订单已支付或支付状态不可用
+omg.pay.order.amount.invalid=订单金额异常
+omg.pay.payment.type.invalid=订单支付方式不是 OMG
+omg.pay.credential.unavailable=该门店暂未启用 OMG 支付
+omg.pay.attempt.exists=该订单已有待处理的支付尝试
+omg.pay.configuration.invalid=OMG 支付配置无效
+omg.pay.creation.failed=OMG 支付创建失败,请稍后重试

+ 12 - 0
ruoyi-admin/src/main/resources/i18n/messages_en_US.properties

@@ -226,3 +226,15 @@ omg.payment.merchantTradeNo.required=OMG MerchantTradeNo is required
 omg.payment.storeId.required=OMG store id is required
 omg.payment.merchantId.required=OMG merchant id is required
 omg.payment.amount.invalid=OMG payment amount must be greater than 0
+omg.pay.auth.required=Please sign in first
+omg.pay.order.required=The order number is required
+omg.pay.order.not.available=The order does not exist or is not available to this user
+omg.pay.multi.store.unsupported=OMG Pay does not support multi-store orders yet
+omg.pay.order.state.not.payable=The current order state cannot be paid
+omg.pay.order.already.paid=The order is already paid or its payment state is unavailable
+omg.pay.order.amount.invalid=The order amount is invalid
+omg.pay.payment.type.invalid=The order payment method is not OMG Pay
+omg.pay.credential.unavailable=OMG Pay is not enabled for this store
+omg.pay.attempt.exists=This order already has a pending payment attempt
+omg.pay.configuration.invalid=The OMG Pay configuration is invalid
+omg.pay.creation.failed=The OMG Pay checkout could not be created; please try again later

+ 12 - 0
ruoyi-admin/src/main/resources/i18n/messages_vi.properties

@@ -226,3 +226,15 @@ omg.payment.merchantTradeNo.required=OMG MerchantTradeNo là bắt buộc
 omg.payment.storeId.required=ID cửa hàng OMG là bắt buộc
 omg.payment.merchantId.required=Mã thương nhân OMG là bắt buộc
 omg.payment.amount.invalid=Số tiền thanh toán OMG phải lớn hơn 0
+omg.pay.auth.required=Vui lòng đăng nhập trước
+omg.pay.order.required=Vui lòng nhập mã đơn hàng
+omg.pay.order.not.available=Đơn hàng không tồn tại hoặc người dùng không có quyền truy cập
+omg.pay.multi.store.unsupported=OMG Pay chưa hỗ trợ đơn hàng từ nhiều cửa hàng
+omg.pay.order.state.not.payable=Trạng thái đơn hàng hiện tại không thể thanh toán
+omg.pay.order.already.paid=Đơn hàng đã được thanh toán hoặc trạng thái thanh toán không khả dụng
+omg.pay.order.amount.invalid=Số tiền đơn hàng không hợp lệ
+omg.pay.payment.type.invalid=Phương thức thanh toán của đơn hàng không phải OMG Pay
+omg.pay.credential.unavailable=Cửa hàng này chưa bật OMG Pay
+omg.pay.attempt.exists=Đơn hàng này đã có một lần thanh toán đang chờ xử lý
+omg.pay.configuration.invalid=Cấu hình OMG Pay không hợp lệ
+omg.pay.creation.failed=Không thể tạo trang thanh toán OMG Pay; vui lòng thử lại sau

+ 12 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties

@@ -227,3 +227,15 @@ omg.payment.merchantTradeNo.required=OMG MerchantTradeNo 不能为空
 omg.payment.storeId.required=OMG 门店 ID 不能为空
 omg.payment.merchantId.required=OMG 商户号不能为空
 omg.payment.amount.invalid=OMG 支付金额必须大于 0
+omg.pay.auth.required=请先登录
+omg.pay.order.required=订单号不能为空
+omg.pay.order.not.available=订单不存在或无权操作
+omg.pay.multi.store.unsupported=多门店订单暂不支持 OMG 支付
+omg.pay.order.state.not.payable=当前订单状态不可支付
+omg.pay.order.already.paid=订单已支付或支付状态不可用
+omg.pay.order.amount.invalid=订单金额异常
+omg.pay.payment.type.invalid=订单支付方式不是 OMG
+omg.pay.credential.unavailable=该门店暂未启用 OMG 支付
+omg.pay.attempt.exists=该订单已有待处理的支付尝试
+omg.pay.configuration.invalid=OMG 支付配置无效
+omg.pay.creation.failed=OMG 支付创建失败,请稍后重试

+ 12 - 0
ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties

@@ -227,3 +227,15 @@ omg.payment.merchantTradeNo.required=OMG MerchantTradeNo 不能為空
 omg.payment.storeId.required=OMG 門店 ID 不能為空
 omg.payment.merchantId.required=OMG 商戶號不能為空
 omg.payment.amount.invalid=OMG 支付金額必須大於 0
+omg.pay.auth.required=請先登入
+omg.pay.order.required=訂單號不能為空
+omg.pay.order.not.available=訂單不存在或無權操作
+omg.pay.multi.store.unsupported=多門店訂單暫不支援 OMG 支付
+omg.pay.order.state.not.payable=目前訂單狀態不可支付
+omg.pay.order.already.paid=訂單已付款或付款狀態不可用
+omg.pay.order.amount.invalid=訂單金額異常
+omg.pay.payment.type.invalid=訂單付款方式不是 OMG
+omg.pay.credential.unavailable=此門店尚未啟用 OMG 支付
+omg.pay.attempt.exists=此訂單已有待處理的支付嘗試
+omg.pay.configuration.invalid=OMG 支付設定無效
+omg.pay.creation.failed=OMG 支付建立失敗,請稍後再試

+ 30 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgLegacyRetirementTest.java

@@ -0,0 +1,30 @@
+package com.ruoyi.app.omgpay;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Arrays;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class OmgLegacyRetirementTest {
+    @Test
+    void oldRuntimeClassesAreAbsentAndNewControllerExposesOnlyCreate() {
+        assertThrows(ClassNotFoundException.class, () -> Class.forName("com.ruoyi.app.pay.OmgPayController"));
+        assertThrows(ClassNotFoundException.class, () -> Class.forName("com.ruoyi.app.task.OmgReconcileTask"));
+        assertThrows(ClassNotFoundException.class, () -> Class.forName("com.ruoyi.system.domain.PosOrderOmgPayment"));
+        assertNotNull(OmgPaymentController.class.getAnnotation(RestController.class));
+        Set<String> postPaths = Arrays.stream(OmgPaymentController.class.getDeclaredMethods())
+                .filter(method -> method.isAnnotationPresent(PostMapping.class))
+                .flatMap(method -> Arrays.stream(method.getAnnotation(PostMapping.class).value()))
+                .collect(Collectors.toSet());
+        assertEquals(Set.of("/create"), postPaths);
+        assertTrue(Arrays.stream(OmgPaymentController.class.getDeclaredMethods())
+                .map(method -> method.getName().toLowerCase())
+                .noneMatch(name -> name.contains("notify") || name.contains("query")
+                        || name.contains("paymentinfo") || name.contains("return") || name.contains("refund")));
+    }
+}

+ 24 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgMerchantTradeNoGeneratorTest.java

@@ -0,0 +1,24 @@
+package com.ruoyi.app.omgpay;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class OmgMerchantTradeNoGeneratorTest {
+    @Test
+    void generatesUniqueTwentyCharacterUppercaseIdentifiers() {
+        OmgMerchantTradeNoGenerator generator = new OmgMerchantTradeNoGenerator();
+        Set<String> values = new HashSet<>();
+        for (int index = 0; index < 1_000; index++) {
+            String value = generator.generate();
+            assertEquals(20, value.length());
+            assertTrue(value.startsWith("OMG"));
+            assertTrue(value.matches("[A-Z0-9]{20}"));
+            assertTrue(values.add(value));
+        }
+    }
+}

+ 139 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java

@@ -0,0 +1,139 @@
+package com.ruoyi.app.omgpay;
+
+import ch.qos.logback.classic.Logger;
+import ch.qos.logback.classic.spi.ILoggingEvent;
+import ch.qos.logback.core.read.ListAppender;
+import com.ruoyi.app.omgpay.dto.OmgCreatePaymentRequest;
+import com.ruoyi.app.omgpay.dto.OmgCreatePaymentResponse;
+import com.ruoyi.app.omgpay.dto.OmgPaymentErrorResponse;
+import com.ruoyi.common.core.domain.AjaxResult;
+import com.ruoyi.common.utils.MessageUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.slf4j.LoggerFactory;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+import java.lang.reflect.Method;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+class OmgPaymentControllerTest {
+    private MockedStatic<MessageUtils> messages;
+
+    @BeforeEach
+    void setUpMessages() {
+        messages = mockStatic(MessageUtils.class);
+        messages.when(() -> MessageUtils.message(anyString())).thenAnswer(invocation -> invocation.getArgument(0));
+    }
+
+    @AfterEach
+    void closeMessages() {
+        messages.close();
+    }
+
+    @Test
+    void exposesOnlyExplicitCreateContract() throws Exception {
+        RequestMapping mapping = OmgPaymentController.class.getAnnotation(RequestMapping.class);
+        assertArrayEquals(new String[]{"/pay/omg"}, mapping.value());
+        Method create = OmgPaymentController.class.getDeclaredMethod("create", String.class, OmgCreatePaymentRequest.class);
+        assertArrayEquals(new String[]{"/create"}, create.getAnnotation(PostMapping.class).value());
+        assertEquals("token", create.getParameters()[0].getAnnotation(RequestHeader.class).name());
+        assertNotNull(create.getParameters()[1].getAnnotation(RequestBody.class));
+        assertEquals(Set.of("orderId"), java.util.Arrays.stream(OmgCreatePaymentRequest.class.getDeclaredFields())
+                .map(java.lang.reflect.Field::getName).collect(Collectors.toSet()));
+        assertTrue(java.util.Arrays.stream(create.getParameterTypes()).noneMatch(Map.class::isAssignableFrom));
+    }
+
+    @Test
+    void returnsSuccessShapeAndSafeDiagnosticLogs() {
+        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
+        when(resolver.requireUserId("SECRET_TOKEN")).thenReturn(5L);
+        OmgCreatePaymentResponse response = new OmgCreatePaymentResponse("stage", Map.of("CheckMacValue", "A".repeat(64)));
+        when(service.create(5L, "DD-1\r\nforged=true")).thenReturn(new OmgPaymentCreateOutcome(
+                response, 9L, "DD-1", 5L, 77L, 100, "OMG12***4567"));
+        OmgPaymentController controller = new OmgPaymentController(resolver, service);
+        OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
+        request.setOrderId("DD-1\r\nforged=true");
+        ListAppender<ILoggingEvent> logs = captureLogs();
+
+        AjaxResult result = controller.create("SECRET_TOKEN", request);
+
+        assertSame(response, result.get("data"));
+        String messages = logs.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.joining("\n"));
+        assertTrue(messages.contains("attemptId=9"));
+        assertFalse(messages.contains("SECRET_TOKEN"));
+        assertFalse(messages.contains("\r"));
+        assertFalse(messages.contains("forged=true"));
+        assertFalse(messages.contains("A".repeat(64)));
+        assertFalse(messages.contains("CheckMacValue"));
+    }
+
+    @Test
+    void mapsBusinessAndUnexpectedErrorsToStableStatus() {
+        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
+        when(resolver.requireUserId(anyString())).thenReturn(5L);
+        when(service.create(5L, null)).thenThrow(new OmgPaymentBusinessException(OmgPaymentErrorCode.ORDER_REQUIRED));
+        OmgPaymentController controller = new OmgPaymentController(resolver, service);
+        AjaxResult business = controller.create("token", null);
+        assertEquals("ORDER_REQUIRED", ((OmgPaymentErrorResponse) business.get("data")).status());
+
+        RuntimeException failure = new RuntimeException("unexpected");
+        when(service.create(5L, "DD-2")).thenThrow(failure);
+        OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
+        request.setOrderId("DD-2");
+        ListAppender<ILoggingEvent> logs = captureLogs();
+        AjaxResult unexpected = controller.create("token", request);
+        assertEquals("PAYMENT_CREATION_FAILED", ((OmgPaymentErrorResponse) unexpected.get("data")).status());
+        assertTrue(logs.list.stream().anyMatch(event -> event.getThrowableProxy() != null
+                && event.getThrowableProxy().getMessage().equals("unexpected")));
+    }
+
+    @Test
+    void mapsEveryBusinessErrorToItsStableStatus() {
+        for (OmgPaymentErrorCode code : OmgPaymentErrorCode.values()) {
+            OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+            OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
+            when(resolver.requireUserId("token")).thenReturn(5L);
+            when(service.create(5L, "DD-1")).thenThrow(new OmgPaymentBusinessException(code, 77L));
+            OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
+            request.setOrderId("DD-1");
+
+            AjaxResult result = new OmgPaymentController(resolver, service).create("token", request);
+
+            assertEquals(code.name(), ((OmgPaymentErrorResponse) result.get("data")).status());
+        }
+    }
+
+    @Test
+    void mapsResolverFailureWithoutLoggingToken() {
+        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+        when(resolver.requireUserId("NEVER_LOG_THIS")).thenThrow(
+                new OmgPaymentBusinessException(OmgPaymentErrorCode.AUTH_REQUIRED));
+        OmgPaymentController controller = new OmgPaymentController(resolver, mock(OmgPaymentCreateService.class));
+        ListAppender<ILoggingEvent> logs = captureLogs();
+
+        AjaxResult result = controller.create("NEVER_LOG_THIS", null);
+
+        assertEquals("AUTH_REQUIRED", ((OmgPaymentErrorResponse) result.get("data")).status());
+        assertTrue(logs.list.stream().noneMatch(event -> event.getFormattedMessage().contains("NEVER_LOG_THIS")));
+    }
+
+    private static ListAppender<ILoggingEvent> captureLogs() {
+        Logger logger = (Logger) LoggerFactory.getLogger(OmgPaymentController.class);
+        ListAppender<ILoggingEvent> appender = new ListAppender<>();
+        appender.start();
+        logger.addAppender(appender);
+        return appender;
+    }
+}

+ 197 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java

@@ -0,0 +1,197 @@
+package com.ruoyi.app.omgpay;
+
+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.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.Arguments;
+import org.junit.jupiter.params.provider.MethodSource;
+import org.springframework.dao.DuplicateKeyException;
+
+import java.util.Map;
+import java.util.stream.Stream;
+
+import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.*;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.*;
+
+class OmgPaymentCreateServiceTest {
+    private IOmgPaymentAttemptService attempts;
+    private IPosStoreOmgService credentials;
+    private OmgMerchantTradeNoGenerator generator;
+    private OmgPaymentFormFactory formFactory;
+    private OmgPaymentCreateService service;
+
+    @BeforeEach
+    void setUp() {
+        attempts = mock(IOmgPaymentAttemptService.class);
+        credentials = mock(IPosStoreOmgService.class);
+        generator = mock(OmgMerchantTradeNoGenerator.class);
+        formFactory = mock(OmgPaymentFormFactory.class);
+        service = new OmgPaymentCreateService(attempts, credentials, generator, formFactory);
+    }
+
+    @ParameterizedTest(name = "{0}")
+    @MethodSource("invalidOrders")
+    void rejectsInvalidOrderFactsBeforeGeneration(String name, OmgPaymentOrderSnapshot order,
+                                                   OmgPaymentErrorCode expected) {
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
+
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.create(5L, "DD-1"));
+
+        assertEquals(expected, error.getCode());
+        verify(attempts, never()).createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt());
+        verifyNoInteractions(generator, formFactory);
+    }
+
+    @Test
+    void rejectsExistingCreatedAttemptBeforeCredentialOrFormWork() {
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
+        when(attempts.selectActiveCreatedByDdId("DD-1")).thenReturn(new OmgPaymentAttempt());
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.create(5L, "DD-1"));
+        assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
+        verifyNoInteractions(generator, formFactory, credentials);
+    }
+
+    @Test
+    void createsAttemptAndReturnsOnlyAuthorizedFormAndSafeMetadata() {
+        OmgPaymentOrderSnapshot order = payableOrder();
+        PosStoreOmg credential = credential();
+        OmgPaymentAttempt attempt = new OmgPaymentAttempt();
+        attempt.setId(9L);
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
+        when(credentials.getEnabledCredential(77L)).thenReturn(credential);
+        when(generator.generate()).thenReturn("OMG12345678901234567");
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(new OmgPaymentForm(OmgPaymentFormFactory.STAGE_GATEWAY_URL, Map.of("CheckMacValue", "A".repeat(64))));
+        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt())).thenReturn(attempt);
+
+        OmgPaymentCreateOutcome outcome = service.create(5L, "DD-1");
+
+        assertEquals(9L, outcome.attemptId());
+        assertEquals("CREATED", outcome.response().status());
+        assertEquals("OMG12***4567", outcome.maskedMerchantTradeNo());
+        assertTrue(outcome.response().formFields().containsKey("CheckMacValue"));
+        var orderOfCalls = inOrder(attempts, credentials, generator, formFactory);
+        orderOfCalls.verify(attempts).selectOrderForUpdate("DD-1");
+        orderOfCalls.verify(attempts).selectActiveCreatedByDdId("DD-1");
+        orderOfCalls.verify(credentials).getEnabledCredential(77L);
+        orderOfCalls.verify(generator).generate();
+        orderOfCalls.verify(formFactory).create("DD-1", 100, "1000031", "KEY", "IV",
+                "OMG12345678901234567");
+        orderOfCalls.verify(attempts).createCreated("DD-1", "OMG12345678901234567", 77L, "1000031", 100);
+    }
+
+    @Test
+    void duplicateActiveRowWinsOverTradeNumberRetry() {
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
+        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
+        when(generator.generate()).thenReturn("OMG12345678901234567");
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(new OmgPaymentForm("stage", Map.of()));
+        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt()))
+                .thenThrow(new DuplicateKeyException("collision"));
+        when(attempts.selectActiveCreatedByDdId("DD-1")).thenReturn(null, new OmgPaymentAttempt());
+
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.create(5L, "DD-1"));
+        assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
+        verify(generator, times(1)).generate();
+    }
+
+    @Test
+    void retriesOnlyConfirmedTradeNumberCollisionAndSucceeds() {
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
+        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
+        when(generator.generate()).thenReturn("OMG11111111111111111", "OMG22222222222222222");
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(new OmgPaymentForm("stage", Map.of()));
+        OmgPaymentAttempt inserted = new OmgPaymentAttempt();
+        inserted.setId(11L);
+        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt()))
+                .thenThrow(new DuplicateKeyException("trade collision")).thenReturn(inserted);
+        when(attempts.selectByMerchantTradeNo("OMG11111111111111111")).thenReturn(new OmgPaymentAttempt());
+
+        assertEquals(11L, service.create(5L, "DD-1").attemptId());
+        verify(generator, times(2)).generate();
+        verify(formFactory, times(2)).create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString());
+    }
+
+    @Test
+    void stopsAfterThreeConfirmedTradeNumberCollisions() {
+        when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
+        when(credentials.getEnabledCredential(77L)).thenReturn(credential());
+        when(generator.generate()).thenReturn("OMG11111111111111111", "OMG22222222222222222",
+                "OMG33333333333333333");
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+                .thenReturn(new OmgPaymentForm("stage", Map.of()));
+        when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt()))
+                .thenThrow(new DuplicateKeyException("trade collision"));
+        when(attempts.selectByMerchantTradeNo(anyString())).thenReturn(new OmgPaymentAttempt());
+
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.create(5L, "DD-1"));
+        assertEquals(PAYMENT_CREATION_FAILED, error.getCode());
+        verify(generator, times(3)).generate();
+    }
+
+    private static Stream<Arguments> invalidOrders() {
+        OmgPaymentOrderSnapshot otherOwner = payableOrder();
+        otherOwner.setUserId(6L);
+        OmgPaymentOrderSnapshot child = payableOrder();
+        child.setParentDdId("PARENT");
+        OmgPaymentOrderSnapshot noStore = payableOrder();
+        noStore.setStoreId(null);
+        OmgPaymentOrderSnapshot stateThree = payableOrder();
+        stateThree.setState(3L);
+        OmgPaymentOrderSnapshot stateFour = payableOrder();
+        stateFour.setState(4L);
+        OmgPaymentOrderSnapshot paid = payableOrder();
+        paid.setPayStatus(1L);
+        OmgPaymentOrderSnapshot wrongType = payableOrder();
+        wrongType.setPayType("1");
+        OmgPaymentOrderSnapshot zeroAmount = payableOrder();
+        zeroAmount.setAmount(0);
+        OmgPaymentOrderSnapshot missingCredential = payableOrder();
+        return Stream.of(
+                Arguments.of("missing order", null, ORDER_NOT_AVAILABLE),
+                Arguments.of("other owner", otherOwner, ORDER_NOT_AVAILABLE),
+                Arguments.of("multi-store child", child, MULTI_STORE_ORDER_NOT_SUPPORTED),
+                Arguments.of("missing store", noStore, ORDER_NOT_AVAILABLE),
+                Arguments.of("state 3", stateThree, ORDER_STATE_NOT_PAYABLE),
+                Arguments.of("state 4", stateFour, ORDER_STATE_NOT_PAYABLE),
+                Arguments.of("already paid", paid, ORDER_ALREADY_PAID),
+                Arguments.of("wrong payment type", wrongType, PAYMENT_TYPE_INVALID),
+                Arguments.of("nonpositive amount", zeroAmount, ORDER_AMOUNT_INVALID),
+                Arguments.of("missing credential", missingCredential, STORE_CREDENTIAL_UNAVAILABLE));
+    }
+
+    private static OmgPaymentOrderSnapshot payableOrder() {
+        OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
+        order.setDdId("DD-1");
+        order.setParentDdId("DD-1");
+        order.setStoreId(77L);
+        order.setUserId(5L);
+        order.setAmount(100);
+        order.setState(0L);
+        order.setPayStatus(0L);
+        order.setPayType("2");
+        return order;
+    }
+
+    private static PosStoreOmg credential() {
+        PosStoreOmg credential = new PosStoreOmg();
+        credential.setMerchantId("1000031");
+        credential.setHashKey("KEY");
+        credential.setHashIv("IV");
+        return credential;
+    }
+}

+ 63 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java

@@ -0,0 +1,63 @@
+package com.ruoyi.app.omgpay;
+
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneOffset;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.ArgumentMatchers.anyMap;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class OmgPaymentFormFactoryTest {
+    @Test
+    void buildsExactStageOnlySignedForm() {
+        OmgPaymentProperties properties = properties("https://foodieapi.waimai-paotui.com/pay/omg/notify");
+        OmgCheckMacSigner signer = mock(OmgCheckMacSigner.class);
+        when(signer.sign(anyMap(), eq("KEY"), eq("IV"))).thenReturn("A".repeat(64));
+        OmgPaymentFormFactory factory = new OmgPaymentFormFactory(properties, signer,
+                Clock.fixed(Instant.parse("2026-08-13T07:30:23Z"), ZoneOffset.UTC));
+
+        OmgPaymentForm form = factory.create("DD-<script>#|訂單", 100, "1000031", "KEY", "IV", "OMG12345678901234567");
+
+        assertEquals(OmgPaymentFormFactory.STAGE_GATEWAY_URL, form.gatewayUrl());
+        assertEquals(Set.of("MerchantID", "MerchantTradeNo", "MerchantTradeDate", "PaymentType",
+                "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "ChoosePayment", "EncryptType",
+                "InvoiceMark", "NeedExtraPaidInfo", "ExpireDate", "StoreExpireDate",
+                "BarcodeATMExpireDate", "CheckMacValue"), form.fields().keySet());
+        assertEquals("2026/08/13 15:30:23", form.fields().get("MerchantTradeDate"));
+        assertEquals("ALL", form.fields().get("ChoosePayment"));
+        assertEquals("Y", form.fields().get("NeedExtraPaidInfo"));
+        assertEquals("Foodie order DDscript", form.fields().get("TradeDesc"));
+        assertFalse(form.fields().get("ItemName").contains("|"));
+        assertTrue(form.fields().get("ItemName").length() <= 120);
+        verify(signer).sign(org.mockito.ArgumentMatchers.argThat(fields -> fields.size() == 15
+                && !fields.containsKey("CheckMacValue")), eq("KEY"), eq("IV"));
+        assertThrows(UnsupportedOperationException.class, () -> form.fields().put("extra", "value"));
+    }
+
+    @ParameterizedTest
+    @ValueSource(strings = {"http://example.com/pay/omg/notify", "https://example.com/wrong",
+            "https://example.com/pay/omg/notify?q=1", "https://example.com/pay/omg/notify#x",
+            "https://user@example.com/pay/omg/notify", "/pay/omg/notify"})
+    void rejectsUnsafeReturnUrls(String returnUrl) {
+        assertThrows(IllegalArgumentException.class, () -> properties(returnUrl).requireSafeReturnUrl());
+    }
+
+    private static OmgPaymentProperties properties(String returnUrl) {
+        OmgPaymentProperties properties = new OmgPaymentProperties();
+        properties.setReturnUrl(returnUrl);
+        return properties;
+    }
+}

+ 13 - 70
ruoyi-admin/src/test/java/com/ruoyi/app/order/OrderLifecycleServiceTest.java

@@ -8,12 +8,8 @@ import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.system.domain.PosOrder;
 import com.ruoyi.system.domain.PosOrderLinePayment;
 import com.ruoyi.system.domain.PosOrderLineRefund;
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import com.ruoyi.system.domain.PosOrderOmgRefund;
 import com.ruoyi.system.service.IPosOrderLinePaymentService;
 import com.ruoyi.system.service.IPosOrderLineRefundService;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
 import com.ruoyi.system.service.IPosOrderService;
 import com.ruoyi.system.service.IPointsTransactionService;
 import com.ruoyi.system.service.IUserWalletService;
@@ -45,8 +41,6 @@ class OrderLifecycleServiceTest {
     private OrderLogHelper orderLogHelper;
     private IUserWalletService userWalletService;
     private IPointsTransactionService pointsTransactionService;
-    private IPosOrderOmgPaymentService omgPaymentService;
-    private IPosOrderOmgRefundService omgRefundService;
     private OrderLifecycleService service;
 
     @BeforeEach
@@ -56,27 +50,18 @@ class OrderLifecycleServiceTest {
         orderLogHelper = mock(OrderLogHelper.class);
         userWalletService = mock(IUserWalletService.class);
         pointsTransactionService = mock(IPointsTransactionService.class);
-        omgPaymentService = mock(IPosOrderOmgPaymentService.class);
-        omgRefundService = mock(IPosOrderOmgRefundService.class);
         service = new OrderLifecycleService(posOrderService, billingService, orderLogHelper,
-                userWalletService, pointsTransactionService, omgPaymentService, omgRefundService);
+                userWalletService, pointsTransactionService);
     }
 
     @Test
     void exposesOnlyLegalNextOrderAndDeliveryStates() {
         PosOrder order = deliveryOrder();
         when(posOrderService.getById(10L)).thenReturn(order);
-        PosOrderOmgPayment payment = new PosOrderOmgPayment();
-        payment.setId(1L);
-        payment.setPayStatus(1);
-        when(omgPaymentService.getLatestRefundableByDdId(order.getDdId())).thenReturn(payment);
-        when(omgRefundService.listByPayment(1L)).thenReturn(List.of());
-
         AdminOrderStatusContext context = service.getStatusContext(10L);
 
         assertEquals(List.of(2L), context.getAllowedOrderStates());
         assertEquals(List.of(2L, 3L), context.getAllowedDeliveryStatuses());
-        assertTrue(context.getCanRefundOmg());
         assertFalse(context.getCanConfirmOfflinePayment());
     }
 
@@ -93,8 +78,8 @@ class OrderLifecycleServiceTest {
         attempt.setStatus("WAITING_AUTH");
         when(linePaymentService.getByDdId(order.getDdId())).thenReturn(List.of(attempt));
         OrderLifecycleService lineService = new OrderLifecycleService(posOrderService, billingService,
-                orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService,
-                omgRefundService, linePaymentService, lineRefundService);
+                orderLogHelper, userWalletService, pointsTransactionService,
+                linePaymentService, lineRefundService);
 
         assertEquals(List.of(1L, 4L), lineService.getStatusContext(20L).getAllowedOrderStates());
 
@@ -119,8 +104,8 @@ class OrderLifecycleServiceTest {
         active.setActiveDdId(order.getDdId());
         when(linePaymentService.getByDdId(order.getDdId())).thenReturn(List.of(active, paid));
         OrderLifecycleService lineService = new OrderLifecycleService(posOrderService, billingService,
-                orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService,
-                omgRefundService, linePaymentService, lineRefundService);
+                orderLogHelper, userWalletService, pointsTransactionService,
+                linePaymentService, lineRefundService);
 
         AdminOrderStatusContext context = lineService.getStatusContext(20L);
 
@@ -140,8 +125,8 @@ class OrderLifecycleServiceTest {
         paid.setStatus("PAID");
         when(linePaymentService.getByDdId(order.getDdId())).thenReturn(List.of(paid));
         OrderLifecycleService lineService = new OrderLifecycleService(posOrderService, billingService,
-                orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService,
-                omgRefundService, linePaymentService, lineRefundService);
+                orderLogHelper, userWalletService, pointsTransactionService,
+                linePaymentService, lineRefundService);
 
         assertFalse(lineService.getStatusContext(20L).getCanRefundLine());
         assertThrows(ServiceException.class, () -> lineService.reserveLineRefund(20L, "ADMIN"));
@@ -165,8 +150,8 @@ class OrderLifecycleServiceTest {
         when(lineRefundService.createIfAbsent(paid, "ADMIN")).thenReturn(reservation);
         when(posOrderService.update(any(PosOrder.class), any(Wrapper.class))).thenReturn(false);
         OrderLifecycleService lineService = new OrderLifecycleService(posOrderService, billingService,
-                orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService,
-                omgRefundService, linePaymentService, lineRefundService);
+                orderLogHelper, userWalletService, pointsTransactionService,
+                linePaymentService, lineRefundService);
 
         assertEquals(8L, lineService.reserveLineRefund(order.getId(), "ADMIN"));
         assertThrows(ServiceException.class, () -> lineService.completePaidLineOrder(order.getId()));
@@ -188,8 +173,8 @@ class OrderLifecycleServiceTest {
         when(linePaymentService.getByDdId(order.getDdId())).thenReturn(List.of(paid));
         when(posOrderService.update(any(PosOrder.class), any(Wrapper.class))).thenReturn(true);
         OrderLifecycleService lineService = new OrderLifecycleService(posOrderService, billingService,
-                orderLogHelper, userWalletService, pointsTransactionService, omgPaymentService,
-                omgRefundService, linePaymentService, lineRefundService);
+                orderLogHelper, userWalletService, pointsTransactionService,
+                linePaymentService, lineRefundService);
 
         lineService.completePaidLineOrder(order.getId());
 
@@ -310,7 +295,7 @@ class OrderLifecycleServiceTest {
 
     @Test
     void rejectsOmgOrderFromOfflinePaymentPath() {
-        PosOrder order = pickupOrder("7", 0L);
+        PosOrder order = pickupOrder("2", 0L);
         when(posOrderService.getById(20L)).thenReturn(order);
 
         assertThrows(ServiceException.class, () -> service.confirmOfflinePayment(20L,
@@ -386,27 +371,6 @@ class OrderLifecycleServiceTest {
         verify(billingService).setQishouBilling(order);
     }
 
-    @Test
-    void synchronizesSystemOmgRefundToAllThreeBusinessStates() {
-        PosOrder order = pickupOrder("7", 1L);
-        order.setState(1L);
-        when(posOrderService.getById(20L)).thenReturn(order);
-        doAnswer(invocation -> {
-            PosOrder update = invocation.getArgument(0);
-            order.setState(update.getState());
-            order.setPayStatus(update.getPayStatus());
-            order.setAfterSaleStatus(update.getAfterSaleStatus());
-            return true;
-        }).when(posOrderService).update(any(PosOrder.class), any(Wrapper.class));
-
-        AdminOrderStatusContext context = service.finalizeSystemOmgRefund(20L);
-
-        assertEquals(4L, context.getState());
-        assertEquals(2L, context.getPayStatus());
-        assertEquals(3L, context.getAfterSaleStatus());
-        verify(orderLogHelper).logSync(anyString(), anyInt(), any(), anyString(), anyString());
-    }
-
     @Test
     void lineRefundCompletesAfterSaleAndExistingRefundSideEffects() {
         PosOrder order = pickupOrder("3", 2L);
@@ -428,33 +392,12 @@ class OrderLifecycleServiceTest {
         verify(orderLogHelper).logSync(anyString(), anyInt(), any(), anyString(), anyString());
     }
 
-    @Test
-    void exposesManualRefundLocalCompensationAfterLedgerAlreadyCompleted() {
-        PosOrder order = pickupOrder("7", 1L);
-        order.setState(1L);
-        PosOrderOmgPayment payment = new PosOrderOmgPayment();
-        payment.setId(3L);
-        payment.setPayStatus(3);
-        payment.setPayType("ATM_TAISHIN");
-        PosOrderOmgRefund completed = new PosOrderOmgRefund();
-        completed.setRtnCode(1);
-        when(posOrderService.getById(20L)).thenReturn(order);
-        when(omgPaymentService.getLatestRefundableByDdId(order.getDdId())).thenReturn(payment);
-        when(omgRefundService.listByPayment(3L)).thenReturn(List.of(completed));
-
-        AdminOrderStatusContext context = service.getStatusContext(20L);
-
-        assertTrue(context.getCanConfirmManualOmgRefund());
-        assertFalse(context.getCanRefundOmg());
-        assertFalse(context.getManualRefundPending());
-    }
-
     private PosOrder deliveryOrder() {
         PosOrder order = new PosOrder();
         order.setId(10L);
         order.setDdId("202608100001");
         order.setType(0L);
-        order.setPayType("7");
+        order.setPayType("2");
         order.setState(2L);
         order.setDeliveryStatus(2L);
         order.setPayStatus(1L);

+ 0 - 382
ruoyi-admin/src/test/java/com/ruoyi/app/pay/OmgPayControllerTest.java

@@ -1,382 +0,0 @@
-package com.ruoyi.app.pay;
-
-import ch.qos.logback.classic.Logger;
-import ch.qos.logback.classic.spi.ILoggingEvent;
-import ch.qos.logback.core.read.ListAppender;
-import com.ruoyi.app.order.OrderLifecycleService;
-import com.ruoyi.app.pay.dto.OmgCallbackRequest;
-import com.ruoyi.app.pay.dto.OmgOrderRequest;
-import com.ruoyi.app.pay.dto.OmgRefundOutcome;
-import com.ruoyi.app.utils.omg.OmgPay;
-import com.ruoyi.common.core.domain.AjaxResult;
-import com.ruoyi.common.utils.MessageUtils;
-import com.ruoyi.system.domain.IpnLog;
-import com.ruoyi.system.domain.PosOrder;
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import com.ruoyi.system.domain.PosOrderOmgRefund;
-import com.ruoyi.system.domain.PosStoreOmg;
-import com.ruoyi.system.service.IIpnLogService;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
-import com.ruoyi.system.service.IPosOrderService;
-import com.ruoyi.system.service.IPosStoreOmgService;
-import jakarta.servlet.http.HttpServletRequest;
-import org.junit.jupiter.api.Test;
-import org.mockito.ArgumentCaptor;
-import org.slf4j.LoggerFactory;
-import org.springframework.beans.MutablePropertyValues;
-import org.springframework.test.util.ReflectionTestUtils;
-import org.springframework.validation.DataBinder;
-import org.springframework.web.bind.annotation.ModelAttribute;
-import org.springframework.web.bind.annotation.PathVariable;
-import org.springframework.web.bind.annotation.RequestBody;
-import org.springframework.web.bind.annotation.RequestHeader;
-import org.springframework.web.bind.annotation.RequestParam;
-
-import java.lang.reflect.Method;
-import java.lang.reflect.Parameter;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.junit.jupiter.api.Assertions.assertFalse;
-import static org.junit.jupiter.api.Assertions.assertThrows;
-import static org.junit.jupiter.api.Assertions.assertTrue;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.anyInt;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.mockStatic;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.times;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-class OmgPayControllerTest {
-
-    @Test
-    void controllerDoesNotDependOnHttpServletRequest() {
-        boolean usesServletRequest = Arrays.stream(OmgPayController.class.getDeclaredMethods())
-                .flatMap(method -> Arrays.stream(method.getParameterTypes()))
-                .anyMatch(HttpServletRequest.class::equals);
-
-        assertFalse(usesServletRequest);
-    }
-
-    @Test
-    void authenticatedEndpointsReceiveTokenAsRequestHeader() {
-        for (String methodName : List.of("create", "getPaymentInfo", "refund", "query")) {
-            boolean hasTokenHeader = Arrays.stream(method(methodName).getParameters())
-                    .anyMatch(parameter -> parameter.getType() == String.class
-                            && parameter.isAnnotationPresent(RequestHeader.class)
-                            && "token".equals(parameter.getName()));
-            assertTrue(hasTokenHeader, methodName + " should receive token via @RequestHeader String token");
-        }
-    }
-
-    @Test
-    void orderCommandEndpointsReceiveTypedDtoInsteadOfRequestParams() {
-        for (String methodName : List.of("create", "refund", "query")) {
-            Parameter[] parameters = method(methodName).getParameters();
-            assertFalse(Arrays.stream(parameters)
-                    .anyMatch(parameter -> parameter.isAnnotationPresent(RequestParam.class)));
-            assertTrue(Arrays.stream(parameters)
-                    .anyMatch(parameter -> parameter.getType().getPackageName().equals("com.ruoyi.app.pay.dto")));
-        }
-    }
-
-    @Test
-    void orderCommandEndpointsUseRequestBodyWithoutAutomaticBeanValidation() {
-        for (String methodName : List.of("create", "refund", "query")) {
-            Parameter request = Arrays.stream(method(methodName).getParameters())
-                    .filter(parameter -> parameter.getType() == OmgOrderRequest.class)
-                    .findFirst()
-                    .orElseThrow();
-
-            RequestBody requestBody = request.getAnnotation(RequestBody.class);
-            assertTrue(requestBody != null, methodName + " should bind its DTO with @RequestBody");
-            assertFalse(requestBody.required(), methodName + " should let business validation handle an empty body");
-            assertFalse(request.isAnnotationPresent(ModelAttribute.class));
-            assertFalse(Arrays.stream(request.getAnnotations())
-                    .anyMatch(annotation -> annotation.annotationType().getPackageName()
-                            .startsWith("jakarta.validation")));
-        }
-    }
-
-    @Test
-    void orderRequestDtoDoesNotContainBeanValidationAnnotations() {
-        assertFalse(Arrays.stream(OmgOrderRequest.class.getDeclaredFields())
-                .flatMap(field -> Arrays.stream(field.getAnnotations()))
-                .anyMatch(annotation -> annotation.annotationType().getPackageName()
-                        .startsWith("jakarta.validation")));
-    }
-
-    @Test
-    void omgCallbacksReceiveTypedDto() {
-        for (String methodName : List.of("notify", "returnCallback", "paymentInfoCallback")) {
-            assertTrue(Arrays.stream(method(methodName).getParameterTypes())
-                    .anyMatch(type -> type.getPackageName().equals("com.ruoyi.app.pay.dto")));
-        }
-    }
-
-    @Test
-    void omgFormCallbacksUseModelAttributeInsteadOfRequestBody() {
-        for (String methodName : List.of("notify", "returnCallback", "paymentInfoCallback")) {
-            Parameter callback = Arrays.stream(method(methodName).getParameters())
-                    .filter(parameter -> parameter.getType() == OmgCallbackRequest.class)
-                    .findFirst()
-                    .orElseThrow();
-
-            assertTrue(callback.isAnnotationPresent(ModelAttribute.class));
-            assertFalse(callback.isAnnotationPresent(RequestBody.class));
-        }
-    }
-
-    @Test
-    void paymentInfoOrderIdRemainsAPathVariable() {
-        Parameter orderId = Arrays.stream(method("getPaymentInfo").getParameters())
-                .filter(parameter -> "orderid".equals(parameter.getName()))
-                .findFirst()
-                .orElseThrow();
-
-        assertTrue(orderId.isAnnotationPresent(PathVariable.class));
-        assertFalse(orderId.isAnnotationPresent(RequestParam.class));
-    }
-
-    @Test
-    void invalidOrderRequestsReturnInternationalizedBusinessError() {
-        OmgPayController controller = new OmgPayController();
-        OmgOrderRequest blank = new OmgOrderRequest();
-        OmgOrderRequest whitespace = new OmgOrderRequest();
-        whitespace.setOrderid("   ");
-
-        try (var messages = mockStatic(MessageUtils.class)) {
-            messages.when(() -> MessageUtils.message("no.order.id.error")).thenReturn("localized order error");
-
-            for (AjaxResult result : List.of(
-                    controller.create("token", null),
-                    controller.refund("token", blank),
-                    controller.query("token", whitespace))) {
-                assertEquals("localized order error", result.get(AjaxResult.MSG_TAG));
-            }
-        }
-    }
-
-    @Test
-    void orderRequestValidationDoesNotImposeALengthLimit() {
-        OmgOrderRequest request = new OmgOrderRequest();
-        request.setOrderid("x".repeat(1000));
-
-        Boolean invalid = ReflectionTestUtils.invokeMethod(
-                new OmgPayController(), "invalidOrderRequest", request);
-
-        assertFalse(Boolean.TRUE.equals(invalid));
-    }
-
-    @Test
-    void callbackDtoRetainsOriginalOmgParameterNames() {
-        OmgCallbackRequest request = new OmgCallbackRequest();
-        request.setMerchantID(new String[]{"M123"});
-        request.setRtnCode(new String[]{"1"});
-        request.setCheckMacValue(new String[]{"SIGNED"});
-
-        assertEquals(Map.of("MerchantID", "M123", "RtnCode", "1", "CheckMacValue", "SIGNED"),
-                request.toParameterMap());
-    }
-
-    @Test
-    void callbackDtoRejectsOversizedParameters() {
-        OmgCallbackRequest request = new OmgCallbackRequest();
-        request.setRtnMsg(new String[]{"x".repeat(4097)});
-
-        assertThrows(IllegalArgumentException.class,
-                request::toParameterMap);
-    }
-
-    @Test
-    void callbackDtoRejectsDuplicateParameters() {
-        OmgCallbackRequest request = new OmgCallbackRequest();
-        request.setMerchantID(new String[]{"first", "second"});
-
-        assertThrows(IllegalArgumentException.class,
-                request::toParameterMap);
-    }
-
-    @Test
-    void springBindsOmgFormFieldNamesToCallbackDto() {
-        OmgCallbackRequest request = new OmgCallbackRequest();
-        MutablePropertyValues values = new MutablePropertyValues();
-        values.add("MerchantID", new String[]{"M123"});
-        values.add("vAccount", new String[]{"991234"});
-        values.add("auth_code", new String[]{"AUTH"});
-
-        new DataBinder(request).bind(values);
-
-        assertEquals(Map.of("MerchantID", "M123", "vAccount", "991234", "auth_code", "AUTH"),
-                request.toParameterMap());
-    }
-
-    @Test
-    void omgCallbacksRecordSanitizedContentWithOmgType() {
-        OmgPayController controller = new OmgPayController();
-        IIpnLogService ipnLogService = mock(IIpnLogService.class);
-        ReflectionTestUtils.setField(controller, "ipnLogService", ipnLogService);
-
-        OmgCallbackRequest callback = new OmgCallbackRequest();
-        callback.setRtnMsg(new String[]{"received"});
-        callback.setCheckMacValue(new String[]{"signature"});
-        callback.setAuthCode(new String[]{"authorization"});
-
-        assertEquals("1|OK", controller.notify(callback, "127.0.0.1"));
-        assertEquals("1|OK", controller.paymentInfoCallback(callback, "127.0.0.1"));
-
-        ArgumentCaptor<IpnLog> captor = ArgumentCaptor.forClass(IpnLog.class);
-        verify(ipnLogService, times(2)).insertIpnLog(captor.capture());
-        for (IpnLog log : captor.getAllValues()) {
-            assertEquals("omg", log.getType());
-            assertEquals("{\"RtnMsg\":\"received\"}", log.getIpnLog());
-        }
-    }
-
-    @Test
-    void createsOnlyOneManualRefundTodo() {
-        RefundFixture fixture = refundFixture("ATM_TAISHIN");
-        PosOrderOmgRefund pending = new PosOrderOmgRefund();
-        when(fixture.refundService.listByPayment(1L))
-                .thenReturn(List.of(), List.of(pending));
-
-        assertEquals(OmgRefundOutcome.Status.MANUAL_PENDING,
-                fixture.controller.refundOrderOutcome(fixture.order).getStatus());
-        assertEquals(OmgRefundOutcome.Status.MANUAL_PENDING,
-                fixture.controller.refundOrderOutcome(fixture.order).getStatus());
-
-        verify(fixture.refundService).record(1L, fixture.order.getDdId(), "T123", null,
-                100, null, "延期支付方式无退款API,待人工在 OMG 后台处理", "");
-    }
-
-    @Test
-    void reportsRefundedOnlyAfterGatewayAndLedgerSucceed() throws Exception {
-        RefundFixture fixture = refundFixture("Credit_CreditCard");
-        PosStoreOmg credential = credential();
-        when(fixture.paymentService.markRefunding(1L)).thenReturn(1);
-        when(fixture.storeService.getCredentialByMerchantId("M123")).thenReturn(credential);
-        when(fixture.omgPay.doAction(anyString(), any(), anyString(), anyString(), anyString(), anyInt()))
-                .thenReturn(Map.of("RtnCode", "1", "RtnMsg", "Succeeded"));
-        when(fixture.paymentService.markRefunded(1L)).thenReturn(1);
-
-        OmgRefundOutcome outcome = fixture.controller.refundOrderOutcome(fixture.order);
-
-        assertEquals(OmgRefundOutcome.Status.REFUNDED, outcome.getStatus());
-        verify(fixture.paymentService).markRefunded(1L);
-        verify(fixture.orderLifecycleService, never()).finalizeSystemOmgRefund(10L);
-    }
-
-    @Test
-    void keepsRefundingStateWhenGatewayOutcomeIsUnknown() throws Exception {
-        RefundFixture fixture = refundFixture("Credit_CreditCard");
-        when(fixture.paymentService.markRefunding(1L)).thenReturn(1);
-        when(fixture.storeService.getCredentialByMerchantId("M123")).thenReturn(credential());
-        when(fixture.omgPay.doAction(anyString(), any(), anyString(), anyString(), anyString(), anyInt()))
-                .thenThrow(new RuntimeException("timeout with sensitive gateway detail"));
-
-        Logger logger = (Logger) LoggerFactory.getLogger(OmgPayController.class);
-        ListAppender<ILoggingEvent> appender = new ListAppender<>();
-        appender.start();
-        logger.addAppender(appender);
-
-        OmgRefundOutcome outcome;
-        try {
-            outcome = fixture.controller.refundOrderOutcome(fixture.order);
-        } finally {
-            logger.detachAppender(appender);
-        }
-
-        assertEquals(OmgRefundOutcome.Status.UNKNOWN, outcome.getStatus());
-        assertFalse(appender.list.stream()
-                .map(ILoggingEvent::getFormattedMessage)
-                .anyMatch(message -> message.contains("sensitive gateway detail")));
-        verify(fixture.paymentService, never()).restorePaidFromRefunding(1L);
-    }
-
-    @Test
-    void manualConfirmationUsesLedgerCasAndRecordsCompletion() {
-        RefundFixture fixture = refundFixture("ATM_TAISHIN");
-        PosOrderOmgRefund pending = new PosOrderOmgRefund();
-        when(fixture.refundService.listByPayment(1L)).thenReturn(List.of(pending));
-        when(fixture.paymentService.markRefunding(1L)).thenReturn(1);
-        when(fixture.paymentService.markRefunded(1L)).thenReturn(1);
-
-        OmgRefundOutcome outcome = fixture.controller.confirmManualRefundOutcome(fixture.order);
-
-        assertEquals(OmgRefundOutcome.Status.REFUNDED, outcome.getStatus());
-        verify(fixture.paymentService).markRefunding(1L);
-        verify(fixture.paymentService).markRefunded(1L);
-        verify(fixture.refundService).record(1L, fixture.order.getDdId(), "T123", null,
-                100, 1, "管理员确认已在 OMG 后台完成人工退款", "");
-    }
-
-    private RefundFixture refundFixture(String paymentType) {
-        RefundFixture fixture = new RefundFixture();
-        fixture.controller = new OmgPayController();
-        fixture.paymentService = mock(IPosOrderOmgPaymentService.class);
-        fixture.refundService = mock(IPosOrderOmgRefundService.class);
-        fixture.storeService = mock(IPosStoreOmgService.class);
-        fixture.omgPay = mock(OmgPay.class);
-        fixture.orderLifecycleService = mock(OrderLifecycleService.class);
-        ReflectionTestUtils.setField(fixture.controller, "paymentService", fixture.paymentService);
-        ReflectionTestUtils.setField(fixture.controller, "refundService", fixture.refundService);
-        ReflectionTestUtils.setField(fixture.controller, "storeOmgService", fixture.storeService);
-        ReflectionTestUtils.setField(fixture.controller, "omgPay", fixture.omgPay);
-        ReflectionTestUtils.setField(fixture.controller, "orderLifecycleService", fixture.orderLifecycleService);
-        ReflectionTestUtils.setField(fixture.controller, "orderLogHelper",
-                mock(com.ruoyi.system.utils.OrderLogHelper.class));
-        ReflectionTestUtils.setField(fixture.controller, "baseUrl", "https://payment.example.test");
-
-        fixture.order = new PosOrder();
-        fixture.order.setId(10L);
-        fixture.order.setDdId("202608100003");
-        fixture.order.setPayType("7");
-        fixture.order.setPayStatus(1L);
-        fixture.order.setAmount(100);
-
-        PosOrderOmgPayment payment = new PosOrderOmgPayment();
-        payment.setId(1L);
-        payment.setDdId(fixture.order.getDdId());
-        payment.setPayStatus(1);
-        payment.setPayType(paymentType);
-        payment.setAmount(100);
-        payment.setTradeNo("T123");
-        payment.setMerchantTradeNo("MT123");
-        payment.setMerchantId("M123");
-        payment.setStoreId(77L);
-        when(fixture.paymentService.getLatestByDdId(fixture.order.getDdId())).thenReturn(payment);
-        return fixture;
-    }
-
-    private PosStoreOmg credential() {
-        PosStoreOmg credential = new PosStoreOmg();
-        credential.setStoreId(77L);
-        credential.setMerchantId("M123");
-        credential.setHashKey("hash-key");
-        credential.setHashIv("hash-iv");
-        return credential;
-    }
-
-    private Method method(String name) {
-        return Arrays.stream(OmgPayController.class.getDeclaredMethods())
-                .filter(method -> method.getName().equals(name))
-                .findFirst()
-                .orElseThrow();
-    }
-
-    private static class RefundFixture {
-        private OmgPayController controller;
-        private IPosOrderOmgPaymentService paymentService;
-        private IPosOrderOmgRefundService refundService;
-        private IPosStoreOmgService storeService;
-        private OmgPay omgPay;
-        private OrderLifecycleService orderLifecycleService;
-        private PosOrder order;
-    }
-}

+ 0 - 90
ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgPayment.java

@@ -1,90 +0,0 @@
-package com.ruoyi.system.domain;
-
-import com.baomidou.mybatisplus.annotation.FieldFill;
-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 lombok.Data;
-
-import java.util.Date;
-
-/**
- * OMG 支付交易流水 pos_order_omg_payment
- *
- * <p>每笔 OMG 交易一条,承载幂等(按 trade_no = OMG TradeNo)与对账明细。不复用蓝新 pos_order_payment。
- * 幂等:回调按 trade_no 查重,已 pay_status=1 则跳过(仍回 1|OK)。
- * trade_no 唯一索引(MySQL 允许多个 NULL,发起时尚无 trade_no)。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-@Data
-@TableName(value = "pos_order_omg_payment")
-public class PosOrderOmgPayment {
-
-    private static final long serialVersionUID = 1L;
-
-    /** 主键 */
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /** 系统订单号(关联 pos_order.dd_id) */
-    private String ddId;
-
-    /** 商店交易编号 MerchantTradeNo(发起生成) */
-    private String merchantTradeNo;
-
-    /** OMG 交易编号 TradeNo(回调获得,幂等键,唯一) */
-    private String tradeNo;
-
-    /** 门店ID */
-    private Long storeId;
-
-    /** OMG 商店代号(发起时门店凭证) */
-    private String merchantId;
-
-    /** 发起方式:ALL */
-    private String choosePayment;
-
-    /** 回覆 PaymentType(Credit_CreditCard / BarcodeATM_CHINATRUST / ATM 系列 / CVS 系列 / AFTEE_AFTEE) */
-    private String payType;
-
-    /** 交易金额(整数元 = 订单 amount) */
-    private Integer amount;
-
-    /** 回调 RtnCode(1=成功) */
-    private Integer rtnCode;
-
-    /** 回调 RtnMsg */
-    private String rtnMsg;
-
-    /** 0 未支付 / 1 已支付 / 2 失败 / 3 已退款 / 4 退款中 */
-    private Integer payStatus;
-
-    /** 1=当前活跃尝试 0=已轮换历史(保留接迟到 notify);与 payStatus 正交,轮换只翻 is_active 不动 payStatus */
-    private Integer isActive;
-
-    /** OMG 延期支付真实 ExpireDate,仅由 paymentInfo 回调解析写入;NULL=未知(信用卡/未取号) */
-    private Date expireDate;
-
-    /** 授权码(信用卡回调) */
-    private String authCode;
-
-    /** 支付完成时间(回调 PaymentDate) */
-    private Date payTime;
-
-    /** 订单创建时间(回调 TradeDate) */
-    private Date tradeDate;
-
-    /** 回调原始报文(调试/对账) */
-    private String callbackRaw;
-
-    /** 创建时间 */
-    @TableField(fill = FieldFill.INSERT)
-    private Date createTime;
-
-    /** 更新时间 */
-    @TableField(fill = FieldFill.INSERT_UPDATE)
-    private Date updateTime;
-}

+ 0 - 57
ruoyi-system/src/main/java/com/ruoyi/system/domain/PosOrderOmgRefund.java

@@ -1,57 +0,0 @@
-package com.ruoyi.system.domain;
-
-import com.baomidou.mybatisplus.annotation.FieldFill;
-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 lombok.Data;
-
-import java.util.Date;
-
-/**
- * OMG 退款记录 pos_order_omg_refund
- *
- * <p>每次 DoAction 调用一条,与支付流水解耦,支持多次动作与对账。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-@Data
-@TableName(value = "pos_order_omg_refund")
-public class PosOrderOmgRefund {
-
-    private static final long serialVersionUID = 1L;
-
-    /** 主键 */
-    @TableId(type = IdType.AUTO)
-    private Long id;
-
-    /** 关联 pos_order_omg_payment.id */
-    private Long paymentId;
-
-    /** 系统订单号 */
-    private String ddId;
-
-    /** OMG 交易编号 TradeNo */
-    private String tradeNo;
-
-    /** DoAction 动作: C/R/E/N */
-    private String action;
-
-    /** 退款/操作金额 */
-    private Integer amount;
-
-    /** 响应 RtnCode(1=成功) */
-    private Integer rtnCode;
-
-    /** 响应 RtnMsg */
-    private String rtnMsg;
-
-    /** 响应原始报文 */
-    private String callbackRaw;
-
-    /** 操作时间 */
-    @TableField(fill = FieldFill.INSERT)
-    private Date createTime;
-}

+ 0 - 73
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgPaymentMapper.java

@@ -1,73 +0,0 @@
-package com.ruoyi.system.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import org.apache.ibatis.annotations.Param;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * OMG 支付交易流水 Mapper。insert / updateById 用 BaseMapper。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-public interface PosOrderOmgPaymentMapper extends BaseMapper<PosOrderOmgPayment> {
-
-    /** 按 OMG 交易号查(回调幂等键 TradeNo)。 */
-    PosOrderOmgPayment selectByTradeNo(@Param("tradeNo") String tradeNo);
-
-    /** 按商店交易编号查(MerchantTradeNo)。 */
-    PosOrderOmgPayment selectByMerchantTradeNo(@Param("merchantTradeNo") String merchantTradeNo);
-
-    /** 按订单号查最新一笔流水(取最近发起/支付记录)。 */
-    PosOrderOmgPayment selectLatestByDdId(@Param("ddId") String ddId);
-
-    PosOrderOmgPayment selectLatestPaidByDdId(@Param("ddId") String ddId);
-
-    /** 复用候选:活跃未付(is_active=1, pay_status=0, trade_no IS NULL 未取号)且在新鲜期内,供 create 复用 MTN。 */
-    PosOrderOmgPayment selectActiveForReuse(@Param("ddId") String ddId, @Param("freshMin") int freshMin);
-
-    /** 轮换:同订单活跃未付行 is_active 1→0(CAS,不动 pay_status),保留旧行接迟到 notify。 */
-    int markActiveHistorical(@Param("ddId") String ddId, @Param("updateTime") java.util.Date updateTime);
-
-    /** 全量未付流水(pay_status=0,含已轮换历史行),reconcile 遍历补单用。 */
-    List<PosOrderOmgPayment> listUnpaidByDdId(@Param("ddId") String ddId);
-
-    /** 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退),退款/读侧用。 */
-    PosOrderOmgPayment selectLatestRefundableByDdId(@Param("ddId") String ddId);
-
-    /** 是否存在已付/退款中/已退行(pay_status IN(1,3,4))。 */
-    int existsPaidByDdId(@Param("ddId") String ddId);
-
-    /**
-     * 扫描补单窗口内未核销的漏单 ddId(定时补单方案B 用)。
-     * 每订单取最新一笔 pay_status=0 的流水(older 发起视为放弃),关联 pos_order 排除已取消(state=4)。
-     */
-    List<String> selectLeakOrderDdIds(@Param("windowStart") Date windowStart,
-                                      @Param("graceCutoff") Date graceCutoff,
-                                      @Param("batchSize") int batchSize);
-
-    int markSuccessIfUnpaid(@Param("id") Long id, @Param("tradeNo") String tradeNo,
-                            @Param("payType") String payType, @Param("rtnCode") Integer rtnCode,
-                            @Param("rtnMsg") String rtnMsg, @Param("authCode") String authCode,
-                            @Param("payTime") java.util.Date payTime,
-                            @Param("tradeDate") java.util.Date tradeDate,
-                            @Param("callbackRaw") String callbackRaw,
-                            @Param("updateTime") java.util.Date updateTime);
-
-    int markFailIfUnpaid(@Param("id") Long id, @Param("rtnCode") Integer rtnCode,
-                         @Param("rtnMsg") String rtnMsg, @Param("callbackRaw") String callbackRaw,
-                         @Param("updateTime") java.util.Date updateTime);
-
-    int markPaymentInfoIfOpen(@Param("id") Long id, @Param("tradeNo") String tradeNo,
-                              @Param("callbackRaw") String callbackRaw,
-                              @Param("updateTime") java.util.Date updateTime);
-
-    int markRefundingIfPaid(@Param("id") Long id, @Param("updateTime") java.util.Date updateTime);
-
-    int markRefundedIfRefunding(@Param("id") Long id, @Param("updateTime") java.util.Date updateTime);
-
-    int restorePaidIfRefunding(@Param("id") Long id, @Param("updateTime") java.util.Date updateTime);
-}

+ 0 - 22
ruoyi-system/src/main/java/com/ruoyi/system/mapper/PosOrderOmgRefundMapper.java

@@ -1,22 +0,0 @@
-package com.ruoyi.system.mapper;
-
-import com.baomidou.mybatisplus.core.mapper.BaseMapper;
-import com.ruoyi.system.domain.PosOrderOmgRefund;
-import org.apache.ibatis.annotations.Param;
-
-import java.util.List;
-
-/**
- * OMG 退款记录 Mapper。insert 用 BaseMapper。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-public interface PosOrderOmgRefundMapper extends BaseMapper<PosOrderOmgRefund> {
-
-    /** 按支付流水ID查退款记录。 */
-    List<PosOrderOmgRefund> selectByPaymentId(@Param("paymentId") Long paymentId);
-
-    /** 按订单号查退款记录。 */
-    List<PosOrderOmgRefund> selectByDdId(@Param("ddId") String ddId);
-}

+ 1 - 0
ruoyi-system/src/main/java/com/ruoyi/system/omgpay/mapper/OmgPaymentAttemptMapper.java

@@ -7,5 +7,6 @@ import org.apache.ibatis.annotations.Param;
 public interface OmgPaymentAttemptMapper {
     OmgPaymentOrderSnapshot selectOrderForUpdate(@Param("ddId") String ddId);
     OmgPaymentAttempt selectActiveCreatedByDdId(@Param("ddId") String ddId);
+    OmgPaymentAttempt selectByMerchantTradeNo(@Param("merchantTradeNo") String merchantTradeNo);
     int insertCreated(OmgPaymentAttempt row);
 }

+ 1 - 0
ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/IOmgPaymentAttemptService.java

@@ -6,6 +6,7 @@ import com.ruoyi.system.omgpay.domain.OmgPaymentOrderSnapshot;
 public interface IOmgPaymentAttemptService {
     OmgPaymentOrderSnapshot selectOrderForUpdate(String ddId);
     OmgPaymentAttempt selectActiveCreatedByDdId(String ddId);
+    OmgPaymentAttempt selectByMerchantTradeNo(String merchantTradeNo);
     OmgPaymentAttempt createCreated(String ddId, String merchantTradeNo, Long storeId,
                                     String merchantId, Integer amount);
 }

+ 5 - 0
ruoyi-system/src/main/java/com/ruoyi/system/omgpay/service/impl/OmgPaymentAttemptServiceImpl.java

@@ -33,6 +33,11 @@ public class OmgPaymentAttemptServiceImpl implements IOmgPaymentAttemptService {
         return StrUtil.isBlank(ddId) ? null : mapper.selectActiveCreatedByDdId(ddId.trim());
     }
 
+    @Override
+    public OmgPaymentAttempt selectByMerchantTradeNo(String merchantTradeNo) {
+        return StrUtil.isBlank(merchantTradeNo) ? null : mapper.selectByMerchantTradeNo(merchantTradeNo.trim());
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public OmgPaymentAttempt createCreated(String ddId, String merchantTradeNo, Long storeId,

+ 0 - 82
ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgPaymentService.java

@@ -1,82 +0,0 @@
-package com.ruoyi.system.service;
-
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * OMG 支付流水 Service(落库、幂等核销、状态流转)。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-public interface IPosOrderOmgPaymentService {
-
-    /** 发起支付时落流水(pay_status=0),返回带主键的流水。 */
-    PosOrderOmgPayment createPayment(String ddId, String merchantTradeNo, Long storeId,
-                                     String merchantId, Integer amount, String choosePayment);
-
-    /** 按 OMG 交易号查(回调幂等键)。 */
-    PosOrderOmgPayment getByTradeNo(String tradeNo);
-
-    /** 按商店交易编号查。 */
-    PosOrderOmgPayment getByMerchantTradeNo(String merchantTradeNo);
-
-    /** 按订单号查最新一笔流水。 */
-    PosOrderOmgPayment getLatestByDdId(String ddId);
-
-    PosOrderOmgPayment getLatestPaidByDdId(String ddId);
-
-    /** 复用候选(活跃未付未取号 + 新鲜期内),create 复用 MTN 用。freshMin=新鲜期分钟。 */
-    PosOrderOmgPayment getActiveForReuse(String ddId, int freshMin);
-
-    /** 轮换:活跃未付行 is_active 1→0(不动 pay_status),create 换号前调用。 */
-    int markActiveHistorical(String ddId);
-
-    /** 全量未付流水(pay_status=0),reconcile 遍历补单用。 */
-    List<PosOrderOmgPayment> listUnpaidByDdId(String ddId);
-
-    /** 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退),退款/读侧用。 */
-    PosOrderOmgPayment getLatestRefundableByDdId(String ddId);
-
-    /** 是否存在已付/退款中/已退行。 */
-    boolean existsPaidByDdId(String ddId);
-
-    /**
-     * 标记支付成功(幂等:已 pay_status=1 返回 0 不重复更新)。
-     *
-     * @param paymentId 流水主键
-     * @param tradeNo   OMG 交易号(回调获得,落库)
-     * @param payType   回覆 PaymentType(如 Credit_CreditCard)
-     * @param rtnCode   回调 RtnCode(1=成功)
-     * @param rtnMsg    回调 RtnMsg
-     * @param authCode  授权码(信用卡)
-     * @param payTime   支付完成时间(回调 PaymentDate)
-     * @param callbackRaw 回调原始报文
-     * @return 实际更新行数(0=幂等跳过)
-     */
-    int markSuccess(Long paymentId, String tradeNo, String payType, Integer rtnCode,
-                    String rtnMsg, String authCode, Date payTime, Date tradeDate, String callbackRaw);
-
-    /** 标记支付失败(pay_status=2)。 */
-    int markFail(Long paymentId, Integer rtnCode, String rtnMsg, String callbackRaw);
-
-    /** 标记已退款(pay_status=3)。 */
-    int markRefunded(Long paymentId);
-
-    int markRefunding(Long paymentId);
-
-    int restorePaidFromRefunding(Long paymentId);
-
-    /** 记录 ATM/超商取号回调(更新 tradeNo + callbackRaw,不改 pay_status)。 */
-    int markPaymentInfo(Long paymentId, String tradeNo, String callbackRaw);
-
-    /**
-     * 扫描补单窗口内未核销的漏单 ddId 列表(定时补单方案B 用)。
-     * @param windowStart  扫描起点(now - windowHours),过期单不再扫
-     * @param graceCutoff  扫描终点(now - graceMinutes),给回调/重试留送达时间
-     * @param batchSize    单轮最多处理笔数
-     */
-    List<String> scanLeakOrders(Date windowStart, Date graceCutoff, int batchSize);
-}

+ 0 - 24
ruoyi-system/src/main/java/com/ruoyi/system/service/IPosOrderOmgRefundService.java

@@ -1,24 +0,0 @@
-package com.ruoyi.system.service;
-
-import com.ruoyi.system.domain.PosOrderOmgRefund;
-
-import java.util.List;
-
-/**
- * OMG 退款记录 Service。每次 DoAction 调用一条,与支付流水解耦。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-public interface IPosOrderOmgRefundService {
-
-    /** 记录一次退款/取消动作(DoAction 响应或人工待处理)。 */
-    int record(Long paymentId, String ddId, String tradeNo, String action,
-               Integer amount, Integer rtnCode, String rtnMsg, String callbackRaw);
-
-    /** 按支付流水查退款记录。 */
-    List<PosOrderOmgRefund> listByPayment(Long paymentId);
-
-    /** 按订单号查退款记录。 */
-    List<PosOrderOmgRefund> listByDdId(String ddId);
-}

+ 0 - 154
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImpl.java

@@ -1,154 +0,0 @@
-package com.ruoyi.system.service.impl;
-
-import com.ruoyi.system.domain.PosOrderOmgPayment;
-import com.ruoyi.system.mapper.PosOrderOmgPaymentMapper;
-import com.ruoyi.system.service.IPosOrderOmgPaymentService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.Date;
-import java.util.List;
-
-/** OMG payment ledger service. */
-@Service
-public class PosOrderOmgPaymentServiceImpl implements IPosOrderOmgPaymentService {
-
-    private static final int PAY_STATUS_UNPAID = 0;
-    private static final int RTN_MSG_MAX_LENGTH = 200;
-
-    @Autowired
-    private PosOrderOmgPaymentMapper posOrderOmgPaymentMapper;
-
-    @Override
-    public PosOrderOmgPayment createPayment(String ddId, String merchantTradeNo, Long storeId,
-                                            String merchantId, Integer amount, String choosePayment) {
-        PosOrderOmgPayment row = new PosOrderOmgPayment();
-        row.setDdId(ddId);
-        row.setMerchantTradeNo(merchantTradeNo);
-        row.setStoreId(storeId);
-        row.setMerchantId(merchantId);
-        row.setAmount(amount);
-        row.setChoosePayment(choosePayment);
-        row.setPayStatus(PAY_STATUS_UNPAID);
-        Date now = new Date();
-        row.setCreateTime(now);
-        row.setUpdateTime(now);
-        posOrderOmgPaymentMapper.insert(row);
-        return row;
-    }
-
-    @Override
-    public PosOrderOmgPayment getByTradeNo(String tradeNo) {
-        return tradeNo == null || tradeNo.isEmpty() ? null : posOrderOmgPaymentMapper.selectByTradeNo(tradeNo);
-    }
-
-    @Override
-    public PosOrderOmgPayment getByMerchantTradeNo(String merchantTradeNo) {
-        return merchantTradeNo == null || merchantTradeNo.isEmpty()
-                ? null : posOrderOmgPaymentMapper.selectByMerchantTradeNo(merchantTradeNo);
-    }
-
-    @Override
-    public PosOrderOmgPayment getLatestByDdId(String ddId) {
-        return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectLatestByDdId(ddId);
-    }
-
-    @Override
-    public PosOrderOmgPayment getLatestPaidByDdId(String ddId) {
-        return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectLatestPaidByDdId(ddId);
-    }
-
-    @Override
-    public PosOrderOmgPayment getActiveForReuse(String ddId, int freshMin) {
-        return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectActiveForReuse(ddId, freshMin);
-    }
-
-    @Override
-    public int markActiveHistorical(String ddId) {
-        if (ddId == null || ddId.isEmpty()) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.markActiveHistorical(ddId, new Date());
-    }
-
-    @Override
-    public List<PosOrderOmgPayment> listUnpaidByDdId(String ddId) {
-        return ddId == null || ddId.isEmpty()
-                ? java.util.Collections.emptyList()
-                : posOrderOmgPaymentMapper.listUnpaidByDdId(ddId);
-    }
-
-    @Override
-    public PosOrderOmgPayment getLatestRefundableByDdId(String ddId) {
-        return ddId == null || ddId.isEmpty() ? null : posOrderOmgPaymentMapper.selectLatestRefundableByDdId(ddId);
-    }
-
-    @Override
-    public boolean existsPaidByDdId(String ddId) {
-        return ddId != null && !ddId.isEmpty() && posOrderOmgPaymentMapper.existsPaidByDdId(ddId) > 0;
-    }
-
-    @Override
-    public int markSuccess(Long paymentId, String tradeNo, String payType, Integer rtnCode,
-                           String rtnMsg, String authCode, Date payTime, Date tradeDate, String callbackRaw) {
-        if (paymentId == null) {
-            return 0;
-        }
-        // CAS: only an open attempt can become paid; refunded/refunding attempts are terminal.
-        return posOrderOmgPaymentMapper.markSuccessIfUnpaid(paymentId, tradeNo, payType, rtnCode,
-                truncateRtnMsg(rtnMsg), authCode, payTime, tradeDate, callbackRaw, new Date());
-    }
-
-    @Override
-    public int markFail(Long paymentId, Integer rtnCode, String rtnMsg, String callbackRaw) {
-        if (paymentId == null) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.markFailIfUnpaid(paymentId, rtnCode,
-                truncateRtnMsg(rtnMsg), callbackRaw, new Date());
-    }
-
-    @Override
-    public int markRefunded(Long paymentId) {
-        if (paymentId == null) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.markRefundedIfRefunding(paymentId, new Date());
-    }
-
-    @Override
-    public int markRefunding(Long paymentId) {
-        if (paymentId == null) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.markRefundingIfPaid(paymentId, new Date());
-    }
-
-    @Override
-    public int restorePaidFromRefunding(Long paymentId) {
-        if (paymentId == null) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.restorePaidIfRefunding(paymentId, new Date());
-    }
-
-    @Override
-    public int markPaymentInfo(Long paymentId, String tradeNo, String callbackRaw) {
-        if (paymentId == null) {
-            return 0;
-        }
-        return posOrderOmgPaymentMapper.markPaymentInfoIfOpen(paymentId, tradeNo, callbackRaw, new Date());
-    }
-
-    @Override
-    public List<String> scanLeakOrders(Date windowStart, Date graceCutoff, int batchSize) {
-        return posOrderOmgPaymentMapper.selectLeakOrderDdIds(windowStart, graceCutoff, Math.max(1, batchSize));
-    }
-
-    private String truncateRtnMsg(String rtnMsg) {
-        if (rtnMsg == null || rtnMsg.length() <= RTN_MSG_MAX_LENGTH) {
-            return rtnMsg;
-        }
-        return rtnMsg.substring(0, RTN_MSG_MAX_LENGTH);
-    }
-}

+ 0 - 64
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImpl.java

@@ -1,64 +0,0 @@
-package com.ruoyi.system.service.impl;
-
-import com.ruoyi.system.domain.PosOrderOmgRefund;
-import com.ruoyi.system.mapper.PosOrderOmgRefundMapper;
-import com.ruoyi.system.service.IPosOrderOmgRefundService;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-
-import java.util.Date;
-import java.util.List;
-
-/**
- * OMG 退款记录 Service 实现。
- *
- * @author ruoyi
- * @date 2026-07-29
- */
-@Service
-public class PosOrderOmgRefundServiceImpl implements IPosOrderOmgRefundService {
-
-    private static final int RTN_MSG_MAX_LENGTH = 200;
-
-    @Autowired
-    private PosOrderOmgRefundMapper posOrderOmgRefundMapper;
-
-    @Override
-    public int record(Long paymentId, String ddId, String tradeNo, String action,
-                      Integer amount, Integer rtnCode, String rtnMsg, String callbackRaw) {
-        PosOrderOmgRefund row = new PosOrderOmgRefund();
-        row.setPaymentId(paymentId);
-        row.setDdId(ddId);
-        row.setTradeNo(tradeNo);
-        row.setAction(action);
-        row.setAmount(amount);
-        row.setRtnCode(rtnCode);
-        row.setRtnMsg(truncateRtnMsg(rtnMsg));
-        row.setCallbackRaw(callbackRaw);
-        row.setCreateTime(new Date());
-        return posOrderOmgRefundMapper.insert(row);
-    }
-
-    @Override
-    public List<PosOrderOmgRefund> listByPayment(Long paymentId) {
-        if (paymentId == null) {
-            return java.util.Collections.emptyList();
-        }
-        return posOrderOmgRefundMapper.selectByPaymentId(paymentId);
-    }
-
-    @Override
-    public List<PosOrderOmgRefund> listByDdId(String ddId) {
-        if (ddId == null || ddId.isEmpty()) {
-            return java.util.Collections.emptyList();
-        }
-        return posOrderOmgRefundMapper.selectByDdId(ddId);
-    }
-
-    private String truncateRtnMsg(String rtnMsg) {
-        if (rtnMsg == null || rtnMsg.length() <= RTN_MSG_MAX_LENGTH) {
-            return rtnMsg;
-        }
-        return rtnMsg.substring(0, RTN_MSG_MAX_LENGTH);
-    }
-}

+ 0 - 163
ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgPaymentMapper.xml

@@ -1,163 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.ruoyi.system.mapper.PosOrderOmgPaymentMapper">
-
-    <sql id="paymentColumns">
-        id, dd_id AS ddId, merchant_trade_no AS merchantTradeNo, trade_no AS tradeNo,
-        store_id AS storeId, merchant_id AS merchantId, choose_payment AS choosePayment,
-        pay_type AS payType, amount, rtn_code AS rtnCode, rtn_msg AS rtnMsg,
-        pay_status AS payStatus, auth_code AS authCode, pay_time AS payTime,
-        trade_date AS tradeDate, callback_raw AS callbackRaw,
-        is_active AS isActive, expire_date AS expireDate,
-        create_time AS createTime, update_time AS updateTime
-    </sql>
-
-    <!-- 按 OMG 交易号查(回调幂等键) -->
-    <select id="selectByTradeNo" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE trade_no = #{tradeNo}
-    </select>
-
-    <!-- 按商店交易编号查 -->
-    <select id="selectByMerchantTradeNo" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE merchant_trade_no = #{merchantTradeNo}
-    </select>
-
-    <!-- 按订单号查最新一笔流水 -->
-    <select id="selectLatestByDdId" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId}
-        ORDER BY id DESC
-        LIMIT 1
-    </select>
-
-    <select id="selectLatestPaidByDdId" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId}
-          AND pay_status IN (1,3,4)
-          AND trade_no IS NOT NULL
-          AND trade_no &lt;&gt; ''
-        ORDER BY id DESC
-        LIMIT 1
-    </select>
-
-    <!-- 扫描补单窗口内未核销漏单(每订单最新一笔 pay_status=0),关联订单排除已取消 state=4 -->
-        <!-- 扫描补单窗口内有任意未核销(pay_status=0)流水的订单 ddId(多 MTN 订单全部未付行都进扫描,
-         不再只取 MAX(id) 一条,避免漏补较早 MTN),关联订单排除已取消 state=4 -->
-    <select id="selectLeakOrderDdIds" resultType="java.lang.String">
-        SELECT DISTINCT p.dd_id
-        FROM pos_order_omg_payment p
-        INNER JOIN pos_order o ON o.dd_id = p.dd_id
-        WHERE EXISTS (
-            SELECT 1 FROM pos_order_omg_payment
-            WHERE dd_id = p.dd_id AND pay_status = 0
-              AND create_time &gt;= #{windowStart}
-              AND create_time &lt;= #{graceCutoff}
-        )
-        AND (o.state IS NULL OR o.state &lt;&gt; 4)
-        ORDER BY p.dd_id ASC
-        LIMIT #{batchSize}
-    </select>
-
-    <!-- 全量未付流水(pay_status=0,is_active 不限,含已轮换历史行)供 reconcileByQuery 遍历补单 -->
-    <select id="listUnpaidByDdId" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId} AND pay_status = 0
-        ORDER BY id ASC
-    </select>
-
-    <!-- 最新可退款行(pay_status IN(1,3,4):已付/退款中/已退) -->
-    <select id="selectLatestRefundableByDdId" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId} AND pay_status IN (1,3,4)
-        ORDER BY id DESC
-        LIMIT 1
-    </select>
-
-    <!-- 是否存在已付/退款中/已退行(校验用):pay_status IN(1,3,4) -->
-    <select id="existsPaidByDdId" parameterType="String" resultType="int">
-        SELECT COUNT(*) FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId} AND pay_status IN (1,3,4)
-    </select>
-
-    <!-- 复用候选:活跃未付(is_active=1, pay_status=0)且未取号(trade_no IS NULL)且新鲜期内,create 复用 MTN(连点防堆积) -->
-    <select id="selectActiveForReuse" resultType="com.ruoyi.system.domain.PosOrderOmgPayment">
-        SELECT <include refid="paymentColumns"/>
-        FROM pos_order_omg_payment
-        WHERE dd_id = #{ddId} AND is_active = 1 AND pay_status = 0
-          AND trade_no IS NULL
-          AND create_time &gt;= DATE_ADD(NOW(), INTERVAL -#{freshMin} MINUTE)
-        ORDER BY id DESC
-        LIMIT 1
-    </select>
-
-    <!-- 轮换:活跃未付行 is_active 1→0(CAS,不动 pay_status),保留旧行接迟到 notify -->
-    <update id="markActiveHistorical">
-        UPDATE pos_order_omg_payment SET is_active = 0, update_time = #{updateTime}
-        WHERE dd_id = #{ddId} AND is_active = 1 AND pay_status = 0
-    </update>
-
-    <update id="markSuccessIfUnpaid">
-        UPDATE pos_order_omg_payment
-        SET trade_no = #{tradeNo}, pay_status = 1, pay_type = #{payType},
-            rtn_code = #{rtnCode}, rtn_msg = #{rtnMsg}, auth_code = #{authCode},
-            pay_time = #{payTime}, trade_date = #{tradeDate}, callback_raw = #{callbackRaw},
-            update_time = #{updateTime}
-        WHERE id = #{id}
-          AND pay_status IN (0, 2)
-          AND #{tradeNo} IS NOT NULL
-          AND #{tradeNo} &lt;&gt; ''
-          AND (trade_no IS NULL OR trade_no = '' OR trade_no = #{tradeNo})
-    </update>
-
-    <update id="markFailIfUnpaid">
-        UPDATE pos_order_omg_payment
-        SET pay_status = 2, rtn_code = #{rtnCode}, rtn_msg = #{rtnMsg},
-            callback_raw = #{callbackRaw}, update_time = #{updateTime}
-        WHERE id = #{id} AND pay_status = 0
-    </update>
-
-    <update id="markPaymentInfoIfOpen">
-        UPDATE pos_order_omg_payment
-        SET trade_no = CASE WHEN #{tradeNo} IS NULL OR #{tradeNo} = '' THEN trade_no ELSE #{tradeNo} END,
-            callback_raw = #{callbackRaw}, update_time = #{updateTime}
-        WHERE id = #{id}
-          AND pay_status = 0
-          AND (#{tradeNo} IS NULL OR #{tradeNo} = ''
-               OR trade_no IS NULL OR trade_no = '' OR trade_no = #{tradeNo})
-    </update>
-
-    <update id="markRefundingIfPaid">
-        UPDATE pos_order_omg_payment
-        SET pay_status = 4, update_time = #{updateTime}
-        WHERE id = #{id} AND pay_status = 1
-    </update>
-
-    <update id="markRefundedIfRefunding">
-        UPDATE pos_order_omg_payment
-        SET pay_status = 3, update_time = #{updateTime}
-        WHERE id = #{id} AND pay_status = 4
-    </update>
-
-    <update id="restorePaidIfRefunding">
-        UPDATE pos_order_omg_payment
-        SET pay_status = 1, update_time = #{updateTime}
-        WHERE id = #{id} AND pay_status = 4
-    </update>
-
-</mapper>

+ 0 - 29
ruoyi-system/src/main/resources/mapper/chanting/PosOrderOmgRefundMapper.xml

@@ -1,29 +0,0 @@
-<?xml version="1.0" encoding="UTF-8" ?>
-<!DOCTYPE mapper
-PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
-"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
-<mapper namespace="com.ruoyi.system.mapper.PosOrderOmgRefundMapper">
-
-    <sql id="refundColumns">
-        id, payment_id AS paymentId, dd_id AS ddId, trade_no AS tradeNo,
-        action, amount, rtn_code AS rtnCode, rtn_msg AS rtnMsg,
-        callback_raw AS callbackRaw, create_time AS createTime
-    </sql>
-
-    <select id="selectByPaymentId" parameterType="Long"
-            resultType="com.ruoyi.system.domain.PosOrderOmgRefund">
-        SELECT <include refid="refundColumns"/>
-        FROM pos_order_omg_refund
-        WHERE payment_id = #{paymentId}
-        ORDER BY id DESC
-    </select>
-
-    <select id="selectByDdId" parameterType="String"
-            resultType="com.ruoyi.system.domain.PosOrderOmgRefund">
-        SELECT <include refid="refundColumns"/>
-        FROM pos_order_omg_refund
-        WHERE dd_id = #{ddId}
-        ORDER BY id DESC
-    </select>
-
-</mapper>

+ 8 - 0
ruoyi-system/src/main/resources/mapper/omgpay/OmgPaymentAttemptMapper.xml

@@ -25,6 +25,14 @@
         LIMIT 1
     </select>
 
+    <select id="selectByMerchantTradeNo"
+            resultType="com.ruoyi.system.omgpay.domain.OmgPaymentAttempt">
+        SELECT <include refid="attemptColumns"/>
+        FROM pos_order_omg_attempt
+        WHERE merchant_trade_no = #{merchantTradeNo}
+        LIMIT 1
+    </select>
+
     <!-- active_dd_id is a DB-generated key invariant, so Java inserts only the created facts. -->
     <insert id="insertCreated" useGeneratedKeys="true" keyProperty="id">
         INSERT INTO pos_order_omg_attempt

+ 0 - 72
ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgPaymentServiceImplTest.java

@@ -1,72 +0,0 @@
-package com.ruoyi.system.service.impl;
-
-import com.ruoyi.system.mapper.PosOrderOmgPaymentMapper;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import java.util.Date;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.ArgumentMatchers.eq;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class PosOrderOmgPaymentServiceImplTest {
-
-    @Mock
-    private PosOrderOmgPaymentMapper paymentMapper;
-
-    @InjectMocks
-    private PosOrderOmgPaymentServiceImpl paymentService;
-
-    @Test
-    void markSuccessDoesNothingWithoutPaymentId() {
-        int updated = paymentService.markSuccess(null, "trade-no", "Credit_CreditCard", 1,
-                "ok", "123456", new Date(), new Date(), "{}");
-
-        assertEquals(0, updated);
-        verify(paymentMapper, never()).markSuccessIfUnpaid(any(), any(), any(), any(), any(),
-                any(), any(), any(), any(), any());
-    }
-
-    @Test
-    void markSuccessTruncatesGatewayMessageToDatabaseLimit() {
-        String longMessage = "x".repeat(250);
-        when(paymentMapper.markSuccessIfUnpaid(eq(1L), eq("trade-no"), eq("Credit_CreditCard"),
-                eq(1), eq("x".repeat(200)), eq("123456"), any(), any(), eq("{}"), any()))
-                .thenReturn(1);
-
-        int updated = paymentService.markSuccess(1L, "trade-no", "Credit_CreditCard", 1,
-                longMessage, "123456", new Date(), new Date(), "{}");
-
-        assertEquals(1, updated);
-    }
-
-    @Test
-    void markFailTruncatesGatewayMessageToDatabaseLimit() {
-        String longMessage = "y".repeat(250);
-        when(paymentMapper.markFailIfUnpaid(eq(2L), eq(0), eq("y".repeat(200)), eq("{}"), any()))
-                .thenReturn(1);
-
-        int updated = paymentService.markFail(2L, 0, longMessage, "{}");
-
-        assertEquals(1, updated);
-    }
-
-    @Test
-    void refundTransitionsUseCompareAndSetMapperMethods() {
-        when(paymentMapper.markRefundingIfPaid(eq(3L), any())).thenReturn(1);
-        when(paymentMapper.markRefundedIfRefunding(eq(3L), any())).thenReturn(1);
-        when(paymentMapper.restorePaidIfRefunding(eq(3L), any())).thenReturn(1);
-
-        assertEquals(1, paymentService.markRefunding(3L));
-        assertEquals(1, paymentService.markRefunded(3L));
-        assertEquals(1, paymentService.restorePaidFromRefunding(3L));
-    }
-}

+ 0 - 38
ruoyi-system/src/test/java/com/ruoyi/system/service/impl/PosOrderOmgRefundServiceImplTest.java

@@ -1,38 +0,0 @@
-package com.ruoyi.system.service.impl;
-
-import com.ruoyi.system.domain.PosOrderOmgRefund;
-import com.ruoyi.system.mapper.PosOrderOmgRefundMapper;
-import org.junit.jupiter.api.Test;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.mockito.ArgumentCaptor;
-import org.mockito.InjectMocks;
-import org.mockito.Mock;
-import org.mockito.junit.jupiter.MockitoExtension;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-import static org.mockito.ArgumentMatchers.any;
-import static org.mockito.Mockito.verify;
-import static org.mockito.Mockito.when;
-
-@ExtendWith(MockitoExtension.class)
-class PosOrderOmgRefundServiceImplTest {
-
-    @Mock
-    private PosOrderOmgRefundMapper refundMapper;
-
-    @InjectMocks
-    private PosOrderOmgRefundServiceImpl refundService;
-
-    @Test
-    void recordTruncatesGatewayMessageToDatabaseLimit() {
-        ArgumentCaptor<PosOrderOmgRefund> rowCaptor = ArgumentCaptor.forClass(PosOrderOmgRefund.class);
-        when(refundMapper.insert(any(PosOrderOmgRefund.class))).thenReturn(1);
-
-        int inserted = refundService.record(1L, "order-1", "trade-1", "R", 100,
-                0, "z".repeat(250), "{}");
-
-        assertEquals(1, inserted);
-        verify(refundMapper).insert(rowCaptor.capture());
-        assertEquals("z".repeat(200), rowCaptor.getValue().getRtnMsg());
-    }
-}