Spring Boot 流程引擎服务:流程定义/实例/任务、审批人解析、 回调分发、oapi 接口及单元测试。 保留远程 .gitea/workflows/build.yml 与 docker/Dockerfile, 新增 .gitignore 排除 target/ 构建产物。
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
package com.yunhai.flowcenter.service;
|
||||
|
||||
import com.yunhai.flowcenter.engine.FlowDefModel;
|
||||
import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.engine.FlowEvent;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.repository.FlwBranchRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwTaskRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 办件生命周期:代发起(幂等 + R4 重启重提)、撤回。
|
||||
*
|
||||
* <p><b>R4 驳回/撤回后重提语义(开工说明 §7#1,详设定稿)</b>:biz uk 命中已终态办件
|
||||
* (30 已拒绝 / 40 已撤回)时复用同一办件行"重启推进"——不新建、不报错:定义刷新为当前
|
||||
* 已发布版本、提交快照全量覆盖、旧分支行清空、残留待办失效、status 回 10、提交时间刷新、
|
||||
* version 自增(镜像方按 instanceVersion 单调推进到 10)。任务历史保留(1/2/3 状态行不动),
|
||||
* 轨迹跨轮连续。status=10/20(在途/已完成)命中则纯幂等返回原办件(duplicated=true)。
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class FlowInstanceService {
|
||||
|
||||
private final FlwInstanceRepository instanceRepository;
|
||||
private final FlwDefRepository defRepository;
|
||||
private final FlwTaskRepository taskRepository;
|
||||
private final FlwBranchRepository branchRepository;
|
||||
private final FlowEngine flowEngine;
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
|
||||
/** 代发起申请体(oapi apply 与未来原生发起共用字段) */
|
||||
@RequiredArgsConstructor
|
||||
public static class ApplyCommand {
|
||||
public final Long schId;
|
||||
public final FlwDef def;
|
||||
public final String bizType;
|
||||
public final String bizId;
|
||||
public final String bizTitle;
|
||||
public final String bizUrl;
|
||||
public final String bizSnapshot;
|
||||
public final Integer submitterId;
|
||||
public final String submitterName;
|
||||
public final String formDataJson;
|
||||
}
|
||||
|
||||
/** 代发起结果:instanceId + duplicated(命中既有办件=true,含重启重提) */
|
||||
@Transactional
|
||||
public ApplyOutcome apply(ApplyCommand cmd) {
|
||||
FlwInstance existing = instanceRepository
|
||||
.findBySchIdAndBizTypeAndBizId(cmd.schId, cmd.bizType, cmd.bizId).orElse(null);
|
||||
if (existing != null) {
|
||||
int status = existing.getStatus();
|
||||
if (status == FlwEnums.InstanceStatus.APPROVING.getCode()
|
||||
|| status == FlwEnums.InstanceStatus.COMPLETED.getCode()) {
|
||||
return new ApplyOutcome(existing.getId(), true);
|
||||
}
|
||||
// 30/40:R4 重启推进(复用办件行,定义/快照/时间刷新,重新走 START)
|
||||
restart(existing, cmd);
|
||||
return new ApplyOutcome(existing.getId(), true);
|
||||
}
|
||||
FlwInstance instance = new FlwInstance();
|
||||
instance.setSchId(cmd.schId);
|
||||
instance.setDefId(cmd.def.getId());
|
||||
instance.setDefCode(cmd.def.getCode());
|
||||
instance.setDefVersion(cmd.def.getVersion());
|
||||
instance.setBizType(cmd.bizType);
|
||||
instance.setBizId(cmd.bizId);
|
||||
instance.setBizTitle(cmd.bizTitle);
|
||||
instance.setBizUrl(cmd.bizUrl);
|
||||
instance.setBizSnapshot(cmd.bizSnapshot);
|
||||
instance.setFormDataJson(cmd.formDataJson);
|
||||
instance.setSubmitterId(cmd.submitterId);
|
||||
instance.setSubmitterName(cmd.submitterName);
|
||||
instance.setStatus(FlwEnums.InstanceStatus.APPROVING.getCode());
|
||||
instance.setSubmitTime(LocalDateTime.now());
|
||||
instanceRepository.saveAndFlush(instance);
|
||||
flowEngine.startAdvance(instance, cmd.def);
|
||||
publishSubmitted(instance);
|
||||
return new ApplyOutcome(instance.getId(), false);
|
||||
}
|
||||
|
||||
private void restart(FlwInstance instance, ApplyCommand cmd) {
|
||||
log.info("[flow-center] R4 重启推进:instance={} 原 status={} biz={}/{}",
|
||||
instance.getId(), instance.getStatus(), cmd.bizType, cmd.bizId);
|
||||
instance.setDefId(cmd.def.getId());
|
||||
instance.setDefCode(cmd.def.getCode());
|
||||
instance.setDefVersion(cmd.def.getVersion());
|
||||
instance.setBizTitle(cmd.bizTitle);
|
||||
instance.setBizUrl(cmd.bizUrl);
|
||||
instance.setBizSnapshot(cmd.bizSnapshot);
|
||||
instance.setFormDataJson(cmd.formDataJson);
|
||||
instance.setSubmitterId(cmd.submitterId);
|
||||
instance.setSubmitterName(cmd.submitterName);
|
||||
instance.setStatus(FlwEnums.InstanceStatus.APPROVING.getCode());
|
||||
instance.setSubmitTime(LocalDateTime.now());
|
||||
instance.setFinishTime(null);
|
||||
instanceRepository.saveAndFlush(instance);
|
||||
taskRepository.invalidatePendingByInstance(instance.getId());
|
||||
branchRepository.deleteByInstance(instance.getId());
|
||||
flowEngine.startAdvance(instance, cmd.def);
|
||||
publishSubmitted(instance);
|
||||
}
|
||||
|
||||
/**
|
||||
* 撤回(概览 §5.3):仅当尚无任何任务被办理时允许;全部待办失效、分支失效、办件=40。
|
||||
*/
|
||||
@Transactional
|
||||
public void withdraw(Long schId, Long instanceId) {
|
||||
FlwInstance instance = requireInstance(schId, instanceId);
|
||||
if (instance.getStatus() != FlwEnums.InstanceStatus.APPROVING.getCode()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "仅审批中的办件可撤回");
|
||||
}
|
||||
if (!taskRepository.findByInstanceIdAndStatusIn(instanceId,
|
||||
java.util.Arrays.asList(FlwEnums.TaskStatus.APPROVED.getCode(),
|
||||
FlwEnums.TaskStatus.REJECTED.getCode())).isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "已有任务被办理,不能撤回");
|
||||
}
|
||||
instance.setStatus(FlwEnums.InstanceStatus.WITHDRAWN.getCode());
|
||||
instance.setFinishTime(LocalDateTime.now());
|
||||
instanceRepository.saveAndFlush(instance);
|
||||
taskRepository.invalidatePendingByInstance(instanceId);
|
||||
branchRepository.invalidateRunningByInstance(instanceId);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("result", "WITHDRAWN");
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.WITHDRAWN, instanceId, null, data));
|
||||
}
|
||||
|
||||
public FlwInstance requireInstance(Long schId, Long instanceId) {
|
||||
FlwInstance instance = instanceRepository.findById(instanceId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在:" + instanceId));
|
||||
if (!instance.getSchId().equals(schId)) {
|
||||
throw new BizException(ErrorCode.FORBIDDEN, "办件不属于该学校");
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
public FlwDef requireDef(Long defId) {
|
||||
return defRepository.findById(defId)
|
||||
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "流程定义不存在:" + defId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 催办(概览 §5.3):提交人/管理员对办件的全部待办任务各催一次(URGE 事件 → UserMsg 通道,
|
||||
* msgNo=urge:{taskId}:{N} 幂等);次数上限取定义 settings.urge.maxTimes(默认 3;
|
||||
* enabled=false → 0 = 不限次)。非提交人调用须是本校管理员(控制器已按 BoaUser 判定放行)。
|
||||
*/
|
||||
@Transactional
|
||||
public void urgeInstance(Long schId, Long instanceId, Integer operatorId) {
|
||||
FlwInstance instance = requireInstance(schId, instanceId);
|
||||
FlowDefModel model = flowEngine.parseModel(requireDef(instance.getDefId()));
|
||||
int maxTimes = urgeMaxTimes(model);
|
||||
java.util.List<com.yunhai.flowcenter.entity.FlwTask> pendings =
|
||||
taskRepository.findByInstanceIdAndStatus(instanceId,
|
||||
com.yunhai.flowcenter.enums.FlwEnums.TaskStatus.PENDING.getCode());
|
||||
if (pendings.isEmpty()) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "无待办任务可催办");
|
||||
}
|
||||
for (com.yunhai.flowcenter.entity.FlwTask task : pendings) {
|
||||
if (maxTimes > 0 && task.getUrgeCount() >= maxTimes) {
|
||||
continue;
|
||||
}
|
||||
task.setUrgeCount(task.getUrgeCount() + 1);
|
||||
taskRepository.save(task);
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("taskId", task.getId());
|
||||
data.put("urgeCount", task.getUrgeCount());
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.URGE,
|
||||
instanceId, task.getId(), data));
|
||||
}
|
||||
}
|
||||
|
||||
private static int urgeMaxTimes(FlowDefModel model) {
|
||||
if (model == null || model.settings == null || model.settings.urge == null) {
|
||||
return 3;
|
||||
}
|
||||
FlowDefModel.Urge urge = model.settings.urge;
|
||||
if (Boolean.FALSE.equals(urge.enabled)) {
|
||||
return 0;
|
||||
}
|
||||
return urge.maxTimes == null || urge.maxTimes <= 0 ? 3 : urge.maxTimes;
|
||||
}
|
||||
|
||||
private void publishSubmitted(FlwInstance instance) {
|
||||
FlowDefModel model = flowEngine.parseModel(requireDef(instance.getDefId()));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("nodeSummary", FlowEngine.nodeSummary(model));
|
||||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.SUBMITTED, instance.getId(), null, data));
|
||||
}
|
||||
|
||||
/** 代发起结果值对象 */
|
||||
@SuppressWarnings("unused")
|
||||
public static class ApplyOutcome {
|
||||
private final Long instanceId;
|
||||
private final boolean duplicated;
|
||||
|
||||
public ApplyOutcome(Long instanceId, boolean duplicated) {
|
||||
this.instanceId = instanceId;
|
||||
this.duplicated = duplicated;
|
||||
}
|
||||
|
||||
public Long getInstanceId() {
|
||||
return instanceId;
|
||||
}
|
||||
|
||||
public boolean isDuplicated() {
|
||||
return duplicated;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user