From 7e222f19b883c56c5b496d8684af12b7142f5b87 Mon Sep 17 00:00:00 2001 From: chenxing Date: Mon, 28 Sep 2026 16:52:37 +0800 Subject: [PATCH] =?UTF-8?q?feat(app):=20=E6=96=B0=E5=A2=9E=20/oapi/app/aut?= =?UTF-8?q?horized-codes=20=E5=88=A4=E6=9D=83=E7=AB=AF=E7=82=B9=EF=BC=88?= =?UTF-8?q?=E6=B6=88=E6=81=AF=E8=B7=B3=E8=BD=AC=E6=94=B6=E5=8F=A3=20C1?= =?UTF-8?q?=EF=BC=89=E2=80=94=E2=80=94findUserAuthorizedAppIds=20=E5=88=A4?= =?UTF-8?q?=E6=9D=83=20+=20listAppCodesByIds=20=E6=98=A0=E5=B0=84=20app=5F?= =?UTF-8?q?code=EF=BC=88=E7=A9=BA=E5=80=BC=E8=B7=B3=E8=BF=87/=E9=A6=96?= =?UTF-8?q?=E7=8E=B0=E5=8E=BB=E9=87=8D=EF=BC=89=EF=BC=9B=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E9=9D=A2=E4=B8=8E=E6=8E=A7=E5=88=B6=E5=99=A8=E7=94=A8=E4=BE=8B?= =?UTF-8?q?=E8=A1=A5=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../controller/oapi/AppController.java | 19 +++++ .../service/IApplicationInfoService.java | 10 +++ .../service/impl/ApplicationInfoService.java | 19 +++++ .../AppControllerAuthorizedCodesTest.java | 74 +++++++++++++++++++ .../impl/ApplicationInfoLookupTest.java | 27 +++++++ 5 files changed, 149 insertions(+) create mode 100644 src/test/java/com/yunhai/userbase/controller/oapi/AppControllerAuthorizedCodesTest.java diff --git a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java index 549a075..c467440 100644 --- a/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java +++ b/src/main/java/com/yunhai/userbase/controller/oapi/AppController.java @@ -8,6 +8,7 @@ import com.yunhai.userbase.entity.ApplicationInfo; import com.yunhai.userbase.model.application.ApplicationInfoVo; import com.yunhai.userbase.service.IAppCategoryService; import com.yunhai.userbase.service.IApplicationInfoService; +import com.yunhai.userbase.service.IGroupAuthzService; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; @@ -33,6 +34,9 @@ public class AppController { @Autowired private IApplicationInfoService applicationInfoService; + @Autowired + private IGroupAuthzService groupAuthzService; + /** * 按主键 ID 查询应用信息(带缓存)。
* 与客户端契约 {@code GET /oapi/app/{appId}} 对应;查不到返回 null(HTTP 200 + data:null 信封)。
@@ -73,6 +77,21 @@ public class AppController { return toVo(applicationInfoService.getByAppKey(appKey)); } + /** + * 查询用户有权应用的 app_code 集合(消息跳转判权,C1)。
+ * 判权复用 {@code IGroupAuthzService.findUserAuthorizedAppIds}(组开通 ∪ 启用角色模块所属应用, + * 超级管理员返回全部),再映射为 app_code(空值行跳过)。只读,不透出 appSecret。
+ * 消费方(UserHome 消息代理)以「集合是否包含消息 jump_app」决定是否显示跳转入口; + * 本端点不可达时消费方放行(不判权),绝不因判权通道故障批量隐藏入口。 + * + * @param userId 用户ID(必填) + * @return 有权应用的 app_code 集合(可能为空,不为 null) + */ + @GetMapping("authorized-codes") + public List authorizedCodes(@RequestParam("userId") Integer userId) { + return applicationInfoService.listAppCodesByIds(groupAuthzService.findUserAuthorizedAppIds(userId)); + } + private ApplicationInfoVo toVo(ApplicationInfo info) { if (info == null) { return null; diff --git a/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java index 2c0b611..242896d 100644 --- a/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java +++ b/src/main/java/com/yunhai/userbase/service/IApplicationInfoService.java @@ -11,6 +11,7 @@ import com.yunhai.userbase.model.application.PushSettingVo; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; +import java.util.Collection; import java.util.List; /** @@ -45,6 +46,15 @@ public interface IApplicationInfoService { */ ApplicationInfo getByAppKey(Integer appKey); + /** + * 按应用 id 集合取技术标识 app_code 列表(/oapi/app/authorized-codes 数据面,判权由调用方完成)。
+ * 顺序跟随入参集合;app_code 为空的行跳过(存量历史行允许 null);结果按首次出现去重。 + * + * @param ids 应用主键 id 集合 + * @return app_code 列表(可能为空,不为 null) + */ + List listAppCodesByIds(Collection ids); + /** * 保存/更新应用信息(写后同步更新缓存)。 */ diff --git a/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java b/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java index c080064..dd71a6a 100644 --- a/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java +++ b/src/main/java/com/yunhai/userbase/service/impl/ApplicationInfoService.java @@ -37,8 +37,11 @@ import javax.persistence.criteria.Predicate; import javax.persistence.criteria.Root; import javax.transaction.Transactional; import java.util.ArrayList; +import java.util.Collection; +import java.util.HashSet; import java.util.List; import java.util.Objects; +import java.util.Set; import java.util.UUID; @Service @@ -120,6 +123,22 @@ public class ApplicationInfoService implements IApplicationInfoService { return id == null ? null : getById(id); } + @Override + public List listAppCodesByIds(Collection ids) { + List codes = new ArrayList<>(); + if (ids == null || ids.isEmpty()) { + return codes; + } + Set seen = new HashSet<>(); + for (ApplicationInfo info : applicationInfoRepository.findAllById(ids)) { + String code = info.getAppCode(); + if (StringUtils.isNotBlank(code) && seen.add(code)) { + codes.add(code); + } + } + return codes; + } + /** * 通用保存:方法返回后发布变更事件,缓存失效由 AFTER_COMMIT 监听器统一执行。 */ diff --git a/src/test/java/com/yunhai/userbase/controller/oapi/AppControllerAuthorizedCodesTest.java b/src/test/java/com/yunhai/userbase/controller/oapi/AppControllerAuthorizedCodesTest.java new file mode 100644 index 0000000..a8c1c11 --- /dev/null +++ b/src/test/java/com/yunhai/userbase/controller/oapi/AppControllerAuthorizedCodesTest.java @@ -0,0 +1,74 @@ +package com.yunhai.userbase.controller.oapi; + +import com.yunhai.userbase.service.IApplicationInfoService; +import com.yunhai.userbase.service.IGroupAuthzService; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * /oapi/app/authorized-codes 判权端点单测(消息跳转收口 C1)。
+ * 控制器薄委托:判权来源 {@code IGroupAuthzService.findUserAuthorizedAppIds}(超管返回全部 / + * 普通用户返回子集 / 空权限返回空),数据面 {@code listAppCodesByIds} 映射 app_code + * (空值跳过/去重语义见 ApplicationInfoLookupTest)。 + */ +@DisplayName("AppController.authorizedCodes - 有权 app_code 集合") +class AppControllerAuthorizedCodesTest { + + @InjectMocks + private AppController controller; + + @Mock + private IGroupAuthzService groupAuthzService; + + @Mock + private IApplicationInfoService applicationInfoService; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + } + + @Test + void 超管_返回全部应用code() { + when(groupAuthzService.findUserAuthorizedAppIds(10006)).thenReturn(Arrays.asList(100011, 100013, 100015)); + when(applicationInfoService.listAppCodesByIds(Arrays.asList(100011, 100013, 100015))) + .thenReturn(Arrays.asList("flow-center", "notice", "userhome")); + + List codes = controller.authorizedCodes(10006); + + assertEquals(Arrays.asList("flow-center", "notice", "userhome"), codes); + verify(applicationInfoService).listAppCodesByIds(Arrays.asList(100011, 100013, 100015)); + } + + @Test + void 普通用户_仅返回有权子集() { + when(groupAuthzService.findUserAuthorizedAppIds(201)).thenReturn(Collections.singletonList(100013)); + when(applicationInfoService.listAppCodesByIds(Collections.singletonList(100013))) + .thenReturn(Collections.singletonList("notice")); + + assertEquals(Collections.singletonList("notice"), controller.authorizedCodes(201)); + } + + @Test + void 空权限_返回空集合() { + when(groupAuthzService.findUserAuthorizedAppIds(202)).thenReturn(Collections.emptyList()); + when(applicationInfoService.listAppCodesByIds(Collections.emptyList())) + .thenReturn(Collections.emptyList()); + + assertEquals(Collections.emptyList(), controller.authorizedCodes(202)); + verify(groupAuthzService).findUserAuthorizedAppIds(202); + verify(applicationInfoService).listAppCodesByIds(Collections.emptyList()); + } +} diff --git a/src/test/java/com/yunhai/userbase/service/impl/ApplicationInfoLookupTest.java b/src/test/java/com/yunhai/userbase/service/impl/ApplicationInfoLookupTest.java index e5df073..4921d57 100644 --- a/src/test/java/com/yunhai/userbase/service/impl/ApplicationInfoLookupTest.java +++ b/src/test/java/com/yunhai/userbase/service/impl/ApplicationInfoLookupTest.java @@ -108,4 +108,31 @@ class ApplicationInfoLookupTest { assertNull(existing.getAppNature(), "性质同样不得被 null 覆盖"); verify(applicationInfoRepository, never()).existsByAppCodeAndIdNot(any(), any()); } + + @Test + void listAppCodesByIds_空appCode行跳过_按首现去重() { + ApplicationInfo center = flowCenter(); + ApplicationInfo legacy = new ApplicationInfo(); + legacy.setId(100012); + legacy.setAppCode(null); + ApplicationInfo notice = new ApplicationInfo(); + notice.setId(100013); + notice.setAppCode("notice"); + when(applicationInfoRepository.findAllById(any())).thenReturn( + java.util.Arrays.asList(center, legacy, notice, center)); + + java.util.List codes = applicationInfoService.listAppCodesByIds( + java.util.Arrays.asList(100011, 100012, 100013)); + + assertEquals(java.util.Arrays.asList("flow-center", "notice"), codes, + "null app_code 跳过(存量历史行),重复去重"); + } + + @Test + void listAppCodesByIds_空入参返回空不出站() { + assertEquals(java.util.Collections.emptyList(), applicationInfoService.listAppCodesByIds(null)); + assertEquals(java.util.Collections.emptyList(), applicationInfoService.listAppCodesByIds( + java.util.Collections.emptyList())); + verify(applicationInfoRepository, never()).findAllById(any()); + } }