feat: 提交流程中心后端代码
build / build-push (push) Successful in 28s

Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、
回调分发、oapi 接口及单元测试。
保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile,
新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
chenxing
2026-09-15 10:42:56 +08:00
parent 0296b2b5dd
commit c1103aeb29
65 changed files with 7119 additions and 0 deletions
@@ -0,0 +1,161 @@
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.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
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 FlowEngine flowEngine;
private final ObjectMapper objectMapper;
/** 定义列表:status 空=全量(各流程线各版本都展示,管理端"版本历史"内聚在行操作里) */
public List<FlwDef> list(Long schId, Integer status) {
if (status != null) {
return defRepository.findBySchIdAndStatusOrderByUpdateTimeDesc(schId, status);
}
return defRepository.findBySchIdOrderByUpdateTimeDesc(schId);
}
/** 全量列表(管理端首页:各流程线最新版本+状态聚合展示简化为全量返回) */
public List<FlwDef> listAll(Long schId) {
return defRepository.findPublishedLatestPerGuid(schId);
}
public FlwDef get(Long schId, Long id) {
FlwDef def = requireDef(schId, id);
return def;
}
/**
* 保存(新建语义按 baseGuid/同名草稿分派,避免 uk(sch,code,version) 冲突):
* - id 非空:草稿直改;已发布/停用=复制升版本(同 flowGuid,version=max+1)
* - id 空 + baseGuid 非空:在该流程线上开新版本草稿(version=max+1,flowGuid 不变)——前端"升版本"
* - id 空 + 同名草稿存在:复用该草稿直改
* - id 空 + 全新 code:新流程线(flowGuid 生成,version=1)
*/
@Transactional
public FlwDef save(Long schId, Long id, String code, String name, String contentJson, String baseGuid) {
// 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()) {
// 在既有流程线上开新版本草稿
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());
} 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());
}
} else {
def = requireDef(schId, id);
if (!def.getCode().equals(code.trim())) {
throw new BizException(ErrorCode.PARAM_ERROR, "流程编码不可修改");
}
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());
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());
}
return defRepository.save(def);
}
/** 发布:静态校验(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());
Set<String> enabledDeptKeys = mapRepository.findEnabledBySchId(schId).stream()
.map(FlwAssigneeMap::getDeptKey).collect(Collectors.toSet());
DefValidator.validate(model, enabledDeptKeys);
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);
}
/** 同一流程线全部版本(降序) */
public List<FlwDef> versions(Long schId, String flowGuid) {
return defRepository.findBySchIdAndFlowGuidOrderByVersionDesc(schId, flowGuid);
}
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;
}
}