t11-schema: /oapi/form/schema 支持按 form_uuid 查询(uuid 优先 + version 精确取行)
build / build-push (push) Failing after 2m2s

flow-center 在流程定义发布期拉契约快照的数据源(v2 方案 §4.4),code 查询路径逐字节不变:

- Controller:新增可选 uuid(32 位串)/version(Integer),code 由必填放宽为可选;uuid 与
  code 同传时 uuid 优先(类头 javadoc 写明形态与用途);响应形状零改动,仍是同一个 SchemaView。
- FormDefService.resolvePublishedByUuid(纯新增):域校验复用 readDomainForm(本校 ∪ 平台级,
  他校与不存在的 uuid 同一回答,不给探测面);不给 version=入口口径——要求 PUBLISHED 且
  currentVersion 非空,文案与错误码族对齐 resolvePublished,并额外挡软删行(code 形态取域
  SQL 带 is_deleted=0,不挡就是同一张表两份可读集);给 version=溯源口径——pinnedForData
  同款,不吃生命周期、不过滤软删,缺行给 PARAM「版本行不存在:<appCode>/<code> v<n>」。
- OapiFormService:五参重载只做入口分流,原三参方法体一字未动(numstat 删除行=0);只给
  version 不给 uuid 显式拒(静默忽略会让调用方误以为拿到的是那个版本)。SchemaView 组装段
  按任务书 W2-1「原方法体不重构不挪动」保留两份,靠 F1 的 groups 逐字比对作漂移闸。

问题单 P106~P111 已回填执行手册 §5;《契约》§9 增补两形态并新开 §9.1(参数表/示例/错误码/
「uuid 形态不经遮蔽」与「两条分支口径不同源」两段说明)。

