Kaynağa Gözat

合并 OMG 信用卡与 Apple Pay 双入口

qmj 1 hafta önce
ebeveyn
işleme
c8bc515655
21 değiştirilmiş dosya ile 297 ekleme ve 85 silme
  1. 6 2
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java
  2. 16 6
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java
  3. 1 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentErrorCode.java
  4. 6 3
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java
  5. 35 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentMethod.java
  6. 7 3
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentRetryService.java
  7. 9 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgCreatePaymentRequest.java
  8. 9 0
      ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/dto/OmgRetryPaymentRequest.java
  9. 1 0
      ruoyi-admin/src/main/resources/i18n/messages.properties
  10. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_en_US.properties
  11. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_vi.properties
  12. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_CN.properties
  13. 1 0
      ruoyi-admin/src/main/resources/i18n/messages_zh_TW.properties
  14. 30 7
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java
  15. 33 17
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java
  16. 56 27
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java
  17. 23 0
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentMethodTest.java
  18. 35 4
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentRetryControllerTest.java
  19. 21 11
      ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentRetryServiceTest.java
  20. 3 3
      specs/020-omg-payment-rebuild/plan.md
  21. 2 2
      specs/020-omg-payment-rebuild/tasks.md

+ 6 - 2
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentController.java

@@ -91,12 +91,14 @@ public class OmgPaymentController {
     public AjaxResult create(@RequestHeader(name = "token") String token,
                              @RequestBody(required = false) OmgCreatePaymentRequest request) {
         String orderId = request == null ? null : request.getOrderId();
+        String paymentMethodValue = request == null ? null : request.getPaymentMethod();
         String safeOrderId = safeLogOrderId(orderId);
         Long safeUserId = null;
         try {
             safeUserId = tokenUserResolver.requireUserId(token);
+            OmgPaymentMethod paymentMethod = OmgPaymentMethod.require(paymentMethodValue);
             log.info("OMG payment create started orderId={}, userId={}", safeOrderId, safeUserId);
-            OmgPaymentCreateOutcome outcome = createService.create(safeUserId, orderId);
+            OmgPaymentCreateOutcome outcome = createService.create(safeUserId, orderId, paymentMethod);
             log.info("OMG payment create succeeded orderId={}, userId={}, storeId={}, "
                             + "attemptId={}, amount={}, status=CREATED, merchantTradeNo={}",
                     safeLogOrderId(outcome.orderId()), outcome.userId(), outcome.storeId(), outcome.attemptId(),
@@ -143,12 +145,14 @@ public class OmgPaymentController {
     public AjaxResult retry(@RequestHeader(name = "token") String token,
                             @RequestBody(required = false) OmgRetryPaymentRequest request) {
         String orderId = request == null ? null : request.getOrderId();
+        String paymentMethodValue = request == null ? null : request.getPaymentMethod();
         String safeOrderId = safeLogOrderId(orderId);
         Long safeUserId = null;
         try {
             safeUserId = tokenUserResolver.requireUserId(token);
+            OmgPaymentMethod paymentMethod = OmgPaymentMethod.require(paymentMethodValue);
             log.info("OMG payment retry started orderId={}, userId={}", safeOrderId, safeUserId);
-            OmgPaymentCreateOutcome outcome = retryService.retry(safeUserId, orderId);
+            OmgPaymentCreateOutcome outcome = retryService.retry(safeUserId, orderId, paymentMethod);
             log.info("OMG payment retry succeeded orderId={}, userId={}, storeId={}, "
                             + "attemptId={}, amount={}, status=CREATED, merchantTradeNo={}",
                     safeLogOrderId(outcome.orderId()), outcome.userId(), outcome.storeId(), outcome.attemptId(),

+ 16 - 6
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentCreateService.java

@@ -35,7 +35,8 @@ public class OmgPaymentCreateService {
     }
 
     @Transactional(rollbackFor = Exception.class)
-    public OmgPaymentCreateOutcome create(Long userId, String orderId) {
+    public OmgPaymentCreateOutcome create(Long userId, String orderId, OmgPaymentMethod paymentMethod) {
+        requirePaymentMethod(paymentMethod);
         if (userId == null) {
             throw business(AUTH_REQUIRED);
         }
@@ -49,7 +50,7 @@ public class OmgPaymentCreateService {
         validateCredential(credential, order.getStoreId());
         log.info("OMG payment validation passed orderId={}, userId={}, storeId={}",
                 safeLogOrderId(order.getDdId()), userId, order.getStoreId());
-        return createWithBoundedTradeNumberRetries(order, userId, credential);
+        return createWithBoundedTradeNumberRetries(order, userId, credential, paymentMethod);
     }
 
     /**
@@ -58,7 +59,9 @@ public class OmgPaymentCreateService {
      */
     @Transactional(rollbackFor = Exception.class)
     public OmgPaymentCreateOutcome replaceActiveForRetry(Long userId, String orderId,
-                                                         String expectedMerchantTradeNo) {
+                                                         String expectedMerchantTradeNo,
+                                                         OmgPaymentMethod paymentMethod) {
+        requirePaymentMethod(paymentMethod);
         if (userId == null) {
             throw business(AUTH_REQUIRED);
         }
@@ -78,17 +81,18 @@ public class OmgPaymentCreateService {
         log.info("OMG payment retry replacing attempt orderId={}, userId={}, storeId={}, oldMerchantTradeNo={}",
                 safeLogOrderId(order.getDdId()), userId, order.getStoreId(),
                 maskMerchantTradeNo(active.getMerchantTradeNo()));
-        return createWithBoundedTradeNumberRetries(order, userId, credential);
+        return createWithBoundedTradeNumberRetries(order, userId, credential, paymentMethod);
     }
 
     private OmgPaymentCreateOutcome createWithBoundedTradeNumberRetries(
-            OmgPaymentOrderSnapshot order, Long userId, PosStoreOmg credential) {
+            OmgPaymentOrderSnapshot order, Long userId, PosStoreOmg credential,
+            OmgPaymentMethod paymentMethod) {
         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);
+                        credential.getHashKey(), credential.getHashIv(), merchantTradeNo, paymentMethod);
             } catch (IllegalArgumentException error) {
                 throw business(PAYMENT_CONFIGURATION_INVALID, order.getStoreId());
             }
@@ -123,6 +127,12 @@ public class OmgPaymentCreateService {
         return normalized;
     }
 
+    private static void requirePaymentMethod(OmgPaymentMethod paymentMethod) {
+        if (paymentMethod == null) {
+            throw business(PAYMENT_METHOD_INVALID);
+        }
+    }
+
     private static void validateOrder(Long userId, String orderId, OmgPaymentOrderSnapshot order) {
         if (order == null || !userId.equals(order.getUserId())) {
             throw business(ORDER_NOT_AVAILABLE);

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

@@ -9,6 +9,7 @@ public enum OmgPaymentErrorCode {
     ORDER_ALREADY_PAID("omg.pay.order.already.paid"),
     ORDER_AMOUNT_INVALID("omg.pay.order.amount.invalid"),
     PAYMENT_TYPE_INVALID("omg.pay.payment.type.invalid"),
+    PAYMENT_METHOD_INVALID("omg.pay.payment.method.invalid"),
     STORE_CREDENTIAL_UNAVAILABLE("omg.pay.credential.unavailable"),
     PAYMENT_ATTEMPT_EXISTS("omg.pay.attempt.exists"),
     PAYMENT_CONFIGURATION_INVALID("omg.pay.configuration.invalid"),

+ 6 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentFormFactory.java

@@ -34,7 +34,8 @@ public class OmgPaymentFormFactory {
     }
 
     public OmgPaymentForm create(String orderId, Integer amount, String merchantId,
-                                 String hashKey, String hashIv, String merchantTradeNo) {
+                                 String hashKey, String hashIv, String merchantTradeNo,
+                                 OmgPaymentMethod paymentMethod) {
         String safeOrderId = safeOrderReference(orderId);
         LinkedHashMap<String, String> fields = new LinkedHashMap<>();
         fields.put("MerchantID", merchantId);
@@ -46,8 +47,10 @@ public class OmgPaymentFormFactory {
         fields.put("ItemName", "Order " + safeOrderId);
         fields.put("ReturnURL", properties.requireSafeReturnUrl());
         fields.put("OrderResultURL", properties.requireSafeOrderResultUrl());
-        fields.put("ChoosePayment", "ALL");
-        fields.put("IgnorePayment", "ATM#CVS#BarcodeATM");
+        fields.put("ChoosePayment", paymentMethod.getChoosePayment());
+        if (paymentMethod.isUnionPayDisabled()) {
+            fields.put("UnionPay", "2");
+        }
         fields.put("EncryptType", "1");
         fields.put("InvoiceMark", "N");
         fields.put("NeedExtraPaidInfo", "Y");

+ 35 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentMethod.java

@@ -0,0 +1,35 @@
+package com.ruoyi.app.omgpay;
+
+/** Maps the App payment-method allowlist to one OMG hosted payment channel. */
+public enum OmgPaymentMethod {
+    CREDIT("Credit", true),
+    APPLE_PAY("ApplePay", false);
+
+    private final String choosePayment;
+    private final boolean unionPayDisabled;
+
+    OmgPaymentMethod(String choosePayment, boolean unionPayDisabled) {
+        this.choosePayment = choosePayment;
+        this.unionPayDisabled = unionPayDisabled;
+    }
+
+    public String getChoosePayment() {
+        return choosePayment;
+    }
+
+    public boolean isUnionPayDisabled() {
+        return unionPayDisabled;
+    }
+
+    public static OmgPaymentMethod require(String value) {
+        try {
+            return value == null ? invalid() : valueOf(value);
+        } catch (IllegalArgumentException error) {
+            return invalid();
+        }
+    }
+
+    private static OmgPaymentMethod invalid() {
+        throw new OmgPaymentBusinessException(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID);
+    }
+}

+ 7 - 3
ruoyi-admin/src/main/java/com/ruoyi/app/omgpay/OmgPaymentRetryService.java

@@ -4,6 +4,7 @@ import com.ruoyi.app.omgpay.dto.OmgQueryPaymentResponse;
 import org.springframework.stereotype.Service;
 
 import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.ORDER_ALREADY_PAID;
+import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.PAYMENT_METHOD_INVALID;
 import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.PAYMENT_RETRY_NOT_AVAILABLE;
 
 /** Verifies the gateway state before replacing an abandoned payment attempt. */
@@ -18,7 +19,10 @@ public class OmgPaymentRetryService {
         this.createService = createService;
     }
 
-    public OmgPaymentCreateOutcome retry(Long userId, String orderId) {
+    public OmgPaymentCreateOutcome retry(Long userId, String orderId, OmgPaymentMethod paymentMethod) {
+        if (paymentMethod == null) {
+            throw business(PAYMENT_METHOD_INVALID);
+        }
         OmgQueryPaymentResponse query = queryService.query(userId, orderId);
         if (query == null || query.status() == null) {
             throw business(PAYMENT_RETRY_NOT_AVAILABLE);
@@ -27,10 +31,10 @@ public class OmgPaymentRetryService {
             throw business(ORDER_ALREADY_PAID);
         }
         if ("FAILED".equals(query.status())) {
-            return createService.create(userId, orderId);
+            return createService.create(userId, orderId, paymentMethod);
         }
         if ("UNPAID".equals(query.status()) && isBlank(query.paymentType())) {
-            return createService.replaceActiveForRetry(userId, orderId, query.merchantTradeNo());
+            return createService.replaceActiveForRetry(userId, orderId, query.merchantTradeNo(), paymentMethod);
         }
         throw business(PAYMENT_RETRY_NOT_AVAILABLE);
     }

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

@@ -2,6 +2,7 @@ package com.ruoyi.app.omgpay.dto;
 
 public class OmgCreatePaymentRequest {
     private String orderId;
+    private String paymentMethod;
 
     public String getOrderId() {
         return orderId;
@@ -10,4 +11,12 @@ public class OmgCreatePaymentRequest {
     public void setOrderId(String orderId) {
         this.orderId = orderId;
     }
+
+    public String getPaymentMethod() {
+        return paymentMethod;
+    }
+
+    public void setPaymentMethod(String paymentMethod) {
+        this.paymentMethod = paymentMethod;
+    }
 }

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

@@ -2,6 +2,7 @@ package com.ruoyi.app.omgpay.dto;
 
 public class OmgRetryPaymentRequest {
     private String orderId;
+    private String paymentMethod;
 
     public String getOrderId() {
         return orderId;
@@ -10,4 +11,12 @@ public class OmgRetryPaymentRequest {
     public void setOrderId(String orderId) {
         this.orderId = orderId;
     }
+
+    public String getPaymentMethod() {
+        return paymentMethod;
+    }
+
+    public void setPaymentMethod(String paymentMethod) {
+        this.paymentMethod = paymentMethod;
+    }
 }

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

@@ -234,6 +234,7 @@ omg.pay.order.state.not.payable=当前订单状态不可支付
 omg.pay.order.already.paid=订单已支付或支付状态不可用
 omg.pay.order.amount.invalid=订单金额异常
 omg.pay.payment.type.invalid=订单支付方式不是 OMG
+omg.pay.payment.method.invalid=请选择信用卡或 Apple Pay
 omg.pay.credential.unavailable=该门店暂未启用 OMG 支付
 omg.pay.attempt.exists=该订单已有待处理的支付尝试
 omg.pay.configuration.invalid=OMG 支付配置无效

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

@@ -237,6 +237,7 @@ 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.payment.method.invalid=Please select Credit Card or Apple 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

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

@@ -237,6 +237,7 @@ omg.pay.order.state.not.payable=Trạng thái đơn hàng hiện tại không th
 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.payment.method.invalid=Vui lòng chọn Thẻ tín dụng hoặc Apple 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ệ

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

@@ -238,6 +238,7 @@ omg.pay.order.state.not.payable=当前订单状态不可支付
 omg.pay.order.already.paid=订单已支付或支付状态不可用
 omg.pay.order.amount.invalid=订单金额异常
 omg.pay.payment.type.invalid=订单支付方式不是 OMG
+omg.pay.payment.method.invalid=请选择信用卡或 Apple Pay
 omg.pay.credential.unavailable=该门店暂未启用 OMG 支付
 omg.pay.attempt.exists=该订单已有待处理的支付尝试
 omg.pay.configuration.invalid=OMG 支付配置无效

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

@@ -238,6 +238,7 @@ omg.pay.order.state.not.payable=目前訂單狀態不可支付
 omg.pay.order.already.paid=訂單已付款或付款狀態不可用
 omg.pay.order.amount.invalid=訂單金額異常
 omg.pay.payment.type.invalid=訂單付款方式不是 OMG
+omg.pay.payment.method.invalid=請選擇信用卡或 Apple Pay
 omg.pay.credential.unavailable=此門店尚未啟用 OMG 支付
 omg.pay.attempt.exists=此訂單已有待處理的支付嘗試
 omg.pay.configuration.invalid=OMG 支付設定無效

+ 30 - 7
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentControllerTest.java

@@ -19,6 +19,7 @@ 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.Field;
 import java.lang.reflect.Method;
 import java.util.Map;
 import java.util.Set;
@@ -49,8 +50,8 @@ class OmgPaymentControllerTest {
         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()));
+        assertEquals(Set.of("orderId", "paymentMethod"), java.util.Arrays.stream(OmgCreatePaymentRequest.class.getDeclaredFields())
+                .map(Field::getName).collect(Collectors.toSet()));
         assertTrue(java.util.Arrays.stream(create.getParameterTypes()).noneMatch(Map.class::isAssignableFrom));
     }
 
@@ -60,16 +61,18 @@ class OmgPaymentControllerTest {
         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(
+        when(service.create(5L, "DD-1\r\nforged=true", OmgPaymentMethod.CREDIT)).thenReturn(new OmgPaymentCreateOutcome(
                 response, 9L, "DD-1", 5L, 77L, 100, "OMG12***4567"));
         OmgPaymentController controller = controller(resolver, service);
         OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
         request.setOrderId("DD-1\r\nforged=true");
+        request.setPaymentMethod("CREDIT");
         ListAppender<ILoggingEvent> logs = captureLogs();
 
         AjaxResult result = controller.create("SECRET_TOKEN", request);
 
         assertSame(response, result.get("data"));
+        verify(service).create(5L, "DD-1\r\nforged=true", OmgPaymentMethod.CREDIT);
         String messages = logs.list.stream().map(ILoggingEvent::getFormattedMessage).collect(Collectors.joining("\n"));
         assertTrue(messages.contains("attemptId=9"));
         assertFalse(messages.contains("SECRET_TOKEN"));
@@ -84,15 +87,15 @@ class OmgPaymentControllerTest {
         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 = controller(resolver, service);
         AjaxResult business = controller.create("token", null);
-        assertEquals("ORDER_REQUIRED", ((OmgPaymentErrorResponse) business.get("data")).status());
+        assertEquals("PAYMENT_METHOD_INVALID", ((OmgPaymentErrorResponse) business.get("data")).status());
 
         RuntimeException failure = new RuntimeException("unexpected");
-        when(service.create(5L, "DD-2")).thenThrow(failure);
+        when(service.create(5L, "DD-2", OmgPaymentMethod.CREDIT)).thenThrow(failure);
         OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
         request.setOrderId("DD-2");
+        request.setPaymentMethod("CREDIT");
         ListAppender<ILoggingEvent> logs = captureLogs();
         AjaxResult unexpected = controller.create("token", request);
         assertEquals("PAYMENT_CREATION_FAILED", ((OmgPaymentErrorResponse) unexpected.get("data")).status());
@@ -106,9 +109,10 @@ class OmgPaymentControllerTest {
             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));
+            when(service.create(5L, "DD-1", OmgPaymentMethod.CREDIT)).thenThrow(new OmgPaymentBusinessException(code, 77L));
             OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
             request.setOrderId("DD-1");
+            request.setPaymentMethod("CREDIT");
 
             AjaxResult result = controller(resolver, service).create("token", request);
 
@@ -130,6 +134,25 @@ class OmgPaymentControllerTest {
         assertTrue(logs.list.stream().noneMatch(event -> event.getFormattedMessage().contains("NEVER_LOG_THIS")));
     }
 
+    @Test
+    void rejectsInvalidPaymentMethodsWithoutCallingCreateService() {
+        OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+        OmgPaymentCreateService service = mock(OmgPaymentCreateService.class);
+        when(resolver.requireUserId("token")).thenReturn(5L);
+        OmgPaymentController controller = controller(resolver, service);
+
+        for (String paymentMethod : new String[]{null, "ALL", "Credit", " CREDIT "}) {
+            OmgCreatePaymentRequest request = new OmgCreatePaymentRequest();
+            request.setOrderId("DD-1");
+            request.setPaymentMethod(paymentMethod);
+
+            AjaxResult result = controller.create("token", request);
+
+            assertEquals("PAYMENT_METHOD_INVALID", ((OmgPaymentErrorResponse) result.get("data")).status());
+        }
+        verifyNoInteractions(service);
+    }
+
     private static ListAppender<ILoggingEvent> captureLogs() {
         Logger logger = (Logger) LoggerFactory.getLogger(OmgPaymentController.class);
         ListAppender<ILoggingEvent> appender = new ListAppender<>();

+ 33 - 17
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentCreateServiceTest.java

@@ -44,7 +44,7 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.create(5L, "DD-1"));
+                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
 
         assertEquals(expected, error.getCode());
         verify(attempts, never()).createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
@@ -57,7 +57,7 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectOrderForUpdate("DD-1")).thenReturn(payableOrder());
         when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(new OmgPaymentAttempt());
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.create(5L, "DD-1"));
+                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
         assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
         verifyNoInteractions(generator, formFactory, credentials);
     }
@@ -74,12 +74,14 @@ class OmgPaymentCreateServiceTest {
         when(credentials.getEnabledCredential(77L)).thenReturn(credential());
         when(attempts.supersedeCreated(8L)).thenReturn(1);
         when(generator.generate()).thenReturn("OMG12345678901234567");
-        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentForm("stage", Map.of()));
         when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                 anyString(), anyString())).thenReturn(created);
 
-        OmgPaymentCreateOutcome outcome = service.replaceActiveForRetry(5L, "DD-1", "OMGOLD");
+        OmgPaymentCreateOutcome outcome = service.replaceActiveForRetry(5L, "DD-1", "OMGOLD",
+                OmgPaymentMethod.CREDIT);
 
         assertEquals(9L, outcome.attemptId());
         var orderOfCalls = inOrder(attempts, credentials, generator, formFactory);
@@ -89,7 +91,7 @@ class OmgPaymentCreateServiceTest {
         orderOfCalls.verify(attempts).supersedeCreated(8L);
         orderOfCalls.verify(generator).generate();
         orderOfCalls.verify(formFactory).create("DD-1", 100, "1000031", "KEY", "IV",
-                "OMG12345678901234567");
+                "OMG12345678901234567", OmgPaymentMethod.CREDIT);
         orderOfCalls.verify(attempts).createCreated("DD-1", "OMG12345678901234567", 77L,
                 "1000031", 100, "KEY", "IV");
     }
@@ -103,7 +105,7 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(current);
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.replaceActiveForRetry(5L, "DD-1", "OMGOLD"));
+                () -> service.replaceActiveForRetry(5L, "DD-1", "OMGOLD", OmgPaymentMethod.CREDIT));
 
         assertEquals(PAYMENT_RETRY_NOT_AVAILABLE, error.getCode());
         verifyNoInteractions(credentials, generator, formFactory);
@@ -119,12 +121,13 @@ class OmgPaymentCreateServiceTest {
         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()))
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentForm(OmgPaymentFormFactory.STAGE_GATEWAY_URL, Map.of("CheckMacValue", "A".repeat(64))));
         when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                 anyString(), anyString())).thenReturn(attempt);
 
-        OmgPaymentCreateOutcome outcome = service.create(5L, "DD-1");
+        OmgPaymentCreateOutcome outcome = service.create(5L, "DD-1", OmgPaymentMethod.CREDIT);
 
         assertEquals(9L, outcome.attemptId());
         assertEquals("CREATED", outcome.response().status());
@@ -136,7 +139,7 @@ class OmgPaymentCreateServiceTest {
         orderOfCalls.verify(credentials).getEnabledCredential(77L);
         orderOfCalls.verify(generator).generate();
         orderOfCalls.verify(formFactory).create("DD-1", 100, "1000031", "KEY", "IV",
-                "OMG12345678901234567");
+                "OMG12345678901234567", OmgPaymentMethod.CREDIT);
         orderOfCalls.verify(attempts).createCreated("DD-1", "OMG12345678901234567", 77L, "1000031", 100,
                 "KEY", "IV");
     }
@@ -146,7 +149,8 @@ class OmgPaymentCreateServiceTest {
         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()))
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentForm("stage", Map.of()));
         when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                 anyString(), anyString()))
@@ -154,7 +158,7 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectActiveCreatedByDdIdForUpdate("DD-1")).thenReturn(null, new OmgPaymentAttempt());
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.create(5L, "DD-1"));
+                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
         assertEquals(PAYMENT_ATTEMPT_EXISTS, error.getCode());
         verify(generator, times(1)).generate();
     }
