feat(profession): 专业发展发布工具按模块页重写,新增「栏目动态字段」下发机制
build / build-push (push) Successful in 2m15s

对齐 ProfessionView/src/views/profession/RecordEdit.vue:

- profession.publish 字段改为 moduleId/title/grade/subject/ifOpen/remark/contents/fileIds
  (POST_FORM /api/profession/Create;附件走 /api/file/Upload;fullForm 全字段渲染)
- 新增 4 个字典工具:profession.grades / subjects / moduleFields / areaTree,
  并恢复上一轮重写时误删的 profession.myPoints / profession.pointsRank
- ToolDef:新增 extraFields* 声明(字典工具 + 参数名 + 「查询参数 → 表单字段」占位符映射 + 前置锚点)
- FormRequest:新增 ExtraFields(含 dependsOn / param / before / areaCall)
- AgentOrchestrator:抽出共用的 buildReadCall,据此下发 extraFields(栏目字段定义与区划树)
- ModuleFormParityTest:新增 4 条契约测试(字段顺序 / 提交方式 / 控件形态 / 动态字段声明)
This commit is contained in:
lijing
2026-10-13 18:11:10 +08:00
parent cb8764082d
commit 8cfbb949b6
5 changed files with 375 additions and 12 deletions
@@ -4,10 +4,25 @@ 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 {
@@ -63,6 +78,38 @@ public final class ProfessionTools {
"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")
@@ -74,17 +121,72 @@ public final class ProfessionTools {
.p(ToolParam.optional("top", "integer", "取前几名").dft("8"))
.readAt("/home"));
/* ==================== 发布记录 ==================== */
list.add(ToolDef.of("profession.publish", MODULE, "发布专业发展记录",
"发布一条专业发展记录(教案/学习资料/反思等)。调用前必须确认栏目 moduleId、标题与内容。",
"发布一条专业发展记录(教学设计/听课记录/课题研究/教学反思等)。"
+ "调用前必须确认所属栏目 moduleId 与标题;"
+ "正文、年级段、学科、是否公开、附件在表单里补齐,"
+ "栏目自定义字段由表单按所选栏目动态渲染。",
"POST_FORM", "/api/profession/Create")
.p(ToolParam.required("moduleId", "integer", "栏目 id,来自 profession.moduleTree")
.label("所属栏目").optionsFrom("profession.moduleTree"),
ToolParam.required("title", "string", "记录标题")
.label("记录标题").eg("2026年秋季公开课教学设计"),
ToolParam.optional("content", "string", "记录正文")
.label("记录正文").as("textarea"))
.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[]{"公开", "不公开"}));
ps.add(ToolParam.optional("remark", "string", "正文内容(对应模块页的富文本编辑器)")
.label("正文内容").as("textarea").placeholder("请输入正文内容…"));
// 栏目动态字段的打包结果:由表单取回字段定义、渲染后自行生成,用户不直接填
ps.add(ToolParam.optional("contents", "string", "栏目动态字段的取值(JSON 数组,由表单自动生成)")
.label("栏目动态字段").hide());
ps.add(ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
.label("添加附件").as("file").multiple(true).join(",")
// ★ 专业发展的附件上传接口与其余模块不同:/api/file/Upload(不是 /api/base/FileUpload)
.upload("/api/file/Upload"));
return ps.toArray(new ToolParam[0]);
}
}