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
@@ -53,6 +53,8 @@ class ModuleFormParityTest {
// 于是「附件上传地址拼错」这个 bug 在测试里也被一并复制、永远测不出来。
settings.getModules().put("homeleave", "http://base.yunhaixingtong.com/home-leave");
settings.getModules().put("coursehomework", "http://base.yunhaixingtong.com/coursehomework");
// 专业发展:模块挂在自己的路径前缀下(/profession),代取地址与附件上传都要用它
settings.getModules().put("profession", "http://base.yunhaixingtong.com/profession");
registry = new ToolRegistry(settings);
registry.init();
executor = new ToolExecutor(settings, new RestTemplate());
@@ -282,8 +284,120 @@ class ModuleFormParityTest {
assertTrue(body.contains("\"submitType\":[\"image\"]"), "标量要包成数组:" + body);
}
/* ==================== ⑥ 教师专业发展:发布记录(RecordEdit.vue) ==================== */
/**
* 发布字段与模块页 {@code ProfessionView/src/views/profession/RecordEdit.vue} 逐项一致。
*
* <p>★ 这条钉的是一个**真实提交出错**的差异:早先助手只发 {@code content},
* 而模块页发的是 {@code remark}(正文)—— 字段名不对,正文根本没存进去,
* 同时「年级段 / 学科 / 是否公开 / 附件」四项完全缺失,发出去的记录与在系统里发的天然不同。
*/
@Test
void professionPublishFieldsMatchRecordEditForm() {
ToolDef def = registry.get("profession.publish");
assertNotNull(def, "要有「发布专业发展记录」工具");
assertEquals(Arrays.asList(
"moduleId", "title", "grade", "subject", "ifOpen", "remark", "contents", "fileIds"),
paramNames(def),
"字段与顺序都要对齐模块页:标题 → 年级段/学科 → 是否公开 →(栏目动态字段)→ 正文 → 附件");
assertNull(param(def, "content"), "正文的字段名是 remark —— 模块页不叫 content");
}
@Test
void professionPublishIsPostFormFullFormAndStaffOnly() {
ToolDef def = registry.get("profession.publish");
assertEquals("POST_FORM", def.getHttpMethod(), "模块页 create() 走 postFormUrl(form-urlencoded)");
assertEquals("/api/profession/Create", def.getPath());
assertTrue(def.isFormAll(), "发布页要渲染全部字段(与模块页「发布」页一致)");
assertFalse(def.isReadOnly(), "发布是写操作");
assertEquals("/record/new", def.getLandingPath());
assertTrue(def.allowsAny(new java.util.HashSet<>(Arrays.asList(com.yunhai.agent.auth.AgentRole.TEACHER))),
"专业发展是教职工工具");
assertFalse(def.allowsAny(new java.util.HashSet<>(Arrays.asList(com.yunhai.agent.auth.AgentRole.STUDENT))),
"学生不应看到专业发展发布");
}
@Test
void professionPublishFieldControlsMatchModulePage() {
ToolDef def = registry.get("profession.publish");
ToolParam title = param(def, "title");
assertTrue(title.isRequired(), "标题必填");
assertEquals(200, title.getMaxLength(), "模块页 :maxlength=\"200\"");
assertEquals("请输入记录标题", title.getPlaceholderText(), "占位文案照搬模块页");
// 年级段 / 学科:两个可空下拉,选项都来自模块字典(服务端取不到 → 前端带登录态代取)
assertEquals("profession.grades", param(def, "grade").getOptionsFrom());
assertEquals(Arrays.asList("value", "label"), optionsKeysOf(param(def, "grade")),
"年级段字典是 {value,label},不是平台通用的 {k,v}");
assertEquals("profession.subjects", param(def, "subject").getOptionsFrom());
assertEquals(Arrays.asList("subId", "subName"), optionsKeysOf(param(def, "subject")),
"学科字典是 {subId,subName}");
// 是否公开:单选按钮组,默认「公开」(模块页 ifOpen 初值 1)
ToolParam ifOpen = param(def, "ifOpen");
assertEquals("radio", ifOpen.getWidget());
assertEquals(Arrays.asList("1", "0"), ifOpen.getEnumValues());
assertEquals(Arrays.asList("公开", "不公开"), ifOpen.getEnumLabels());
assertEquals("1", ifOpen.getDefaultValue());
// 正文:多行文本,占位文案与模块页的富文本编辑器一致
assertEquals("textarea", param(def, "remark").getWidget());
assertEquals("请输入正文内容…", param(def, "remark").getPlaceholderText());
// 附件:上传接口与其余模块**不同**(/api/file/Upload,不是 /api/base/FileUpload)
ToolParam fileIds = param(def, "fileIds");
assertEquals("file", fileIds.getWidget());
assertEquals(",", fileIds.getJoinSeparator(), "fileIds 在 wire 上是逗号串");
assertEquals("/api/file/Upload", fileIds.getUploadPathOverride());
}
/**
* 栏目动态字段(contents)的代取声明。
*
* <p>这组字段由**栏目配置**决定、构建期枚举不出来 —— 模块页是
* {@code GET /api/moudule/Getdatelist?SerID=<栏目>} 取回后就地渲染,
* 提交时打包成 {@code contents}。助手必须走同一条路,否则发到有自定义字段的栏目会缺数据。
*/
@Test
void professionPublishDeclaresColumnDynamicFields() {
ToolDef def = registry.get("profession.publish");
assertEquals("profession.moduleFields", def.getExtraFieldsFrom());
assertEquals("contents", def.getExtraFieldsParam());
assertEquals("remark", def.getExtraFieldsBefore(),
"模块页把栏目字段排在正文之前,不锚定会掉到附件后面");
assertEquals("moduleId", def.getExtraFieldsDepends().get("SerID"),
"模块侧查询参数叫 SerID,表单字段叫 moduleId —— 必须显式映射");
assertEquals("profession.areaTree", def.getExtraFieldsAreaFrom(),
"级联字段(province_city)要能取到区划树");
assertTrue(param(def, "contents").isHidden(),
"contents 由表单打包生成,不作为字段渲染、也不让模型填");
// 代取地址要带模块真实前缀,且把依赖留成占位符(由前端替换后再发)
ToolDef dict = registry.get("profession.moduleFields");
assertEquals("/api/moudule/Getdatelist", dict.getPath());
assertTrue(param(dict, "SerID").isRequired(), "栏目字段接口必须指定栏目");
BrowserToolRequest.BrowserCall call =
executor.prepareOptionCall(dict, java.util.Collections.singletonList("SerID"));
assertNotNull(call, "代取描述要能生成");
assertEquals("/profession/api/moudule/Getdatelist?SerID={{SerID}}", call.getPathAndQuery());
// 附件与代取地址都用「配置里的真实前缀」,而不是模块 key 拼出来的
assertEquals("/profession",
AgentOrchestrator.pathPrefix("profession", "http://base.yunhaixingtong.com/profession"));
}
/* ==================== 辅助 ==================== */
/** 字典返回对象里「值 / 显示名」取的是哪两个键(optionsKeys 的结果) */
private List<String> optionsKeysOf(ToolParam p) {
if (p == null) {
return null;
}
return Arrays.asList(p.getOptionValueKey(), p.getOptionLabelKey());
}
private ToolParam param(ToolDef def, String name) {
if (def == null || def.getParams() == null) {
return null;