feat(def): updateMeta 元数据原地编辑——已发布可改 name/appKey 不升版本(议题2)
- 独立于 save 的升版本路径:不碰 code/flow_guid/version/status/contentJson 结构 - contentJson 顶层 name 同步(ObjectNode 保序;消除行 name 与 JSON name 脏窗口,拍板 D4) - appKey 校验同 save 口径(显式传入须 flw_app 启用集合;可置空) - 单测 5 例(三不变断言/白名单/补写/跨校)+ E2E 14/14(含升版本路径回归与 oapi 摘要)
This commit is contained in:
@@ -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<String, Object> 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);
|
||||
|
||||
@@ -198,6 +198,49 @@ public class FlowDefAdminService {
|
||||
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 元数据原地编辑(2026-10-10 四议题·议题2,拍板 D4):已发布/停用/草稿均可只改 name/appKey
|
||||
* 而<b>不升版本</b>——结构改动仍走 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<String> enabledDeptKeys(Long schId) {
|
||||
return mapRepository.findEnabledBySchId(schId).stream()
|
||||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user