@@ -164,7 +168,8 @@ class OmgPaymentCreateServiceTest {
         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()))
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentForm("stage", Map.of()));
         OmgPaymentAttempt inserted = new OmgPaymentAttempt();
         inserted.setId(11L);
@@ -174,9 +179,10 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectByMerchantTradeNoForUpdate("OMG11111111111111111"))
                 .thenReturn(new OmgPaymentAttempt());
 
-        assertEquals(11L, service.create(5L, "DD-1").attemptId());
+        assertEquals(11L, service.create(5L, "DD-1", OmgPaymentMethod.CREDIT).attemptId());
         verify(generator, times(2)).generate();
-        verify(formFactory, times(2)).create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString());
+        verify(formFactory, times(2)).create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                eq(OmgPaymentMethod.CREDIT));
     }
 
     @Test
@@ -185,7 +191,8 @@ class OmgPaymentCreateServiceTest {
         when(credentials.getEnabledCredential(77L)).thenReturn(credential());
         when(generator.generate()).thenReturn("OMG11111111111111111", "OMG22222222222222222",
                 "OMG33333333333333333");
-        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString()))
+        when(formFactory.create(anyString(), anyInt(), anyString(), anyString(), anyString(), anyString(),
+                any(OmgPaymentMethod.class)))
                 .thenReturn(new OmgPaymentForm("stage", Map.of()));
         when(attempts.createCreated(anyString(), anyString(), anyLong(), anyString(), anyInt(),
                 anyString(), anyString()))
