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
@@ -539,12 +539,22 @@ class ModuleFormParityTest {
// 收件人是「用户」:v-model 为 "all" 或用户 id 逗号串(components/UserCheckInput.vue)
ToolParam receive = param(def, "receiveList");
assertNotNull(receive, "通知公告按用户下发,必须有 receiveList");
// ★ 用 resolveWidget() 而不是 getWidget():receiveList 没有显式 .as("multiselect"),
// 它是「type=array + optionsFrom 字典」自然推断出的多选(见 ToolParam#resolveWidget)。
assertEquals("multiselect", receive.resolveWidget());
// ★ 2026-10-14:控件换成模块页同款的自绘组件(radio 全部/指定 + 选人弹窗),
// 故这里是**显式** .as("userpicker"),不再靠 type/optionsFrom 推断。
assertEquals("userpicker", receive.resolveWidget(),
"通知对象用模块页同款 UserCheckInput(radio 全部用户/指定用户 + 选人弹窗)");
assertEquals(",", receive.getJoinSeparator());
assertEquals("all", receive.getAllToken(), "勾「全部用户」提交的就是 all");
assertEquals("notify.schoolAll", receive.getOptionsFrom());
assertEquals("notify.schoolAll", receive.getOptionsFrom(),
"弹窗里的组织树仍由 notify.schoolAll 代取(前端取**原始嵌套树**,不拍平)");
// 正文换成富文本(模块页 NewNotice.vue 用 wangEditor v5),并带上「插入图片」的两个地址
ToolParam content = param(def, "content");
assertEquals("richtext", content.resolveWidget(), "正文用富文本编辑器");
assertEquals("/api/file/UploadImage", content.getRichTextUploadOverride(),
"图片上传是独立接口(不是附件的 FileUpload);只写相对路径,前缀由编排层拼");
assertEquals("/api/file/Preview?id={id}", content.getRichTextPreviewOverride(),
"正文内图片必须用 Preview(inline),Down 是 attachment 内嵌显示不出来");
// 校园公告那一套字段一个都不该出现在这里
assertNull(param(def, "pushScopes"), "pushScopes(推送范围)是校园公告的字段");
@@ -620,6 +630,19 @@ class ModuleFormParityTest {
assertEquals("101", com.alibaba.fastjson.JSON
.parseObject(executor.prepareBrowserCall(def, args).getBody()).getString("receiveList"),
"哨兵与具体项互斥:同时选中时具体项优先");
/* ★ 2026-10-14:前端换成 UserCheckInput 后,表单里存的就是**字符串**("all" / "101,102"),
* 不再是数组。这条短路极易出错:normalizeJsonArgs 的第二个循环会把「array 型参数却是标量」
* 包成 ["101,102"](整串塞进一个元素),而模块的 NoticeRequest.receiveList 是 String —— 判非法。
* 之所以现在没事,是因为第一个循环按 joinSeparator 拼过之后就 continue 了。
* 把这个「字符串原样透传」的口径钉住,防止谁把那个 continue 删掉。 */
args.put("receiveList", "101,102");
assertEquals("101,102", com.alibaba.fastjson.JSON
.parseObject(executor.prepareBrowserCall(def, args).getBody()).getString("receiveList"),
"userpicker 直接给逗号串时不能再被包成数组");
args.put("receiveList", "all");
assertEquals("all", com.alibaba.fastjson.JSON
.parseObject(executor.prepareBrowserCall(def, args).getBody()).getString("receiveList"));
}
/* ==================== 辅助 ==================== */