build / build-push (push) Successful in 1m15s
彻底对齐各模块「新建 / 发布」页里的两类控件(此前只有通知公告搬到位):
正文 → wangEditor(widget=richtext)
· 校园公告 content、宿舍通知 content(发布 + 编辑)、家校活动 content(发布 + 编辑)、
作业 description
· 教师专业发展 remark:只声明形态、**不声明** .richText(...) —— 该模块 file 域只有
Upload/Preview/Down(没有 UploadImage),模块页 RecordEdit.vue 也没配 uploadImage,
所以下发空地址,前端据此把工具栏的「图片」组整组摘掉(模块页就是这个形态)
接收对象 → 自绘控件(widget=scopepicker / roompicker)
· 校园公告 pushScopes:换成 ClassScopeInput 同款(全部班级 / 指定班级 + 弹窗班级树);
提交口径不变,仍是 [{scopeType,scopeId,scopeName}]
· 宿舍通知 receiveList:换成模块页「选择接收宿舍」弹窗同款(左楼栋 / 右按楼层分组的房间);
v-model 是字符串(房间 id 逗号串或 ALL 哨兵),与模块 receiveList 的真实契约一致
基建
· ToolParam 新增 richText(upload,preview) / scopePicker(type) / roomPicker() /
pickerSource(页签名, 模块内相对路径);对应 FormField 四个字段
· AgentOrchestrator:新增 resolveModulePath 通用形态,richtext 与 pickerSources 的地址
一律只写模块内相对路径、前缀在这里拼;richtext **没有兜底**(不声明就是空)
· 带 optionCall 的字段会被改写成 select 的白名单改用 isSelfDrawnPicker()
(原先是硬编码 userpicker),并放开为 public 供契约测试复用生产口径
测试(139 tests, 0 failures)
· ModuleFormParityTest 新增两条通用不变量:正文图片地址与弹窗数据源都必须带模块前缀、
不许自己带前缀、不许双斜杠;另加「专业发展不得拿到兜底上传地址」
· FormFieldWidgetTest 新增 richtext(含「不声明 = 空」反向断言)与自绘控件透传两条
· IntentDirectFormTest / DormActivityPublishIntentTest 同步改成新控件口径
204 lines
12 KiB
Java
204 lines
12 KiB
Java
package com.yunhai.agent.tool.defs;
|
||
|
||
import com.yunhai.agent.tool.ToolDef;
|
||
import com.yunhai.agent.tool.ToolParam;
|
||
|
||
import java.util.ArrayList;
|
||
import java.util.LinkedHashMap;
|
||
import java.util.List;
|
||
import java.util.Map;
|
||
|
||
/**
|
||
* 教师专业发展模块工具 —— 后端 Profession,前缀 /api/profession、/api/moudule、/api/points、/api/stats。
|
||
*
|
||
* <p>★ 发布表单的字段以模块前端源码为**权威契约**:
|
||
* {@code ProfessionView/src/views/profession/RecordEdit.vue}(发布/编辑记录页)。
|
||
* 提交体(模块页 {@code save()} 逐行核对):
|
||
* <pre>
|
||
* POST /api/profession/Create (application/x-www-form-urlencoded)
|
||
* { moduleId, title, grade, subject, ifOpen, remark,
|
||
* contents: JSON.stringify([{fieldId, value}]), // 栏目动态字段
|
||
* fileIds: "11,12" } // 逗号串
|
||
* </pre>
|
||
* 与助手此前实现的差异(本轮修正):早先只有 moduleId/title/content 三项,
|
||
* 既缺「年级段 / 学科 / 是否公开 / 正文(字段名应是 remark 而不是 content)」,
|
||
* 也缺栏目动态字段与附件 —— 发出去的记录与在系统里发的天然不同。
|
||
*/
|
||
public final class ProfessionTools {
|
||
|
||
public static final String MODULE = "profession";
|
||
|
||
private ProfessionTools() {
|
||
}
|
||
|
||
public static List<ToolDef> defs() {
|
||
List<ToolDef> list = new ArrayList<>();
|
||
|
||
list.add(ToolDef.of("profession.homeRecords", MODULE, "栏目最新记录",
|
||
"各栏目最新发布的记录块(专业发展首页内容)。",
|
||
"GET", "/api/profession/HomeModuleRecords")
|
||
.p(ToolParam.optional("size", "integer", "每栏取几条").dft("6"))
|
||
.readAt("/home"));
|
||
|
||
list.add(ToolDef.of("profession.list", MODULE, "专业发展记录列表",
|
||
"后台口径的专业发展记录分页查询,可按栏目/教师/时间筛选。",
|
||
"GET", "/api/profession/ProfessionList")
|
||
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||
ToolParam.optional("keyword", "string", "标题关键词"),
|
||
ToolParam.optional("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("starttime", "string", "开始时间 yyyy-MM-dd"),
|
||
ToolParam.optional("endtime", "string", "结束时间 yyyy-MM-dd"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.moduleRecords", MODULE, "栏目文章列表",
|
||
"某个栏目下的文章分页(附同校其他栏目)。",
|
||
"GET", "/api/profession/ModuleRecordList")
|
||
.p(ToolParam.required("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.myRecords", MODULE, "我的栏目文章",
|
||
"查询我在某栏目下发表的文章。",
|
||
"GET", "/api/profession/MyModuleRecordList")
|
||
.p(ToolParam.required("moduleId", "integer", "栏目 id"),
|
||
ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"))
|
||
.readAt("/timeline"));
|
||
|
||
list.add(ToolDef.of("profession.detail", MODULE, "记录详情",
|
||
"查看一条专业发展记录的详情。",
|
||
"GET", "/api/profession/ProfessionDetail")
|
||
.p(ToolParam.required("SerID", "integer", "记录 id"))
|
||
.readAt("/module/list"));
|
||
|
||
list.add(ToolDef.of("profession.moduleTree", MODULE, "栏目树",
|
||
"专业发展的栏目树(含我的栏目),发布记录时选栏目用。",
|
||
"GET", "/api/moudule/ModuleTree")
|
||
.readAt("/record/new"));
|
||
|
||
/* ==================== 发布表单的字典(都必须由前端带登录态代取) ====================
|
||
* 模块的用户态接口只认浏览器 CAS 会话,服务端直取必然 code=302,
|
||
* 因此这里只声明「去哪儿取」,由 AgentOrchestrator 下发 optionCall / extraFields.call。
|
||
*/
|
||
|
||
// 年级段:模块页 RecordEdit.vue 的 grades(<select>,可空)
|
||
list.add(ToolDef.of("profession.grades", MODULE, "年级段字典",
|
||
"发布记录时的「年级段」下拉选项。属发布表单的组成部分,一般不单独调用。",
|
||
"GET", "/api/profession/GetGradeList")
|
||
.readAt("/record/new"));
|
||
|
||
// 学科:{subId, subName},subId 对齐班级管理的 SUB01 编码
|
||
list.add(ToolDef.of("profession.subjects", MODULE, "学科字典",
|
||
"发布记录时的「学科」下拉选项。属发布表单的组成部分,一般不单独调用。",
|
||
"GET", "/api/profession/GetSubList")
|
||
.readAt("/record/new"));
|
||
|
||
// 栏目的动态字段定义:{name, fields:[{id,fieldName,fieldType,ifRequired,...}]}
|
||
list.add(ToolDef.of("profession.moduleFields", MODULE, "栏目动态字段定义",
|
||
"某个栏目挂了哪些自定义字段(字段名/类型/是否必填/选项)。"
|
||
+ "发布表单据它渲染「栏目动态字段」区块,一般不单独调用。",
|
||
"GET", "/api/moudule/Getdatelist")
|
||
.p(ToolParam.required("SerID", "integer", "栏目 id(moduleId)"))
|
||
.readAt("/record/new"));
|
||
|
||
// 行政区划树:级联字段(province_city)用,level=2 省市 / 3 省市区
|
||
list.add(ToolDef.of("profession.areaTree", MODULE, "行政区划树",
|
||
"省市区三级行政区划树,供发布表单的「省市区」级联字段使用。",
|
||
"GET", "/api/profession/GetAreaTree")
|
||
.p(ToolParam.optional("level", "integer", "展开层级:2 省市 / 3 省市区").dft("3"))
|
||
.readAt("/record/new"));
|
||
|
||
list.add(ToolDef.of("profession.myPoints", MODULE, "我的积分",
|
||
"查询我的专业发展积分与来源明细。",
|
||
"GET", "/api/profession/MyPoints")
|
||
.readAt("/timeline"));
|
||
|
||
list.add(ToolDef.of("profession.pointsRank", MODULE, "积分排行",
|
||
"积分排行榜。",
|
||
"GET", "/api/profession/PointsRank")
|
||
.p(ToolParam.optional("top", "integer", "取前几名").dft("8"))
|
||
.readAt("/home"));
|
||
|
||
/* ==================== 发布记录 ==================== */
|
||
|
||
list.add(ToolDef.of("profession.publish", MODULE, "发布专业发展记录",
|
||
"发布一条专业发展记录(教学设计/听课记录/课题研究/教学反思等)。"
|
||
+ "调用前必须确认:所属栏目 moduleId、标题 title;"
|
||
// ★ 与公告 / 宿舍 / 活动 / 作业四个发布工具完全同款的一句(此前唯独这里漏了):
|
||
// 少了它,模型读到「必须确认」就先去反问用户「你要发到哪个栏目」,
|
||
// 答完才弹表单 —— 一件小事被拆成两轮对话(用户反馈的原话)。
|
||
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
|
||
+ "正文、年级段、学科、是否公开、附件在表单里补齐,"
|
||
+ "栏目自定义字段由表单按所选栏目动态渲染。",
|
||
"POST_FORM", "/api/profession/Create")
|
||
.p(professionPublishParams())
|
||
.extraFields("profession.moduleFields", "contents", moduleIdAs("SerID"))
|
||
.extraFieldsBefore("remark")
|
||
.extraFieldsArea("profession.areaTree")
|
||
.fullForm()
|
||
.write("/record/new"));
|
||
|
||
return list;
|
||
}
|
||
|
||
/**
|
||
* (Java 8 没有 {@code Map.of})栏目动态字段的依赖映射:查询参数名 → 表单字段名。
|
||
* <p>模块侧的栏目字段接口查询参数叫 {@code SerID},而表单字段叫 {@code moduleId} —— 两边名字不同。
|
||
*/
|
||
private static Map<String, String> moduleIdAs(String queryParam) {
|
||
Map<String, String> m = new LinkedHashMap<>();
|
||
m.put(queryParam, "moduleId");
|
||
return m;
|
||
}
|
||
|
||
/**
|
||
* 「发布 / 编辑记录」页的完整字段。
|
||
*
|
||
* <p>★ 与 {@code ProfessionView/src/views/profession/RecordEdit.vue} 的表单逐项对齐:
|
||
* <pre>
|
||
* 标题(必填,≤200) → 年级段 / 学科(两个可空下拉) → 是否公开(单选 公开/不公开)
|
||
* → 栏目动态字段(按栏目渲染 → contents) → 正文内容(富文本 → remark) → 附件(fileIds)
|
||
* </pre>
|
||
* 唯一多出来的是「所属栏目」——模块页是从栏目列表点「发布」进来的(栏目由入口决定),
|
||
* 助手没有这个上下文,所以必须让用户选一次。
|
||
*/
|
||
private static ToolParam[] professionPublishParams() {
|
||
List<ToolParam> ps = new ArrayList<>();
|
||
ps.add(ToolParam.required("moduleId", "integer", "栏目 id,来自 profession.moduleTree")
|
||
.label("所属栏目")
|
||
.optionsFrom("profession.moduleTree"));
|
||
ps.add(ToolParam.required("title", "string", "记录标题(200 字以内)")
|
||
.label("记录标题").placeholder("请输入记录标题").maxLength(200)
|
||
.eg("2026年秋季公开课教学设计"));
|
||
ps.add(ToolParam.optional("grade", "string", "年级段,来自 profession.grades")
|
||
.label("年级段")
|
||
// 年级段字典是 {value,label}(不是平台通用的 {k,v} / 业务 VO 的 {id,name}),必须指认键名
|
||
.optionsFrom("profession.grades").optionsKeys("value", "label"));
|
||
ps.add(ToolParam.optional("subject", "string", "学科,取值来自 profession.subjects 的 subId")
|
||
.label("学科")
|
||
.optionsFrom("profession.subjects").optionsKeys("subId", "subName"));
|
||
ps.add(ToolParam.optional("ifOpen", "integer", "是否公开:1 公开 / 0 不公开")
|
||
.label("是否公开").as("radio").dft("1")
|
||
.options(new String[]{"1", "0"}, new String[]{"公开", "不公开"}));
|
||
// ★ 富文本但**不带图片上传**:模块页 RecordEdit.vue 的 editorConfig 只有 placeholder
|
||
// (toolbarConfig = {}),并没有配 MENU_CONF.uploadImage —— 该模块的 file 域只暴露了
|
||
// Upload / Preview / Down(见 profession.ts),**没有** UploadImage。
|
||
// 所以这里不调 .richText(...):上传地址为空时前端会把工具栏的「图片」组去掉,
|
||
// 而不是留一个点了必失败的按钮。
|
||
ps.add(ToolParam.optional("remark", "string", "正文内容(富文本 HTML,对应模块页的 wangEditor;该模块不支持正文插图)")
|
||
.label("正文内容").as("richtext").placeholder("请输入正文内容…"));
|
||
// 栏目动态字段的打包结果:由表单取回字段定义、渲染后自行生成,用户不直接填
|
||
ps.add(ToolParam.optional("contents", "string", "栏目动态字段的取值(JSON 数组,由表单自动生成)")
|
||
.label("栏目动态字段").hide());
|
||
ps.add(ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
|
||
.label("添加附件").as("file").multiple(true).join(",")
|
||
// ★ 专业发展的附件上传接口与其余模块不同:走 file 域(不是 base/FileUpload)。
|
||
// 只写模块内相对路径,前缀由 AgentOrchestrator.resolveUploadPath 拼 → /profession/api/file/Upload
|
||
// (ProfessionView/src/views/profession/RecordEdit.vue 的 uploadAction = fileUploadUrl)
|
||
.upload("/api/file/Upload"));
|
||
return ps.toArray(new ToolParam[0]);
|
||
}
|
||
}
|