build / build-push (push) Successful in 1m30s
- 已办去重: 同一办件一行 + doneCount 累计次数(FlwTaskRepository 查询去重)
- 发起/撤回/重提留痕事件行(状态码 6/7, 无需 DDL); /oapi/flow/progress steps 含撤回/重提步骤
- 管理端新增启用回路: POST /admin/map/enable/{id}、POST /admin/def/enable/{id}; GET /api/flow/def/source 源码查询
- 修复 findEnabledByOrdId 未过滤 status 的隐性缺陷; 新增 DeptLabelSupport 科室标签支撑
- 测试: FlowEngineIT 撤回留痕/已办去重断言、OapiFlowContractIT 撤回步骤断言(94 tests 全过)
- 本地验签修复: 本地 UserBase 无 context-path, yh.open.services 显式覆盖服务目录枚举兜底的 /userbase basePath, 否则出站签名与服务端剥离不对齐 → GetUserInfo 等全部「验签失败」; 生产经 Caddy 挂 /userbase 时删除 services 覆盖
256 lines
12 KiB
Java
256 lines
12 KiB
Java
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);
|
||
appendTraceEvent(instance, FlwEnums.TaskStatus.NODE_SUBMIT, "发起申请",
|
||
FlwEnums.TaskStatus.SUBMITTED, cmd.submitterId, cmd.submitterName);
|
||
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());
|
||
appendTraceEvent(instance, FlwEnums.TaskStatus.NODE_RESUBMIT, "重新提交",
|
||
FlwEnums.TaskStatus.SUBMITTED, cmd.submitterId, cmd.submitterName);
|
||
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,
|
||
FlwEnums.TaskStatus.handledCodes()).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);
|
||
appendTraceEvent(instance, FlwEnums.TaskStatus.NODE_WITHDRAW, "撤回申请",
|
||
FlwEnums.TaskStatus.WITHDRAWN, instance.getSubmitterId(), instance.getSubmitterName());
|
||
Map<String, Object> data = new HashMap<>();
|
||
data.put("result", "WITHDRAWN");
|
||
eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.WITHDRAWN, instanceId, null, data));
|
||
}
|
||
|
||
/**
|
||
* 轨迹留痕事件行(2026-09-18):办件级动作(发起/重新提交、撤回)也落一条 flw_task 行,
|
||
* 让流程轨迹的时间线里这些动作本身可见(原实现只把待办批量置失效,撤回在轨迹上是空白)。
|
||
*
|
||
* <p>行 status=6/7 且 node_id 用保留值(submit/resubmit/withdraw,定义模型里不存在)——
|
||
* 不参与引擎任何判断:不进待办(非 0)、不进已办(handledCodes 不含 6/7)、不挡撤回守卫、
|
||
* 不进超期扫描与分支失效范围、示意图着色与退回目标解析也天然跳过。
|
||
*/
|
||
private void appendTraceEvent(FlwInstance instance, String nodeId, String nodeName,
|
||
FlwEnums.TaskStatus status, Integer operatorId, String operatorName) {
|
||
LocalDateTime now = LocalDateTime.now();
|
||
com.yunhai.flowcenter.entity.FlwTask row = new com.yunhai.flowcenter.entity.FlwTask();
|
||
row.setInstanceId(instance.getId());
|
||
row.setSchId(instance.getSchId());
|
||
row.setNodeId(nodeId);
|
||
row.setNodeName(nodeName);
|
||
// assignee_id 非空:发起/撤回的操作人恒为提交人;缺失(脏数据)时落 0,不产生僵尸待办
|
||
row.setAssigneeId(operatorId == null ? 0 : operatorId);
|
||
row.setAssigneeName(operatorName);
|
||
row.setStatus(status.getCode());
|
||
row.setArriveTime(now);
|
||
row.setHandleTime(now);
|
||
row.setUrgeCount(0);
|
||
taskRepository.save(row);
|
||
}
|
||
|
||
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;
|
||
}
|
||
}
|
||
}
|