From 4293c9628a9d87add00a908fc3bd8aa6a1c05d63 Mon Sep 17 00:00:00 2001 From: chenxing Date: Sat, 10 Oct 2026 19:23:44 +0800 Subject: [PATCH] =?UTF-8?q?feat(def):=20updateMeta=20=E5=85=83=E6=95=B0?= =?UTF-8?q?=E6=8D=AE=E5=8E=9F=E5=9C=B0=E7=BC=96=E8=BE=91=E2=80=94=E2=80=94?= =?UTF-8?q?=E5=B7=B2=E5=8F=91=E5=B8=83=E5=8F=AF=E6=94=B9=20name/appKey=20?= =?UTF-8?q?=E4=B8=8D=E5=8D=87=E7=89=88=E6=9C=AC=EF=BC=88=E8=AE=AE=E9=A2=98?= =?UTF-8?q?2=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 独立于 save 的升版本路径:不碰 code/flow_guid/version/status/contentJson 结构 - contentJson 顶层 name 同步(ObjectNode 保序;消除行 name 与 JSON name 脏窗口,拍板 D4) - appKey 校验同 save 口径(显式传入须 flw_app 启用集合;可置空) - 单测 5 例(三不变断言/白名单/补写/跨校)+ E2E 14/14(含升版本路径回归与 oapi 摘要) --- .../controller/api/FlowAdminController.java | 12 ++ .../service/FlowDefAdminService.java | 43 ++++++ .../FlowDefAdminServiceUpdateMetaTest.java | 123 ++++++++++++++++++ 3 files changed, 178 insertions(+) create mode 100644 src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java diff --git a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java index 48fc231..296f164 100644 --- a/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java +++ b/src/main/java/com/yunhai/flowcenter/controller/api/FlowAdminController.java @@ -104,6 +104,18 @@ public class FlowAdminController { (String) body.get("appKey")); } + /** + * 元数据原地编辑(2026-10-10 议题2):只改 name/appKey 不升版本(version/flowGuid/status/contentJson + * 结构均不动,仅同步 contentJson 顶层 name);已发布亦可。结构改动请走编辑器升版本。 + */ + @PostMapping("/def/updateMeta") + public FlwDef defUpdateMeta(BoaUser user, @RequestBody Map body) { + Long schId = adminGuard.requireAdmin(user, null); + return defAdminService.updateMeta(schId, + body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id"))), + (String) body.get("name"), (String) body.get("appKey")); + } + @PostMapping("/def/publish/{id}") public FlwDef defPublish(BoaUser user, @PathVariable("id") Long id) { return defAdminService.publish(adminGuard.requireAdmin(user, null), id); diff --git a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java index b0767c2..48cc55a 100644 --- a/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java +++ b/src/main/java/com/yunhai/flowcenter/service/FlowDefAdminService.java @@ -198,6 +198,49 @@ public class FlowDefAdminService { return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid); } + /** + * 元数据原地编辑(2026-10-10 四议题·议题2,拍板 D4):已发布/停用/草稿均可只改 name/appKey + * 而不升版本——结构改动仍走 save 的升版本路径,两者完全隔离(本方法不碰 + * code/flow_guid/version/status,不进升版本分支)。appKey 校验同 save 口径(显式传入须在白名单)。 + * content_json 顶层 name 同步更新(只动顶层字段不动结构,消除"行 name 与 JSON name 的脏窗口"; + * 亦修复 API 直调方经 parsed.name 覆盖行字段时读到旧名的口径)。 + * 并发说明:与升版本路径无共享可变行(升版本建新行),不加锁。 + */ + @Transactional + public FlwDef updateMeta(Long schId, Long id, String name, String appKey) { + FlwDef def = requireDef(schId, id); + if (name == null || name.trim().isEmpty()) { + throw new BizException(ErrorCode.PARAM_ERROR, "流程名称不能为空"); + } + String cleanName = name.trim(); + String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim(); + if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) { + throw new BizException(ErrorCode.PARAM_ERROR, + "归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试"); + } + def.setName(cleanName); + def.setAppKey(cleanAppKey); + def.setContentJson(syncTopLevelName(def.getContentJson(), cleanName)); + try { + return defRepository.save(def); + } catch (DataIntegrityViolationException e) { + throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时编辑,请刷新后重试"); + } + } + + /** contentJson 顶层 name 同步(ObjectNode 保字段顺序;结构零改动),返回新 JSON 串 */ + private String syncTopLevelName(String contentJson, String name) { + try { + com.fasterxml.jackson.databind.node.ObjectNode root = (com.fasterxml.jackson.databind.node.ObjectNode) + objectMapper.readTree(contentJson); + root.put("name", name); + return objectMapper.writeValueAsString(root); + } catch (Exception e) { + // contentJson 在 save 期已保证可解析;防御口径:解析失败不同步(行 name 已更新,JSON 维持原值) + return contentJson; + } + } + private Set enabledDeptKeys(Long schId) { return mapRepository.findEnabledBySchId(schId).stream() .map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet()); diff --git a/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java new file mode 100644 index 0000000..f9eb873 --- /dev/null +++ b/src/test/java/com/yunhai/flowcenter/service/FlowDefAdminServiceUpdateMetaTest.java @@ -0,0 +1,123 @@ +package com.yunhai.flowcenter.service; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.yunhai.flowcenter.engine.FlowEngine; +import com.yunhai.flowcenter.entity.FlwDef; +import com.yunhai.flowcenter.repository.FlwAppRepository; +import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository; +import com.yunhai.flowcenter.repository.FlwDefRepository; +import com.yunhai.flowcenter.repository.FlwRouterRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * updateMeta 原地编辑单测(2026-10-10 议题2,拍板 D4): + * 三不变(version/flowGuid/status;contentJson 仅顶层 name)、appKey 白名单校验、 + * contentJson 无顶层 name 时补写、name 非空校验。 + */ +class FlowDefAdminServiceUpdateMetaTest { + + private static final Long SCH = 3L; + + private FlwDefRepository defRepository; + private FlwAppRepository appRepository; + private FlowDefAdminService service; + + @BeforeEach + void setUp() { + defRepository = mock(FlwDefRepository.class); + appRepository = mock(FlwAppRepository.class); + service = new FlowDefAdminService(defRepository, + mock(FlwAssigneeMapRepository.class), + mock(FlwRouterRepository.class), + mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class), + appRepository, + mock(FlowEngine.class), + new ObjectMapper()); + when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0)); + when(appRepository.isEnabledApp(any(), any())).thenReturn(true); + } + + private FlwDef published(String contentJson) { + FlwDef d = new FlwDef(); + d.setId(9L); + d.setSchId(SCH); + d.setFlowGuid("guid123"); + d.setCode("work-leave"); + d.setName("旧名"); + d.setAppKey("100002"); + d.setVersion(4); + d.setStatus(1); + d.setContentJson(contentJson); + return d; + } + + @Test + void 三不变_contentJson仅顶层name() throws Exception { + String json = "{\"code\":\"work-leave\",\"name\":\"旧名\",\"start\":\"start\"," + + "\"nodes\":[{\"id\":\"start\",\"type\":\"START\"}]," + + "\"edges\":[{\"from\":\"start\",\"to\":\"end\"}]}"; + FlwDef def = published(json); + when(defRepository.findById(9L)).thenReturn(Optional.of(def)); + + FlwDef saved = service.updateMeta(SCH, 9L, "新名称", "100008"); + + assertEquals(4, saved.getVersion()); + assertEquals("guid123", saved.getFlowGuid()); + assertEquals(1, saved.getStatus()); + assertEquals("work-leave", saved.getCode()); + assertEquals("新名称", saved.getName()); + assertEquals("100008", saved.getAppKey()); + assertEquals("新名称", new ObjectMapper().readTree(saved.getContentJson()).path("name").asText()); + // 结构零改动:节点/边/其余顶层字段原样保留 + assertEquals("work-leave", new ObjectMapper().readTree(saved.getContentJson()).path("code").asText()); + assertTrue(saved.getContentJson().contains("\"type\":\"START\"")); + } + + @Test + void contentJson无顶层name_补写() throws Exception { + FlwDef def = published("{\"code\":\"c\",\"start\":\"start\",\"nodes\":[],\"edges\":[]}"); + when(defRepository.findById(9L)).thenReturn(Optional.of(def)); + + FlwDef saved = service.updateMeta(SCH, 9L, "补写名", null); + + assertEquals("补写名", new ObjectMapper().readTree(saved.getContentJson()).path("name").asText()); + assertNull(saved.getAppKey()); + } + + @Test + void appKey白名单外_被拦() { + when(appRepository.isEnabledApp(SCH, "888888")).thenReturn(false); + when(defRepository.findById(9L)).thenReturn(Optional.of(published("{}"))); + + Exception e = assertThrows(com.yunhai.platform.apistd.BizException.class, + () -> service.updateMeta(SCH, 9L, "n", "888888")); + assertTrue(e.getMessage().contains("应用设置")); + } + + @Test + void name空_被拦() { + when(defRepository.findById(9L)).thenReturn(Optional.of(published("{}"))); + assertThrows(com.yunhai.platform.apistd.BizException.class, + () -> service.updateMeta(SCH, 9L, " ", null)); + } + + @Test + void 跨校_被拒() { + FlwDef other = published("{}"); + other.setSchId(2L); + when(defRepository.findById(9L)).thenReturn(Optional.of(other)); + assertThrows(com.yunhai.platform.apistd.BizException.class, + () -> service.updateMeta(SCH, 9L, "n", null)); + } +}