diff --git a/src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java b/src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java index 25ca676..e0f0fb5 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/oapi/OapiFlowController.java @@ -1,6 +1,8 @@ package com.yunhai.flowcenter.controller.oapi; +import com.yunhai.flowcenter.model.FlowModels; import com.yunhai.flowcenter.model.OapiModels; +import com.yunhai.flowcenter.service.FlowQueryService; import com.yunhai.flowcenter.service.FlowRouterAdminService; import com.yunhai.flowcenter.service.OapiFlowService; import com.yunhai.platform.utils.model.BasePageResult; @@ -34,6 +36,8 @@ import java.util.List; * - GET /oapi/flow/tasks/todo?schId=&assigneeId=&bizType=&page=&size= → data=分页待办 * - GET /oapi/flow/tasks/by-biz?schId=&bizType=&bizId=&operatorId= → data=当前人在该单据的待办(0/1/N) * - GET /oapi/flow/instances?schId=&bizType=&status=&page=&size= → data=分页办件 + * - GET /oapi/flow/tasks/done?schId=&assigneeId=&page=&size= → data=分页已办(过滤=签名 appKey→d.appKey,P3.5) + * - GET /oapi/flow/cc?schId=&userId=&page=&size= → data=分页抄送我(过滤=签名 appKey→d.appKey,P3.5) * - POST /oapi/flow/task/{taskId}/approve?schId=&operatorId=&operatorName=(body: opinion/edgeName) * - POST /oapi/flow/task/{taskId}/reject?schId=&operatorId=&operatorName=(body: opinion 必填) */ @@ -44,6 +48,7 @@ public class OapiFlowController { private final OapiFlowService flowService; private final FlowRouterAdminService routerAdminService; + private final FlowQueryService queryService; @GetMapping("/list") public List list(@RequestParam("schId") Long schId) { @@ -173,6 +178,35 @@ public class OapiFlowController { return flowService.tasksByBiz(schId, bizType, bizId, operatorId); } + /** + * 我的已办(P3.5 应用内审批四页签):过滤接缝=签名方应用身份(d.appKey=验签 appKey), + * 不收调用方 bizType 裸透传——办件挂本应用定义即含(天然覆盖新旧 bizType 存量),共享定义 + * 跨应用不泄漏(P2-02 裁决)。assigneeId 由子应用后端从 CAS 会话取。 + */ + @GetMapping("/tasks/done") + public BasePageResult tasksDone( + @RequestParam("schId") Long schId, + @RequestParam("assigneeId") Integer assigneeId, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "size", required = false) Integer size, + @RequestAttribute(value = "yh.app.auth", required = false) String appKey) { + return queryService.pageDone(schId, assigneeId, appKey, page, size); + } + + /** + * 抄送我的(P3.5,NOTIFY 节点落库行,sent_time 倒序):过滤接缝同 tasks/done + * (d.appKey=签名 appKey,不收 bizType)。userId 由子应用后端从 CAS 会话取。 + */ + @GetMapping("/cc") + public BasePageResult cc( + @RequestParam("schId") Long schId, + @RequestParam("userId") Integer userId, + @RequestParam(value = "page", required = false) Integer page, + @RequestParam(value = "size", required = false) Integer size, + @RequestAttribute(value = "yh.app.auth", required = false) String appKey) { + return queryService.pageCc(schId, userId, appKey, page, size); + } + /** 办件分页(oapi):bizType/status 可选过滤 */ @GetMapping("/instances") public BasePageResult instances( diff --git a/src/test/java/com/yunhai/flowcenter/oapi/OapiFlowP35DoneCcContractTest.java b/src/test/java/com/yunhai/flowcenter/oapi/OapiFlowP35DoneCcContractTest.java new file mode 100644 index 0000000..b7c47f9 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/oapi/OapiFlowP35DoneCcContractTest.java @@ -0,0 +1,108 @@ +package com.yunhai.flowcenter.oapi; + +import com.yunhai.flowcenter.controller.oapi.OapiFlowController; +import com.yunhai.flowcenter.model.FlowModels; +import com.yunhai.flowcenter.service.FlowQueryService; +import com.yunhai.flowcenter.service.FlowRouterAdminService; +import com.yunhai.flowcenter.service.OapiFlowService; +import com.yunhai.platform.utils.model.BasePageResult; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.setup.MockMvcBuilders; + +import java.util.Collections; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * P3.5 新增 oapi 端点(/oapi/flow/tasks/done、/oapi/flow/cc)的控制器契约测试(standalone,无 DB)。 + * 核心断言=过滤接缝(P2-02 裁决):过滤键是验签身份 appKey(请求属性 yh.app.auth → + * queryService 第三参,落 d.appKey 过滤),不收调用方 bizType——bizType 参数即使传了 + * 也不参与过滤(防裸透传/跨应用探查)。数据正确性由既有 pageDone/pageCc 的 IT 覆盖, + * 本类只钉 HTTP 契约与接缝。 + */ +@DisplayName("P3.5 oapi done/cc - 签名 appKey 过滤接缝契约") +class OapiFlowP35DoneCcContractTest { + + private FlowQueryService queryService; + private MockMvc mockMvc; + + @BeforeEach + void setUp() { + OapiFlowService flowService = mock(OapiFlowService.class); + FlowRouterAdminService routerAdminService = mock(FlowRouterAdminService.class); + queryService = mock(FlowQueryService.class); + OapiFlowController controller = new OapiFlowController(flowService, routerAdminService, queryService); + mockMvc = MockMvcBuilders.standaloneSetup(controller).build(); + when(queryService.pageDone(any(), any(), any(), any(), any())) + .thenReturn(emptyPage()); + when(queryService.pageCc(any(), any(), any(), any(), any())) + .thenReturn(new BasePageResult<>(Collections.emptyList(), 0, 1, 10, 0, false)); + } + + private static BasePageResult emptyPage() { + return new BasePageResult<>(Collections.emptyList(), 0, 1, 10, 0, false); + } + + @Test + @DisplayName("done:验签 appKey 作为过滤键下传(d.appKey 接缝),分页参数透传") + void donePassesSigningAppKeyAsFilter() throws Exception { + mockMvc.perform(get("/oapi/flow/tasks/done") + .param("schId", "3").param("assigneeId", "10006") + .param("page", "2").param("size", "10") + .requestAttr("yh.app.auth", "100008")) + .andExpect(status().isOk()); + verify(queryService).pageDone(eq(3L), eq(10006), eq("100008"), eq(2), eq(10)); + } + + @Test + @DisplayName("done:调用方带 bizType 也不参与过滤(防裸透传)") + void doneIgnoresCallerBizType() throws Exception { + mockMvc.perform(get("/oapi/flow/tasks/done") + .param("schId", "3").param("assigneeId", "10006") + .param("bizType", "svc-hall") + .requestAttr("yh.app.auth", "100008")) + .andExpect(status().isOk()); + // 第三参(过滤键)必须是签名 appKey,不是 bizType + verify(queryService).pageDone(eq(3L), eq(10006), eq("100008"), Mockito.isNull(), Mockito.isNull()); + } + + @Test + @DisplayName("done:缺 schId/assigneeId → 400,零查询") + void doneMissingRequiredParams() throws Exception { + mockMvc.perform(get("/oapi/flow/tasks/done").param("schId", "3")) + .andExpect(status().is4xxClientError()); + verify(queryService, never()).pageDone(any(), any(), any(), any(), any()); + } + + @Test + @DisplayName("cc:验签 appKey 过滤键 + userId 维度") + void ccPassesSigningAppKeyAsFilter() throws Exception { + mockMvc.perform(get("/oapi/flow/cc") + .param("schId", "3").param("userId", "10006") + .param("page", "1").param("size", "20") + .requestAttr("yh.app.auth", "100008")) + .andExpect(status().isOk()); + verify(queryService).pageCc(eq(3L), eq(10006), eq("100008"), eq(1), eq(20)); + } + + @Test + @DisplayName("cc:缺 userId → 400,零查询") + void ccMissingUserId() throws Exception { + mockMvc.perform(get("/oapi/flow/cc").param("schId", "3")) + .andExpect(status().is4xxClientError()); + verify(queryService, never()).pageCc(any(), any(), any(), any(), any()); + } +}