feat(通知公告): 发布表单支持模块页的富文本正文与选人控件
build / build-push (push) Successful in 2m1s

前端已把 NoticeFront 的 wangEditor 与自绘「通知对象」控件 1:1 搬进助手表单,
后端补上这两个控件所需的契约:

- ToolParam / FormField 新增:
  · pickerSources:选人弹窗的**数据源页签**(形如 {name, 模块内相对地址}),
    由 AgentOrchestrator 拼上模块前缀后下发(沿用「只写相对路径」的铁律);
  · richTextUploadPath / richTextPreviewPath:正文插图的上传与预览地址。
- AgentOrchestrator:把 resolveUploadPath 抽出通用形态 resolveModulePath(幂等、只接受
  模块内相对路径);buildForm 为 widget=userpicker 下发 pickerSources;
  optionCall 强制改写 widget 的白名单放行 userpicker(否则会被降级成 select)。
- NoticeTools:notify.publish 与 notify.editNotice 的 receiveList 改为 userpicker,
  两个数据源为「基础平台」/api/base/GetSchoolAll 与「自定义组」
  /api/contacts/GetGroupTree?type=1;content 走 richtext。

测试:NotifyIntentFormTest / IntentDirectFormTest 补 pickerSources 断言
(**只写相对路径**,不带前缀 —— 前缀拼错是本项目反复出过的坑),
ModuleFormParityTest 补 userpicker 取值口径断言(字符串原样透传,不得包成数组)。
mvn -o test:Tests run: 135, Failures: 0, Errors: 0,BUILD SUCCESS。
This commit is contained in:
lijing
2026-10-14 15:19:24 +08:00
parent 8f0f8a04b3
commit c084c978aa
7 changed files with 273 additions and 30 deletions
@@ -228,9 +228,14 @@ class IntentDirectFormTest {
assertEquals("/list/new", resp.getForm().getLandingPath());
assertTrue(resp.getReply().contains("发布通知"), "引导语应说明这是发布通知,实际:" + resp.getReply());
// 标题(单行)+ 正文(多行)
// 标题(单行)+ 正文(富文本,与模块页的 wangEditor 一致)
assertEquals("text", field(resp, "title").getWidget());
assertEquals("textarea", field(resp, "content").getWidget());
FormField content = field(resp, "content");
assertEquals("richtext", content.getWidget(), "正文用富文本编辑器(模块页 wangEditor)");
assertEquals("/notice/api/file/UploadImage", content.getRichTextUploadPath(),
"正文里插图片要打模块真实前缀 /notice(且不是附件的 FileUpload)");
assertEquals("/notice/api/file/Preview?id={id}", content.getRichTextPreviewPath(),
"图片预览地址模板同样要带前缀,{id} 留给前端替换");
// 来源部门:下拉 + 代取地址(带上模块前缀与固定字典种类)
FormField deptId = field(resp, "deptId");
@@ -241,11 +246,32 @@ class IntentDirectFormTest {
assertEquals("k", deptId.getValueKey());
assertEquals("v", deptId.getLabelKey());
// 通知对象:按用户多选(与校园公告的「推送范围」按班级完全不同)
// 通知对象:模块页同款自绘控件(radio 全部用户/指定用户 + 选人弹窗)。
// ★ 它也有 optionCall,必须确认**没有被降级成 select** ——
// AgentOrchestrator 对「带 optionCall 的字段」会把控件改写成 select(防止取回字典后还是输入框),
// userpicker 靠白名单豁免;漏了就变成普通下拉,正是这次要避免的。
FormField receiveList = field(resp, "receiveList");
assertEquals("multiselect", receiveList.getWidget());
assertNotNull(receiveList.getOptionCall(), "通知对象应带上组织树代取地址");
assertEquals("userpicker", receiveList.getWidget(),
"通知对象应是自绘的「全部用户 / 指定用户」+ 选人弹窗,不能被降级成 select");
assertNotNull(receiveList.getOptionCall(), "通知对象应带上组织树代取地址(弹窗里用它取原始树)");
assertEquals("/notice/api/base/GetSchoolAll", receiveList.getOptionCall().getPathAndQuery());
assertEquals("all", receiveList.getAllToken(), "「全部用户」哨兵值");
// ★ 选人弹窗有两个页签(模块页 UserCheckInput.vue 的 sources)——这里钉住**拼好前缀**的结果。
// 与附件上传同一个坑:只写相对路径、前缀由 resolveModulePath 拼,漏了就线上 404。
// 注意「自定义组」的路径里带 query,拼前缀时不能被吃掉。
assertNotNull(receiveList.getPickerSources(), "选人弹窗的数据源页签要下发到前端");
assertEquals(2, receiveList.getPickerSources().size());
FormField.PickerSource tab0 = receiveList.getPickerSources().get(0);
assertEquals("基础平台", tab0.getName());
assertNotNull(tab0.getCall());
assertEquals("GET", tab0.getCall().getMethod(), "取组织树是只读 GET");
assertEquals("/notice/api/base/GetSchoolAll", tab0.getCall().getPathAndQuery(),
"代取地址必须带模块前缀 /notice");
FormField.PickerSource tab1 = receiveList.getPickerSources().get(1);
assertEquals("自定义组", tab1.getName());
assertEquals("/notice/api/contacts/GetGroupTree?type=1", tab1.getCall().getPathAndQuery(),
"自定义组树同样要带前缀,且 type=1 要保留");
// 校园公告独有的字段绝不能串到通知公告的表单上
for (String foreign : new String[]{"pushScopes", "publishType", "receiptFlag", "topmost", "type"}) {