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 重启重提)、撤回。 * *

R4 驳回/撤回后重提语义(开工说明 §7#1,详设定稿):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 data = new HashMap<>(); data.put("result", "WITHDRAWN"); eventPublisher.publishEvent(FlowEvent.of(FlowEvent.Type.WITHDRAWN, instanceId, null, data)); } /** * 轨迹留痕事件行(2026-09-18):办件级动作(发起/重新提交、撤回)也落一条 flw_task 行, * 让流程轨迹的时间线里这些动作本身可见(原实现只把待办批量置失效,撤回在轨迹上是空白)。 * *

行 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 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 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 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; } } }