Просмотр исходного кода

feat(push): route business notifications by channel

qmj 2 недель назад
Родитель
Сommit
a18a3f257f

+ 96 - 0
ruoyi-admin/src/main/java/com/ruoyi/app/push/BusinessPushService.java

@@ -0,0 +1,96 @@
+package com.ruoyi.app.push;
+
+import com.ruoyi.app.utils.PayPush;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.service.IInfoUserService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.Locale;
+
+@Service
+public class BusinessPushService {
+    private static final Logger log = LoggerFactory.getLogger(BusinessPushService.class);
+
+    private final BusinessPushProperties properties;
+    private final IInfoUserService users;
+    private final FcmPushSender fcm;
+    private final ApnsPushSender apns;
+    private final PayPush uni;
+
+    public BusinessPushService(BusinessPushProperties properties, IInfoUserService users,
+                               FcmPushSender fcm, ApnsPushSender apns, PayPush uni) {
+        this.properties = properties;
+        this.users = users;
+        this.fcm = fcm;
+        this.apns = apns;
+        this.uni = uni;
+    }
+
+    public void send(RecipientApp app, Long userId, String cid,
+                     String title, String content, String payload) {
+        try {
+            if (properties.getMode() == BusinessPushProperties.Mode.UNI) {
+                if (cid == null || cid.isBlank()) {
+                    log.warn("Uni push address missing, app={}, userId={}", app, userId);
+                    return;
+                }
+                sendUni(app, cid, title, content, payload);
+                return;
+            }
+            InfoUser current = userId == null ? null : users.getById(userId);
+            String platform = platform(current);
+            if (platform == null || current.getDeviceToken() == null || current.getDeviceToken().isBlank()) {
+                log.warn("Native push address missing or platform invalid, app={}, userId={}", app, userId);
+                return;
+            }
+            if ("android".equals(platform)) {
+                fcm.send(app, current.getDeviceToken(), title, content, payload);
+            } else {
+                apns.send(app, current.getDeviceToken(), title, content, payload);
+            }
+        } catch (RuntimeException exception) {
+            log.warn("Business push failed, app={}, userId={}, mode={}, category={}",
+                    app, userId, properties.getMode(), exception.getClass().getSimpleName());
+        }
+    }
+
+    public String addressKey(InfoUser recipient) {
+        if (recipient == null) {
+            return null;
+        }
+        if (properties.getMode() == BusinessPushProperties.Mode.UNI) {
+            String cid = recipient.getCid();
+            return cid == null || cid.isBlank() ? null : cid;
+        }
+        try {
+            InfoUser current = recipient.getUserId() == null ? null : users.getById(recipient.getUserId());
+            String platform = platform(current);
+            if (platform == null || current.getDeviceToken() == null || current.getDeviceToken().isBlank()) {
+                return null;
+            }
+            return platform + ":" + current.getDeviceToken();
+        } catch (RuntimeException exception) {
+            log.warn("Cannot resolve push address, userId={}, category={}",
+                    recipient.getUserId(), exception.getClass().getSimpleName());
+            return null;
+        }
+    }
+
+    private void sendUni(RecipientApp app, String cid, String title, String content, String payload) {
+        switch (app) {
+            case USER -> uni.apppush(cid, title, content, payload);
+            case MERCHANT -> uni.shpush(cid, title, content, payload);
+            case RIDER -> uni.qspush(cid, title, content, payload);
+        }
+    }
+
+    private static String platform(InfoUser user) {
+        if (user == null || user.getCidType() == null) {
+            return null;
+        }
+        String normalized = user.getCidType().trim().toLowerCase(Locale.ROOT);
+        return "android".equals(normalized) || "ios".equals(normalized) ? normalized : null;
+    }
+}

+ 16 - 19
ruoyi-admin/src/main/java/com/ruoyi/app/utils/PayPush.java

@@ -4,6 +4,8 @@ import cn.hutool.http.HttpRequest;
 import cn.hutool.http.HttpUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.ruoyi.app.push.BusinessPushService;
+import com.ruoyi.app.push.RecipientApp;
 import com.ruoyi.app.utils.event.PushEventService;
 import com.ruoyi.common.utils.LocaleUtils;
 import com.ruoyi.common.utils.MessageUtils;
