- 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:
@@ -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("保存冲突"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user