feat(p3.5): oapi 增 GET /tasks/done 与 /cc——已办/抄送按签名方应用过滤(P2-02 接缝)
build / build-push (push) Successful in 1m11s

- 两端点复用 FlowQueryService#pageDone/pageCc;过滤键=验签身份 appKey(请求属性
  yh.app.auth → d.appKey),不收调用方 bizType——天然覆盖消费方新旧 bizType 存量、
  防共享定义跨应用泄漏
- 契约测试 5 例(standalone 无 DB):appKey 接缝下传/bizType 不参与过滤/必参缺失 400 零查询
- 全量 262/262 绿(本地含 DB;Stage S5/P3.5)
This commit is contained in:
chenxing
2026-10-20 00:16:06 +08:00
parent 52c27d88bb
commit 93ecc79839
2 changed files with 142 additions and 0 deletions
@@ -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<OapiModels.FlowSummaryVo> list(@RequestParam("schId") Long schId) {
@@ -173,6 +178,35 @@ public class OapiFlowController {
return flowService.tasksByBiz(schId, bizType, bizId, operatorId);
}
/**
* 我的已办(P3.5 应用内审批四页签):过滤接缝=<b>签名方应用身份</b>(d.appKey=验签 appKey),
* 不收调用方 bizType 裸透传——办件挂本应用定义即含(天然覆盖新旧 bizType 存量),共享定义
* 跨应用不泄漏(P2-02 裁决)。assigneeId 由子应用后端从 CAS 会话取。
*/
@GetMapping("/tasks/done")
public BasePageResult<FlowModels.TaskItem> 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<FlowModels.CcItem> 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<OapiModels.InstanceItemVo> instances(
@@ -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)。
* 核心断言=<b>过滤接缝</b>(P2-02 裁决):过滤键是验签身份 appKey(请求属性 yh.app.auth →
* queryService 第三参,落 d.appKey 过滤),<b>不收调用方 bizType</b>——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<FlowModels.TaskItem> 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());
}
}