feat(def): 流程定义唯一键改按流程线管 + 归属应用筛选
build / build-push (push) Successful in 8m31s

- flw_def 唯一键 (sch,code,ver)→(sch,flow_guid,ver)(uk_flw_def_guid_ver,生产/本地已手工切索引):升版本 maxByGuid+1 与唯一键同口径,同编码多条流程线(work-leave 三条线实锤追尾)不再死锁
- 新建永远新开流程线 v1,删除同编码草稿复用;撞唯一键转"保存冲突"友好提示
- 新增 flw_def.app_key 归属应用列:save 全链路透传(升版本缺省继承源线、传非空覆盖);/admin/def/list 支持 appKey 过滤;新增 /admin/def/appkeys 应用清单(flw_def 已标注 ∪ 已注册回调)
- FlowDefAdminServiceSaveTest 10 用例锁 save 分派语义
This commit is contained in:
chenxing
2026-10-10 14:34:25 +08:00
parent 7c92e953ec
commit a1f99fd7e0
6 changed files with 248 additions and 38 deletions
@@ -0,0 +1,171 @@
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.enums.FlwEnums;
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.flowcenter.repository.FlwRouterRepository;
import com.yunhai.platform.apistd.BizException;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.dao.DataIntegrityViolationException;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
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.verify;
import static org.mockito.Mockito.when;
/**
* save() 分派语义(2026-10-10 唯一键改 (sch,flow_guid,version) 后):
* 新建=永远新线 v1(编码只是标签);升版本=复制行 maxByGuid+1;唯一键冲突转友好 BizException。
* 注:@Nested 在 surefire 2.22 的 -Dtest 过滤下发现不了,保持拍平写法。
*/
class FlowDefAdminServiceSaveTest {
private static final Long SCH = 3L;
private static final String JSON = "{\"nodes\":[],\"edges\":[]}";
private FlwDefRepository defRepository;
private FlowDefAdminService service;
@BeforeEach
void setUp() {
defRepository = mock(FlwDefRepository.class);
service = new FlowDefAdminService(defRepository,
mock(FlwAssigneeMapRepository.class),
mock(FlwRouterRepository.class),
mock(com.yunhai.flowcenter.repository.FlwCallbackRegRepository.class),
mock(FlowEngine.class),
new ObjectMapper());
when(defRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));
}
private FlwDef def(long id, String flowGuid, String code, int version, int status) {
FlwDef d = new FlwDef();
d.setId(id);
d.setSchId(SCH);
d.setFlowGuid(flowGuid);
d.setCode(code);
d.setVersion(version);
d.setStatus(status);
d.setContentJson(JSON);
return d;
}
@Test
void 新建_永远新开流程线_版本从1起() {
ArgumentCaptor<FlwDef> cap = ArgumentCaptor.forClass(FlwDef.class);
FlwDef saved = service.save(SCH, null, "work-leave", "请假", JSON, null, null);
verify(defRepository).save(cap.capture());
assertEquals(cap.getValue(), saved);
assertEquals(1, saved.getVersion());
assertEquals(FlwEnums.DefStatus.DRAFT.getCode(), saved.getStatus());
assertEquals("work-leave", saved.getCode());
assertEquals(32, saved.getFlowGuid().length());
}
@Test
void 连续新建同编码_各得独立流程线() {
FlwDef a = service.save(SCH, null, "work-leave", "产假", JSON, null, null);
FlwDef b = service.save(SCH, null, "work-leave", "丧假", JSON, null, null);
assertEquals(1, a.getVersion());
assertEquals(1, b.getVersion());
assertNotEquals(a.getFlowGuid(), b.getFlowGuid());
}
@Test
void baseGuid_在线上开新版本草稿_max加1且保持flowGuid() {
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "G"))
.thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(6);
FlwDef saved = service.save(SCH, null, "work-leave", "请假", JSON, "G", null);
assertEquals("G", saved.getFlowGuid());
assertEquals(7, saved.getVersion());
assertEquals(FlwEnums.DefStatus.DRAFT.getCode(), saved.getStatus());
assertNull(saved.getId());
}
@Test
void 已发布行带id保存_复制升版本() {
when(defRepository.findById(9L)).thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
FlwDef saved = service.save(SCH, 9L, "work-leave", "请假", JSON, null, null);
assertEquals("G", saved.getFlowGuid());
assertEquals(5, saved.getVersion());
assertEquals(FlwEnums.DefStatus.DRAFT.getCode(), saved.getStatus());
assertNull(saved.getId());
}
@Test
void 草稿行带id保存_直改本行() {
FlwDef draft = def(9L, "G", "work-leave", 5, FlwEnums.DefStatus.DRAFT.getCode());
when(defRepository.findById(9L)).thenReturn(Optional.of(draft));
FlwDef saved = service.save(SCH, 9L, "work-leave", "新名字", JSON, null, null);
assertSame(draft, saved);
assertEquals("新名字", saved.getName());
}
@Test
void baseGuid流程线不存在_点名报错() {
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "GX"))
.thenReturn(Optional.empty());
BizException e = assertThrows(BizException.class,
() -> service.save(SCH, null, "work-leave", "请假", JSON, "GX", null));
assertTrue(e.getMessage().contains("baseGuid"));
}
@Test
void 新建_归属应用直接落库_空白规整为null() {
assertEquals("notice", service.save(SCH, null, "work-leave", "请假", JSON, null, "notice").getAppKey());
assertNull(service.save(SCH, null, "work-leave", "请假", JSON, null, " ").getAppKey());
}
@Test
void 升版本_appKey缺省继承源线_传非空则覆盖() {
FlwDef base = def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode());
base.setAppKey("notice");
when(defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(SCH, "G")).thenReturn(Optional.of(base));
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
assertEquals("notice", service.save(SCH, null, "work-leave", "请假", JSON, "G", null).getAppKey());
assertEquals("crm", service.save(SCH, null, "work-leave", "请假", JSON, "G", "crm").getAppKey());
}
@Test
void 草稿直改_传appKey覆盖_不传保留() {
FlwDef draft = def(9L, "G", "work-leave", 5, FlwEnums.DefStatus.DRAFT.getCode());
draft.setAppKey("notice");
when(defRepository.findById(9L)).thenReturn(Optional.of(draft));
assertEquals("notice", service.save(SCH, 9L, "work-leave", "请假", JSON, null, null).getAppKey());
assertEquals("crm", service.save(SCH, 9L, "work-leave", "请假", JSON, null, "crm").getAppKey());
}
@Test
void 并发升版本撞唯一键_转友好提示而非内部服务错误() {
when(defRepository.findById(9L)).thenReturn(Optional.of(def(9L, "G", "work-leave", 4, FlwEnums.DefStatus.PUBLISHED.getCode())));
when(defRepository.maxVersionByGuid(SCH, "G")).thenReturn(4);
when(defRepository.save(any())).thenThrow(new DataIntegrityViolationException("dup"));
BizException e = assertThrows(BizException.class,
() -> service.save(SCH, 9L, "work-leave", "请假", JSON, null, null));
assertTrue(e.getMessage().contains("保存冲突"));
}
}