@@ -193,7 +200,7 @@ class OmgPaymentCreateServiceTest {
         when(attempts.selectByMerchantTradeNoForUpdate(anyString())).thenReturn(new OmgPaymentAttempt());
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.create(5L, "DD-1"));
+                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
         assertEquals(PAYMENT_CREATION_FAILED, error.getCode());
         verify(generator, times(3)).generate();
     }
@@ -233,7 +240,7 @@ class OmgPaymentCreateServiceTest {
         when(credentials.getEnabledCredential(77L)).thenReturn(null);
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.create(5L, "DD-1"));
+                () -> service.create(5L, "DD-1", OmgPaymentMethod.CREDIT));
 
         assertEquals(STORE_CREDENTIAL_UNAVAILABLE, error.getCode());
         verifyNoInteractions(generator, formFactory);
@@ -241,6 +248,15 @@ class OmgPaymentCreateServiceTest {
                 anyString(), anyString());
     }
 
+    @Test
+    void rejectsNullPaymentMethodBeforeAnyDependencyInteraction() {
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.create(5L, "DD-1", null));
+
+        assertEquals(PAYMENT_METHOD_INVALID, error.getCode());
+        verifyNoInteractions(attempts, credentials, generator, formFactory);
+    }
+
     private static OmgPaymentOrderSnapshot payableOrder() {
         OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
         order.setDdId("DD-1");

+ 56 - 27
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentFormFactoryTest.java

@@ -3,11 +3,13 @@ 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 org.mockito.ArgumentCaptor;
 import org.springframework.context.annotation.AnnotationConfigApplicationContext;
 
 import java.time.Clock;
 import java.time.Instant;
 import java.time.ZoneOffset;
+import java.util.LinkedHashMap;
 import java.util.Map;
 import java.util.Set;
 
@@ -24,41 +26,68 @@ import static org.mockito.Mockito.when;
 
 class OmgPaymentFormFactoryTest {
     @Test
-    void buildsSignedFormThatUsesOmgPageForCreditAndApplePay() {
+    void buildsSignedCreditCardForm() {
         OmgPaymentProperties properties = properties();
         OmgCheckMacSigner signer = mock(OmgCheckMacSigner.class);
-        when(signer.sign(anyMap(), eq("KEY"), eq("IV"))).thenReturn("A".repeat(64));
+        String checkMacValue = "A".repeat(64);
+        when(signer.sign(anyMap(), eq("KEY"), eq("IV"))).thenReturn(checkMacValue);
         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");
+        OmgPaymentForm credit = factory.create("DD-<script>#|訂單", 100, "1000031", "KEY", "IV",
+                "OMG12345678901234567", OmgPaymentMethod.CREDIT);
 
-        assertEquals(OmgPaymentFormFactory.STAGE_GATEWAY_URL, form.gatewayUrl());
+        assertEquals(OmgPaymentFormFactory.STAGE_GATEWAY_URL, credit.gatewayUrl());
         assertEquals(Set.of("MerchantID", "MerchantTradeNo", "MerchantTradeDate", "PaymentType",
-                "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "ChoosePayment", "EncryptType",
-                "InvoiceMark", "NeedExtraPaidInfo", "IgnorePayment", "OrderResultURL",
-                "CheckMacValue"), form.fields().keySet());
-        assertEquals("2026/08/13 15:30:23", form.fields().get("MerchantTradeDate"));
-        assertEquals("ALL", form.fields().get("ChoosePayment"));
-        assertEquals("ATM#CVS#BarcodeATM", form.fields().get("IgnorePayment"));
-        assertFalse(form.fields().containsKey("UnionPay"));
-        assertEquals("Y", form.fields().get("NeedExtraPaidInfo"));
+                "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "OrderResultURL", "ChoosePayment",
+                "UnionPay", "EncryptType", "InvoiceMark", "NeedExtraPaidInfo", "CheckMacValue"),
+                credit.fields().keySet());
+        assertEquals("2026/08/13 15:30:23", credit.fields().get("MerchantTradeDate"));
+        assertEquals("Credit", credit.fields().get("ChoosePayment"));
+        assertEquals("2", credit.fields().get("UnionPay"));
+        assertFalse(credit.fields().containsKey("IgnorePayment"));
+        assertEquals("Foodie order DDscript", credit.fields().get("TradeDesc"));
+        assertFalse(credit.fields().get("ItemName").contains("|"));
+        assertTrue(credit.fields().get("ItemName").length() <= 120);
+        assertEquals("Y", credit.fields().get("NeedExtraPaidInfo"));
         assertEquals("https://foodieapi.waimai-paotui.com/pay/omg/result",
-                form.fields().get("OrderResultURL"));
-        assertFalse(form.fields().containsKey("ClientBackURL"));
-        assertFalse(form.fields().containsKey("ClientRedirectURL"));
-        assertFalse(form.fields().containsKey("ExpireDate"));
-        assertFalse(form.fields().containsKey("StoreExpireDate"));
-        assertFalse(form.fields().containsKey("BarcodeATMExpireDate"));
-        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() == 14
-                && "ALL".equals(fields.get("ChoosePayment"))
-                && "ATM#CVS#BarcodeATM".equals(fields.get("IgnorePayment"))
-                && !fields.containsKey("UnionPay")
-                && !fields.containsKey("CheckMacValue")), eq("KEY"), eq("IV"));
-        assertThrows(UnsupportedOperationException.class, () -> form.fields().put("extra", "value"));
+                credit.fields().get("OrderResultURL"));
+        assertEquals(checkMacValue, credit.fields().get("CheckMacValue"));
+        ArgumentCaptor<Map<String, String>> signedFields = ArgumentCaptor.forClass(Map.class);
+        verify(signer).sign(signedFields.capture(), eq("KEY"), eq("IV"));
+        assertEquals(14, signedFields.getValue().size());
+        Map<String, String> expectedSignedFields = new LinkedHashMap<>(credit.fields());
+        expectedSignedFields.remove("CheckMacValue");
+        assertEquals(expectedSignedFields, signedFields.getValue());
+        assertThrows(UnsupportedOperationException.class, () -> credit.fields().put("extra", "value"));
+    }
+
+    @Test
+    void buildsSignedApplePayForm() {
+        OmgPaymentProperties properties = properties();
+        OmgCheckMacSigner signer = mock(OmgCheckMacSigner.class);
+        String checkMacValue = "A".repeat(64);
+        when(signer.sign(anyMap(), eq("KEY"), eq("IV"))).thenReturn(checkMacValue);
+        OmgPaymentFormFactory factory = new OmgPaymentFormFactory(properties, signer,
+                Clock.fixed(Instant.parse("2026-08-13T07:30:23Z"), ZoneOffset.UTC));
+
+        OmgPaymentForm applePay = factory.create("DD-1", 100, "1000031", "KEY", "IV",
+                "OMG12345678901234567", OmgPaymentMethod.APPLE_PAY);
+
+        assertEquals(Set.of("MerchantID", "MerchantTradeNo", "MerchantTradeDate", "PaymentType",
+                "TotalAmount", "TradeDesc", "ItemName", "ReturnURL", "OrderResultURL", "ChoosePayment",
+                "EncryptType", "InvoiceMark", "NeedExtraPaidInfo", "CheckMacValue"),
+                applePay.fields().keySet());
+        assertEquals("ApplePay", applePay.fields().get("ChoosePayment"));
+        assertFalse(applePay.fields().containsKey("UnionPay"));
+        assertFalse(applePay.fields().containsKey("IgnorePayment"));
+        assertEquals(checkMacValue, applePay.fields().get("CheckMacValue"));
+        ArgumentCaptor<Map<String, String>> signedFields = ArgumentCaptor.forClass(Map.class);
+        verify(signer).sign(signedFields.capture(), eq("KEY"), eq("IV"));
+        assertEquals(13, signedFields.getValue().size());
+        Map<String, String> expectedSignedFields = new LinkedHashMap<>(applePay.fields());
+        expectedSignedFields.remove("CheckMacValue");
+        assertEquals(expectedSignedFields, signedFields.getValue());
     }
 
     @ParameterizedTest