验证:mvn -B -o test 全量 272 例绿(0 failures / 0 errors,BUILD SUCCESS)。
新增 11 例=OapiFormServiceIT F1~F7(uuid 已发布版/精确历史版/平台级/防枚举同文案/
未发布·停用·软删/版本行不存在/uuid 优先)+ OapiFormControllerTest 4 例(不传 code 不 400、
version 绑成 Integer 且缺省 null、五参原样透传)。基线 261 → 只增不减;
F8 回归=既有四个 code 形态用例一字未改(该测试文件 diff 为 +144/-0)。
This commit is contained in:
chenxing
2026-10-19 19:56:46 +08:00
parent 7af6a98fc0
commit 13abc2dbc7
5 changed files with 363 additions and 6 deletions
@@ -13,10 +13,17 @@ import org.springframework.web.bind.annotation.RestController;
* {@code yh.app-auth.server.enforce-patterns=[/oapi/*]} 已覆盖本前缀,无签名三件套直接 401)。
*
* <pre>
* GET /oapi/form/schema?schId=&code=[&appCode=] → data=内联 options + valueSchema 的机读 schema(契约 §4)
* GET /oapi/form/answer?schId=&answerId= → data=钉住版本的 schema 引用 + 值文档(契约 §9)
* GET /oapi/form/schema?schId=&amp;code=[&amp;appCode=] → 现形态:data=内联 options + valueSchema 的机读 schema(契约 §4)
* GET /oapi/form/schema?schId=&amp;uuid= → T11 新形态:该表单<b>当前已发布版</b>
* GET /oapi/form/schema?schId=&amp;uuid=&amp;version= → T11 新形态:精确版本行(快照核对/重放)
* GET /oapi/form/answer?schId=&amp;answerId= → data=钉住版本的 schema 引用 + 值文档(契约 §9)
* </pre>
*
* <p>uuid 两形态的用途=给 flow-center 在<b>流程定义发布期</b>拉契约快照嵌进 def 版本行
* (《ysch-flow单一接入件与中心托管体验-实施方案v2-20261019.md》§4.4),运行期中心零依赖。
* <b>uuid 与 code 同传时 uuid 优先</b>(code/appCode 被忽略):uuid 是权威坐标、code 是解析入口,
* 两者同传说明调用方手里是存下来的 uuid,按它取才是它要的那张表。响应形状与 code 形态完全一致。</p>
*
* <p>信封 {@code code=0} 由平台统一响应链负责;消费范式={@code YschOpenCall.raw()} 直调
* (Tier0 逃生舱,不建 SDK form 域门面——≥2 消费方实际调用时才触发晋升评估,方案 §7.5)。</p>
*
@@ -31,12 +38,17 @@ public class OapiFormController {
private final OapiFormService formService;
/** 机读 schema(内联 options + valueSchema) */
/**
* 机读 schema(内联 options + valueSchema);寻址支持 code 与 uuid 两种形态,
* <b>同传时 uuid 优先</b>(见类头),{@code version} 只在 uuid 形态有意义=精确取版本行。
*/
@GetMapping("/schema")
public OapiModels.SchemaView schema(@RequestParam("schId") Long schId,
@RequestParam("code") String code,
@RequestParam(value = "appCode", required = false) String appCode) {
return formService.schema(schId, appCode, code);
@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "appCode", required = false) String appCode,
@RequestParam(value = "uuid", required = false) String uuid,
@RequestParam(value = "version", required = false) Integer version) {
return formService.schema(schId, appCode, code, uuid, version);
}
/** 单条答卷(钉住版本 schema 引用 + 值文档) */
@@ -229,6 +229,47 @@ public class FormDefService {
return form;
}
/**
* 对外读接口的 <b>uuid 形态</b>解析(T11,v2 方案 §4.4:flow-center 在流程定义发布期按 uuid 拉契约快照)。
*
* <p>两条分支刻意<b>不同源</b>,因为它们回答的是两个问题:</p>
* <ul>
* <li><b>不给 version=入口口径</b>:与 {@link #resolvePublished} 同一套生命周期判定与文案
* (未发布/停用都拒、错误码 family 同契约 §6),并额外挡掉软删行——{@code resolvePublished}
* 的取域 SQL 带 {@code is_deleted=0},uuid 形态必须给出同一份可读集,否则"按 code 查不到、
* 按 uuid 查得到"就成了第二套口径(问题单 P107);</li>
* <li><b>给 version=溯源口径</b>:与 {@link #pinnedForData} 同——精确取行、<b>不校验生命周期</b>、
* 不过滤软删。停用/软删表单的历史版本核对是合法需求(方案 §5.4"停用只断填报入口")。</li>
* </ul>
*
* <p>域校验复用 {@link #readDomainForm}(本校 ∪ 平台级):他校 uuid 与不存在的 uuid 同一回答,
* 差别信息就是探测面(契约 §1)。</p>
*/
public Published resolvePublishedByUuid(Long schId, String formUuid, Integer version) {
Form form = readDomainForm(schId, formUuid);
if (version != null) {
FormVersion row = versionRepository.findByFormIdAndVersion(form.getId(), version).orElse(null);
if (row == null) {
throw new BizException(ErrorCode.PARAM_ERROR,
"版本行不存在:" + form.getAppCode() + "/" + form.getCode() + " v" + version);
}
return new Published(form, row);
}
if (Integer.valueOf(1).equals(form.getIsDeleted())) {
throw new BizException(ErrorCode.PARAM_ERROR, "表单不存在:" + formUuid);
}
if (form.getStatus() == null || form.getStatus() != FormEnums.FormStatus.PUBLISHED.getCode()
|| form.getCurrentVersion() == null) {
throw new BizException(ErrorCode.CUSTOM, form.getStatus() != null
&& form.getStatus() == FormEnums.FormStatus.DISABLED.getCode()
? "表单已停用,不再接收填报:" + form.getCode() : "表单尚未发布:" + form.getCode());
}
FormVersion row = versionRepository.findByFormIdAndVersion(form.getId(), form.getCurrentVersion())
.orElseThrow(() -> new BizException(ErrorCode.INTERNAL_SERVER_ERROR,
"当前发布版本行缺失(version=" + form.getCurrentVersion() + "),请联系管理员"));
return new Published(form, row);
}
/**
* 数据面读接口<b>入口把关</b>用的表单定位(T10 方案 §5.2):只按 uuid 取头表、不做域判定,
* 取不到返回 null。域与模块行的判断全在 {@code FormAdminGuard#requireDataView}——
@@ -39,6 +39,31 @@ public class OapiFormService {
private final FormDictService dictService;
private final AnswerRepository answerRepository;
/**
* {@code /oapi/form/schema} 的<b>入口分流</b>(T11):{@code uuid} 非空走按 uuid 取版本行的新路径,
* 否则原样交给 code 形态的 {@link #schema(Long, String, String)}——原实现一行没挪,
* 所以 code 查询路径的行为与响应逐字节不变(任务书 §0-2 红线)。
*
* <p><b>uuid 优先</b>:{@code uuid} 与 {@code code} 同传时只看 uuid,忽略 code 与 appCode
* (uuid 是权威坐标、code 是解析入口,两者同传说明调用方拿的是 flow-center 存的 uuid)。
* {@code version} 只在 uuid 形态有意义,单独传 version 不给 uuid 直接拒——静默忽略会让调用方
* 以为拿到的是那个版本(快照核对场景下这是最坏的错误)。</p>
*
* @param uuid 表单 uuid(可空;契约 §1:32 位、跨版本不变、不可枚举)
* @param version 版本行号(可空=当前已发布版;给值=精确取该行)
*/
public OapiModels.SchemaView schema(Long schId, String appCode, String code, String uuid, Integer version) {
if (version != null && (uuid == null || uuid.trim().isEmpty())) {
throw new BizException(ErrorCode.PARAM_ERROR, "version 需与 uuid 同传(uuid 形态的版本行核对参数)");
}
if (uuid == null || uuid.trim().isEmpty()) {
return schema(schId, appCode, code);
}
requireSchId(schId);
FormDefService.Published published = defService.resolvePublishedByUuid(schId, uuid, version);
return toSchemaView(published);
}
/**
* 机读 schema(契约 §4 形状):字典已内联展开,每个字段附 valueSchema。
*
@@ -103,6 +128,27 @@ public class OapiFormService {
return view;
}
/**
* 已发布解析结果 → 机读 schema 视图(契约 §4 形状)。
*
* <p><b>与 {@link #schema(Long, String, String)} 内的组装段逐行同形</b>:任务书 §0-2/W2-1 要求
* code 形态的原方法体不重构不挪动,故 uuid 形态自带一份而不是抽公共方法(问题单 P108)。
* 要改这里就同时改那边——{@code OapiFormServiceIT} 的 F1 断言两种形态的 groups 等价,是这份重复的漂移闸。</p>
*/
private OapiModels.SchemaView toSchemaView(FormDefService.Published published) {
Form form = published.form();
JsonNode root = FormValueSchemaBuilder.withValueSchemas(expandedRoot(form, published.versionRow().getSchemaJson()));
OapiModels.SchemaView view = new OapiModels.SchemaView();
view.setFormUuid(form.getUuid());
view.setVersion(published.versionRow().getVersion());
view.setCode(form.getCode());
view.setAppCode(form.getAppCode());
view.setAudience(FormEnums.Audience.nameOf(form.getAudience()));
view.setGroups(root.path("groups"));
return view;
}
/** 字典内联展开 → JSON 树(对外自包含,契约 §2.1) */
private JsonNode expandedRoot(Form form, String schemaJson) {
return FormSchemaValidator.parse(dictService.expandSchema(form.getSchId(), schemaJson),