Enforce exact published form version contracts
Local review candidate only. Explicitly selected inherited prerequisites and prior repair patches; unrelated WIP and independent MQ fixes are excluded. No push or deployment authorized.
This commit is contained in:
@@ -6,6 +6,7 @@ import com.yunhai.flowcenter.entity.FlwCallbackReg;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.entity.FlwInstance;
|
||||
import com.yunhai.flowcenter.model.FlowModels;
|
||||
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
|
||||
import com.yunhai.flowcenter.repository.FlwInstanceRepository;
|
||||
import com.yunhai.flowcenter.service.FlowAdminGuard;
|
||||
import com.yunhai.flowcenter.service.FlowCallbackAdminService;
|
||||
@@ -145,7 +146,16 @@ public class FlowAdminController {
|
||||
(String) body.get("contentJson"), (String) body.get("baseGuid"),
|
||||
(String) body.get("appKey"), (String) body.get("changeLog"),
|
||||
(String) body.get("formUuid"),
|
||||
body.get("formVersion") == null ? null : Integer.valueOf(String.valueOf(body.get("formVersion"))));
|
||||
parseFormVersion(body.get("formVersion")));
|
||||
}
|
||||
|
||||
private static Integer parseFormVersion(Object value) {
|
||||
if (value == null) return null;
|
||||
try {
|
||||
return Integer.valueOf(String.valueOf(value));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "formVersion 必须是正整数", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.yunhai.flowcenter.enums;
|
||||
|
||||
import com.yunhai.platform.utils.exception.IErrorCode;
|
||||
|
||||
/** 管理面表单绑定契约:消费方按数值码判断恢复动作,不解析 msg。 */
|
||||
public enum FormBindingErrorCode implements IErrorCode {
|
||||
VERSION_REQUIRED(46101, "关联表单缺少明确版本"),
|
||||
INVALID_COORDINATE(46102, "关联表单坐标或版本不合法"),
|
||||
SCHEMA_UNAVAILABLE(46103, "关联表单 schema 拉取失败,可重试"),
|
||||
SCHEMA_CONTRACT_VIOLATION(46104, "关联表单 schema 坐标核验失败"),
|
||||
RESOURCE_UNAVAILABLE(46105, "关联表单或指定发布版本不存在或不可读"),
|
||||
ACCESS_DENIED(46106, "关联表单访问身份或授权被拒绝");
|
||||
|
||||
private final int code;
|
||||
private final String msg;
|
||||
|
||||
FormBindingErrorCode(int code, String msg) {
|
||||
this.code = code;
|
||||
this.msg = msg;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCode() { return code; }
|
||||
|
||||
@Override
|
||||
public String getMsg() { return msg; }
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import com.yunhai.flowcenter.engine.FlowEngine;
|
||||
import com.yunhai.flowcenter.entity.FlwAssigneeMap;
|
||||
import com.yunhai.flowcenter.entity.FlwDef;
|
||||
import com.yunhai.flowcenter.enums.FlwEnums;
|
||||
import com.yunhai.flowcenter.enums.FormBindingErrorCode;
|
||||
import com.yunhai.flowcenter.repository.FlwAssigneeMapRepository;
|
||||
import com.yunhai.flowcenter.repository.FlwDefRepository;
|
||||
import com.yunhai.platform.apistd.BizException;
|
||||
@@ -192,12 +193,13 @@ public class FlowDefAdminService {
|
||||
boolean pairing = formUuid != null && !clearing && formVersion == null;
|
||||
boolean versionOnly = formUuid == null && formVersion != null;
|
||||
if (pairing || versionOnly) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
throw new BizException(formVersion == null ? FormBindingErrorCode.VERSION_REQUIRED
|
||||
: FormBindingErrorCode.INVALID_COORDINATE,
|
||||
"关联表单必须成对提交 formUuid 与 formVersion(只传一个拒):当前 formUuid="
|
||||
+ (formUuid == null ? "未传" : formUuid.trim()) + ",formVersion=" + formVersion);
|
||||
}
|
||||
if (formUuid != null && !clearing) {
|
||||
checkedSchema(schId, formUuid.trim(), formVersion); // Validate before the first save.
|
||||
checkedSchema(schId, formUuid.trim(), formVersion, "保存"); // Validate before the first save.
|
||||
}
|
||||
FlwDef saved = save(schId, id, code, name, contentJson, baseGuid, appKey, changeLog);
|
||||
if (formUuid != null) {
|
||||
@@ -229,17 +231,8 @@ public class FlowDefAdminService {
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
|
||||
Map<String, Object> snapshot;
|
||||
try {
|
||||
// 管理链始终使用确切版本;缺版本须重新选择,不回落当前发布版。
|
||||
snapshot = checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion());
|
||||
} catch (FormCenterSchemaClient.SchemaUnavailableException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"关联表单快照拉取失败,发布已阻断(可重试):" + e.getMessage());
|
||||
} catch (FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR,
|
||||
"关联表单版本核验失败,发布已阻断(重试无意义,请重新选择表单与版本):" + e.getMessage());
|
||||
}
|
||||
Map<String, Object> snapshot = checkedSchema(schId, def.getFormUuid().trim(),
|
||||
def.getFormVersion(), "发布");
|
||||
List<String> problems = new ArrayList<>();
|
||||
validateExpressionKeysAgainstSnapshot(model, snapshot, problems);
|
||||
validateExpressionTypesAndDomains(model, snapshot, problems);
|
||||
@@ -248,6 +241,8 @@ public class FlowDefAdminService {
|
||||
"发布校验失败(表达式三查):\n" + String.join("\n", problems));
|
||||
}
|
||||
def.setContentJson(embedFormContract(def.getContentJson(), snapshot));
|
||||
} else if (def.getFormVersion() != null) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
@@ -435,22 +430,38 @@ public class FlowDefAdminService {
|
||||
FlowDefModel model = FlowDefModel.parse(objectMapper, def.getContentJson());
|
||||
DefValidator.validate(model, enabledDeptKeys(schId), enabledRouterKeys());
|
||||
if (def.getFormUuid() != null && !def.getFormUuid().trim().isEmpty()) {
|
||||
checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion());
|
||||
checkedSchema(schId, def.getFormUuid().trim(), def.getFormVersion(), "启用");
|
||||
} else if (def.getFormVersion() != null) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE, "关联表单版本必须同时绑定 formUuid");
|
||||
}
|
||||
def.setStatus(FlwEnums.DefStatus.PUBLISHED.getCode());
|
||||
def.setPublishTime(LocalDateTime.now());
|
||||
return defRepository.save(def);
|
||||
}
|
||||
|
||||
private Map<String, Object> checkedSchema(Long schId, String uuid, Integer version) {
|
||||
if (version == null || version < 1) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "关联表单必须选择确切的正整数版本,请重新选择");
|
||||
private Map<String, Object> checkedSchema(Long schId, String uuid, Integer version, String operation) {
|
||||
if (version == null) {
|
||||
throw new BizException(FormBindingErrorCode.VERSION_REQUIRED, "关联表单缺少明确版本,"
|
||||
+ operation + "已阻断,请重新选择表单与版本");
|
||||
}
|
||||
if (version < 1 || uuid == null || uuid.trim().isEmpty()) {
|
||||
throw new BizException(FormBindingErrorCode.INVALID_COORDINATE,
|
||||
"关联表单必须选择确切的正整数版本," + operation + "已阻断,请重新选择");
|
||||
}
|
||||
try {
|
||||
return formCenterSchemaClient.fetchSchema(schId, uuid, version);
|
||||
} catch (FormCenterSchemaClient.SchemaUnavailableException
|
||||
| FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(ErrorCode.PARAM_ERROR, "关联表单核验失败,操作已阻断:" + e.getMessage());
|
||||
} catch (FormCenterSchemaClient.SchemaUnavailableException e) {
|
||||
throw new BizException(FormBindingErrorCode.SCHEMA_UNAVAILABLE,
|
||||
"关联表单快照拉取失败," + operation + "已阻断(可重试)", e);
|
||||
} catch (FormCenterSchemaClient.SchemaContractViolationException e) {
|
||||
throw new BizException(FormBindingErrorCode.SCHEMA_CONTRACT_VIOLATION,
|
||||
"关联表单版本核验失败," + operation + "已阻断(请重新选择表单与版本)", e);
|
||||
} catch (FormCenterSchemaClient.SchemaResourceUnavailableException e) {
|
||||
throw new BizException(FormBindingErrorCode.RESOURCE_UNAVAILABLE,
|
||||
"关联表单或指定发布版本不存在或不可读," + operation + "已阻断", e);
|
||||
} catch (FormCenterSchemaClient.SchemaAccessDeniedException e) {
|
||||
throw new BizException(FormBindingErrorCode.ACCESS_DENIED,
|
||||
"关联表单访问身份或授权被拒绝," + operation + "已阻断,请检查授权", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,9 @@ package com.yunhai.flowcenter.service;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.yunhai.platform.openapi.core.YschOpenCall;
|
||||
import com.yunhai.platform.openapi.core.OpenApiException;
|
||||
import com.yunhai.platform.utils.exception.DException;
|
||||
import com.yunhai.platform.utils.exception.ErrorCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -14,10 +17,10 @@ import java.util.Map;
|
||||
|
||||
/**
|
||||
* form-center schema 快照客户端(2026-10-20 C1 第一刀:发布期契约快照)。<br>
|
||||
* GET {目录解析 form-center}/oapi/form/schema?schId=&uuid= ——按 form_uuid 拉当前已发布版 schema
|
||||
* (T11 建立的 by-uuid 形态;version 缺省=当前发布版)。调用形态对齐 {@link RouteJudgeClient}:
|
||||
* GET {目录解析 form-center}/oapi/form/schema?schId=&uuid=&version= ——只取明确发布版本。
|
||||
* 保存、发布与启用共用精确坐标契约;无 UUID-only 回退。调用形态对齐 YschOpenCall:
|
||||
* 身份=本应用 appKey(签名拦截器由 {@link YschOpenCall} 装配链注入)。<br>
|
||||
* <b>只在发布期调用</b>(拉取失败=发布阻断,可重试);运行期对 form-center 零依赖(快照嵌 def 版本行)。<br>
|
||||
* <b>只在管理期调用</b>(拉取失败=阻断,可重试);运行期对 form-center 零依赖(快照嵌 def 版本行)。<br>
|
||||
* audience 解析:<b>契约 v2 C5/G3 起是字符串数组</b>(breaking,T13 切换)——本客户端按数组读取,
|
||||
* 快照不消费人群语义、只透传形态,兼容旧单值串(读到串自动包一层数组,迁移窗口容错)。
|
||||
*/
|
||||
@@ -60,48 +63,28 @@ public class FormCenterSchemaClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布期快照:{formUuid, formVersion, fields:[{key,type,required,enum?}], audience(数组)}。
|
||||
* fields 由 groups 扁平化(分组只是展示结构,表达式键域与分组无关);
|
||||
* select/radio 类字段的 options 值域收进 enum(供域查③,本轮仅透传)。
|
||||
*/
|
||||
public Map<String, Object> fetchSchema(Long schId, String formUuid) {
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
try {
|
||||
Map<String, String> query = new HashMap<>();
|
||||
query.put("schId", String.valueOf(schId));
|
||||
query.put("uuid", formUuid);
|
||||
Map<String, Object> data = openCall.get(TARGET, SCHEMA_PATH, query, Map.class);
|
||||
if (data == null) {
|
||||
throw new IllegalStateException("schema 返回空 data");
|
||||
}
|
||||
return toSnapshot(data);
|
||||
} catch (RuntimeException e) {
|
||||
last = e;
|
||||
log.warn("[flow-center] form-center schema 快照拉取失败(第 {}/{} 次):uuid={} {}",
|
||||
attempt, MAX_ATTEMPTS, formUuid, e.toString());
|
||||
} catch (Exception e) {
|
||||
last = new IllegalStateException(e.toString(), e);
|
||||
log.warn("[flow-center] form-center schema 快照拉取异常(第 {}/{} 次):uuid={} {}",
|
||||
attempt, MAX_ATTEMPTS, formUuid, e.toString());
|
||||
}
|
||||
/** FC 将不存在、不可读、未发布的精确版本统一拒绝,不能靠文案区分资源存在性。 */
|
||||
public static class SchemaResourceUnavailableException extends RuntimeException {
|
||||
public SchemaResourceUnavailableException(Throwable cause) {
|
||||
super("表单或指定发布版本不存在或不可读", cause);
|
||||
}
|
||||
}
|
||||
|
||||
public static class SchemaAccessDeniedException extends RuntimeException {
|
||||
public SchemaAccessDeniedException(Throwable cause) {
|
||||
super("表单访问身份或授权被拒绝", cause);
|
||||
}
|
||||
throw new SchemaUnavailableException(
|
||||
"关联表单 schema 快照拉取失败(已重试 " + MAX_ATTEMPTS + " 次,发布阻断,可重试)", last);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发布期快照的<b>确切版本</b>形态(v3/TASK_1):{@code GET /oapi/form/schema?schId=&uuid=&version=},
|
||||
* 并把<b>返回坐标与请求坐标逐件核对</b>——不符即 {@link SchemaContractViolationException}(不重试)。
|
||||
*
|
||||
* <p>刻意不与两参 {@link #fetchSchema(Long, String)} 共用循环体:那条是 C1 第一刀的既有行为
|
||||
* ({@code FormCenterSchemaClientTest} 逐条钉住它的重试与文案),改它等于同时动两条契约。
|
||||
* 新管理链(def 带 {@code formVersion})只走本方法。</p>
|
||||
* fields 按分组扁平化,options 值域收进 enum;没有缺省当前版本的重载。
|
||||
*/
|
||||
public Map<String, Object> fetchSchema(Long schId, String formUuid, Integer version) {
|
||||
if (version == null) {
|
||||
throw new IllegalArgumentException("按确切版本取快照必须给 version(缺版本=取当前发布版,那是旧口径)");
|
||||
if (version == null || version < 1 || formUuid == null || formUuid.trim().isEmpty()) {
|
||||
throw new IllegalArgumentException("按确切版本取快照必须给 uuid 与正整数 version");
|
||||
}
|
||||
RuntimeException last = null;
|
||||
for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {
|
||||
@@ -119,6 +102,20 @@ public class FormCenterSchemaClient {
|
||||
return snapshot;
|
||||
} catch (SchemaContractViolationException e) {
|
||||
throw e;
|
||||
} catch (DException e) {
|
||||
int code = e.getCode() == null ? -1 : e.getCode().getCode();
|
||||
if (code == ErrorCode.PARAM_ERROR.getCode()) {
|
||||
throw new SchemaResourceUnavailableException(e);
|
||||
}
|
||||
if (code == ErrorCode.FORBIDDEN.getCode() || code == ErrorCode.UNAUTHORIZED.getCode()) {
|
||||
throw new SchemaAccessDeniedException(e);
|
||||
}
|
||||
last = e;
|
||||
} catch (OpenApiException e) {
|
||||
if (e.getHttpStatus() == 404) {
|
||||
throw new SchemaResourceUnavailableException(e);
|
||||
}
|
||||
last = e;
|
||||
} catch (RuntimeException e) {
|
||||
last = e;
|
||||
log.warn("[flow-center] form-center schema 精确版本拉取失败(第 {}/{} 次):uuid={} v={} {}",
|
||||
@@ -137,9 +134,15 @@ public class FormCenterSchemaClient {
|
||||
private static void requireCoordinate(Map<String, Object> snapshot, String formUuid, Integer version) {
|
||||
Object gotUuid = snapshot.get("formUuid");
|
||||
Object gotVersion = snapshot.get("formVersion");
|
||||
if (gotUuid == null || gotVersion == null
|
||||
Integer parsedVersion;
|
||||
try {
|
||||
parsedVersion = gotVersion == null ? null : Integer.valueOf(String.valueOf(gotVersion));
|
||||
} catch (NumberFormatException e) {
|
||||
throw new SchemaContractViolationException("表单中心返回的版本不是合法整数");
|
||||
}
|
||||
if (gotUuid == null || parsedVersion == null || parsedVersion < 1
|
||||
|| !String.valueOf(gotUuid).equals(formUuid)
|
||||
|| Integer.parseInt(String.valueOf(gotVersion)) != version) {
|
||||
|| !parsedVersion.equals(version)) {
|
||||
throw new SchemaContractViolationException("表单中心返回的坐标与请求不符(请求 "
|
||||
+ formUuid + " v" + version + ",返回 " + gotUuid + " v" + gotVersion + ")");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user