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
@@ -54,8 +54,15 @@ public class FlowAdminController {
@GetMapping("/def/list")
public List<FlwDef> defList(BoaUser user,
@RequestParam(value = "status", required = false) Integer status) {
return defAdminService.list(adminGuard.requireAdmin(user, null), status);
@RequestParam(value = "status", required = false) Integer status,
@RequestParam(value = "appKey", required = false) String appKey) {
return defAdminService.list(adminGuard.requireAdmin(user, null), status, appKey);
}
/** 应用清单("按应用筛选"下拉):本校 flw_def 已标注 ∪ 已注册回调的应用 */
@GetMapping("/def/appkeys")
public List<String> defAppKeys(BoaUser user) {
return defAdminService.distinctAppKeys(adminGuard.requireAdmin(user, null));
}
@GetMapping("/def/get")
@@ -70,7 +77,8 @@ public class FlowAdminController {
Long id = body.get("id") == null ? null : Long.valueOf(String.valueOf(body.get("id")));
return defAdminService.save(schId, id,
(String) body.get("code"), (String) body.get("name"),
(String) body.get("contentJson"), (String) body.get("baseGuid"));
(String) body.get("contentJson"), (String) body.get("baseGuid"),
(String) body.get("appKey"));
}
@PostMapping("/def/publish/{id}")
@@ -20,8 +20,8 @@ import java.time.LocalDateTime;
@Setter
@Entity
@Table(name = "flw_def",
uniqueConstraints = @UniqueConstraint(name = "uk_flw_def_sch_code_ver",
columnNames = {"sch_id", "code", "version"}),
uniqueConstraints = @UniqueConstraint(name = "uk_flw_def_guid_ver",
columnNames = {"sch_id", "flow_guid", "version"}),
indexes = @Index(name = "idx_flw_def_guid", columnList = "sch_id, flow_guid, status, version"))
public class FlwDef extends BaseEntity {
@@ -33,7 +33,10 @@ public class FlwDef extends BaseEntity {
@Column(name = "flow_guid", nullable = false, length = 32)
private String flowGuid;
/** 流程编码(本应用内部管理键) */
/**
* 流程编码(管理端分类/检索标签,可重名)。2026-10-10 起唯一键改为 (sch_id, flow_guid, version),
* 编码不再承担唯一性——多条流程线(如同为请假的产假/丧假)可共用一个编码。
*/
@Column(name = "code", nullable = false, length = 64)
private String code;
@@ -41,6 +44,10 @@ public class FlwDef extends BaseEntity {
@Column(name = "name", nullable = false, length = 128)
private String name;
/** 归属应用(子应用 appKey,可空=未分配;管理端"按应用筛选"维度,2026-10-10) */
@Column(name = "app_key", length = 64)
private String appKey;
/** 版本,发布后不可改,改定义=升版本 */
@Column(name = "version", nullable = false)
private Integer version;
@@ -2,6 +2,8 @@ package com.yunhai.flowcenter.repository;
import com.yunhai.flowcenter.entity.FlwCallbackReg;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import java.util.List;
import java.util.Optional;
@@ -14,4 +16,8 @@ public interface FlwCallbackRegRepository extends JpaRepository<FlwCallbackReg,
List<FlwCallbackReg> findBySchIdAndFlowGuidAndStatus(Long schId, String flowGuid, Integer status);
List<FlwCallbackReg> findBySchIdOrderByUpdateTimeDesc(Long schId);
/** "按应用筛选"下拉数据源之一:本校注册过回调的应用 */
@Query("select distinct r.appKey from FlwCallbackReg r where r.schId = :schId")
List<String> findDistinctAppKeysBySchId(@Param("schId") Long schId);
}
@@ -18,16 +18,12 @@ public interface FlwDefRepository extends JpaRepository<FlwDef, Long> {
/** 任意状态(回调注册只要求流程线存在) */
Optional<FlwDef> findFirstBySchIdAndFlowGuidOrderByVersionDesc(Long schId, String flowGuid);
/** 同名草稿(保存新建时复用,避免 uk 冲突) */
Optional<FlwDef> findFirstBySchIdAndCodeAndStatus(Long schId, String code, Integer status);
@Query("select coalesce(max(d.version), 0) from FlwDef d where d.schId = :schId and d.code = :code")
int maxVersionByCode(@Param("schId") Long schId, @Param("code") String code);
@Query("select coalesce(max(d.version), 0) from FlwDef d where d.schId = :schId and d.flowGuid = :flowGuid")
int maxVersionByGuid(@Param("schId") Long schId, @Param("flowGuid") String flowGuid);
Optional<FlwDef> findFirstBySchIdAndCodeOrderByVersionDesc(Long schId, String code);
/** "按应用筛选"下拉数据源之一:本校 flw_def 已标注的应用 */
@Query("select distinct d.appKey from FlwDef d where d.schId = :schId and d.appKey is not null")
List<String> findDistinctAppKeys(@Param("schId") Long schId);
List<FlwDef> findBySchIdAndFlowGuidOrderByVersionDesc(Long schId, String flowGuid);
@@ -12,12 +12,15 @@ import com.yunhai.flowcenter.repository.FlwDefRepository;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.platform.utils.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.TreeSet;
import java.util.UUID;
import java.util.stream.Collectors;
@@ -32,15 +35,28 @@ public class FlowDefAdminService {
private final FlwDefRepository defRepository;
private final FlwAssigneeMapRepository mapRepository;
private final com.yunhai.flowcenter.repository.FlwRouterRepository routerRepository;
private final com.yunhai.flowcenter.repository.FlwCallbackRegRepository regRepository;
private final FlowEngine flowEngine;
private final ObjectMapper objectMapper;
/** 定义列表:status 空=全量(各流程线各版本都展示,管理端"版本历史"内聚在行操作里) */
public List<FlwDef> list(Long schId, Integer status) {
if (status != null) {
return defRepository.findBySchIdAndStatusOrderByUpdateTimeDesc(schId, status);
/** 定义列表:status/appKey 可选过滤(各流程线各版本都展示;appKey 精确匹配,2026-10-10 按应用筛选) */
public List<FlwDef> list(Long schId, Integer status, String appKey) {
List<FlwDef> rows = status != null
? defRepository.findBySchIdAndStatusOrderByUpdateTimeDesc(schId, status)
: defRepository.findBySchIdOrderByUpdateTimeDesc(schId);
String key = appKey == null ? null : appKey.trim();
if (key != null && !key.isEmpty()) {
rows = rows.stream().filter(d -> key.equals(d.getAppKey())).collect(Collectors.toList());
}
return defRepository.findBySchIdOrderByUpdateTimeDesc(schId);
return rows;
}
/** 应用清单("按应用筛选"下拉数据源):本校 flw_def 已标注 ∪ 已注册回调的应用,去重排序 */
public List<String> distinctAppKeys(Long schId) {
TreeSet<String> keys = new TreeSet<>();
keys.addAll(defRepository.findDistinctAppKeys(schId));
keys.addAll(regRepository.findDistinctAppKeysBySchId(schId));
return new ArrayList<>(keys);
}
/** 全量列表(管理端首页:各流程线最新版本+状态聚合展示简化为全量返回) */
@@ -54,14 +70,15 @@ public class FlowDefAdminService {
}
/**
* 保存(新建语义按 baseGuid/同名草稿分派,避免 uk(sch,code,version) 冲突):
* 保存(2026-10-10 起唯一键为 uk(sch,flow_guid,version),编码降级为可重名的分类标签):
* - id 非空:草稿直改;已发布/停用=复制升版本(同 flowGuid,version=max+1)
* - id 空 + baseGuid 非空:在该流程线上开新版本草稿(version=max+1,flowGuid 不变)——前端"升版本"
* - id 空 + 同名草稿存在:复用该草稿直改
* - id 空 + 全新 code:新流程线(flowGuid 生成,version=1)
* - id 空:永远新开一条流程线(flowGuid 生成,version=1);同编码多条线合法,前端保存前有软提示
* appKey=归属应用(筛选维度):新建/草稿直改直接落库;升版本(baseGuid/已发布行)缺省继承源线,传非空则覆盖
*/
@Transactional
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid) {
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid, String appKey) {
String cleanAppKey = (appKey == null || appKey.trim().isEmpty()) ? null : appKey.trim();
// JSON 先解析再落库(语法错误不进库)
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
@@ -69,8 +86,8 @@ public class FlowDefAdminService {
}
FlwDef def;
if (id == null && baseGuid != null && !baseGuid.trim().isEmpty()) {
// 在既有流程线上开新版本草稿
defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, baseGuid.trim())
// 在既有流程线上开新版本草稿(appKey 缺省继承源线)
FlwDef base = defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, baseGuid.trim())
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "baseGuid 流程线不存在"));
def = new FlwDef();
def.setSchId(schId);
@@ -78,25 +95,24 @@ public class FlowDefAdminService {
def.setCode(code.trim());
def.setVersion(defRepository.maxVersionByGuid(schId, baseGuid.trim()) + 1);
def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
def.setAppKey(cleanAppKey != null ? cleanAppKey : base.getAppKey());
} else if (id == null) {
FlwDef draft = defRepository.findFirstBySchIdAndCodeAndStatus(
schId, code.trim(), FlwEnums.DefStatus.DRAFT.getCode()).orElse(null);
if (draft != null) {
// 同名草稿复用直改
def = draft;
} else {
def = new FlwDef();
def.setSchId(schId);
def.setFlowGuid(UUID.randomUUID().toString().replace("-", "").toLowerCase());
def.setCode(code.trim());
def.setVersion(defRepository.maxVersionByCode(schId, code.trim()) + 1);
def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
}
// 新建:永远新开一条流程线,版本从 1 起(不再做同编码草稿复用)
def = new FlwDef();
def.setSchId(schId);
def.setFlowGuid(UUID.randomUUID().toString().replace("-", "").toLowerCase());
def.setCode(code.trim());
def.setVersion(1);
def.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
def.setAppKey(cleanAppKey);
} else {
def = requireDef(schId, id);
if (!def.getCode().equals(code.trim())) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程编码不可修改");
}
if (cleanAppKey != null) {
def.setAppKey(cleanAppKey);
}
if (def.getStatus() != FlwEnums.DefStatus.DRAFT.getCode()) {
// 升版本:复制行,保持 flowGuid
FlwDef next = new FlwDef();
@@ -105,6 +121,7 @@ public class FlowDefAdminService {
next.setCode(def.getCode());
next.setVersion(defRepository.maxVersionByGuid(schId, def.getFlowGuid()) + 1);
next.setStatus(FlwEnums.DefStatus.DRAFT.getCode());
next.setAppKey(cleanAppKey != null ? cleanAppKey : def.getAppKey());
def = next;
}
}
@@ -118,7 +135,12 @@ public class FlowDefAdminService {
if (parsed.name != null && !parsed.name.trim().isEmpty()) {
def.setName(parsed.name.trim());
}
return defRepository.save(def);
try {
return defRepository.save(def);
} catch (DataIntegrityViolationException e) {
// 撞唯一键 (sch,flow_guid,version):并发升版本同一线时可能撞号,转可理解提示(原样抛出=笼统"内部服务错误")
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时升版本,请返回列表刷新后重试");
}
}
/** 发布:静态校验(START/END 唯一、无孤儿无环、DEPT_KEY 有启用映射)后置为已发布 */