From 76b35b2fb38a0293a709f7c6755c51f62b5ba182 Mon Sep 17 00:00:00 2001 From: chenxing Date: Wed, 16 Sep 2026 16:33:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=A1=A8=E5=8D=95=E4=BD=BF=E7=94=A8?= =?UTF-8?q?=E5=9C=BA=E6=99=AF=E5=8F=AF=E8=A7=81=E9=97=AD=E7=8E=AF=E2=80=94?= =?UTF-8?q?=E2=80=94form/list=20=E8=BF=94=E5=9B=9E=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E4=BA=8B=E9=A1=B9=E6=98=8E=E7=BB=86=20refItems?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - SvcFormVo 增 refItems(ItemBrief: id/itemCode/title/siteCode/status),referenced 同源派生;仅 list 填充,versions/get 不带 - ServiceItemRepository.findBySchIdAndFormCodeIsNotNull 一次查全量按 formCode 内存分组,替代逐表单 count 的 N+1 - ServiceFormServiceTest 5 用例(未绑定/单绑/跨站点/多版本聚合/分组不串) - 前端配套(svc-hall-front 另推):绑定明细弹窗+直达事项编辑、发布未绑定引导、schema 预览、事项编辑页草稿表单提示 --- .../yunhai/svchall/model/svc/SvcModels.java | 22 +++ .../repository/ServiceItemRepository.java | 3 + .../service/impl/ServiceFormService.java | 15 +- .../service/impl/ServiceFormServiceTest.java | 133 ++++++++++++++++++ 4 files changed, 171 insertions(+), 2 deletions(-) create mode 100644 src/test/java/com/yunhai/svchall/service/impl/ServiceFormServiceTest.java 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 f1d07b1..6636181 100644 --- a/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java +++ b/src/main/java/com/yunhai/svchall/model/svc/SvcModels.java @@ -355,6 +355,8 @@ public final class SvcModels { private String schemaJson; private LocalDateTime publishTime; private Boolean referenced; + /** 绑定本表单的事项明细(仅 list 填充;表单页反查使用位置) */ + private List refItems; public static SvcFormVo of(com.yunhai.svchall.entity.ServiceForm form, Boolean referenced) { SvcFormVo vo = new SvcFormVo(); @@ -368,6 +370,26 @@ public final class SvcModels { vo.setReferenced(referenced); return vo; } + + /** 表单绑定事项的摘要行(表单 sch 级共享,跨站点绑定如实展示站点) */ + @Data + public static class ItemBrief { + private Integer id; + private String itemCode; + private String title; + private String siteCode; + private Integer status; + + public static ItemBrief of(com.yunhai.svchall.entity.ServiceItem item) { + ItemBrief brief = new ItemBrief(); + brief.setId(item.getId()); + brief.setItemCode(item.getItemCode()); + brief.setTitle(item.getTitle()); + brief.setSiteCode(item.getSiteCode()); + brief.setStatus(item.getStatus()); + return brief; + } + } } // ===== 办件 ===== diff --git a/src/main/java/com/yunhai/svchall/repository/ServiceItemRepository.java b/src/main/java/com/yunhai/svchall/repository/ServiceItemRepository.java index 2ff26e9..86075c8 100644 --- a/src/main/java/com/yunhai/svchall/repository/ServiceItemRepository.java +++ b/src/main/java/com/yunhai/svchall/repository/ServiceItemRepository.java @@ -40,6 +40,9 @@ public interface ServiceItemRepository extends JpaRepository findBySchIdAndFormCodeIsNotNull(Long schId); + // ===== 门户筛选项计数(已发布口径,group by 一次往返/维度,承袭 CMS 口径)===== @Query("select i.objectKey, count(i) from ServiceItem i" 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 9d94007..0cfeb85 100644 --- a/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java +++ b/src/main/java/com/yunhai/svchall/service/impl/ServiceFormService.java @@ -4,6 +4,7 @@ import com.yunhai.platform.apistd.BizException; import com.yunhai.platform.utils.exception.ErrorCode; import com.yunhai.platform.utils.model.BoaUser; import com.yunhai.svchall.entity.ServiceForm; +import com.yunhai.svchall.entity.ServiceItem; import com.yunhai.svchall.model.svc.SvcModels.DeleteRequest; import com.yunhai.svchall.model.svc.SvcModels.SvcFormSaveRequest; import com.yunhai.svchall.model.svc.SvcModels.SvcFormVo; @@ -18,7 +19,10 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; /** @@ -33,10 +37,14 @@ public class ServiceFormService { private final ServiceItemRepository itemRepository; private final SiteGuard siteGuard; - /** 表单列表(sch 级;同 code 多版本聚合成一行最新版,referenced=是否被事项绑定) */ + /** 表单列表(sch 级;同 code 多版本聚合成一行最新版,referenced=是否被事项绑定,refItems=绑定事项明细) */ public List list(BoaUser user, String siteCode) { SiteInfo site = siteGuard.requireSiteManager(user, siteCode); List forms = formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(site.getSchId()); + Map> refs = new HashMap<>(); + for (ServiceItem item : itemRepository.findBySchIdAndFormCodeIsNotNull(site.getSchId())) { + refs.computeIfAbsent(item.getFormCode(), k -> new ArrayList<>()).add(SvcFormVo.ItemBrief.of(item)); + } List result = new ArrayList<>(); String lastCode = null; for (ServiceForm form : forms) { @@ -44,7 +52,10 @@ public class ServiceFormService { continue; } lastCode = form.getCode(); - result.add(SvcFormVo.of(form, itemRepository.countBySchIdAndFormCode(site.getSchId(), form.getCode()) > 0)); + List briefs = refs.getOrDefault(form.getCode(), Collections.emptyList()); + SvcFormVo vo = SvcFormVo.of(form, !briefs.isEmpty()); + vo.setRefItems(briefs); + result.add(vo); } return result; } diff --git a/src/test/java/com/yunhai/svchall/service/impl/ServiceFormServiceTest.java b/src/test/java/com/yunhai/svchall/service/impl/ServiceFormServiceTest.java new file mode 100644 index 0000000..9de6480 --- /dev/null +++ b/src/test/java/com/yunhai/svchall/service/impl/ServiceFormServiceTest.java @@ -0,0 +1,133 @@ +package com.yunhai.svchall.service.impl; + +import com.yunhai.svchall.entity.ServiceForm; +import com.yunhai.svchall.entity.ServiceItem; +import com.yunhai.svchall.model.svc.SvcModels.SvcFormVo; +import com.yunhai.svchall.repository.ServiceFormRepository; +import com.yunhai.svchall.repository.ServiceItemRepository; +import com.yunhai.svchall.service.SiteDirectory; +import com.yunhai.svchall.service.SiteGuard; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * 表单列表使用场景可见性单测:list() 必须带回"被哪些事项绑定"的明细(refItems), + * 管理员才能从表单页反查使用位置(一期可见闭环);表单 sch 级共享,跨站点绑定如实展示。 + */ +class ServiceFormServiceTest { + + private ServiceFormRepository formRepository; + private ServiceItemRepository itemRepository; + private ServiceFormService service; + private SiteDirectory.SiteInfo site = new SiteDirectory.SiteInfo("S1", "站点一", 1L); + + @BeforeEach + void setUp() { + formRepository = mock(ServiceFormRepository.class); + itemRepository = mock(ServiceItemRepository.class); + SiteGuard siteGuard = mock(SiteGuard.class); + when(siteGuard.requireSiteManager(null, "S1")).thenReturn(site); + service = new ServiceFormService(formRepository, itemRepository, siteGuard); + } + + private ServiceForm form(String code, int version, int status) { + ServiceForm form = new ServiceForm(); + form.setSchId(1L); + form.setCode(code); + form.setName("表单-" + code); + form.setVersion(version); + form.setStatus(status); + form.setSchemaJson("{\"groups\":[]}"); + return form; + } + + private ServiceItem item(Integer id, String siteCode, String itemCode, String title, String formCode) { + ServiceItem item = new ServiceItem(); + item.setSchId(1L); + item.setSiteCode(siteCode); + item.setItemCode(itemCode); + item.setTitle(title); + item.setFormCode(formCode); + item.setStatus(1); + item.setId(id); + return item; + } + + @Test + void unboundFormHasEmptyRefItemsAndNotReferenced() { + when(formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(1L)) + .thenReturn(Arrays.asList(form("leave", 1, 1))); + when(itemRepository.findBySchIdAndFormCodeIsNotNull(1L)).thenReturn(Arrays.asList()); + + SvcFormVo vo = service.list(null, "S1").get(0); + assertFalse(vo.getReferenced()); + assertTrue(vo.getRefItems().isEmpty(), "未绑定表单的 refItems 应为空列表而非 null"); + } + + @Test + void boundFormCarriesRefItemBrief() { + when(formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(1L)) + .thenReturn(Arrays.asList(form("leave", 1, 1))); + when(itemRepository.findBySchIdAndFormCodeIsNotNull(1L)) + .thenReturn(Arrays.asList(item(11, "S1", "IT-001", "请假申请", "leave"))); + + SvcFormVo vo = service.list(null, "S1").get(0); + assertTrue(vo.getReferenced()); + assertEquals(1, vo.getRefItems().size()); + SvcFormVo.ItemBrief brief = vo.getRefItems().get(0); + assertEquals(Integer.valueOf(11), brief.getId()); + assertEquals("IT-001", brief.getItemCode()); + assertEquals("请假申请", brief.getTitle()); + assertEquals("S1", brief.getSiteCode()); + assertEquals(Integer.valueOf(1), brief.getStatus()); + } + + @Test + void crossSiteBindingsAreAllListed() { + // 表单 sch 级共享:同 sch 下另一站点的事项绑了同一张表单,也要在明细里可见 + when(formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(1L)) + .thenReturn(Arrays.asList(form("leave", 1, 1))); + when(itemRepository.findBySchIdAndFormCodeIsNotNull(1L)) + .thenReturn(Arrays.asList( + item(11, "S1", "IT-001", "请假申请", "leave"), + item(12, "S2", "IT-002", "教职工请假", "leave"))); + + SvcFormVo vo = service.list(null, "S1").get(0); + assertEquals(2, vo.getRefItems().size()); + } + + @Test + void versionAggregationKeepsRefItemsOnLatestRow() { + // 同 code 多版本聚合出最新版一行;绑定关系挂在 code 上,明细归到该行 + when(formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(1L)) + .thenReturn(Arrays.asList(form("leave", 2, 0), form("leave", 1, 1))); + when(itemRepository.findBySchIdAndFormCodeIsNotNull(1L)) + .thenReturn(Arrays.asList(item(11, "S1", "IT-001", "请假申请", "leave"))); + + List list = service.list(null, "S1"); + assertEquals(1, list.size()); + assertEquals(Integer.valueOf(2), list.get(0).getVersion()); + assertEquals(1, list.get(0).getRefItems().size()); + } + + @Test + void refItemsAreGroupedToTheirOwnForm() { + when(formRepository.findBySchIdOrderByCodeAscVersionDescIdAsc(1L)) + .thenReturn(Arrays.asList(form("leave", 1, 1), form("seal", 1, 1))); + when(itemRepository.findBySchIdAndFormCodeIsNotNull(1L)) + .thenReturn(Arrays.asList(item(11, "S1", "IT-001", "请假申请", "leave"))); + + List list = service.list(null, "S1"); + assertEquals(0, list.get(1).getRefItems().size(), "seal 表单不应带上 leave 的事项明细"); + assertEquals("seal", list.get(1).getCode()); + } +}