- 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:
@@ -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 有启用映射)后置为已发布 */
|
||||
|
||||
Reference in New Issue
Block a user