对齐 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:
@@ -28,6 +28,7 @@ import org.springframework.stereotype.Service;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.LinkedHashMap;
|
||||
@@ -761,6 +762,29 @@ public class AgentOrchestrator {
|
||||
form.setMissing(missingLabels);
|
||||
form.setAllFields(def.isFormAll());
|
||||
|
||||
// 「栏目动态字段」(专业发展的栏目自定义字段):构建期枚举不出来,只能把「去哪儿取」
|
||||
// 交给前端带用户登录态取回后就地渲染,提交时打包进 extraFieldsParam(contents)。
|
||||
if (StringUtils.isNotBlank(def.getExtraFieldsFrom())) {
|
||||
// 占位符用**模块侧的查询参数名**(如 SerID),映射告诉前端该拿哪个表单字段去填
|
||||
BrowserToolRequest.BrowserCall call =
|
||||
buildReadCall(def.getExtraFieldsFrom(), def.getExtraFieldsDepends().keySet(), caller);
|
||||
if (call != null) {
|
||||
FormRequest.ExtraFields extra = new FormRequest.ExtraFields();
|
||||
extra.setCall(call);
|
||||
extra.setDependsOn(new LinkedHashMap<>(def.getExtraFieldsDepends()));
|
||||
extra.setParam(def.getExtraFieldsParam());
|
||||
extra.setBefore(def.getExtraFieldsBefore());
|
||||
// 级联字段的区划树(含 {{level}} 占位符,按字段配置取 2 或 3 级)
|
||||
if (StringUtils.isNotBlank(def.getExtraFieldsAreaFrom())) {
|
||||
extra.setAreaCall(buildReadCall(def.getExtraFieldsAreaFrom(),
|
||||
Collections.singletonList("level"), caller));
|
||||
}
|
||||
form.setExtraFields(extra);
|
||||
} else {
|
||||
log.warn("栏目动态字段代取不可用:tool={} dict={}", def.getName(), def.getExtraFieldsFrom());
|
||||
}
|
||||
}
|
||||
|
||||
// 已从对话里确定的参数 → 表单里只读回显(值翻译成中文,用户能一眼核对)
|
||||
Map<String, Object> preset = new LinkedHashMap<>();
|
||||
List<Map<String, String>> recognized = new ArrayList<>();
|
||||
@@ -836,21 +860,37 @@ public class AgentOrchestrator {
|
||||
* 服务端直调必然 302,所以只能把地址交给前端、由前端带用户登录态去取。
|
||||
*/
|
||||
private BrowserToolRequest.BrowserCall buildOptionCall(ToolParam p, CallerContext caller) {
|
||||
if (StringUtils.isBlank(p.getOptionsFrom())) {
|
||||
return buildReadCall(p.getOptionsFrom(), p.getDependsOn(), caller);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成「浏览器代取」的调用描述(下拉字典与「栏目动态字段」共用)。
|
||||
*
|
||||
* <p>差别只在取回的东西怎么用:字典取回的是选项(渲染成下拉),
|
||||
* 栏目字段取回的是**字段定义**(前端就地生成控件)。地址拼装与权限校验完全一致:
|
||||
* 只读工具 + 角色可见 + 依赖字段留 {@code {{占位符}}} 由前端替换。
|
||||
*
|
||||
* @param toolName 只读工具名(如 leave.classStudents / profession.moduleFields)
|
||||
* @param dependsOn 需要留占位符的依赖字段名;为空 = 无参调用
|
||||
* @return 调用描述;工具不存在 / 非只读 / 越权 / 模块未配地址时返回 {@code null}
|
||||
*/
|
||||
private BrowserToolRequest.BrowserCall buildReadCall(String toolName, Collection<String> dependsOn,
|
||||
CallerContext caller) {
|
||||
if (StringUtils.isBlank(toolName)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
ToolDef dict = registry.get(p.getOptionsFrom());
|
||||
ToolDef dict = registry.get(toolName);
|
||||
if (dict == null || !dict.isReadOnly()) {
|
||||
return null;
|
||||
}
|
||||
// 与 fetchDictOptions 同源的角色约束:越权的字典不外泄地址
|
||||
// 与 fetchDictOptions 同源的角色约束:越权的字典/字段定义不外泄地址
|
||||
if (settings.isEnforceRolePermission() && !dict.allowsAny(caller.getRoles())) {
|
||||
return null;
|
||||
}
|
||||
return executor.prepareOptionCall(dict, p.getDependsOn());
|
||||
return executor.prepareOptionCall(dict, dependsOn);
|
||||
} catch (Exception e) {
|
||||
log.warn("生成代取选项描述失败 {}:{}", p.getOptionsFrom(), e.toString());
|
||||
log.warn("生成代取描述失败 {}:{}", toolName, e.toString());
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,4 +57,50 @@ public class FormRequest {
|
||||
|
||||
/** 补充提示(可选),如「时间格式为 2026-10-11 08:00:00」 */
|
||||
private String hint;
|
||||
|
||||
/**
|
||||
* 「栏目动态字段」——由业务模块的栏目配置决定、后端在构建期<b>无法枚举</b>的字段。
|
||||
*
|
||||
* <p>背景:教师专业发展的每个栏目(教学设计 / 课题研究…)可以挂一组管理员自定义字段
|
||||
* (`GET /api/moudule/Getdatelist?SerID=<栏目 id>` 返回 `{name, fields:[{id,fieldName,fieldType,…}]}`),
|
||||
* 模块页在「发布」表单里把这一组字段渲染在正文之前,提交时打包成 `contents`
|
||||
* (`JSON.stringify([{fieldId, value}])`)。助手若要真正对齐,就必须在**选好栏目之后**
|
||||
* 把这组字段取回来渲染 —— 但它既不是工具的固定参数,也不在构建期可知。
|
||||
*
|
||||
* <p>做法与下拉字典一致:后端只下发「去哪儿取」({@link ExtraFields#call}),
|
||||
* 前端带用户登录态取回定义、就地渲染、提交时打包进 {@link ExtraFields#param}。
|
||||
* 取不到时不渲染该区块(不阻塞发布,只是少填了自定义字段)。
|
||||
*/
|
||||
private ExtraFields extraFields;
|
||||
|
||||
/** 栏目动态字段的代取描述:`call` 给出地址(含 {{占位符}}),`param` 给出打包后的参数名 */
|
||||
@Data
|
||||
public static class ExtraFields {
|
||||
/** 带占位符的浏览器代取调用(依赖字段有值后由前端替换 {{占位符}} 再发) */
|
||||
private com.yunhai.agent.model.BrowserToolRequest.BrowserCall call;
|
||||
/**
|
||||
* 占位符 → 表单字段名 的映射(如 {@code SerID → moduleId})。
|
||||
*
|
||||
* <p>两个用途:① 全部依赖字段有值才发起请求;② 把地址里的 {@code {{SerID}}}
|
||||
* 换成 {@code moduleId} 的当前值。之所以要映射而不是同名:模块侧查询参数习惯用
|
||||
* {@code SerID} 这类老式命名,与表单字段名并不一致。
|
||||
*/
|
||||
private Map<String, String> dependsOn = new LinkedHashMap<>();
|
||||
/** 渲染结果打包后写进哪个提交参数(专业发展是 contents) */
|
||||
private String param;
|
||||
/** 区块标题(模块页叫「栏目动态字段」) */
|
||||
private String title = "栏目动态字段";
|
||||
/**
|
||||
* 渲染在哪个字段**之前**(对齐模块页的版式:栏目字段在正文与附件之上)。
|
||||
* <p>为空则追加到末尾;字段不存在时同样追加到末尾。
|
||||
*/
|
||||
private String before;
|
||||
/** 响应里字段数组所在的键(默认 fields,即 data.fields) */
|
||||
private String fieldsKey = "fields";
|
||||
/**
|
||||
* 「省市区」级联字段的区划树代取调用(含 {@code {{level}}} 占位符,由字段配置决定 2 或 3)。
|
||||
* <p>为空表示该表单不支持级联字段。
|
||||
*/
|
||||
private com.yunhai.agent.model.BrowserToolRequest.BrowserCall areaCall;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -128,6 +128,67 @@ public class ToolDef {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「栏目动态字段」的来源(可选)——某些业务模块的发布表单里有一组**由栏目配置决定**、
|
||||
* 构建期无法枚举的字段(教师专业发展的 `Getdatelist`)。
|
||||
*
|
||||
* <p>声明后:{@code buildForm} 会把「去哪儿取定义」下发到
|
||||
* {@code FormRequest.extraFields},由前端带用户登录态取回、就地渲染,
|
||||
* 提交时打包进 {@link #extraFieldsParam} 指定的参数(如 {@code contents})。
|
||||
*/
|
||||
private String extraFieldsFrom;
|
||||
|
||||
/** 栏目动态字段打包后的提交参数名(如 contents) */
|
||||
private String extraFieldsParam;
|
||||
|
||||
/** 取栏目字段定义时的「查询参数名 → 表单字段名」映射(如 SerID → moduleId) */
|
||||
private Map<String, String> extraFieldsDepends = new LinkedHashMap<>();
|
||||
|
||||
/** 「省市区」级联字段的区划树来源(只读工具名,可空 = 该表单不支持级联字段) */
|
||||
private String extraFieldsAreaFrom;
|
||||
|
||||
/** 栏目动态字段渲染在哪个字段之前(对齐模块页版式;空 = 追加到末尾) */
|
||||
private String extraFieldsBefore;
|
||||
|
||||
/**
|
||||
* 声明「栏目动态字段」:字段定义从 {@code dictTool} 代取,渲染后打包进 {@code param}。
|
||||
*
|
||||
* <p>★ {@code dependsOn} 要的是「查询参数名 → 表单字段名」的两边映射,而不是同名:
|
||||
* 模块侧查询参数习惯用老式命名(专业发展的栏目字段接口是 {@code SerID}),
|
||||
* 与表单字段名({@code moduleId})并不一致 —— 既要留 {@code {{SerID}}} 占位符,
|
||||
* 也要知道该用表单里哪个字段的值去填它。
|
||||
*
|
||||
* @param dictTool 提供字段定义的只读工具(GET)
|
||||
* @param param 打包后的提交参数名(专业发展是 contents)
|
||||
* @param dependsOn 查询参数名 → 表单字段名(如 SerID → moduleId)
|
||||
*/
|
||||
public ToolDef extraFields(String dictTool, String param, Map<String, String> dependsOn) {
|
||||
this.extraFieldsFrom = dictTool;
|
||||
this.extraFieldsParam = param;
|
||||
if (dependsOn != null) {
|
||||
this.extraFieldsDepends.putAll(dependsOn);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 声明「省市区」级联字段的区划树来源(如 profession.areaTree,地址里带 {{level}} 占位符) */
|
||||
public ToolDef extraFieldsArea(String dictTool) {
|
||||
this.extraFieldsAreaFrom = dictTool;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 把「栏目动态字段」渲染在某个字段之前(对齐模块页的版式)。
|
||||
* <p>模块页是「标题 / 年级段学科 / 是否公开 → 栏目动态字段 → 正文 → 附件」,
|
||||
* 不锚定的话这一组字段会掉在附件后面,用户一眼就能看出与系统不一致。
|
||||
*
|
||||
* @param fieldName 该工具的参数名(如 remark)
|
||||
*/
|
||||
public ToolDef extraFieldsBefore(String fieldName) {
|
||||
this.extraFieldsBefore = fieldName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolDef() {
|
||||
}
|
||||
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user