+ 23 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentMethodTest.java

@@ -0,0 +1,23 @@
+package com.ruoyi.app.omgpay;
+
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class OmgPaymentMethodTest {
+    @Test
+    void requiresOnlyExactCreditOrApplePayValues() {
+        assertEquals(OmgPaymentMethod.CREDIT, OmgPaymentMethod.require("CREDIT"));
+        assertEquals(OmgPaymentMethod.APPLE_PAY, OmgPaymentMethod.require("APPLE_PAY"));
+        for (String invalid : new String[]{"", "credit", "Credit", "ALL", " CREDIT "}) {
+            OmgPaymentBusinessException error = assertThrows(
+                    OmgPaymentBusinessException.class,
+                    () -> OmgPaymentMethod.require(invalid));
+            assertEquals(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID, error.getCode());
+        }
+        assertEquals(OmgPaymentErrorCode.PAYMENT_METHOD_INVALID,
+                assertThrows(OmgPaymentBusinessException.class,
+                        () -> OmgPaymentMethod.require(null)).getCode());
+    }
+}

+ 35 - 4
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentRetryControllerTest.java

@@ -12,8 +12,11 @@ import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestHeader;
 
+import java.lang.reflect.Field;
 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.assertArrayEquals;
 import static org.junit.jupiter.api.Assertions.assertEquals;
