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:
chenxing
2026-10-10 19:23:44 +08:00
parent d6b7a68ec3
commit 4293c9628a
3 changed files with 178 additions and 0 deletions
@@ -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());