diff --git a/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java b/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java
index ebb80a5..8c18e9e 100644
--- a/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java
+++ b/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java
@@ -444,6 +444,12 @@ public final class SvcModels {
* enabled=false → 真的不会翻转,直接告知未接入;enabled=true 且 flowStatus=0 → 排队中,等待轮询。
*/
private Boolean flowEnabled;
+ /**
+ * 办理表单 schema(响应期补发,不入库;仅 detail 出参填):
+ * 前端「申请内容」以此为 key→中文 label/分组的唯一事实源(2026-09-17),版本口径=
+ * 办件 formVersion 精确命中、缺失回落最新已发布版本。无表单事项为 null。
+ */
+ private String formSchemaJson;
public static ApplyVo of(com.yunhai.svchall.entity.ServiceApply a) {
ApplyVo vo = new ApplyVo();
diff --git a/src/main/java/com/yunhai/svchall/service/impl/ServiceApplyService.java b/src/main/java/com/yunhai/svchall/service/impl/ServiceApplyService.java
index 3da5c76..4ff1e0e 100644
--- a/src/main/java/com/yunhai/svchall/service/impl/ServiceApplyService.java
+++ b/src/main/java/com/yunhai/svchall/service/impl/ServiceApplyService.java
@@ -286,7 +286,25 @@ public class ServiceApplyService {
.orElseThrow(() -> new BizException(ErrorCode.PARAM_ERROR, "办件不存在"));
applyViewGuard.requireVisible(apply, user);
}
- return ApplyVo.of(apply);
+ ApplyVo vo = ApplyVo.of(apply);
+ vo.setFormSchemaJson(formSchemaJson(apply));
+ return vo;
+ }
+
+ /**
+ * 申请内容展示 schema(key→中文 label/分组的唯一事实源):办件 → 事项 formCode →
+ * 按提交时 formVersion 定版(缺失回落最新已发布)取 schemaJson。任何一环缺失(无表单
+ * 事项/事项已清理/表单未发布过)返回 null,前端回落按 key 展示,不报错。
+ */
+ private String formSchemaJson(ServiceApply apply) {
+ if (apply.getFormVersion() == null || apply.getItemId() == null) {
+ return null;
+ }
+ ServiceItem item = itemRepository.findById(apply.getItemId()).orElse(null);
+ if (item == null || item.getFormCode() == null || item.getFormCode().isEmpty()) {
+ return null;
+ }
+ return formService.schemaAt(apply.getSchId(), item.getFormCode(), apply.getFormVersion());
}
// ===== 管理面 =====
diff --git a/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java b/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java
index 0cfeb85..fb17136 100644
--- a/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java
+++ b/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java
@@ -157,4 +157,18 @@ public class ServiceFormService {
public Optional latestPublished(Long schId, String code) {
return formRepository.findFirstBySchIdAndCodeAndStatusOrderByVersionDesc(schId, code, 1);
}
+
+ /**
+ * 详情展示取 schema:办件提交时的 formVersion 精确命中(快照纪律的展示侧对齐——
+ * 表单后续改版不改历史办件的字段含义);该版本已不可考(如被整 code 清理)回落最新已发布版本,
+ * 再不行给 null(前端回落按 key 展示)。
+ */
+ public String schemaAt(Long schId, String code, Integer version) {
+ Optional form = version == null ? Optional.empty()
+ : formRepository.findBySchIdAndCodeAndVersion(schId, code, version);
+ if (!form.isPresent()) {
+ form = latestPublished(schId, code);
+ }
+ return form.map(ServiceForm::getSchemaJson).orElse(null);
+ }
}
diff --git a/src/test/java/com/yunhai/svchall/service/impl/ServiceApplyDetailSchemaTest.java b/src/test/java/com/yunhai/svchall/service/impl/ServiceApplyDetailSchemaTest.java
new file mode 100644
index 0000000..ca67e02
--- /dev/null
+++ b/src/test/java/com/yunhai/svchall/service/impl/ServiceApplyDetailSchemaTest.java
@@ -0,0 +1,115 @@
+package com.yunhai.svchall.service.impl;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.yunhai.platform.utils.model.BoaUser;
+import com.yunhai.svchall.entity.ServiceApply;
+import com.yunhai.svchall.entity.ServiceForm;
+import com.yunhai.svchall.entity.ServiceItem;
+import com.yunhai.svchall.model.svc.SvcModels.ApplyVo;
+import com.yunhai.svchall.repository.ServiceApplyRepository;
+import com.yunhai.svchall.repository.ServiceItemRepository;
+import com.yunhai.svchall.service.FlowClient;
+import com.yunhai.svchall.service.SiteDirectory;
+import com.yunhai.svchall.service.SiteGuard;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.util.Optional;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+/**
+ * 办件详情补发表单 schema(2026-09-17):前端「申请内容」不再以英文 key 当标签,
+ * 改为按 schemaJson 的 groups/fields 显示中文 label 与分组;版本口径=办件提交时
+ * formVersion 精确命中,缺失回落最新已发布版本(快照纪律的展示侧对齐)。
+ */
+@DisplayName("办件详情 formSchemaJson:版本精确命中/回落/无表单三态")
+class ServiceApplyDetailSchemaTest {
+
+ private ServiceApplyRepository applyRepository;
+ private ServiceItemRepository itemRepository;
+ private ServiceFormService formService;
+ private ServiceApplyService service;
+ private ServiceApply apply;
+
+ @BeforeEach
+ void setUp() {
+ applyRepository = mock(ServiceApplyRepository.class);
+ itemRepository = mock(ServiceItemRepository.class);
+ formService = mock(ServiceFormService.class);
+ service = new ServiceApplyService(applyRepository, itemRepository, formService,
+ mock(AttachmentService.class), mock(FlowClient.class), mock(SiteGuard.class),
+ mock(SiteDirectory.class),
+ new com.yunhai.svchall.service.ApplyViewGuard(mock(FlowClient.class)),
+ new ObjectMapper(),
+ new org.springframework.transaction.support.TransactionTemplate(
+ mock(org.springframework.transaction.PlatformTransactionManager.class)));
+
+ apply = new ServiceApply();
+ ReflectionTestUtils.setField(apply, "id", 42);
+ apply.setSchId(1L);
+ apply.setSiteCode("S1");
+ apply.setItemId(7);
+ apply.setItemCode("ITEM-1");
+ apply.setFormVersion(2);
+ apply.setFormDataJson("{\"name\":\"大猫咪2\"}");
+ apply.setSubmitterId(9);
+ apply.setSubmitterName("张三");
+ when(applyRepository.findByIdAndSchId(42, 1L)).thenReturn(Optional.of(apply));
+
+ ServiceItem item = new ServiceItem();
+ ReflectionTestUtils.setField(item, "id", 7);
+ item.setSchId(1L);
+ item.setItemCode("ITEM-1");
+ item.setFormCode("FORM-1");
+ when(itemRepository.findById(7)).thenReturn(Optional.of(item));
+ }
+
+ private BoaUser submitter() {
+ BoaUser user = mock(BoaUser.class, Mockito.RETURNS_DEEP_STUBS);
+ when(user.getId()).thenReturn(9);
+ when(user.getType()).thenReturn(2);
+ when(user.getCurrentRole().getSchId()).thenReturn(1);
+ return user;
+ }
+
+ private ServiceForm form(String schema) {
+ ServiceForm f = new ServiceForm();
+ f.setSchemaJson(schema);
+ return f;
+ }
+
+ @Test
+ @DisplayName("formVersion 精确命中 → 出参 formSchemaJson = 该版本 schema")
+ void detail_withVersionMatch_returnsSchema() {
+ String schema = "{\"groups\":[{\"title\":\"基本信息\",\"fields\":[{\"key\":\"name\",\"label\":\"姓名\"}]}]}";
+ when(formService.schemaAt(1L, "FORM-1", 2)).thenReturn(schema);
+ ApplyVo vo = service.detail(submitter(), "S1", 42);
+ assertEquals(schema, vo.getFormSchemaJson());
+ }
+
+ @Test
+ @DisplayName("办件无 formVersion(无表单事项)→ formSchemaJson 为 null")
+ void detail_withoutFormVersion_returnsNullSchema() {
+ apply.setFormVersion(null);
+ ApplyVo vo = service.detail(submitter(), "S1", 42);
+ assertNull(vo.getFormSchemaJson());
+ }
+
+ @Test
+ @DisplayName("事项未绑定表单 → formSchemaJson 为 null(不外溢查询)")
+ void detail_itemWithoutFormCode_returnsNullSchema() {
+ ServiceItem noForm = new ServiceItem();
+ ReflectionTestUtils.setField(noForm, "id", 7);
+ noForm.setFormCode(null);
+ when(itemRepository.findById(7)).thenReturn(Optional.of(noForm));
+ ApplyVo vo = service.detail(submitter(), "S1", 42);
+ assertNull(vo.getFormSchemaJson());
+ }
+}