@@ -26,22 +29,26 @@ import static org.mockito.Mockito.when;
 
 class OmgPaymentRetryControllerTest {
     @Test
-    void exposesAuthenticatedOrderIdOnlyRetryContractAndReturnsNewForm() throws Exception {
+    void exposesAuthenticatedRetryContractAndReturnsNewForm() throws Exception {
         Method method = OmgPaymentController.class.getDeclaredMethod(
                 "retry", String.class, OmgRetryPaymentRequest.class);
         assertArrayEquals(new String[]{"/retry"}, method.getAnnotation(PostMapping.class).value());
         assertEquals("token", method.getParameters()[0].getAnnotation(RequestHeader.class).name());
         assertNotNull(method.getParameters()[1].getAnnotation(RequestBody.class));
+        assertEquals(Set.of("orderId", "paymentMethod"),
+                java.util.Arrays.stream(OmgRetryPaymentRequest.class.getDeclaredFields())
+                        .map(Field::getName).collect(Collectors.toSet()));
 
         OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
         OmgPaymentRetryService retryService = mock(OmgPaymentRetryService.class);
         OmgCreatePaymentResponse response = new OmgCreatePaymentResponse(
                 "CREATED", "https://stage.example", Map.of("MerchantTradeNo", "OMGNEW"));
         when(resolver.requireUserId("token")).thenReturn(5L);
-        when(retryService.retry(5L, "DD-1")).thenReturn(new OmgPaymentCreateOutcome(
+        when(retryService.retry(5L, "DD-1", OmgPaymentMethod.APPLE_PAY)).thenReturn(new OmgPaymentCreateOutcome(
                 response, 9L, "DD-1", 5L, 77L, 100, "OMGNE***0001"));
         OmgRetryPaymentRequest request = new OmgRetryPaymentRequest();
         request.setOrderId("DD-1");
+        request.setPaymentMethod("APPLE_PAY");
 
         AjaxResult result;
         try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
@@ -50,6 +57,7 @@ class OmgPaymentRetryControllerTest {
         }
 
         assertSame(response, result.get("data"));
+        org.mockito.Mockito.verify(retryService).retry(5L, "DD-1", OmgPaymentMethod.APPLE_PAY);
     }
 
     @Test
@@ -59,10 +67,11 @@ class OmgPaymentRetryControllerTest {
             OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
             OmgPaymentRetryService retryService = mock(OmgPaymentRetryService.class);
             when(resolver.requireUserId("token")).thenReturn(5L);
-            when(retryService.retry(5L, "DD-1")).thenThrow(
+            when(retryService.retry(5L, "DD-1", OmgPaymentMethod.CREDIT)).thenThrow(
                     new OmgPaymentBusinessException(OmgPaymentErrorCode.PAYMENT_RETRY_NOT_AVAILABLE));
             OmgRetryPaymentRequest request = new OmgRetryPaymentRequest();
             request.setOrderId("DD-1");
+            request.setPaymentMethod("CREDIT");
             OmgPaymentController controller = controller(resolver, retryService);
 
             AjaxResult business = controller.retry("token", request);
@@ -70,13 +79,35 @@ class OmgPaymentRetryControllerTest {
                     ((OmgPaymentErrorResponse) business.get("data")).status());
 
             org.mockito.Mockito.doThrow(new IllegalStateException("db"))
-                    .when(retryService).retry(5L, "DD-1");
+                    .when(retryService).retry(5L, "DD-1", OmgPaymentMethod.CREDIT);
             AjaxResult unexpected = controller.retry("token", request);
             assertEquals("PAYMENT_RETRY_FAILED",
                     ((OmgPaymentErrorResponse) unexpected.get("data")).status());
         }
     }
 
+    @Test
+    void rejectsInvalidPaymentMethodsWithoutCallingRetryService() {
+        try (MockedStatic<MessageUtils> messages = mockStatic(MessageUtils.class)) {
+            messages.when(() -> MessageUtils.message(anyString())).thenAnswer(call -> call.getArgument(0));
+            OmgPaymentTokenUserResolver resolver = mock(OmgPaymentTokenUserResolver.class);
+            OmgPaymentRetryService retryService = mock(OmgPaymentRetryService.class);
+            when(resolver.requireUserId("token")).thenReturn(5L);
+            OmgPaymentController controller = controller(resolver, retryService);
+
+            for (String paymentMethod : new String[]{null, "ALL", "Credit", " CREDIT "}) {
+                OmgRetryPaymentRequest request = new OmgRetryPaymentRequest();
+                request.setOrderId("DD-1");
+                request.setPaymentMethod(paymentMethod);
+
+                AjaxResult result = controller.retry("token", request);
+
+                assertEquals("PAYMENT_METHOD_INVALID", ((OmgPaymentErrorResponse) result.get("data")).status());
+            }
+            org.mockito.Mockito.verifyNoInteractions(retryService);
+        }
+    }
+
     private static OmgPaymentController controller(OmgPaymentTokenUserResolver resolver,
                                                     OmgPaymentRetryService retryService) {
         return new OmgPaymentController(resolver, mock(OmgPaymentCreateService.class),

+ 21 - 11
ruoyi-admin/src/test/java/com/ruoyi/app/omgpay/OmgPaymentRetryServiceTest.java

@@ -8,6 +8,7 @@ import org.junit.jupiter.api.Test;
 import java.util.Map;
 
 import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.ORDER_ALREADY_PAID;
+import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.PAYMENT_METHOD_INVALID;
 import static com.ruoyi.app.omgpay.OmgPaymentErrorCode.PAYMENT_RETRY_NOT_AVAILABLE;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertSame;
@@ -34,26 +35,26 @@ class OmgPaymentRetryServiceTest {
     void replacesVerifiedUnpaidAttemptWithoutSelectedPaymentType() {
         OmgPaymentCreateOutcome expected = outcome();
         when(queryService.query(5L, "DD-1")).thenReturn(query("UNPAID", "0", null));
-        when(createService.replaceActiveForRetry(5L, "DD-1", "OMGOLD"))
+        when(createService.replaceActiveForRetry(5L, "DD-1", "OMGOLD", OmgPaymentMethod.CREDIT))
                 .thenReturn(expected);
 
-        OmgPaymentCreateOutcome actual = service.retry(5L, "DD-1");
+        OmgPaymentCreateOutcome actual = service.retry(5L, "DD-1", OmgPaymentMethod.CREDIT);
 
         assertSame(expected, actual);
-        verify(createService).replaceActiveForRetry(5L, "DD-1", "OMGOLD");
-        verify(createService, never()).create(5L, "DD-1");
+        verify(createService).replaceActiveForRetry(5L, "DD-1", "OMGOLD", OmgPaymentMethod.CREDIT);
+        verify(createService, never()).create(5L, "DD-1", OmgPaymentMethod.CREDIT);
     }
 
     @Test
     void createsFreshAttemptAfterVerifiedGatewayFailureClosedOldAttempt() {
         OmgPaymentCreateOutcome expected = outcome();
         when(queryService.query(5L, "DD-1")).thenReturn(query("FAILED", "10200095", null));
-        when(createService.create(5L, "DD-1")).thenReturn(expected);
+        when(createService.create(5L, "DD-1", OmgPaymentMethod.APPLE_PAY)).thenReturn(expected);
 
-        assertSame(expected, service.retry(5L, "DD-1"));
+        assertSame(expected, service.retry(5L, "DD-1", OmgPaymentMethod.APPLE_PAY));
 
-        verify(createService).create(5L, "DD-1");
-        verify(createService, never()).replaceActiveForRetry(5L, "DD-1", "OMGOLD");
+        verify(createService).create(5L, "DD-1", OmgPaymentMethod.APPLE_PAY);
+        verify(createService, never()).replaceActiveForRetry(5L, "DD-1", "OMGOLD", OmgPaymentMethod.APPLE_PAY);
     }
 
     @Test
@@ -61,7 +62,7 @@ class OmgPaymentRetryServiceTest {
         when(queryService.query(5L, "DD-1")).thenReturn(query("PAID", "1", "Credit_CreditCard"));
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.retry(5L, "DD-1"));
+                () -> service.retry(5L, "DD-1", OmgPaymentMethod.CREDIT));
 
         assertEquals(ORDER_ALREADY_PAID, error.getCode());
         verifyNoInteractions(createService);
@@ -72,7 +73,7 @@ class OmgPaymentRetryServiceTest {
         when(queryService.query(5L, "DD-1")).thenReturn(query("UNPAID", "0", "Credit_CreditCard"));
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.retry(5L, "DD-1"));
+                () -> service.retry(5L, "DD-1", OmgPaymentMethod.CREDIT));
 
         assertEquals(PAYMENT_RETRY_NOT_AVAILABLE, error.getCode());
         verifyNoInteractions(createService);
@@ -83,12 +84,21 @@ class OmgPaymentRetryServiceTest {
         when(queryService.query(5L, "DD-1")).thenReturn(query("UNKNOWN", "7", null));
 
         OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
-                () -> service.retry(5L, "DD-1"));
+                () -> service.retry(5L, "DD-1", OmgPaymentMethod.CREDIT));
 
         assertEquals(PAYMENT_RETRY_NOT_AVAILABLE, error.getCode());
         verifyNoInteractions(createService);
     }
 
+    @Test
+    void rejectsNullPaymentMethodBeforeQuery() {
+        OmgPaymentBusinessException error = assertThrows(OmgPaymentBusinessException.class,
+                () -> service.retry(5L, "DD-1", null));
+
+        assertEquals(PAYMENT_METHOD_INVALID, error.getCode());
+        verifyNoInteractions(queryService, createService);
+    }
+
     private static OmgQueryPaymentResponse query(String status, String tradeStatus, String paymentType) {
         return new OmgQueryPaymentResponse(status, tradeStatus, "OMGOLD", "GW1", 100,
                 null, "2026/08/14 11:17:01", paymentType, "0", "0");

+ 3 - 3
specs/020-omg-payment-rebuild/plan.md

@@ -1242,7 +1242,7 @@ mvn -pl ruoyi-admin -am -DskipTests package
 - Modify: `ruoyi-admin/src/main/resources/i18n/messages.properties`、`messages_zh_CN.properties`、`messages_zh_TW.properties`、`messages_en_US.properties`、`messages_vi.properties`:提供同一错误 key 的五套文案。
 - Modify: `specs/020-omg-payment-rebuild/tasks.md`:实现提交后只更新 T105/T106,统一验证和 App 任务保持未完成。
 
-### Task 1: create/retry 支付渠道白名单服务端批次
+### Task 18: create/retry 支付渠道白名单服务端批次
 
 **Interfaces:**
 
@@ -1457,11 +1457,11 @@ git commit -m "实现 OMG 信用卡与 Apple Pay 双入口"
 
 提交后状态只能报告为“已提交”,不能报告“已验证”。
 
-### Task 2: 全部 OMG 功能完成后的统一验证
+### Task 19: 全部 OMG 功能完成后的统一验证
 
 **Interfaces:**
 
-- Consumes: Task 1 的独立实现提交与项目现有 OMG 全部功能。
+- Consumes: Task 18 的独立实现提交与项目现有 OMG 全部功能。
 - Produces: 定向测试、完整 OMG 回归、模块构建和 stage 双渠道人工证据。
 
 - [ ] **Step 1: 使用 JDK 21 运行渠道定向测试**

+ 2 - 2
specs/020-omg-payment-rebuild/tasks.md

@@ -173,8 +173,8 @@
 ## Phase 18: App 信用卡与 Apple Pay 双入口
 
 - [x] T104 [US1] 更新现有规格、研究结论、实施计划、API 契约、快速验收与 App 交接文档,以本阶段设计取代 `ALL + IgnorePayment` 方案
-- [ ] T105 [US1] 在同一批次补充 create/retry DTO、Controller、Service 与表单工厂测试源码,覆盖 `CREDIT/APPLE_PAY`、非法枚举无副作用、动态字段集合和完整签名输入
-- [ ] T106 [US1] 为 create/retry 增加受控 `paymentMethod`,统一映射 `CREDIT -> Credit + UnionPay=2`、`APPLE_PAY -> ApplePay`,新增五语言 `PAYMENT_METHOD_INVALID`,禁止客户端 OMG 原始参数
+- [x] T105 [US1] 在同一批次补充 create/retry DTO、Controller、Service 与表单工厂测试源码,覆盖 `CREDIT/APPLE_PAY`、非法枚举无副作用、动态字段集合和完整签名输入
+- [x] T106 [US1] 为 create/retry 增加受控 `paymentMethod`,统一映射 `CREDIT -> Credit + UnionPay=2`、`APPLE_PAY -> ApplePay`,新增五语言 `PAYMENT_METHOD_INVALID`,禁止客户端 OMG 原始参数
 - [ ] T107 [US1] 在用户端 App 增加信用卡/Apple Pay i18n 选择,并让首次支付与重新支付分别提交本次选择;当前工作区未包含 App 源码,需提供用户端仓库后实施
 - [ ] T108 [US1] 全部 OMG 计划功能调整完成后,使用 JDK 21 统一运行渠道定向测试、OMG 回归和模块构建;核对最终 diff、暂存范围及无 SQL 变更
 - [ ] T109 [US1] 在 OMG stage 分别验收信用卡和 Apple Pay 直接流程,确认不出现超商快付、AFTEE、银联或其他渠道选择项