diff --git a/src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java b/src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java index b68822d..72c2488 100644 --- a/src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java +++ b/src/main/java/com/yunhai/flowcenter/repository/FlwCcRepository.java @@ -20,4 +20,7 @@ public interface FlwCcRepository extends JpaRepository { /** 办件轨迹/进度留痕 */ List findByInstanceIdOrderByIdAsc(Long instanceId); + + /** 可见性守卫用(F7 前置补丁):本办件是否抄送过该人——只读可见,不授予办理权 */ + boolean existsByInstanceIdAndUserId(Long instanceId, Integer userId); } diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowAccessGuard.java b/src/main/java/com/yunhai/flowcenter/service/FlowAccessGuard.java index df522b7..6eaf4c9 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowAccessGuard.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowAccessGuard.java @@ -1,6 +1,7 @@ package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.entity.FlwInstance; +import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwTaskRepository; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; @@ -14,6 +15,8 @@ import java.util.List; /** * 用户面访问守卫(概览 §十):提交人只读写自己办件;办理人只读写分派给自己的任务; * 管理员本校可查、不可代批。 + * 2026-10-17(F7 前置补丁):放行口径增「该办件 flw_cc 行接收人」——抄送接收人点开工作台 + * "抄送我的"行或抄送消息回落链接此前一律 403,抄送自留因此不闭环。只读可见,办理权仍按 assigneeId。 * 管理员口径与 FlowAdminGuard / svc-hall ApplyViewGuard / Notice NoticeFlowGuard 对齐: * type==1(超管)或 roleId∈{2,5,7}(校级管理员)——三者必须同口径,否则校级管理员 * 能列办件却在详情页 403(本类原先只认 type==1,2026-09-14 校正)。 @@ -26,8 +29,9 @@ public class FlowAccessGuard { private static final List ADMIN_ROLE_IDS = Arrays.asList(2, 5, 7); private final FlwTaskRepository taskRepository; + private final FlwCcRepository ccRepository; - /** 办件详情可见:提交人本人 / 该办件任务办理人 / 本校管理员(超管或校级管理员) */ + /** 办件详情可见:提交人本人 / 该办件任务办理人 / 本校管理员(超管或校级管理员)/ 该办件抄送接收人 */ public void requireInstanceVisible(FlwInstance instance, BoaUser user) { if (user == null || user.getId() == null) { throw new BizException(ErrorCode.FORBIDDEN, "未登录"); @@ -37,12 +41,19 @@ public class FlowAccessGuard { } if (!taskRepository.findByInstanceIdOrderByIdAsc(instance.getId()).stream() .anyMatch(t -> user.getId().equals(t.getAssigneeId()))) { - if (!isAdminOf(instance, user)) { + // F7 前置补丁:抄送接收人可读(工作台"抄送我的"行与抄送消息回落链接不再 403)。 + // 只授予可见,办理权仍在 FlowTaskService 按 assigneeId 复核。 + if (!isCcRecipient(instance, user) && !isAdminOf(instance, user)) { throw new BizException(ErrorCode.FORBIDDEN, "仅提交人/办理人/本校管理员可查看该办件"); } } } + /** 该用户是否本办件的抄送接收人(flw_cc 行,逐人一行) */ + private boolean isCcRecipient(FlwInstance instance, BoaUser user) { + return ccRepository.existsByInstanceIdAndUserId(instance.getId(), user.getId()); + } + /** * 本校管理员(可查不可代批;办理权在 FlowTaskService 里再按 assigneeId 校验)。 * 超管与校级管理员均限本校——跨校仍 403,不放大可见范围。 diff --git a/src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java b/src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java index d342f47..f118a55 100644 --- a/src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java +++ b/src/main/java/com/yunhai/flowcenter/service/OapiFlowService.java @@ -504,7 +504,9 @@ public class OapiFlowService { * 办件进度嵌入页换址(token 模式,与换图同构):同一 token 两处消费——嵌入页数据 * /open/flow/embed/data 与流程图 /open/flow/diagram/img 均免签认领该 token。定位二选一: * instanceId(与办件直接复核)/ bizType+bizId(对账口径,办件不存在与 progress 同文案)。 - * 用户级可见性(本人/办理人/管理员)由子应用后端在调本端点前完成——oapi 层只认 schId。 + * 用户级可见性(本人/办理人/管理员/该办件抄送接收人)由子应用后端在调本端点前完成——oapi 层只认 schId。 + * 2026-10-17 F7 前置补丁后,中心用户面口径({@link FlowAccessGuard#requireInstanceVisible})已含抄送 + * 接收人;签发侧必须与之一致,否则"抄送我的"点开得、进度嵌入页换址却拒发(2026-10-17 记)。 */ public OapiModels.EmbedUrlVo embedUrl(Long schId, Long instanceId, String bizType, String bizId, String mode) { // 配置缺失优先于办件定位:fail-fast 纪律要的是"无论如何都不发打不开的地址" diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowAccessGuardTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowAccessGuardTest.java index 52ed3ba..eb7c75b 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowAccessGuardTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowAccessGuardTest.java @@ -2,6 +2,7 @@ package com.yunhai.flowcenter.service; import com.yunhai.flowcenter.entity.FlwInstance; import com.yunhai.flowcenter.entity.FlwTask; +import com.yunhai.flowcenter.repository.FlwCcRepository; import com.yunhai.flowcenter.repository.FlwTaskRepository; import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.model.BoaUser; @@ -15,6 +16,7 @@ import java.util.Collections; import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -26,15 +28,18 @@ import static org.mockito.Mockito.when; class FlowAccessGuardTest { private FlwTaskRepository taskRepository; + private FlwCcRepository ccRepository; private FlowAccessGuard guard; private FlwInstance instance; @BeforeEach void setUp() { taskRepository = mock(FlwTaskRepository.class); - guard = new FlowAccessGuard(taskRepository); + ccRepository = mock(FlwCcRepository.class); + guard = new FlowAccessGuard(taskRepository, ccRepository); when(taskRepository.findByInstanceIdOrderByIdAsc(anyLong())) .thenReturn(Collections.emptyList()); + when(ccRepository.existsByInstanceIdAndUserId(anyLong(), anyInt())).thenReturn(false); instance = new FlwInstance(); ReflectionTestUtils.setField(instance, "id", 100L); @@ -94,4 +99,29 @@ class FlowAccessGuardTest { assertThrows(BizException.class, () -> guard.requireInstanceVisible(instance, user(37, 1, null))); } + + /** F7 前置补丁:抄送接收人只读可见(工作台"抄送我的"行、抄送消息回落链接不再 403) */ + @Test + void ccRecipientCanView() { + when(ccRepository.existsByInstanceIdAndUserId(100L, 44)).thenReturn(true); + + assertDoesNotThrow(() -> guard.requireInstanceVisible(instance, user(44, 2, 990001, 9))); + } + + /** 陌生人(非提交人/办理人/管理员/抄送人)仍 403——补丁不放大可见范围 */ + @Test + void strangerSameSchoolIsStillDenied() { + BizException e = assertThrows(BizException.class, + () -> guard.requireInstanceVisible(instance, user(45, 2, 990001, 9))); + assertEquals("仅提交人/办理人/本校管理员可查看该办件", e.getMessage()); + } + + /** 跨校抄送接收人不存在即拒;抄送行必须命中本办件(instanceId 维度,不按 schId 泛化) */ + @Test + void ccLookupIsScopedToInstance() { + when(ccRepository.existsByInstanceIdAndUserId(100L, 46)).thenReturn(false); + + assertThrows(BizException.class, + () -> guard.requireInstanceVisible(instance, user(46, 2, 990001, 9))); + } } diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java index ffb9922..5788ad4 100644 --- a/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDiagramServiceTest.java @@ -389,8 +389,10 @@ class FlowDiagramServiceTest { taskService = mock(FlowTaskService.class); instanceService = mock(FlowInstanceService.class); diagramService = mock(FlowDiagramService.class); - // 真守卫(同 FlowAccessGuardTest 口径):提交人/办理人/本校管理员 - FlowAccessGuard accessGuard = new FlowAccessGuard(taskRepository); + // 真守卫(同 FlowAccessGuardTest 口径):提交人/办理人/本校管理员/抄送接收人 + // 抄送分支本组用例不覆盖,空 mock 即 false(陌生人仍 403 的断言不受影响) + FlowAccessGuard accessGuard = new FlowAccessGuard(taskRepository, + mock(com.yunhai.flowcenter.repository.FlwCcRepository.class)); controller = new FlowUserController(queryService, taskService, instanceService, accessGuard, diagramService, mock(com.yunhai.flowcenter.service.DiagramImageService.class)); }