Explorar o código

三方登录同手机号多账号改为明确提示;Apple/Google用户端绑定限定会员账号

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
qmj hai 11 horas
pai
achega
6cb60da882

+ 16 - 9
ruoyi-admin/src/main/java/com/ruoyi/app/user/InfoUserController.java

@@ -1143,12 +1143,10 @@ public class InfoUserController extends BaseController {
             return error(MessageUtils.message("no.user.jcaptcha.error"));
         }
 
-        InfoUser user;
-        if (accountType != null) {
-            user = findOauthBindingUser(accountType, phone);
-        } else {
-            user = infoUserService.getuser(phone);
-        }
+        // apple/google 用户端(accountType 为空)同样按会员渠道查询:
+        // getuser 不带 userType 过滤,同手机号命中商家/骑手账号会错绑或多行异常。
+        OAuthAccountType bindingChannel = accountType != null ? accountType : OAuthAccountType.USER;
+        InfoUser user = findOauthBindingUser(bindingChannel, phone);
         if (accountType != null && !accountType.allowCreate()) {
             if (user == null) {
                 return error(MessageUtils.message("no.user.not.exist"));
@@ -1176,8 +1174,7 @@ public class InfoUserController extends BaseController {
             if (!infoUserService.saveOrUpdate(info)) {
                 throw new ServiceException(MessageUtils.message("no.system.error"));
             }
-            user = accountType == null
-                    ? infoUserService.getuser(phone) : findOauthBindingUser(accountType, phone);
+            user = findOauthBindingUser(bindingChannel, phone);
             if (user == null) {
                 return error(MessageUtils.message("no.user.not.exist"));
             }
@@ -1260,13 +1257,23 @@ public class InfoUserController extends BaseController {
         }
     }
 
+    /**
+     * 按渠道查待绑定账号。不能用 getOne:会员/商家/骑手共用 info_user 表,
+     * 同手机号可能命中多行(如一号注册多个商家账号),getOne 多行会抛底层异常导致接口 500;
+     * 命中多行时明确提示用户联系平台,由平台侧人工处理绑定归属。
+     */
     private InfoUser findOauthBindingUser(OAuthAccountType channel, String phone) {
         LambdaQueryWrapper<InfoUser> query = new LambdaQueryWrapper<InfoUser>()
                 .eq(channel.allowCreate(), InfoUser::getPhone, phone)
                 .eq(!channel.allowCreate(), InfoUser::getTelPhone, phone)
                 .eq(InfoUser::getDelFlag, "0")
                 .in(InfoUser::getUserType, channel.userTypes());
-        return infoUserService.getOne(query);
+        List<InfoUser> matches = infoUserService.list(query);
+        if (matches.size() > 1) {
+            log.warn("[OAuth] 手机号关联多个账号 channel={}, phone={}", channel, maskPhone(phone));
+            throw new ServiceException(MessageUtils.message("no.oauth.phone.duplicate"));
+        }
+        return matches.isEmpty() ? null : matches.get(0);
     }
 
     /**

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

@@ -188,6 +188,7 @@ no.oauth.token.invalid={0}登录凭证无效
 no.oauth.token.expired={0}登录凭证已过期
 no.oauth.audience.mismatch={0}凭证校验未通过
 no.oauth.verify.fail={0}登录校验失败
+no.oauth.phone.duplicate=该手机号关联多个账号,请联系平台
 
 # 订单发票校验(010)
 no.invoice.choice.invalid=发票类型不合法:{0}

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

@@ -192,6 +192,7 @@ no.oauth.token.invalid={0} login credential is invalid
 no.oauth.token.expired={0} login credential has expired
 no.oauth.audience.mismatch={0} credential verification failed
 no.oauth.verify.fail={0} login verification failed
+no.oauth.phone.duplicate=This phone number is linked to multiple accounts. Please contact the platform.
 
 # Order invoice validation (010)
 no.invoice.choice.invalid=Invalid invoice type: {0}

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

@@ -187,6 +187,7 @@ no.oauth.token.invalid=ข้อมูลรับรองการเข้า
 no.oauth.token.expired=ข้อมูลรับรองการเข้าสู่ระบบ {0} หมดอายุแล้ว
 no.oauth.audience.mismatch=การตรวจสอบข้อมูลรับรอง {0} ไม่ผ่าน
 no.oauth.verify.fail=การตรวจสอบการเข้าสู่ระบบ {0} ไม่สำเร็จ
+no.oauth.phone.duplicate=หมายเลขโทรศัพท์นี้เชื่อมโยงกับบัญชีหลายบัญชี โปรดติดต่อแพลตฟอร์ม
 
 # ตรวจสอบใบกำกับภาษีของคำสั่งซื้อ (010)
 no.invoice.choice.invalid=ประเภทใบกำกับภาษีไม่ถูกต้อง: {0}

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

@@ -192,6 +192,7 @@ no.oauth.token.invalid=Thông tin đăng nhập {0} không hợp lệ
 no.oauth.token.expired=Thông tin đăng nhập {0} đã hết hạn
 no.oauth.audience.mismatch=Xác minh thông tin {0} không thành công
 no.oauth.verify.fail=Xác minh đăng nhập {0} thất bại
+no.oauth.phone.duplicate=Số điện thoại này được liên kết với nhiều tài khoản. Vui lòng liên hệ nền tảng.
 
 # Kiểm tra hóa đơn đơn hàng (010)
 no.invoice.choice.invalid=Loại hóa đơn không hợp lệ: {0}

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

@@ -192,6 +192,7 @@ no.oauth.token.invalid={0}登录凭证无效
 no.oauth.token.expired={0}登录凭证已过期
 no.oauth.audience.mismatch={0}凭证校验未通过
 no.oauth.verify.fail={0}登录校验失败
+no.oauth.phone.duplicate=该手机号关联多个账号,请联系平台
 
 # 订单发票校验(010)
 no.invoice.choice.invalid=发票类型不合法:{0}

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

@@ -192,6 +192,7 @@ no.oauth.token.invalid={0}登入憑證無效
 no.oauth.token.expired={0}登入憑證已過期
 no.oauth.audience.mismatch={0}憑證校驗未通過
 no.oauth.verify.fail={0}登入校驗失敗
+no.oauth.phone.duplicate=該手機號關聯多個帳號,請聯繫平台
 
 # 訂單發票校驗(010)
 no.invoice.choice.invalid=發票類型不合法:{0}

+ 35 - 5
ruoyi-admin/src/test/java/com/ruoyi/app/user/InfoUserControllerTest.java

@@ -99,6 +99,7 @@ class InfoUserControllerTest {
         messageSource.addMessage("no.system.error", Locale.getDefault(), "系统错误");
         messageSource.addMessage("no.user.jcaptcha.error", Locale.getDefault(), "验证码错误");
         messageSource.addMessage("no.oauth.tempkey.expired", Locale.getDefault(), "登录凭证已过期");
+        messageSource.addMessage("no.oauth.phone.duplicate", Locale.getDefault(), "该手机号关联多个账号,请联系平台");
         beanFactory.registerSingleton("messageSource", messageSource);
         beanFactory.registerSingleton("redisCache", mock(RedisCache.class));
         new SpringUtils().postProcessBeanFactory(beanFactory);
@@ -392,7 +393,8 @@ class InfoUserControllerTest {
 
         InfoUser createdUser = activeUser(99L, "0");
         createdUser.setPhone("0912345678");
-        when(infoUserService.getOne(any(Wrapper.class))).thenReturn(null, createdUser);
+        when(infoUserService.list(any(Wrapper.class)))
+                .thenReturn(java.util.List.of(), java.util.List.of(createdUser));
         when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
 
         AjaxResult result = controller.oauthBindPhone(request);
@@ -488,7 +490,7 @@ class InfoUserControllerTest {
         OAuthBindDto request = oauthRequest(provider);
         InfoUser business = activeUser(72L, role);
         business.setTelPhone(request.getPhone());
-        when(infoUserService.getOne(any(Wrapper.class))).thenReturn(business);
+        when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(business));
         when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
 
         AjaxResult result = controller.oauthBindPhone(request);
@@ -503,7 +505,7 @@ class InfoUserControllerTest {
         assertEquals(provider, binding.getValue().getProvider());
         verify(infoUserService, never()).getuser(any());
         ArgumentCaptor<Wrapper<InfoUser>> query = ArgumentCaptor.forClass(Wrapper.class);
-        verify(infoUserService).getOne(query.capture());
+        verify(infoUserService).list(query.capture());
         assertTrue(query.getValue().getSqlSegment().contains("tel_phone"));
     }
 
@@ -589,7 +591,8 @@ class InfoUserControllerTest {
         OAuthBindDto request = oauthRequest(provider);
         InfoUser user = activeUser(72L, "0");
         user.setPhone(request.getPhone());
-        when(infoUserService.getuser(request.getPhone())).thenReturn(null, user);
+        when(infoUserService.list(any(Wrapper.class)))
+                .thenReturn(java.util.List.of(), java.util.List.of(user));
         when(infoUserService.saveOrUpdate(any(InfoUser.class))).thenReturn(true);
 
         AjaxResult result = controller.oauthBindPhone(request);
@@ -608,7 +611,7 @@ class InfoUserControllerTest {
         OAuthBindDto request = oauthRequest(provider);
         InfoUser user = activeUser(72L, "5");
         user.setSubaccountStatus("0");
-        when(infoUserService.getOne(any(Wrapper.class))).thenReturn(user);
+        when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(user));
         doThrow(new ServiceException("owner unavailable")).when(merchantStoreAccessService).resolve(72L);
         AjaxResult result = controller.oauthBindPhone(request);
         assertEquals(HttpStatus.ERROR, result.get(AjaxResult.CODE_TAG));
@@ -641,6 +644,33 @@ class InfoUserControllerTest {
         verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
     }
 
+    @Test
+    void oauthBindPhoneRejectsPhoneLinkedToMultipleAccounts() {
+        OAuthBindDto request = oauthRequest("apple");
+        when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(
+                activeUser(71L, "0"), activeUser(72L, "0")));
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> controller.oauthBindPhone(request));
+
+        assertEquals("该手机号关联多个账号,请联系平台", exception.getMessage());
+        verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
+        verify(infoUserService, never()).saveOrUpdate(any(InfoUser.class));
+    }
+
+    @Test
+    void merchantOauthBindPhoneRejectsPhoneLinkedToMultipleStores() {
+        OAuthBindDto request = oauthRequest("line_merchant");
+        when(infoUserService.list(any(Wrapper.class))).thenReturn(java.util.List.of(
+                activeUser(71L, "1"), activeUser(72L, "1")));
+
+        ServiceException exception = assertThrows(ServiceException.class,
+                () -> controller.oauthBindPhone(request));
+
+        assertEquals("该手机号关联多个账号,请联系平台", exception.getMessage());
+        verify(infoUserOauthMapper, never()).insert(any(InfoUserOauth.class));
+    }
+
     private OAuthBindDto oauthRequest(String provider) {
         OAuthBindDto request = new OAuthBindDto();
         request.setTempKey("business-key");