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.model.FlowApplyRetryModels; 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; /** * 引擎实例生命周期与迁移前代发起。正式命令通过 create/startFormalInstance 建立独立轮次实例, * 命令受理、坐标锁和轮次分配归 FormalApplyCommandService;下述 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; /** 调用方验签身份快照(yh.app.auth;本地/无签名场景为 null) */ public final String applyAppKey; /** 调用方 app_code 快照(by-key 一次换得;UserBase 不可达为 null→消息回落发送方归属) */ public final String applyAppCode; /** 发起时指派快照 JSON(oapi apply 校验后渲染;null=无指派) */ public final String assignJson; /** * 受理载荷指纹(2026-10-25 T04,SHA-256 hex;非构造参数——13 参构造被 * {@code FlowEngineIT} 逐位钉住,由 {@code OapiFlowService} 在构建后赋值; * null=存量调用方,指纹核对缺省关)。 */ public String payloadHash; } /** 代发起结果: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.setAssignJson(cmd.assignJson); instance.setApplyAppKey(cmd.applyAppKey); instance.setApplyAppCode(cmd.applyAppCode); 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) { restart(instance, cmd, null, null); } /** * R4 重启推进(语义不变:定义刷新、快照覆盖、旧分支清空、status 回 10、version 自增、轨迹跨轮连续)。 * 新增的两个参数只是随行落库的请求标识与轮次,不改任何节点算法(任务书 C.3 红线: * 不为修复幂等而重写流程节点算法)。旧 apply 路径传 null ⇒ 行为与历史逐字一致。 */ private void restart(FlwInstance instance, ApplyCommand cmd, String requestToken, Integer round) { 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.setAssignJson(cmd.assignJson); // 调用方身份快照跟随最新一次 apply 刷新(重启=新一轮代发起,调用方可能已变) instance.setApplyAppKey(cmd.applyAppKey); instance.setApplyAppCode(cmd.applyAppCode); instance.setSubmitterId(cmd.submitterId); instance.setSubmitterName(cmd.submitterName); // 新一轮才更新请求标识与轮次;同标识重投走不到这里(applyGuarded 已分支挡掉) if (requestToken != null) { instance.setApplyRequestToken(requestToken); } if (round != null) { instance.setApplyRound(round); } if (cmd.payloadHash != null) { // 新一轮命令重新取指纹(新轮=新命令,指纹跟随最新受理载荷) instance.setApplyPayloadHash(cmd.payloadHash); } 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, instance.getSubmitterId(), instance.getSubmitterName(), 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, instance.getSubmitterId(), instance.getSubmitterName(), 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; } } // ==================== 请求标识与轮次守卫(v2 §8,TASK_2 C.3) ==================== /** * 守卫参数。刻意不做成 {@code ApplyCommand} 的新字段:那个 13 参构造被基线集成测试 * {@code FlowEngineIT} 逐位钉住,加参数会让整棵测试树编译失败(同 svc-hall 的 setter 注入先例)。 * 旧 {@link #apply(ApplyCommand)} 路径行为逐字不变,只有 {@link #applyGuarded} 吃这个策略。 */ @RequiredArgsConstructor public static class ApplyRetryPolicy { public final String requestToken; public final Integer attempt; public final boolean retryOnly; /** * 期望的前一轮次(2026-10-25 T04 服务端轮次收口,可选):显式申报"我基于第 N 轮重提"。 * 非空时:行轮次 ≠ N ⇒ STALE_ROUND(不重启);通过则服务端分配 round=N+1, * 不信客户端 attempt。null=既有客户端 attempt 语义(存量兼容)。 */ public final Integer expectedPrevRound; public ApplyRetryPolicy(String requestToken, Integer attempt, boolean retryOnly) { this(requestToken, attempt, retryOnly, null); } } /** 守卫化结果:除 instanceId/duplicated 外,把"命中的是哪一种情形"落成稳定分支码 */ @RequiredArgsConstructor public static class GuardedOutcome { public final Long instanceId; public final boolean duplicated; public final String branch; public final Integer applyRound; } /** * 带守卫的代发起(新 {@code /oapi/flow/apply/v2} 唯一入口)。 * *

判定顺序(每一支都只回读现状,不改流程状态机一行算法):

*
    *
  1. 先核归属:行上的调用应用/提交者/流程线与本次请求不符 ⇒ 拒绝, * 不能把别的应用相同 bizType 键当自己的结果返回(C.3)。 * 应用身份两侧都非空才判(本地无签名的历史行为 null,不因此把存量件判死);
  2. *
  3. 同 {@code requestToken} 命中 ⇒ {@code SAME_REQUEST}:这是同一次命令的重投, * 即使行已终态也不重启——那正是"响应丢失后重发"与"用户重提"的分界;
  4. *
  5. 10/20 命中 ⇒ {@code IDEMPOTENT}(既有语义不变);
  6. *
  7. 30/40 命中:{@code retryOnly=true} ⇒ {@code REPLAY_ONLY} 不重启; * 请求轮次 ≤ 行轮次 ⇒ {@code STALE_ROUND} 不重启; * 否则才是新一轮授权重提 ⇒ {@code RESTARTED}(走既有 R4 重启推进);
  8. *
  9. 无行 ⇒ 新建 + {@code CREATED}。
  10. *
*/ @Transactional public GuardedOutcome applyGuarded(ApplyCommand cmd, ApplyRetryPolicy policy) { FlwInstance existing = instanceRepository .findBySchIdAndBizTypeAndBizId(cmd.schId, cmd.bizType, cmd.bizId).orElse(null); if (existing != null) { return settleExisting(existing, cmd, policy); } String token = policy == null ? null : trimToNull(policy.requestToken); Integer round = policy == null || policy.attempt == null ? Integer.valueOf(1) : policy.attempt; createInstance(cmd, token, round); return new GuardedOutcome(instanceOf(cmd), false, FlowApplyRetryModels.Branch.CREATED, round); } private GuardedOutcome settleExisting(FlwInstance existing, ApplyCommand cmd, ApplyRetryPolicy policy) { requireSameOwner(existing, cmd); String token = policy == null ? null : trimToNull(policy.requestToken); // 行上轮次为 NULL(v2 之前建的存量件)按 1 处理:这样"没有更高轮次"就不能重启别人的旧件, // 新一轮授权重提(attempt≥2)才走得动 R4——存量行的守卫缺省是**关**而不是开。 int currentRound = existing.getApplyRound() == null ? 1 : existing.getApplyRound(); if (token != null && token.equals(existing.getApplyRequestToken())) { // T04(2026-10-25):同 token 必须同指纹——同 ID 不同载荷是两份命令撞同一 token, // 按冲突拒绝,不伪装成幂等成功。存量行(指纹 NULL)或请求无指纹时跳过(守卫缺省关)。 if (existing.getApplyPayloadHash() != null && cmd.payloadHash != null && !existing.getApplyPayloadHash().equals(cmd.payloadHash)) { log.warn("[flow-center] 同请求标识但载荷指纹不一致(冲突拒绝):instance={} biz={}/{}", existing.getId(), cmd.bizType, cmd.bizId); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.CONFLICT, existing.getApplyRound()); } log.info("[flow-center] 同请求标识重投命中,只回读现状不重启:instance={} biz={}/{} status={}", existing.getId(), cmd.bizType, cmd.bizId, existing.getStatus()); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.SAME_REQUEST, existing.getApplyRound()); } int status = existing.getStatus(); if (status == FlwEnums.InstanceStatus.APPROVING.getCode() || status == FlwEnums.InstanceStatus.COMPLETED.getCode()) { // T04:带 token 的请求撞上在途/通过件且 token 不符=另一份命令(审计 P0-2: // "10/20 即使异命令也回 IDEMPOTENT"是把他人命令的成功当自己的)。无 token 的 // 存量调用方保持 IDEMPOTENT(守卫缺省关)。 if (token != null) { log.warn("[flow-center] 在途/通过件命中且请求 token 不符(冲突拒绝):instance={} biz={}/{}", existing.getId(), cmd.bizType, cmd.bizId); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.CONFLICT, existing.getApplyRound()); } return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.IDEMPOTENT, existing.getApplyRound()); } if (policy != null && policy.retryOnly) { // 恢复链的"查无才补发"落在已有行上:30/40 也绝不重启(C.2 点名) log.warn("[flow-center] retryOnly 命中终态办件,拒绝重启:instance={} status={}", existing.getId(), status); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.REPLAY_ONLY, existing.getApplyRound()); } // T04 服务端轮次:显式 expectedPrevRound 时——行轮次必须恰为 N(否则陈旧),服务端只分配 N+1; // 未显式时保持客户端 attempt 语义(存量兼容)。 Integer expectedPrev = policy == null ? null : policy.expectedPrevRound; int incomingRound; if (expectedPrev != null) { if (expectedPrev != currentRound) { log.warn("[flow-center] 重提前置轮次不符(不重启):instance={} 行轮次={} 期望前轮={}", existing.getId(), currentRound, expectedPrev); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.STALE_ROUND, existing.getApplyRound()); } incomingRound = currentRound + 1; } else { incomingRound = policy == null || policy.attempt == null ? 1 : policy.attempt; if (incomingRound <= currentRound) { log.warn("[flow-center] 陈旧轮次重投(不重启):instance={} 行轮次={} 请求轮次={}", existing.getId(), currentRound, incomingRound); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.STALE_ROUND, existing.getApplyRound()); } } restart(existing, cmd, token, incomingRound); return new GuardedOutcome(existing.getId(), true, FlowApplyRetryModels.Branch.RESTARTED, incomingRound); } /** Formal command caller must hold the coordinate lock in the same transaction. */ @Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY) public FlwInstance createFormalInstance(ApplyCommand cmd, String token, Integer round, String source) { return createInstance(cmd, token, round, source); } @Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY) public void startFormalInstance(FlwInstance instance, ApplyCommand cmd) { flowEngine.startAdvance(instance, cmd.def); publishSubmitted(instance); } private void createInstance(ApplyCommand cmd, String token, Integer round) { createInstance(cmd, token, round, null); } private FlwInstance createInstance(ApplyCommand cmd, String token, Integer round, String source) { FlwInstance instance = new FlwInstance(); instance.setSourcePrincipalId(source); instance.setLegacySlot(source == null ? 1 : null); 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.setAssignJson(cmd.assignJson); instance.setApplyAppKey(cmd.applyAppKey); instance.setApplyAppCode(cmd.applyAppCode); instance.setApplyRequestToken(token); instance.setApplyRound(round); instance.setApplyPayloadHash(cmd.payloadHash); 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); if (source == null) { flowEngine.startAdvance(instance, cmd.def); publishSubmitted(instance); } return instance; } /** * 旧 {@link #apply(ApplyCommand)} 的建件段复用(行为逐字不变,只是把 new 出来的那段抽出来)。 * 这里不复用 createInstance 是因为旧路径不写 token/round(保持存量行 NULL,不伪装"有守卫")。 */ private Long instanceOf(ApplyCommand cmd) { FlwInstance created = instanceRepository .findBySchIdAndBizTypeAndBizId(cmd.schId, cmd.bizType, cmd.bizId).orElse(null); return created == null ? null : created.getId(); } /** * 归属复核:调用应用 / 提交者 / 流程线三者任一明确不符都拒(C.3"不能把别的应用 * 相同 bizType 键当自己的结果")。两侧都非空才判——历史行没有 token/appKey 快照, * 不能因为"当年没记"就把存量件判成别人的。 */ private void requireSameOwner(FlwInstance existing, ApplyCommand cmd) { if (existing.getApplyAppKey() != null && cmd.applyAppKey != null && !existing.getApplyAppKey().equals(cmd.applyAppKey)) { throw new BizException(ErrorCode.FORBIDDEN, "该业务键的办件属于另一调用应用(bizType=" + cmd.bizType + " bizId=" + cmd.bizId + ")"); } if (existing.getSubmitterId() != null && cmd.submitterId != null && !existing.getSubmitterId().equals(cmd.submitterId)) { throw new BizException(ErrorCode.FORBIDDEN, "该业务键的办件提交者与本次请求不符"); } String existingFlowGuid = defRepository.findById(existing.getDefId()) .map(def -> def.getFlowGuid()).orElse(null); if (existingFlowGuid != null && cmd.def != null && cmd.def.getFlowGuid() != null && !existingFlowGuid.equals(cmd.def.getFlowGuid())) { throw new BizException(ErrorCode.FORBIDDEN, "该业务键的办件绑定的流程线不同(行=" + existingFlowGuid + " 请求=" + cmd.def.getFlowGuid() + ")"); } } private static String trimToNull(String raw) { if (raw == null) { return null; } String value = raw.trim(); return value.isEmpty() ? null : value; } }