t1~t2+精修+TASK_4: 定义绑定表单UUID/版本成对核验、发起恢复守卫、schema兼容边界草案
build / build-push (push) Successful in 1m10s
build / build-push (push) Successful in 1m10s
含10-22精修: 保存/发布/启用按确切uuid+version远端核验、升版继承两列、取消关联清空版本、缺版本不回落最新。TASK_4=docs/schema-extension-compatibility-next-tasks.md(纯文档)。
This commit is contained in:
@@ -6,6 +6,7 @@ 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;
|
||||
@@ -106,6 +107,15 @@ public class FlowInstanceService {
|
||||
}
|
||||
|
||||
private void restart(FlwInstance instance, ApplyCommand cmd) {
|
||||
restart(instance, cmd, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* R4 重启推进(语义不变:定义刷新、快照覆盖、旧分支清空、status 回 10、version 自增、轨迹跨轮连续)。
|
||||
* 新增的两个参数只是<b>随行落库的请求标识与轮次</b>,不改任何节点算法(任务书 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());
|
||||
@@ -121,6 +131,13 @@ public class FlowInstanceService {
|
||||
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);
|
||||
}
|
||||
instance.setStatus(FlwEnums.InstanceStatus.APPROVING.getCode());
|
||||
instance.setSubmitTime(LocalDateTime.now());
|
||||
instance.setFinishTime(null);
|
||||
@@ -267,4 +284,165 @@ public class FlowInstanceService {
|
||||
return duplicated;
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 请求标识与轮次守卫(v2 §8,TASK_2 C.3) ====================
|
||||
|
||||
/**
|
||||
* 守卫参数。<b>刻意不做成 {@code ApplyCommand} 的新字段</b>:那个 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;
|
||||
}
|
||||
|
||||
/** 守卫化结果:除 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} 唯一入口)。
|
||||
*
|
||||
* <p>判定顺序(每一支都只回读现状,不改流程状态机一行算法):</p>
|
||||
* <ol>
|
||||
* <li><b>先核归属</b>:行上的调用应用/提交者/流程线与本次请求不符 ⇒ 拒绝,
|
||||
* <b>不能把别的应用相同 bizType 键当自己的结果</b>返回(C.3)。
|
||||
* 应用身份两侧都非空才判(本地无签名的历史行为 null,不因此把存量件判死);</li>
|
||||
* <li>同 {@code requestToken} 命中 ⇒ {@code SAME_REQUEST}:这是同一次命令的重投,
|
||||
* 即使行已终态也<b>不重启</b>——那正是"响应丢失后重发"与"用户重提"的分界;</li>
|
||||
* <li>10/20 命中 ⇒ {@code IDEMPOTENT}(既有语义不变);</li>
|
||||
* <li>30/40 命中:{@code retryOnly=true} ⇒ {@code REPLAY_ONLY} 不重启;
|
||||
* 请求轮次 ≤ 行轮次 ⇒ {@code STALE_ROUND} 不重启;
|
||||
* 否则才是新一轮授权重提 ⇒ {@code RESTARTED}(走既有 R4 重启推进);</li>
|
||||
* <li>无行 ⇒ 新建 + {@code CREATED}。</li>
|
||||
* </ol>
|
||||
*/
|
||||
@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())) {
|
||||
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()) {
|
||||
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());
|
||||
}
|
||||
int 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);
|
||||
}
|
||||
|
||||
/** 新建实例(与旧 apply 同一段代码形状:不复制第二份推进逻辑) */
|
||||
private void createInstance(ApplyCommand cmd, String token, Integer round) {
|
||||
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.setApplyRequestToken(token);
|
||||
instance.setApplyRound(round);
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 旧 {@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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user