- flw_app(sch,app_key uk):学校池(UserBase list-by-school,学校组单点已开通、clientType=null 不过滤端) diff 同步——新增即 enabled=1、改名静默刷新、池外保留;UserBase 不可达降级本地行 - /admin/app/list(触发同步)/ /admin/app/enable;defSave 显式 appKey 须 ∈ 启用集合(不传不拦,升版本继承不受影响) - SQL 备案:flw_app 建表 + UserBase 菜单 107 新增/104 下线(人工执行) - 单测 +8;E2E 13/13(local-debug/four-topics-c-e2e.py) - 关联 SDK 修复:BoaApp.createTime @JsonFormat 对齐 UserBase 时间契约(BasePlatform 单独 commit,Nexus -73)
223 lines
11 KiB
Java
223 lines
11 KiB
Java
package com.yunhai.flowcenter.service;
|
||
|
||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||
import com.yunhai.flowcenter.engine.DefValidator;
|
||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||
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.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;
|
||
|
||
/**
|
||
* 流程定义管理(M1:JSON 文本编辑模式,概览 §9.2):
|
||
* 保存=草稿直改/已发布升版本(flowGuid 跨版本不变);发布跑静态校验(把"任务无主"拦在发布期)。
|
||
*/
|
||
@Service
|
||
@RequiredArgsConstructor
|
||
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 com.yunhai.flowcenter.repository.FlwAppRepository appRepository;
|
||
private final FlowEngine flowEngine;
|
||
private final ObjectMapper objectMapper;
|
||
|
||
/** 定义列表: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 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);
|
||
}
|
||
|
||
/** 全量列表(管理端首页:各流程线最新版本+状态聚合展示简化为全量返回) */
|
||
public List<FlwDef> listAll(Long schId) {
|
||
return defRepository.findPublishedLatestPerGuid(schId);
|
||
}
|
||
|
||
public FlwDef get(Long schId, Long id) {
|
||
FlwDef def = requireDef(schId, id);
|
||
return def;
|
||
}
|
||
|
||
/**
|
||
* 保存(2026-10-10 起唯一键为 uk(sch,flow_guid,version),编码降级为可重名的分类标签):
|
||
* - id 非空:草稿直改;已发布/停用=复制升版本(同 flowGuid,version=max+1)
|
||
* - id 空 + baseGuid 非空:在该流程线上开新版本草稿(version=max+1,flowGuid 不变)——前端"升版本"
|
||
* - id 空:永远新开一条流程线(flowGuid 生成,version=1);同编码多条线合法,前端保存前有软提示
|
||
* appKey=归属应用(筛选维度):新建/草稿直改直接落库;升版本(baseGuid/已发布行)缺省继承源线,传非空则覆盖
|
||
*/
|
||
@Transactional
|
||
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();
|
||
// 归属应用白名单(2026-10-10 应用设置):显式传入非空时必须 ∈ flw_app(sch, enabled=1);
|
||
// 不传(升版本继承源线)不拦。不在学校池/未开启的应用由此拦下(拍板 D2:不做存量兜底)
|
||
if (cleanAppKey != null && !appRepository.isEnabledApp(schId, cleanAppKey)) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||
"归属应用未开启或不存在(" + cleanAppKey + "):请在 流程管理 > 应用设置 中开启后重试");
|
||
}
|
||
// JSON 先解析再落库(语法错误不进库)
|
||
FlowDefModel parsed = FlowDefModel.parse(objectMapper, contentJson);
|
||
if (code == null || code.trim().isEmpty() || name == null || name.trim().isEmpty()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "code/name 必填");
|
||
}
|
||
FlwDef def;
|
||
if (id == null && baseGuid != null && !baseGuid.trim().isEmpty()) {
|
||
// 在既有流程线上开新版本草稿(appKey 缺省继承源线)
|
||
FlwDef base = defRepository.findFirstBySchIdAndFlowGuidOrderByVersionDesc(schId, baseGuid.trim())
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "baseGuid 流程线不存在"));
|
||
def = new FlwDef();
|
||
def.setSchId(schId);
|
||
def.setFlowGuid(baseGuid.trim());
|
||
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) {
|
||
// 新建:永远新开一条流程线,版本从 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();
|
||
next.setSchId(schId);
|
||
next.setFlowGuid(def.getFlowGuid());
|
||
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;
|
||
}
|
||
}
|
||
def.setCode(code.trim());
|
||
def.setName(name.trim());
|
||
def.setContentJson(contentJson);
|
||
// 定义 JSON 里的 code/name 与行字段一致性校验(给绑定回显用行字段,JSON 内为主)
|
||
if (parsed.code != null && !parsed.code.trim().isEmpty() && !parsed.code.trim().equals(def.getCode())) {
|
||
def.setCode(parsed.code.trim());
|
||
}
|
||
if (parsed.name != null && !parsed.name.trim().isEmpty()) {
|
||
def.setName(parsed.name.trim());
|
||
}
|
||
try {
|
||
return defRepository.save(def);
|
||
} catch (DataIntegrityViolationException e) {
|
||
// 撞唯一键 (sch,flow_guid,version):并发升版本同一线时可能撞号,转可理解提示(原样抛出=笼统"内部服务错误")
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "保存冲突:该流程可能正被他人同时升版本,请返回列表刷新后重试");
|
||
}
|
||
}
|
||
|
||
/** 发布:静态校验(START/END 唯一、无孤儿无环、DEPT_KEY 有启用映射)后置为已发布 */
|
||
@Transactional
|
||
public FlwDef publish(Long schId, Long id) {
|
||
FlwDef def = requireDef(schId, id);
|
||
if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "已是发布状态");
|
||
}
|
||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||
def.setPublishTime(LocalDateTime.now());
|
||
return defRepository.save(def);
|
||
}
|
||
|
||
/** 停用:不可新发起,存量跑完 */
|
||
@Transactional
|
||
public void offline(Long schId, Long id) {
|
||
FlwDef def = requireDef(schId, id);
|
||
def.setStatus(FlwEnums.DefStatus.DISABLED.getCode());
|
||
defRepository.save(def);
|
||
}
|
||
|
||
/**
|
||
* 启用(2026-09-18):只对"停用"版恢复为已发布,与 offline 对称。
|
||
* 复用发布期的静态校验(把"任务无主"重新拦一次)——若建的时候某科室还有启用办理人、之后
|
||
* 被停用,这里会点名报错(如"科室 X 无启用办理人"),管理员在办理人映射页点"启用"即可解。
|
||
*/
|
||
@Transactional
|
||
public FlwDef enable(Long schId, Long id) {
|
||
FlwDef def = requireDef(schId, id);
|
||
if (def.getStatus() == FlwEnums.DefStatus.PUBLISHED.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "该流程已是发布状态,无需启用");
|
||
}
|
||
if (def.getStatus() != FlwEnums.DefStatus.DISABLED.getCode()) {
|
||
throw new BizException(ErrorCode.PARAM_ERROR, "仅停用的流程可启用;草稿请走发布");
|
||
}
|
||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||
def.setPublishTime(LocalDateTime.now());
|
||
return defRepository.save(def);
|
||
}
|
||
|
||
/** 同一流程线全部版本(降序) */
|
||
public List<FlwDef> versions(Long schId, String flowGuid) {
|
||
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
|
||
}
|
||
|
||
private Set<String> enabledDeptKeys(Long schId) {
|
||
return mapRepository.findEnabledBySchId(schId).stream()
|
||
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
|
||
}
|
||
|
||
/** CONDITION routerKey 发布校验白名单(flw_router 启用键全集,全局不分校) */
|
||
private Set<String> enabledRouterKeys() {
|
||
return routerRepository.findByStatusOrderByIdAsc(1).stream()
|
||
.map(com.yunhai.flowcenter.entity.FlwRouter::getRouterKey)
|
||
.collect(Collectors.toSet());
|
||
}
|
||
|
||
private FlwDef requireDef(Long schId, Long id) {
|
||
FlwDef def = defRepository.findById(id)
|
||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义不存在:" + id));
|
||
if (!def.getSchId().equals(schId)) {
|
||
throw new BizException(ErrorCode.FORBIDDEN, "流程定义不属于该学校");
|
||
}
|
||
return def;
|
||
}
|
||
|
||
}
|