feat(form): 发布表单与各模块「新建发布」页逐字段对齐(含审批流与作业两步发布)
build / build-push (push) Successful in 1m2s

把 5 个模块「新建/发布」页的字段与提交体逐个核对后,补齐此前缺失或形态不符的部分:

- 部门名镜像:公告/宿舍/活动三处在模块页是「字典 id + 反查名称」一起提交。
  新增 mirrorLabelOf 机制 —— 该字段不渲染控件、不进模型 schema,提交时按源字段
  选中项的显示名填充(deptName)
- 公告审批流:新增 flow 控件(widget=flow)+ 教师组织树字典 notice.teaTree,
  提交 flow:[{name,vote,approverTeaIds}];publishType 值域修正为 direct/approve;
  存草稿改用模块自己的独立接口 notice.saveDraft(POST /api/notice/SaveDraft)
- 活动分类:category 提交的是**分类名称**(模块页 :value="String(c.v)"),
  optionsKeys 由 ("k","v") 改为 ("v","v")
- 请假附件:fileIds 在 HomeLeave 是 number[](JSON 数组),去掉逗号串拼接;
  连续请假按 continuousLeaveNum 逐天展开成 continuousLeaveTime[](服务端/浏览器同源)
- 作业发布(整块新增):homework.publish / schedule / published / drafts /
  teachingSubjects / classTree。模块没有一步发布的接口,用 ToolDef.then 声明两步提交
  (Create 落草稿 → Publish | Schedule);附件是对象数组
  attachments:[{fileId,fileUrl,fileType,fileName,sort}],上传走 file 域
  /coursehomework/api/file/Upload
- 执行器:新增 followUp 两步提交(浏览器侧同源)、数组参数给标量时包成单元素数组、
  首步响应 data 取资源 id(对象形态取 .id,避免拼出 /homeworks/{"id":1}/publish)
- 新增 ModuleFormParityTest(13 项)逐条锁住上述契约

回归:mvn clean package → Tests run: 103, Failures: 0, Errors: 0, BUILD SUCCESS
真机:headless Chrome 走「表单 → 浏览器代执行 → 模块接口」,50 项断言全绿
This commit is contained in:
lijing
2026-10-13 11:29:03 +08:00
parent 322d589b3e
commit c16356db6e
14 changed files with 1318 additions and 52 deletions
@@ -663,9 +663,12 @@ public class AgentOrchestrator {
}
for (ToolParam p : shown) {
FormField f = p.toFormField();
// 附件控件:补上传地址(模块的附件上传只认浏览器 CAS 会话,必须前端带登录态 POST)
// 附件控件:补上传地址(模块的附件上传只认浏览器 CAS 会话,必须前端带登录态 POST)。
// 缺省按 「/<模块>/api/base/FileUpload」,个别模块不同(作业是 /api/file/Upload)时由参数覆盖。
if ("file".equals(f.getWidget())) {
f.setUploadPath("/" + def.getModule() + "/api/base/FileUpload");
f.setUploadPath(StringUtils.isNotBlank(p.getUploadPathOverride())
? p.getUploadPathOverride()
: "/" + def.getModule() + "/api/base/FileUpload");
}
// 字典类字段(如请假类型、班级、学生):优先交给前端用**用户自己的登录态**代取。
//
@@ -677,8 +680,10 @@ public class AgentOrchestrator {
BrowserToolRequest.BrowserCall optionCall = buildOptionCall(p, caller);
if (optionCall != null) {
f.setOptionCall(optionCall);
// 多选字段保持 multiselect(接收班级/宿舍/审批人),其余走单选下拉
if (!"multiselect".equals(f.getWidget())) {
// ★ 多值控件必须保留原样:multiselect(接收班级/宿舍)、scopes(公告推送范围)、
// flow(公告审批流)都是「选项从字典代取 + 用户多选」,降级成单选下拉会直接丢掉多选能力。
String w = f.getWidget();
if (!"multiselect".equals(w) && !"scopes".equals(w) && !"flow".equals(w)) {
f.setWidget("select");
}
// 有内置兜底的(如请假类型 病假/事假)先摆上,前端取到学校实时字典后覆盖
@@ -688,7 +693,10 @@ public class AgentOrchestrator {
List<Map<String, String>> opts = dictOptions(p.getOptionsFrom(), caller, dictCache);
if (!opts.isEmpty()) {
f.setOptions(opts);
f.setWidget("select");
String w2 = f.getWidget();
if (!"multiselect".equals(w2) && !"scopes".equals(w2) && !"flow".equals(w2)) {
f.setWidget("select");
}
f.setOptionsFallback(false);
}
}