@@ -12,10 +14,12 @@ import com.ruoyi.common.utils.spring.SpringUtils;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.stereotype.Component;
 
 import java.util.List;
 import java.util.Locale;
 
+@Component
 public class PayPush {
     private static final Logger log = LoggerFactory.getLogger(PayPush.class);
 
@@ -108,14 +112,7 @@ public class PayPush {
         if (pushEventService != null) {
             pushEventService.PublisherEvent(userId, title, content, body);
         }
-        if (cid == null || cid.trim().isEmpty()) {
-            log.error("商家推送失败:Device token is empty, userId:{}", userId);
-            return;
-        }
-
-        push.shpush(cid, title, content, body);
-
-
+        sendBusiness(RecipientApp.MERCHANT, userId, cid, title, content, body);
     }
 
     /**
@@ -153,13 +150,7 @@ public class PayPush {
         if (pushEventService != null) {
             pushEventService.PublisherEvent(userId, title, content, body);
         }
-        if (cid == null || cid.trim().isEmpty()) {
-            log.error("骑手推送失败:Device token is empty, userId:{}", userId);
-            return;
-        }
-        log.info("useId{}:cid{}:cidType{}:content:{}", userId, cid, cidType, content);
-        log.info("给骑手发送进入uni推送:{},{},{},{}", userId, cid, cidType, content);
-        push.qspush(cid, title, content, body);
+        sendBusiness(RecipientApp.RIDER, userId, cid, title, content, body);
     }
 
     /**
@@ -198,11 +189,17 @@ public class PayPush {
         if (pushEventService != null) {
             pushEventService.PublisherEvent(userId, title, content, body);
         }
-        if (cid == null || cid.trim().isEmpty()) {
-            log.error("Device token is empty, cannot send push message");
-            return;
+        sendBusiness(RecipientApp.USER, userId, cid, title, content, body);
+    }
+
+    private static void sendBusiness(RecipientApp app, Long userId, String cid,
+                                     String title, String content, String body) {
+        try {
+            SpringUtils.getBean(BusinessPushService.class).send(app, userId, cid, title, content, body);
+        } catch (RuntimeException exception) {
+            log.warn("Business push routing unavailable, app={}, userId={}, category={}",
+                    app, userId, exception.getClass().getSimpleName());
         }
-        push.apppush(cid, title, content, body);
     }
 
     /**

+ 130 - 0
ruoyi-admin/src/test/java/com/ruoyi/app/push/BusinessPushServiceTest.java

@@ -0,0 +1,130 @@
+package com.ruoyi.app.push;
+
+import com.ruoyi.app.utils.PayPush;
+import com.ruoyi.app.utils.event.PushEventService;
+import com.ruoyi.common.utils.spring.SpringUtils;
+import com.ruoyi.system.domain.InfoUser;
+import com.ruoyi.system.service.IInfoUserService;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.clearInvocations;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+class BusinessPushServiceTest {
+    private final BusinessPushProperties properties = new BusinessPushProperties();
+    private final IInfoUserService users = mock(IInfoUserService.class);
+    private final FcmPushSender fcm = mock(FcmPushSender.class);
+    private final ApnsPushSender apns = mock(ApnsPushSender.class);
+    private final PayPush uni = mock(PayPush.class);
+
+    @Test
+    void defaultModeUsesOnlyExistingUniCloudEndpoints() {
+        BusinessPushService service = service();
+
+        service.send(RecipientApp.USER, 1L, "user-cid", "title", "body", "{}");
+        service.send(RecipientApp.MERCHANT, 2L, "merchant-cid", "title", "body", "{}");
+        service.send(RecipientApp.RIDER, 3L, "rider-cid", "title", "body", "{}");
+
+        verify(uni).apppush("user-cid", "title", "body", "{}");
+        verify(uni).shpush("merchant-cid", "title", "body", "{}");
+        verify(uni).qspush("rider-cid", "title", "body", "{}");
+        verify(users, never()).getById(any());
+        verify(fcm, never()).send(any(), any(), any(), any(), any());
+        verify(apns, never()).send(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void nativeModeRoutesAllSixAppAndPlatformCombinationsUsingLatestToken() {
+        properties.setMode(BusinessPushProperties.Mode.NATIVE);
+        BusinessPushService service = service();
+        long userId = 10L;
+        for (RecipientApp app : RecipientApp.values()) {
+            for (String platform : new String[]{"android", "ios"}) {
+                InfoUser user = user(userId, platform, "native-" + app + "-" + platform);
+                when(users.getById(userId)).thenReturn(user);
+                service.send(app, userId, null, "title", "body", "{\"ddId\":7}");
+                if ("android".equals(platform)) {
+                    verify(fcm).send(app, user.getDeviceToken(), "title", "body", "{\"ddId\":7}");
+                } else {
+                    verify(apns).send(app, user.getDeviceToken(), "title", "body", "{\"ddId\":7}");
+                }
+                clearInvocations(fcm, apns);
+                userId++;
+            }
+        }
+        verify(uni, never()).apppush(any(), any(), any(), any());
+        verify(uni, never()).shpush(any(), any(), any(), any());
+        verify(uni, never()).qspush(any(), any(), any(), any());
+    }
+
+    @Test
+    void nativeAddressUsesLatestAccountAndRejectsMissingOrUnknownPlatform() {
+        properties.setMode(BusinessPushProperties.Mode.NATIVE);
+        BusinessPushService service = service();
+        InfoUser stale = user(9L, "android", "old-token");
+        InfoUser latest = user(9L, " IOS ", "new-token");
+        when(users.getById(9L)).thenReturn(latest);
+
+        assertEquals("ios:new-token", service.addressKey(stale));
+        service.send(RecipientApp.USER, 9L, "uni-cid", "title", "body", "{}");
+        verify(apns).send(RecipientApp.USER, "new-token", "title", "body", "{}");
+        latest.setCidType("unknown");
+        assertNull(service.addressKey(stale));
+        service.send(RecipientApp.USER, 9L, "uni-cid", "title", "body", "{}");
+        latest.setCidType("android");
+        latest.setDeviceToken(" ");
+        assertNull(service.addressKey(stale));
+        service.send(RecipientApp.USER, 9L, "uni-cid", "title", "body", "{}");
+        verify(fcm, never()).send(any(), any(), any(), any(), any());
+    }
+
+    @Test
+    void nativeSenderFailureDoesNotFallBackOrEscapeToBusiness() {
+        properties.setMode(BusinessPushProperties.Mode.NATIVE);
+        when(users.getById(9L)).thenReturn(user(9L, "android", "native-token"));
+        doThrow(new IllegalStateException("FCM_UNREGISTERED"))
+                .when(fcm).send(eq(RecipientApp.USER), eq("native-token"), any(), any(), any());
+        BusinessPushService service = service();
+
+        assertDoesNotThrow(() -> service.send(RecipientApp.USER, 9L, "uni-cid", "title", "body", "{}"));
+
+        verify(uni, never()).apppush(any(), any(), any(), any());
+    }
+
+    @Test
+    void payPushWrapperPublishesOneMessageBeforeRouting() {
+        BusinessPushService router = mock(BusinessPushService.class);
+        PushEventService events = mock(PushEventService.class);
+        try (MockedStatic<SpringUtils> beans = mockStatic(SpringUtils.class)) {
+            beans.when(() -> SpringUtils.getBean(BusinessPushService.class)).thenReturn(router);
+
+            PayPush.shPush(new PayPush(), events, 4L, "merchant-cid", "title", "body", "{}", "ios");
+
+            verify(events).PublisherEvent(4L, "title", "body", "{}");
+            verify(router).send(RecipientApp.MERCHANT, 4L, "merchant-cid", "title", "body", "{}");
+        }
+    }
+
+    private BusinessPushService service() {
+        return new BusinessPushService(properties, users, fcm, apns, uni);
+    }
+
+    private InfoUser user(Long id, String platform, String token) {
+        InfoUser user = new InfoUser();
+        user.setUserId(id);
+        user.setCidType(platform);
+        user.setDeviceToken(token);
+        return user;
+    }
+}