| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318 |
- package com.ruoyi.app.omgpay;
- import com.ruoyi.app.order.DeliveryOrderNotificationService;
- import com.ruoyi.app.push.BusinessPushService;
- import com.ruoyi.app.push.RecipientApp;
- import com.ruoyi.app.omgpay.dto.OmgNotifyField;
- import com.ruoyi.app.omgpay.dto.OmgNotifyRequest;
- import com.ruoyi.app.utils.event.PushEventService;
- import com.ruoyi.common.utils.spring.SpringUtils;
- import com.ruoyi.system.domain.InfoUser;
- import com.ruoyi.system.domain.PosOrder;
- 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.IInfoUserService;
- import com.ruoyi.system.service.IPosOrderService;
- import org.junit.jupiter.api.AfterAll;
- import org.junit.jupiter.api.BeforeAll;
- import org.junit.jupiter.api.BeforeEach;
- import org.junit.jupiter.api.Test;
- import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
- import org.springframework.beans.factory.support.DefaultListableBeanFactory;
- import org.springframework.context.support.StaticMessageSource;
- import org.springframework.test.util.ReflectionTestUtils;
- import java.util.ArrayList;
- import java.math.BigDecimal;
- import java.util.LinkedHashMap;
- import java.util.List;
- import java.util.Locale;
- import java.util.Map;
- import static org.junit.jupiter.api.Assertions.*;
- import static org.mockito.ArgumentMatchers.any;
- import static org.mockito.ArgumentMatchers.anyString;
- import static org.mockito.ArgumentMatchers.eq;
- import static org.mockito.Mockito.*;
- class OmgPaymentNotifyServiceTest {
- private static final String HASH_KEY = "5294y06JbISpM5x9";
- private static final String HASH_IV = "v77hoKGq4kWxNNIS";
- private static ConfigurableListableBeanFactory originalBeanFactory;
- private IOmgPaymentAttemptService attempts;
- private OmgPaymentNotifyService service;
- @BeforeAll
- static void initializeBeanFactory() {
- // 支付成功推送按收件人用户语言解析文案(zh_TW 为无上下文时的默认语言),纯单测下需手动挂 messageSource
- originalBeanFactory = (ConfigurableListableBeanFactory)
- ReflectionTestUtils.getField(SpringUtils.class, "beanFactory");
- DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
- StaticMessageSource messageSource = new StaticMessageSource();
- Locale defaultLocale = new Locale("zh", "TW");
- messageSource.addMessage("no.message.push.message", defaultLocale, "消息");
- messageSource.addMessage("no.message.push.payment.success", defaultLocale, "支付成功");
- beanFactory.registerSingleton("messageSource", messageSource);
- new SpringUtils().postProcessBeanFactory(beanFactory);
- }
- @AfterAll
- static void restoreBeanFactory() {
- new SpringUtils().postProcessBeanFactory(originalBeanFactory);
- }
- private OmgPaymentAttempt attempt;
- @BeforeEach
- void setUp() {
- attempts = mock(IOmgPaymentAttemptService.class);
- service = new OmgPaymentNotifyService(attempts, new OmgCheckMacSigner());
- attempt = new OmgPaymentAttempt();
- attempt.setId(7L);
- attempt.setDdId("DD-1");
- attempt.setMerchantTradeNo("OMG123");
- attempt.setMerchantId("1000031");
- attempt.setAmount(100);
- attempt.setHashKeySnapshot(HASH_KEY);
- attempt.setHashIvSnapshot(HASH_IV);
- attempt.setAttemptStatus(0);
- when(attempts.selectByMerchantTradeNo("OMG123")).thenReturn(attempt);
- when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
- }
- @Test
- void successfulCallbackUsesSnapshotAndMarksOnlyPaymentFacts() {
- OmgPaymentOrderSnapshot order = order(4L, 0L);
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- assertTrue(service.process(signedRequest(1, 0, 100)));
- var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
- verify(attempts).markPaid(facts.capture());
- assertEquals("GW123", facts.getValue().getTradeNo());
- assertEquals(1, facts.getValue().getRtnCode());
- assertEquals(0, facts.getValue().getSimulatePaid());
- verify(attempts).supersedeOtherCreated("DD-1", 7L);
- verify(attempts).markOrderPaid("DD-1");
- }
- @Test
- void simulatedSuccessOnCancelledOrderIsStillPaid() {
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(4L, 0L));
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- when(attempts.countOtherPaidAttempts("DD-1", 7L)).thenReturn(1);
- assertTrue(service.process(signedRequest(1, 1, 100)));
- var facts = org.mockito.ArgumentCaptor.forClass(OmgPaymentAttempt.class);
- verify(attempts).markPaid(facts.capture());
- assertEquals(1, facts.getValue().getSimulatePaid());
- }
- @Test
- void failureMarksAttemptFailedWithoutChangingOrder() {
- when(attempts.markFailed(any())).thenReturn(1);
- assertTrue(service.process(signedRequest(10200095, 0, 100)));
- verify(attempts).markFailed(any());
- verify(attempts, never()).markOrderPaid(anyString());
- }
- @Test
- void supersededAttemptAcceptsFailureWithoutLosingSupersededState() {
- attempt.setAttemptStatus(3);
- assertTrue(service.process(signedRequest(10200095, 0, 100)));
- verify(attempts, never()).markFailed(any());
- verify(attempts, never()).markOrderPaid(anyString());
- }
- @Test
- void paidAttemptCannotBeDowngradedByLateFailure() {
- attempt.setAttemptStatus(1);
- assertTrue(service.process(signedRequest(10200095, 0, 100)));
- verify(attempts, never()).markFailed(any());
- verify(attempts, never()).markOrderPaid(anyString());
- }
- @Test
- void failedAttemptMayLaterBecomePaid() {
- attempt.setAttemptStatus(2);
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- assertTrue(service.process(signedRequest(1, 0, 100)));
- verify(attempts).markPaid(any());
- verify(attempts).markOrderPaid("DD-1");
- }
- @Test
- void paidDeliveryOrderIsOpenedToRidersAfterOmgSettlement() {
- IPosOrderService orders = mock(IPosOrderService.class);
- DeliveryOrderNotificationService notifications = mock(DeliveryOrderNotificationService.class);
- ReflectionTestUtils.setField(service, "posOrderService", orders);
- ReflectionTestUtils.setField(service, "deliveryOrderNotificationService", notifications);
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- PosOrder paidOrder = new PosOrder();
- paidOrder.setDdId("DD-1");
- paidOrder.setType(0L);
- paidOrder.setState(0L);
- paidOrder.setDeliveryStatus(0L);
- paidOrder.setAfterSaleStatus(0L);
- when(orders.getOne(any())).thenReturn(paidOrder);
- assertTrue(service.process(signedRequest(1, 0, 100)));
- assertEquals(1L, paidOrder.getPayStatus());
- verify(notifications).notifyOrderAvailable(paidOrder);
- }
- @Test
- void firstPaidCallbackPushesPaymentSuccessToOrderUser() {
- IInfoUserService users = mock(IInfoUserService.class);
- PushEventService pushEvents = mock(PushEventService.class);
- BusinessPushService push = mock(BusinessPushService.class);
- ReflectionTestUtils.setField(service, "infoUserService", users);
- ReflectionTestUtils.setField(service, "pushEventService", pushEvents);
- ReflectionTestUtils.setField(service, "businessPushService", push);
- OmgPaymentOrderSnapshot order = order(0L, 0L);
- order.setUserId(9L);
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order);
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- InfoUser user = new InfoUser();
- user.setUserId(9L); // cid 为空时仍需走统一路由,由当前模式决定能否外发
- user.setCidType("ios");
- user.setDeviceToken("native-token");
- when(users.getById(9L)).thenReturn(user);
- assertTrue(service.process(signedRequest(1, 0, 100)));
- verify(pushEvents).PublisherEvent(eq(9L), anyString(), anyString(), anyString());
- verify(push).send(eq(RecipientApp.USER), eq(9L), eq(null), anyString(), anyString(), anyString());
- }
- @Test
- void duplicatePaidCallbackDoesNotPushPaymentSuccessAgain() {
- IInfoUserService users = mock(IInfoUserService.class);
- PushEventService pushEvents = mock(PushEventService.class);
- ReflectionTestUtils.setField(service, "infoUserService", users);
- ReflectionTestUtils.setField(service, "pushEventService", pushEvents);
- attempt.setAttemptStatus(1); // 已支付 attempt:重复回调走 duplicate 分支
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 1L));
- InfoUser user = new InfoUser();
- user.setUserId(9L);
- when(users.getById(9L)).thenReturn(user);
- assertTrue(service.process(signedRequest(1, 0, 100)));
- verify(pushEvents, never()).PublisherEvent(any(), anyString(), anyString(), anyString());
- }
- @Test
- void rejectsSignatureMerchantAndAmountMismatchWithoutMutation() {
- OmgNotifyRequest badSignature = signedRequest(1, 0, 100);
- List<OmgNotifyField> changed = new ArrayList<>(badSignature.getFields());
- changed.set(changed.size() - 1, new OmgNotifyField("CheckMacValue", "0".repeat(64)));
- assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", changed, null)));
- assertFalse(service.process(signedRequest(1, 0, 101)));
- assertFalse(service.process(signedRequest(1, 0, 100, "OTHER")));
- verify(attempts, never()).markPaid(any());
- verify(attempts, never()).markFailed(any());
- }
- @Test
- void validExtraFieldIsRequiredBySignatureVerification() {
- OmgNotifyRequest signed = signedRequest(1, 0, 100);
- List<OmgNotifyField> fieldsWithoutExtra = signed.getFields().stream()
- .filter(field -> !"FutureEmptyField".equals(field.name())).toList();
- assertFalse(service.process(new OmgNotifyRequest("127.0.0.1", "raw", fieldsWithoutExtra, null)));
- verify(attempts, never()).markPaid(any());
- }
- @Test
- void verifiedPaidQueryUsesTheSameIrreversibleSettlementAsCallback() {
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 0L));
- when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
- when(attempts.markPaid(any())).thenReturn(1);
- when(attempts.markOrderPaid("DD-1")).thenReturn(1);
- OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
- OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 1,
- "TradeStatus=1", "GW123", "Credit_CreditCard", null, null,
- new BigDecimal("0.00"), null);
- assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));
- verify(attempts).markPaid(any());
- verify(attempts).supersedeOtherCreated("DD-1", 7L);
- verify(attempts).markOrderPaid("DD-1");
- }
- @Test
- void failedQueryCannotDowngradeAnAlreadyPaidAttempt() {
- attempt.setAttemptStatus(1);
- when(attempts.selectOrderForUpdate("DD-1")).thenReturn(order(0L, 1L));
- when(attempts.selectByMerchantTradeNoForUpdate("OMG123")).thenReturn(attempt);
- OmgPaymentGatewayFacts facts = new OmgPaymentGatewayFacts(
- OmgPaymentFactSource.QUERY, "1000031", "OMG123", 100, 10200095,
- "TradeStatus=10200095", null, null, null, null,
- new BigDecimal("0.00"), null);
- assertEquals(OmgPaymentSettlementResult.PAID, service.synchronizeVerifiedQuery(facts));
- verify(attempts, never()).markFailed(any());
- }
- private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount) {
- return signedRequest(rtnCode, simulatePaid, amount, "1000031");
- }
- private OmgNotifyRequest signedRequest(int rtnCode, int simulatePaid, int amount, String merchantId) {
- Map<String, String> fields = new LinkedHashMap<>();
- fields.put("MerchantID", merchantId);
- fields.put("MerchantTradeNo", "OMG123");
- fields.put("StoreID", "");
- fields.put("RtnCode", String.valueOf(rtnCode));
- fields.put("RtnMsg", rtnCode == 1 ? "Succeeded" : "Failed");
- fields.put("TradeNo", "GW123");
- fields.put("TradeAmt", String.valueOf(amount));
- fields.put("PaymentDate", rtnCode == 1 ? "2026/08/13 12:00:00" : "");
- fields.put("PaymentType", "Credit_CreditCard");
- fields.put("PaymentTypeChargeFee", "0");
- fields.put("TradeDate", "2026/08/13 11:59:00");
- fields.put("SimulatePaid", String.valueOf(simulatePaid));
- fields.put("CustomField1", "");
- fields.put("CustomField2", "");
- fields.put("CustomField3", "");
- fields.put("CustomField4", "");
- fields.put("FutureEmptyField", "");
- fields.put("CheckMacValue", new OmgCheckMacSigner().sign(fields, HASH_KEY, HASH_IV));
- List<OmgNotifyField> decoded = fields.entrySet().stream()
- .map(entry -> new OmgNotifyField(entry.getKey(), entry.getValue())).toList();
- return new OmgNotifyRequest("127.0.0.1", "raw", decoded, null);
- }
- private static OmgPaymentOrderSnapshot order(long state, long payStatus) {
- OmgPaymentOrderSnapshot order = new OmgPaymentOrderSnapshot();
- order.setDdId("DD-1");
- order.setState(state);
- order.setPayStatus(payStatus);
- return order;
- }
- }
|