feat: 表单一期放行扩到 11 类(+checkbox/multiselect/phone/image)+ 契约审计修复
build / build-push (push) Successful in 2m9s

- FormValidator:PHASE1_TYPES 加 checkbox/multiselect/phone/image;options 必填口径扩到
  select/radio/checkbox/multiselect(4 类);validateValue 补多选(值须在 options 内)分支,
  image 并入 upload 的附件数组通道
- 审计修复:maxFiles 上限判定改为 maxFiles>0(缺省或 0=不限),与前端设计器「留空不限」
  及渲染器口径对齐——原实现把 0 当上限,会让 maxFiles:0 的表单任何附件都被拒
- ServiceForm 注释纠偏:契约类名 FormValidator(原误写 FormSchemaValidator)、一期 11 类
- 测试:FormValidatorTest +6(新 4 类 schema、多分组计数、options 必填、多选越界、phone
  pattern/maxLength、image maxFiles/非数组、precision 非白名单)、ServiceFormServiceTest +4
  (冻结行不可变契约:带已发布版本 id 被拒 / 不传 id 按 code 升版出草稿 / 草稿就地更新)

验证:mvn test 102/102 绿(基线 92)。
This commit is contained in:
chenxing
2026-09-16 20:22:02 +08:00
parent 9a47351725
commit 47beb4eaa4
4 changed files with 223 additions and 13 deletions
@@ -1,7 +1,9 @@
package com.yunhai.svchall.service.impl;
import com.yunhai.platform.apistd.BizException;
import com.yunhai.svchall.entity.ServiceForm;
import com.yunhai.svchall.entity.ServiceItem;
import com.yunhai.svchall.model.svc.SvcModels.SvcFormSaveRequest;
import com.yunhai.svchall.model.svc.SvcModels.SvcFormVo;
import com.yunhai.svchall.repository.ServiceFormRepository;
import com.yunhai.svchall.repository.ServiceItemRepository;
@@ -9,19 +11,24 @@ 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 org.mockito.ArgumentCaptor;
import java.util.Arrays;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* 表单列表使用场景可见性单测:list() 必须带回"被哪些事项绑定"的明细(refItems),
* 表单列表使用场景可见性 + 保存升版契约单测:list() 必须带回"被哪些事项绑定"的明细(refItems),
* 管理员才能从表单页反查使用位置(一期可见闭环);表单 sch 级共享,跨站点绑定如实展示。
* save() 侧冻结「行不可变」:已发布/停用版本只接受"不传 id"的重提(按 code 升版),带 id 一律拒。
*/
class ServiceFormServiceTest {
@@ -130,4 +137,71 @@ class ServiceFormServiceTest {
assertEquals(0, list.get(1).getRefItems().size(), "seal 表单不应带上 leave 的事项明细");
assertEquals("seal", list.get(1).getCode());
}
// ===== save():行不可变契约下「编辑已发布表单=不传 id 重提,后端按 code 升版」 =====
private SvcFormSaveRequest req(Integer id) {
SvcFormSaveRequest r = new SvcFormSaveRequest();
r.setId(id);
r.setCode("leave");
r.setName("请假申请单");
r.setSchemaJson("{\"groups\":[{\"title\":\"基本信息\",\"fields\":["
+ "{\"key\":\"reason\",\"label\":\"事由\",\"type\":\"textarea\",\"required\":true}]}]}");
return r;
}
@Test
void savingPublishedVersionWithIdIsRejected() {
// 设计器必须自行「去掉 id」——带已发布版本的 id 提交一律拒(P1 回归:曾导致编辑主路径整体不可用)
when(formRepository.findByIdAndSchId(9, 1L)).thenReturn(Optional.of(form("leave", 1, 1)));
assertThrows(BizException.class, () -> service.save(null, "S1", req(9)),
"已发布版本不可就地改");
}
@Test
void savingWithoutIdOnPublishedCodeClonesNewDraftVersion() {
ServiceForm published = form("leave", 1, 1);
published.setId(9);
when(formRepository.findBySchIdAndCodeOrderByVersionDesc(1L, "leave"))
.thenReturn(Arrays.asList(published));
when(formRepository.save(org.mockito.ArgumentMatchers.any(ServiceForm.class)))
.thenAnswer(inv -> {
ServiceForm f = inv.getArgument(0);
f.setId(10);
return f;
});
assertEquals(Integer.valueOf(10), service.save(null, "S1", req(null)));
ArgumentCaptor<ServiceForm> saved = ArgumentCaptor.forClass(ServiceForm.class);
verify(formRepository).save(saved.capture());
assertEquals(Integer.valueOf(2), saved.getValue().getVersion(), "已发布版本重提应升一版");
assertEquals(Integer.valueOf(0), saved.getValue().getStatus(), "升版产物必须是草稿");
assertEquals("leave", saved.getValue().getCode());
}
@Test
void savingWithoutIdOnExistingDraftUpdatesItInPlace() {
ServiceForm draft = form("leave", 2, 0);
draft.setId(10);
when(formRepository.findBySchIdAndCodeOrderByVersionDesc(1L, "leave"))
.thenReturn(Arrays.asList(draft));
when(formRepository.save(draft)).thenReturn(draft);
assertEquals(Integer.valueOf(10), service.save(null, "S1", req(null)));
assertEquals(Integer.valueOf(2), draft.getVersion(), "已有草稿不再升版,就地更新");
assertEquals("请假申请单", draft.getName());
}
@Test
void savingDraftWithIdUpdatesItInPlace() {
// 设计器装载到草稿版本(status=0)时应带 id:就地更新,不产生多余版本行
ServiceForm draft = form("leave", 2, 0);
draft.setId(10);
when(formRepository.findByIdAndSchId(10, 1L)).thenReturn(Optional.of(draft));
when(formRepository.save(draft)).thenReturn(draft);
assertEquals(Integer.valueOf(10), service.save(null, "S1", req(10)));
verify(formRepository).save(draft);
}
}