把 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:
@@ -33,8 +33,8 @@ public final class RolePolicy {
|
||||
private static final Set<String> STAFF_ONLY = new HashSet<>(Arrays.asList(
|
||||
// 请假:登记(教师给学生请假,type=1 免审)、审批与统计
|
||||
"leave.register", "leave.classStudents", "leave.stats", "leave.audit", "leave.batchPass",
|
||||
// 公告:发布与审批
|
||||
"notice.sent", "notice.deptDics", "notice.publish",
|
||||
// 公告:发布与审批(另含存草稿与审批人选人的教师树)
|
||||
"notice.sent", "notice.deptDics", "notice.publish", "notice.saveDraft", "notice.teaTree",
|
||||
"notice.approvalList", "notice.approve", "notice.reject",
|
||||
// 宿舍通知:发布 / 编辑 / 阅读情况 / 回执处理 / 催办 / 统计
|
||||
"dorm.publish", "dorm.editNotice", "dorm.sendList",
|
||||
@@ -52,7 +52,10 @@ public final class RolePolicy {
|
||||
"activity.readDetail", "activity.signups", "activity.signupDetail", "activity.review",
|
||||
"activity.remind", "activity.expedite", "activity.pull", "activity.top", "activity.statistics",
|
||||
// 家校活动:发布表单的字典与选班级
|
||||
"activity.deptDic", "activity.categoryDic", "activity.classTree"
|
||||
"activity.deptDic", "activity.categoryDic", "activity.classTree",
|
||||
// 课程作业:教师端发布(发布/定时/我的作业/草稿)与发布表单的学科、班级树
|
||||
"homework.publish", "homework.schedule", "homework.published", "homework.drafts",
|
||||
"homework.teachingSubjects", "homework.classTree"
|
||||
));
|
||||
|
||||
/** 学生专属 */
|
||||
|
||||
@@ -52,5 +52,27 @@ public class BrowserToolRequest {
|
||||
private String body;
|
||||
/** POST 时的 Content-Type */
|
||||
private String contentType;
|
||||
|
||||
/**
|
||||
* 两步提交的第二步(可选):首步成功后,用首步响应的 {@code data} 替换
|
||||
* {@link FollowUp#pathAndQuery} 里的 {@code {data}} 再发一次。
|
||||
*
|
||||
* <p>只用于「模块没有一步到位的接口」的场景(如作业:先落草稿 → 再发布/定时)。
|
||||
* 第二步的地址由服务端在准备阶段生成(参数已替换完毕),前端不参与拼接。
|
||||
*/
|
||||
private FollowUp followUp;
|
||||
}
|
||||
|
||||
/** 首步成功后的续跑请求(路径里的 {@code {data}} 用首步响应的 data 替换) */
|
||||
@Data
|
||||
public static class FollowUp {
|
||||
/** GET / POST_JSON / POST_FORM */
|
||||
private String method;
|
||||
/** 路径模板,可含 {data}(首步响应的 data,如新作业 id) */
|
||||
private String pathAndQuery;
|
||||
/** 请求体(可含 {{参数名}} 占位,已在准备阶段替换) */
|
||||
private String body;
|
||||
/** Content-Type */
|
||||
private String contentType;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,6 +57,35 @@ public class FormField {
|
||||
*/
|
||||
private Integer scopeType;
|
||||
|
||||
/**
|
||||
* 「镜像字段」:本字段不由用户填写({@code hidden}),提交时自动填入同一表单中
|
||||
* {@code mirrorOf} 指定字段**选中项的显示名**。
|
||||
*
|
||||
* <p>★ 用途:公告/宿舍/活动三个模块页在提交时会用字典把部门 id 反查成名称一并发出
|
||||
* ({@code deptId} + {@code deptName},后端字典缺失时回退该名称)。助手只有下拉选中的 id,
|
||||
* 但前端手里就有选项的 label —— 由它顺手镜像过去,提交体才和模块页一模一样。
|
||||
*/
|
||||
private String mirrorOf;
|
||||
|
||||
/**
|
||||
* 「连续请假」展开说明:{@code {count: 天数字段, start: 开始时间字段, end: 结束时间字段}}。
|
||||
*
|
||||
* <p>作用在请假工具上:当本字段({@code continuousLeave})取 1 时,前端按
|
||||
* {@code continuousLeaveNum} 天把 {@code startDate}/{@code endDate} 复制成逐天时间段,
|
||||
* 生成模块要的 {@code continuousLeaveTime}({@code [{startDate,endDate}]})——
|
||||
* 与模块页 {@code LeaveTimeSection.regenerate()} 的口径一致。
|
||||
*/
|
||||
private Map<String, String> dailyExpand;
|
||||
|
||||
/**
|
||||
* {@code widget=file} 且为 true 时,提交的不是 fileId 数组,而是模块要的**附件对象数组**。
|
||||
*
|
||||
* <p>作业模块(coursehomework V2)的 {@code attachments} 形如
|
||||
* {@code [{fileId,fileUrl,fileType,fileName,sort}]},与其余模块的 {@code fileIds="1,2"} 不同,
|
||||
* 故由前端按上传结果组装成对象。
|
||||
*/
|
||||
private boolean fileAsObject;
|
||||
|
||||
private boolean required;
|
||||
|
||||
/** 说明(来自工具定义,写清格式与含义) */
|
||||
|
||||
@@ -100,6 +100,34 @@ public class ToolDef {
|
||||
*/
|
||||
private boolean formAll;
|
||||
|
||||
/**
|
||||
* 「两步提交」的第二步路径模板(可选)。首步成功后,用首步返回的 {@code data} 续跑。
|
||||
*
|
||||
* <p>★ 用途:作业模块(coursehomework V2)没有"一步发布"的接口 —— 模块页自己也是
|
||||
* {@code Create(落草稿) → Publish/Schedule}。为了让用户在助手里填一次表单就发出去,
|
||||
* 把第二步声明在这里:路径里的 {@code {data}} 用首步返回的作业 id 替换,
|
||||
* {@link #followUpBody} 里的 {@code {{参数名}}} 在准备阶段就用表单实参替换好。
|
||||
*/
|
||||
private String followUpPath;
|
||||
|
||||
/** 第二步的请求体模板(JSON 字符串,可含 {@code {{参数名}}} 占位),为空表示无请求体 */
|
||||
private String followUpBody;
|
||||
|
||||
/** 第二步的请求方式,缺省 POST */
|
||||
private String followUpMethod = "POST";
|
||||
|
||||
/**
|
||||
* 声明两步提交的第二步:首步成功后用它接着发一次请求。
|
||||
*
|
||||
* @param pathTemplate 路径模板,可含 {@code {data}}(首步响应的 data,如新作业 id)
|
||||
* @param bodyTemplate 请求体模板,可含 {@code {{参数名}}};不需要请求体传 null
|
||||
*/
|
||||
public ToolDef then(String pathTemplate, String bodyTemplate) {
|
||||
this.followUpPath = pathTemplate;
|
||||
this.followUpBody = bodyTemplate;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ToolDef() {
|
||||
}
|
||||
|
||||
@@ -222,6 +250,10 @@ public class ToolDef {
|
||||
JSONObject props = new JSONObject();
|
||||
JSONArray required = new JSONArray();
|
||||
for (ToolParam p : params) {
|
||||
// 「镜像字段」(如 deptName)由前端按所选部门的显示名自动带上,不让模型填
|
||||
if (p.getMirrorLabelOf() != null) {
|
||||
continue;
|
||||
}
|
||||
props.put(p.getName(), p.toSchema());
|
||||
if (p.isRequired()) {
|
||||
required.add(p.getName());
|
||||
|
||||
@@ -143,7 +143,12 @@ public class ToolExecutor {
|
||||
}
|
||||
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
return parseResponse(def, resp, cost);
|
||||
ToolResult first = parseResponse(def, resp, cost);
|
||||
// ★ 两步提交(如作业「先落草稿 → 再发布/定时」):首步成功才续跑,首步失败原样返回
|
||||
if (StringUtils.isNotBlank(def.getFollowUpPath()) && first.isSuccess()) {
|
||||
return runFollowUpOnServer(def, resp, merged, base, headers, start);
|
||||
}
|
||||
return first;
|
||||
|
||||
} catch (Exception e) {
|
||||
long cost = System.currentTimeMillis() - start;
|
||||
@@ -225,9 +230,116 @@ public class ToolExecutor {
|
||||
throw new IllegalArgumentException("不支持的请求方式:" + method);
|
||||
}
|
||||
call.setMethod(method);
|
||||
call.setFollowUp(buildFollowUp(def, merged, base));
|
||||
return call;
|
||||
}
|
||||
|
||||
/* ==================== 两步提交(先落草稿 → 再发布/定时) ==================== */
|
||||
|
||||
/**
|
||||
* 组装「首步成功后的续跑请求」。
|
||||
*
|
||||
* <p>模块里有些动作没有一步到位的接口(作业发布就是 {@code Create → Publish|Schedule})。
|
||||
* 声明 {@link ToolDef#then} 后,准备阶段就把第二步的地址与请求体拼好,
|
||||
* 只留 {@code {data}}(首步返回的资源 id)给执行期替换 —— 前端与服务端同一套逻辑。
|
||||
*
|
||||
* @param merged 已合并固定参数的实参表(用于替换请求体里的 {@code {{参数名}}})
|
||||
* @param base 模块根地址(用于拼出带模块前缀的相对路径)
|
||||
*/
|
||||
private com.yunhai.agent.model.BrowserToolRequest.FollowUp buildFollowUp(
|
||||
ToolDef def, Map<String, Object> merged, String base) {
|
||||
if (StringUtils.isBlank(def.getFollowUpPath())) {
|
||||
return null;
|
||||
}
|
||||
com.yunhai.agent.model.BrowserToolRequest.FollowUp fu =
|
||||
new com.yunhai.agent.model.BrowserToolRequest.FollowUp();
|
||||
String m = StringUtils.isBlank(def.getFollowUpMethod()) ? "POST" : def.getFollowUpMethod().toUpperCase();
|
||||
fu.setMethod(m);
|
||||
fu.setPathAndQuery(stripOrigin(followUpFullPath(def, base)));
|
||||
if (StringUtils.isNotBlank(def.getFollowUpBody())) {
|
||||
fu.setBody(fillPlaceholders(def.getFollowUpBody(), merged));
|
||||
}
|
||||
if ("POST_JSON".equals(m)) {
|
||||
fu.setContentType("application/json;charset=UTF-8");
|
||||
} else if ("POST_FORM".equals(m)) {
|
||||
fu.setContentType("application/x-www-form-urlencoded;charset=UTF-8");
|
||||
}
|
||||
return fu;
|
||||
}
|
||||
|
||||
/** 第二步的完整地址(模块根 + 模板),供服务端直调与相对路径裁剪共用 */
|
||||
private String followUpFullPath(ToolDef def, String base) {
|
||||
return trimEnd(base) + def.getFollowUpPath();
|
||||
}
|
||||
|
||||
/** 把 {@code {{参数名}}} 替换成实参值(用于第二步请求体) */
|
||||
private String fillPlaceholders(String template, Map<String, Object> merged) {
|
||||
String out = template;
|
||||
if (merged != null) {
|
||||
for (Map.Entry<String, Object> e : merged.entrySet()) {
|
||||
String v = e.getValue() == null ? "" : String.valueOf(e.getValue());
|
||||
out = out.replace("{{" + e.getKey() + "}}", v);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
/** 首步响应里的业务数据(用于 {@code {data}}),取不到返回 null */
|
||||
private String extractDataNode(String body) {
|
||||
if (StringUtils.isBlank(body)) return null;
|
||||
try {
|
||||
JSONObject o = JSON.parseObject(body);
|
||||
Object d = o == null ? null : o.get("data");
|
||||
if (d == null) return null;
|
||||
// 常见两种形态:直接回 id(作业创建接口 data=123),或回一个对象(data={id:123,...})。
|
||||
// 对象形态必须取出 id 再填进路径,直接 JSON 序列化会拼出 /homeworks/{"id":123}/publish。
|
||||
if (d instanceof JSONObject) {
|
||||
Object id = ((JSONObject) d).get("id");
|
||||
if (id == null) return null;
|
||||
String s = String.valueOf(id);
|
||||
return StringUtils.isBlank(s) || "null".equals(s) ? null : s;
|
||||
}
|
||||
String s = String.valueOf(d);
|
||||
return StringUtils.isBlank(s) || "null".equals(s) ? null : s;
|
||||
} catch (Exception ignore) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务端侧的两步提交:首步成功后用返回的 id 续跑第二步,返回第二步的结果
|
||||
* (首步拿到 id 却发不出去时,如实报错而不谎报成功)。
|
||||
*/
|
||||
private ToolResult runFollowUpOnServer(ToolDef def, ResponseEntity<String> firstResp,
|
||||
Map<String, Object> merged, String base,
|
||||
HttpHeaders headers, long start) {
|
||||
String dataId = extractDataNode(firstResp.getBody());
|
||||
if (StringUtils.isBlank(dataId)) {
|
||||
return ToolResult.fail(def, "第一步已提交,但没拿回记录 id,无法继续「" + def.getTitle() + "」的第二步",
|
||||
System.currentTimeMillis() - start);
|
||||
}
|
||||
com.yunhai.agent.model.BrowserToolRequest.FollowUp fu = buildFollowUp(def, merged, base);
|
||||
String url = followUpFullPath(def, base).replace("{data}", dataId);
|
||||
try {
|
||||
HttpHeaders h = new HttpHeaders();
|
||||
h.putAll(headers);
|
||||
HttpEntity<String> entity;
|
||||
if (fu.getBody() != null) {
|
||||
h.setContentType(MediaType.parseMediaType(fu.getContentType()));
|
||||
entity = new HttpEntity<>(fu.getBody(), h);
|
||||
} else {
|
||||
entity = new HttpEntity<>(h);
|
||||
}
|
||||
ResponseEntity<String> resp = restTemplate.exchange(url, HttpMethod.valueOf(fu.getMethod()),
|
||||
entity, String.class);
|
||||
return parseResponse(def, resp, System.currentTimeMillis() - start);
|
||||
} catch (Exception e) {
|
||||
log.error("工具 {} 第二步失败 url={} err={}", def.getName(), url, e.toString());
|
||||
return ToolResult.fail(def, "第一步已提交,但第二步失败:" + e.getMessage(),
|
||||
System.currentTimeMillis() - start);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析浏览器回传的模块响应 —— 解析规则与服务端直调 {@link #execute} 完全一致
|
||||
* ({@code {code,msg,data}} 包装、{@code code=302} 识别、非包装响应原样返回)。
|
||||
@@ -486,9 +598,82 @@ public class ToolExecutor {
|
||||
}
|
||||
out.put(e.getKey(), v);
|
||||
}
|
||||
// 数组参数却给了标量(模型偶尔写 "image" 而不是 ["image"])→ 包成单元素数组,
|
||||
// 免得后端 @RequestBody List 反序列化直接 400
|
||||
for (ToolParam p2 : def.getParams()) {
|
||||
Object v = out.get(p2.getName());
|
||||
if (v == null) continue;
|
||||
if ("array".equalsIgnoreCase(StringUtils.trimToEmpty(p2.getType()))
|
||||
&& !(v instanceof List) && !(v instanceof JSONArray)) {
|
||||
out.put(p2.getName(), new ArrayList<>(java.util.Collections.singletonList(String.valueOf(v))));
|
||||
}
|
||||
}
|
||||
out.putAll(expandDailyArgs(def, out));
|
||||
return out;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「连续请假」派生字段:{@code continuousLeave=1} 时,按 {@code continuousLeaveNum} 天
|
||||
* 把 {@code startDate}/{@code endDate} 复制成逐天时间段,产出模块要的
|
||||
* {@code continuousLeaveTime:[{startDate,endDate}]}。
|
||||
*
|
||||
* <p>与模块页 {@code LeaveTimeSection.regenerate()} 口径一致(每天一段,时分沿用基准)。
|
||||
* 放在执行器里做,浏览器代执行与服务端直调共用同一份逻辑。
|
||||
*/
|
||||
private Map<String, Object> expandDailyArgs(ToolDef def, Map<String, Object> args) {
|
||||
Map<String, Object> extra = new LinkedHashMap<>();
|
||||
for (ToolParam p : def.getParams()) {
|
||||
Map<String, String> spec = p.getDailyExpandSpec();
|
||||
if (spec == null || spec.isEmpty()) continue;
|
||||
Object flag = args.get(p.getName());
|
||||
if (flag == null || "0".equals(String.valueOf(flag).trim())
|
||||
|| "false".equalsIgnoreCase(String.valueOf(flag).trim())) {
|
||||
continue;
|
||||
}
|
||||
String countField = spec.get("count");
|
||||
String startField = spec.get("start");
|
||||
String endField = spec.get("end");
|
||||
String s = args.get(startField) == null ? null : String.valueOf(args.get(startField));
|
||||
String e = args.get(endField) == null ? null : String.valueOf(args.get(endField));
|
||||
if (StringUtils.isBlank(s) || StringUtils.isBlank(e)) continue;
|
||||
int days = 1;
|
||||
try {
|
||||
days = Integer.parseInt(String.valueOf(args.get(countField)).trim());
|
||||
} catch (Exception ignore) {
|
||||
/* 天数缺失/非法 → 按 1 天处理 */
|
||||
}
|
||||
days = Math.max(1, Math.min(30, days));
|
||||
List<Map<String, String>> rows = new ArrayList<>();
|
||||
for (int i = 0; i < days; i++) {
|
||||
Map<String, String> row = new LinkedHashMap<>();
|
||||
row.put("startDate", shiftDays(s, i));
|
||||
row.put("endDate", shiftDays(e, i));
|
||||
rows.add(row);
|
||||
}
|
||||
extra.put("continuousLeaveTime", rows);
|
||||
// 首段的起止时间对齐第一天(与模块页 payload() 的 startDate/endDate 口径一致)
|
||||
if (!rows.isEmpty()) {
|
||||
extra.put("startDate", rows.get(0).get("startDate"));
|
||||
extra.put("endDate", rows.get(0).get("endDate"));
|
||||
}
|
||||
}
|
||||
return extra;
|
||||
}
|
||||
|
||||
/** 把 {@code yyyy-MM-dd HH:mm:ss} 往后推 n 天(解析失败原样返回) */
|
||||
private String shiftDays(String dt, int n) {
|
||||
if (n == 0 || StringUtils.isBlank(dt)) return dt;
|
||||
try {
|
||||
java.text.SimpleDateFormat f = new java.text.SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
java.util.Calendar c = java.util.Calendar.getInstance();
|
||||
c.setTime(f.parse(dt));
|
||||
c.add(java.util.Calendar.DAY_OF_MONTH, n);
|
||||
return f.format(c.getTime());
|
||||
} catch (Exception e) {
|
||||
return dt;
|
||||
}
|
||||
}
|
||||
|
||||
/** 把数组/集合统一成字符串列表(供 join 与「全部」哨兵判断用) */
|
||||
private List<String> toStringList(Object v) {
|
||||
List<String> out = new ArrayList<>();
|
||||
|
||||
@@ -108,6 +108,18 @@ public class ToolParam {
|
||||
*/
|
||||
private Integer scopeType;
|
||||
|
||||
/** 「镜像字段」指向的源字段名(隐藏字段,提交时填入源字段选中项的显示名,如 deptId → deptName) */
|
||||
private String mirrorLabelOf;
|
||||
|
||||
/** 「连续请假」展开说明:{@code {count,start,end}} 三个字段名(见 {@link #expandDaily}) */
|
||||
private Map<String, String> dailyExpandSpec;
|
||||
|
||||
/** {@code widget=file} 时提交附件对象数组而非 fileId 数组(作业模块 attachments) */
|
||||
private boolean fileObject;
|
||||
|
||||
/** 附件上传地址覆盖(缺省由编排层按 {@code /<模块>/api/base/FileUpload} 兜底) */
|
||||
private String uploadPathOverride;
|
||||
|
||||
/** 是否在表单里隐藏(如由系统固定填入的参数,用户无需看见) */
|
||||
private boolean hidden;
|
||||
|
||||
@@ -265,6 +277,58 @@ public class ToolParam {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明为「审批流」控件(校园公告的 {@code flow})。
|
||||
*
|
||||
* <p>表单会渲染成:是否走审批 + 多步(每步可选或签/会签)+ 每步多选审批人
|
||||
* (候选人来自 {@link #optionsFrom} 指向的教师树工具);提交时前端拼成
|
||||
* {@code [{name:"第1步",vote:"any"|"all",approverTeaIds:[...]}]}。
|
||||
*/
|
||||
public ToolParam flow() {
|
||||
this.widget = "flow";
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 声明本字段为**隐藏的镜像字段**:用户不填,提交时自动带上源字段选中项的显示名。
|
||||
*
|
||||
* <p>用于 {@code deptName} 这类「模块页会随包提交、但界面上没有对应输入框」的字段:
|
||||
* 三个模块页都是「选部门 → 用字典把 id 反查成名称 → 一起提交」。
|
||||
*/
|
||||
public ToolParam mirrorLabelOf(String sourceField) {
|
||||
this.mirrorLabelOf = sourceField;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 连续请假展开:本字段取 1 时,前端按 {@code countField} 的天数把
|
||||
* {@code startField}/{@code endField} 复制成逐天时间段,生成 {@code continuousLeaveTime}。
|
||||
*
|
||||
* @param countField 连续天数字段名(如 continuousLeaveNum)
|
||||
* @param startField 基准开始时间字段名(如 startDate)
|
||||
* @param endField 基准结束时间字段名(如 endDate)
|
||||
*/
|
||||
public ToolParam expandDaily(String countField, String startField, String endField) {
|
||||
this.dailyExpandSpec = new LinkedHashMap<>();
|
||||
this.dailyExpandSpec.put("count", countField);
|
||||
this.dailyExpandSpec.put("start", startField);
|
||||
this.dailyExpandSpec.put("end", endField);
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 附件控件提交**对象数组**(作业模块 {@code attachments:[{fileId,fileUrl,fileType,fileName,sort}]}) */
|
||||
public ToolParam asAttachments() {
|
||||
this.widget = "file";
|
||||
this.fileObject = true;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 覆盖附件上传地址(缺省由编排层按 {@code /<模块>/api/base/FileUpload} 兜底) */
|
||||
public ToolParam upload(String path) {
|
||||
this.uploadPathOverride = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
/** 表单里显示的名字 */
|
||||
public String labelOf() {
|
||||
return StringUtils.isNotBlank(label) ? label : name;
|
||||
@@ -355,6 +419,9 @@ public class ToolParam {
|
||||
f.setAllToken(allToken);
|
||||
f.setAllTokenLabel(allTokenLabel);
|
||||
f.setScopeType(scopeType);
|
||||
f.setMirrorOf(mirrorLabelOf);
|
||||
f.setDailyExpand(dailyExpandSpec);
|
||||
f.setFileAsObject(fileObject);
|
||||
// 选项是内置兜底(不是学校实时字典)时如实告知,前端可提示用户
|
||||
f.setOptionsFallback(isUsingFallbackOptions());
|
||||
return f;
|
||||
|
||||
@@ -112,12 +112,16 @@ public final class ActivityTools {
|
||||
.label("活动标题").eg("秋季亲子运动会"),
|
||||
ToolParam.required("content", "string", "活动说明(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("活动说明").as("textarea").eg("为增进家校互动,学校拟举办秋季亲子运动会……"),
|
||||
ToolParam.required("category", "string", "活动分类,来自 activity.categoryDic 的 k 字段")
|
||||
ToolParam.required("category", "string", "活动分类:**分类名称**(模块页存的是名称,不是 id)")
|
||||
.label("活动分类")
|
||||
.optionsFrom("activity.categoryDic").optionsKeys("k", "v"),
|
||||
.optionsFrom("activity.categoryDic").optionsKeys("v", "v"),
|
||||
ToolParam.required("deptId", "string", "主办部门 id,来自 activity.deptDic 的 k 字段")
|
||||
.label("主办部门")
|
||||
.optionsFrom("activity.deptDic").optionsKeys("k", "v"),
|
||||
// 模块页会用字典把部门 id 反查成名称一起提交,这里由前端按所选下拉项的名称镜像过去
|
||||
ToolParam.optional("deptName", "string", "主办部门名称(由所选部门自动带出,无需填写)")
|
||||
.label("主办部门名称")
|
||||
.mirrorLabelOf("deptId"),
|
||||
ToolParam.required("venue", "string", "活动地点")
|
||||
.label("活动地点").eg("学校操场"),
|
||||
ToolParam.required("startTime", "string", "活动开始时间 yyyy-MM-dd HH:mm:ss")
|
||||
|
||||
@@ -63,48 +63,38 @@ public final class CampusNoticeTools {
|
||||
|
||||
list.add(ToolDef.of("notice.publish", MODULE, "发布校园公告",
|
||||
"发布一条校园公告。字段与校园公告模块的「新建公告」页**完全一致**:"
|
||||
+ "标题、重要程度、来源部门、接收班级、置顶、短信提醒、学员阅读回执、附件、正文、定时发送、发布方式。"
|
||||
+ "标题、重要程度、来源部门、接收班级、置顶、短信提醒、学员阅读回执、附件、正文、审批流程、定时发送、发布方式。"
|
||||
+ "调用前必须确认:标题 title、正文 content、发文部门 deptId;"
|
||||
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
|
||||
+ "发文部门必须来自字典,不要编造。"
|
||||
+ "接收班级 pushScopes 默认「全部班级」;要定向到具体班级时由表单多选。"
|
||||
+ "发布方式 publishType:direct 直接发布(默认)/ draft 存为草稿。"
|
||||
+ "发布方式 publishType:direct 直接发布(默认)/ approve 提交审批(此时须同时给出 flow 审批流程)。"
|
||||
+ "只存草稿请改用「存为公告草稿」工具(模块页的存草稿走的是另一个接口)。"
|
||||
+ "timingSendTime 非空 = 定时发送。",
|
||||
"POST_JSON", "/api/notice/Create")
|
||||
.p(ToolParam.required("title", "string", "公告标题(200 字以内)")
|
||||
.label("公告标题").eg("关于国庆假期安排的通知"),
|
||||
ToolParam.required("content", "string", "公告正文(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("公告正文").as("textarea").eg("各位老师、同学:经学校研究决定……"),
|
||||
ToolParam.optional("type", "integer", "重要程度:1 普通 / 2 重要")
|
||||
.label("重要程度").dft("1")
|
||||
.options(new String[]{"1", "2"}, new String[]{"普通", "重要"}),
|
||||
ToolParam.required("deptId", "string", "发文部门 id,来自 notice.deptDics 的 k 字段")
|
||||
.label("发文部门")
|
||||
.optionsFrom("notice.deptDics").optionsKeys("k", "v"),
|
||||
ToolParam.optional("pushScopes", "array", "接收班级:留空或含全部 = 全部班级;也可多选具体班级")
|
||||
.label("接收班级")
|
||||
.optionsFrom("notice.stuTree").optionsKeys("id", "name")
|
||||
.scopes(4).allOption("ALL", "全部班级"),
|
||||
ToolParam.optional("topmost", "integer", "是否置顶")
|
||||
.label("置顶").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||
ToolParam.optional("smsFlag", "integer", "是否同时发短信提醒")
|
||||
.label("短信提醒").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||
ToolParam.optional("receiptFlag", "integer", "是否需要学员阅读回执(要回执时收件人需点确认)")
|
||||
.label("学员阅读回执").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||
ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
|
||||
.label("添加附件").as("file").multiple(true).join(","),
|
||||
ToolParam.optional("timingSendTime", "string", "定时发送时间 yyyy-MM-dd HH:mm:ss(留空=立即发布)")
|
||||
.label("定时发送").eg("2026-10-13 08:00:00"),
|
||||
ToolParam.optional("publishType", "string", "发布方式:direct 直接发布 / draft 存为草稿")
|
||||
.label("发布方式").dft("direct")
|
||||
.options(new String[]{"direct", "draft"}, new String[]{"直接发布", "存为草稿"}))
|
||||
.p(noticePublishParams())
|
||||
.fullForm()
|
||||
.write("/list/new")
|
||||
.modId(null));
|
||||
|
||||
list.add(ToolDef.of("notice.saveDraft", MODULE, "存为公告草稿",
|
||||
"把一条校园公告存为草稿(不发布,可在公告模块的草稿箱继续编辑)。"
|
||||
+ "字段与「新建公告」页完全一致;只想先打个底稿时用本工具。",
|
||||
"POST_JSON", "/api/notice/SaveDraft")
|
||||
.p(noticePublishParams(false))
|
||||
.fullForm()
|
||||
.write("/drafts")
|
||||
.modId(null));
|
||||
|
||||
list.add(ToolDef.of("notice.teaTree", MODULE, "组织树(教师)",
|
||||
"查询本校教师组织树(学部 / 年级 / 教研组 / 教师),用于配置公告审批人。"
|
||||
+ "仅在用户明确要「按组织范围选审批人」时使用;"
|
||||
+ "发布公告时**不要**先调它 —— 表单的「审批流程」选人会自动代取该树。",
|
||||
"GET", "/api/base/getTeaTree")
|
||||
.fix("toUser", true)
|
||||
.readAt("/list/new")
|
||||
.modId(null));
|
||||
|
||||
list.add(ToolDef.of("notice.stuTree", MODULE, "组织树(学员)",
|
||||
"查询本校组织树(学部 / 年级 / 班级),用于发布公告时选择接收班级。"
|
||||
+ "仅在用户明确要「按组织范围选人」时使用;"
|
||||
@@ -144,4 +134,66 @@ public final class CampusNoticeTools {
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
/**
|
||||
* 「新建公告」页的完整字段(发布与存草稿共用)。
|
||||
*
|
||||
* <p>★ 与模块页 {@code NewNotice.vue#buildBody} 逐项对齐:{@code title/content/type/deptId/
|
||||
* deptName/pushScopes/topmost/receiptFlag/fileIds/smsFlag/timingSendTime/flow/publishType}。
|
||||
*
|
||||
* @param withPublishType 是否带「发布方式」—— 存草稿走的是 {@code SaveDraft} 接口,
|
||||
* 模块页在这一步并不传 {@code publishType}
|
||||
*/
|
||||
private static ToolParam[] noticePublishParams(boolean withPublishType) {
|
||||
List<ToolParam> ps = new ArrayList<>();
|
||||
ps.add(ToolParam.required("title", "string", "公告标题(200 字以内)")
|
||||
.label("公告标题").eg("关于国庆假期安排的通知"));
|
||||
ps.add(ToolParam.required("content", "string", "公告正文(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("公告正文").as("textarea").eg("各位老师、同学:经学校研究决定……"));
|
||||
ps.add(ToolParam.optional("type", "integer", "重要程度:1 普通 / 2 重要")
|
||||
.label("重要程度").dft("1")
|
||||
.options(new String[]{"1", "2"}, new String[]{"普通", "重要"}));
|
||||
ps.add(ToolParam.required("deptId", "string", "发文部门 id,来自 notice.deptDics 的 k 字段")
|
||||
.label("发文部门")
|
||||
.optionsFrom("notice.deptDics").optionsKeys("k", "v"));
|
||||
// 模块页会用字典把部门 id 反查成名称一起提交,这里由前端按所选下拉项的名称镜像过去
|
||||
ps.add(ToolParam.optional("deptName", "string", "发文部门名称(由所选部门自动带出,无需填写)")
|
||||
.label("发文部门名称")
|
||||
.mirrorLabelOf("deptId"));
|
||||
ps.add(ToolParam.optional("pushScopes", "array", "接收班级:留空或含全部 = 全部班级;也可多选具体班级")
|
||||
.label("接收班级")
|
||||
.optionsFrom("notice.stuTree").optionsKeys("id", "name")
|
||||
.scopes(4).allOption("ALL", "全部班级"));
|
||||
ps.add(ToolParam.optional("topmost", "integer", "是否置顶")
|
||||
.label("置顶").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
ps.add(ToolParam.optional("smsFlag", "integer", "是否同时发短信提醒")
|
||||
.label("短信提醒").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
ps.add(ToolParam.optional("receiptFlag", "integer", "是否需要学员阅读回执(要回执时收件人需点确认)")
|
||||
.label("学员阅读回执").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
ps.add(ToolParam.optional("fileIds", "array", "附件文件 id 列表(由表单的附件上传得到)")
|
||||
.label("添加附件").as("file").multiple(true).join(","));
|
||||
ps.add(ToolParam.optional("timingSendTime", "string", "定时发送时间 yyyy-MM-dd HH:mm:ss(留空=立即发布)")
|
||||
.label("定时发送").eg("2026-10-13 08:00:00"));
|
||||
// 审批流:模块页的 ApprovalFlowEditor(可多步,每步或签/会签,审批人来自教师树)
|
||||
ps.add(ToolParam.optional("flow", "array",
|
||||
"审批流程:走审批时填写。提交时按「走审批流程 + 多步 + 每步审批人与表决方式」组装成 "
|
||||
+ "[{name,vote,approverTeaIds}];不填 = 不走审批(直接发布)")
|
||||
.label("审批流程")
|
||||
.flow()
|
||||
// 教师树叶子形如 {id,name,...},其中 id 即 teaId(与模块页选人弹窗口径一致)
|
||||
.optionsFrom("notice.teaTree").optionsKeys("id", "name"));
|
||||
if (withPublishType) {
|
||||
ps.add(ToolParam.optional("publishType", "string", "发布方式:direct 直接发布 / approve 提交审批")
|
||||
.label("发布方式").dft("direct")
|
||||
.options(new String[]{"direct", "approve"}, new String[]{"直接发布", "提交审批"}));
|
||||
}
|
||||
return ps.toArray(new ToolParam[0]);
|
||||
}
|
||||
|
||||
private static ToolParam[] noticePublishParams() {
|
||||
return noticePublishParams(true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@ public final class DormNoticeTools {
|
||||
ToolParam.required("deptId", "string", "发文部门 id,来自 dorm.deptDic 的 k 字段")
|
||||
.label("发文部门")
|
||||
.optionsFrom("dorm.deptDic").optionsKeys("k", "v"),
|
||||
// 模块页会用字典把部门 id 反查成名称一起提交,这里由前端按所选下拉项的名称镜像过去
|
||||
ToolParam.optional("deptName", "string", "发文部门名称(由所选部门自动带出,无需填写)")
|
||||
.label("发文部门名称")
|
||||
.mirrorLabelOf("deptId"),
|
||||
ToolParam.optional("level", "string", "紧急程度:normal 普通 / important 重要 / urgent 紧急")
|
||||
.label("紧急程度").dft("normal")
|
||||
.options(new String[]{"normal", "important", "urgent"},
|
||||
|
||||
@@ -7,20 +7,42 @@ import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 课程作业模块工具(学生端 V2)—— 后端 CourseHomework,前缀 /api/student/homeworks。
|
||||
* 课程作业模块工具(后端 CourseHomework)。
|
||||
*
|
||||
* <p>学生端接口路径带资源 id({@code /{id}/...}),执行器支持 {@code {name}} 路径占位。
|
||||
* <p>两侧接口并列:
|
||||
* <ul>
|
||||
* <li><b>学生端 V2</b> —— 前缀 {@code /api/student/homeworks},路径带资源 id
|
||||
* (执行器支持 {@code {name}} 路径占位);</li>
|
||||
* <li><b>教师端 V2</b> —— 前缀 {@code /api/teacher/homeworks},发布页在
|
||||
* {@code NewHomework.vue}。★ 模块没有「一步发布」的接口,模块页自己也是
|
||||
* {@code Create(落草稿) → Publish | Schedule},故这里用 {@link ToolDef#then}
|
||||
* 声明第二步,让用户在助手里填一次表单就发出去。</li>
|
||||
* </ul>
|
||||
*/
|
||||
public final class HomeworkTools {
|
||||
|
||||
public static final String MODULE = "coursehomework";
|
||||
|
||||
private static final String TEACHER = "/api/teacher/homeworks";
|
||||
|
||||
/** 附件上传地址(作业模块走 file 域,与其余模块的 base/FileUpload 不同) */
|
||||
private static final String UPLOAD_PATH = "/coursehomework/api/file/Upload";
|
||||
|
||||
private HomeworkTools() {
|
||||
}
|
||||
|
||||
public static List<ToolDef> defs() {
|
||||
List<ToolDef> list = new ArrayList<>();
|
||||
|
||||
addStudentSide(list);
|
||||
addTeacherPublishSide(list);
|
||||
return list;
|
||||
}
|
||||
|
||||
/* ==================== 学生端 ==================== */
|
||||
|
||||
private static void addStudentSide(List<ToolDef> list) {
|
||||
|
||||
list.add(ToolDef.of("homework.myList", MODULE, "我的作业列表",
|
||||
"查询我的作业:待做 / 已完成 / 已截止 / 全部。用户问“今天有什么作业”“还有几项没交”时用它。",
|
||||
"GET", "/api/student/homeworks")
|
||||
@@ -51,7 +73,112 @@ public final class HomeworkTools {
|
||||
ToolParam.optional("text", "string", "作答文本内容")
|
||||
.label("作答内容").as("textarea").eg("我的解题思路是……"))
|
||||
.write("/stu/today"));
|
||||
}
|
||||
|
||||
return list;
|
||||
/* ==================== 教师端:发布作业(模块页 页 1) ==================== */
|
||||
|
||||
private static void addTeacherPublishSide(List<ToolDef> list) {
|
||||
|
||||
list.add(ToolDef.of("homework.teachingSubjects", MODULE, "我的任教学科",
|
||||
"查询当前教师的任教学科(发布作业时选「学科」用)。"
|
||||
+ "仅在用户明确问「我教哪些学科」时使用;"
|
||||
+ "发布作业时**不要**先调它 —— 表单的「学科」下拉会自动带出可选值。",
|
||||
"GET", TEACHER + "/subjects/teaching")
|
||||
.readAt("/new"));
|
||||
|
||||
list.add(ToolDef.of("homework.classTree", MODULE, "任教班级树",
|
||||
"年级 → 班级 树(叶子是班级,仅含本人任教班级;发布作业时选接收班级用)。"
|
||||
+ "仅在用户明确要「按年级选班」时使用;"
|
||||
+ "发布作业时**不要**先调它 —— 表单的「接收班级」多选会自动代取该树。",
|
||||
"GET", TEACHER + "/classes")
|
||||
.readAt("/new"));
|
||||
|
||||
// ---------- 立即发布(Create 落草稿 → Publish) ----------
|
||||
list.add(ToolDef.of("homework.publish", MODULE, "发布作业",
|
||||
"教师发布一条作业(学生端随即可见)。字段与课程作业模块的「新建作业」页**完全一致**:"
|
||||
+ "标题、学科、接收班级、截止时间、提交方式、逾期提交、截止提醒、附件、作业内容。"
|
||||
+ "调用前必须确认:标题 title、学科 subject、接收班级 classIds;"
|
||||
+ "缺失就直接调用本工具,系统会弹出**完整表单**让用户填写,不要用文字反复追问;"
|
||||
+ "学科与班级必须来自本人任教范围,不要编造。"
|
||||
+ "提交方式 submitType 可多选(拍照/上传附件/文字作答),勾「仅查阅」则学生无需提交;"
|
||||
+ "「仅查阅」与其余三种互斥。"
|
||||
+ "需要定时发布请改用「定时发布作业」。",
|
||||
"POST_JSON", TEACHER)
|
||||
.p(homeworkPublishParams(false))
|
||||
// ★ 模块没有一步发布的接口:先 Create 落草稿,再用返回的作业 id 发布
|
||||
.then(TEACHER + "/{data}/publish", null)
|
||||
.fullForm()
|
||||
.write("/new"));
|
||||
|
||||
// ---------- 定时发布(Create 落草稿 → Schedule) ----------
|
||||
list.add(ToolDef.of("homework.schedule", MODULE, "定时发布作业",
|
||||
"教师定时发布一条作业(到点自动发布)。字段与「新建作业」页完全一致,"
|
||||
+ "额外需要定时发布时间 scheduledAt(必须晚于当前时间)。",
|
||||
"POST_JSON", TEACHER)
|
||||
.p(homeworkPublishParams(true))
|
||||
.then(TEACHER + "/{data}/schedule", "{\"scheduledAt\":\"{{scheduledAt}}\"}")
|
||||
.fullForm()
|
||||
.write("/new"));
|
||||
|
||||
list.add(ToolDef.of("homework.published", MODULE, "我发布的作业",
|
||||
"分页查询本人已发布的作业(含进度)。用户问「我发过哪些作业」「那条作业交得怎么样」时用它。",
|
||||
"GET", TEACHER + "/published")
|
||||
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||||
ToolParam.optional("keyword", "string", "作业标题关键词"),
|
||||
ToolParam.optional("subject", "string", "按学科筛选"))
|
||||
.readAt("/list"));
|
||||
|
||||
list.add(ToolDef.of("homework.drafts", MODULE, "我的作业草稿",
|
||||
"分页查询本人的作业草稿(尚未发布的)。",
|
||||
"GET", TEACHER + "/drafts")
|
||||
.p(ToolParam.optional("pageIndex", "integer", "页码").dft("1"),
|
||||
ToolParam.optional("pageSize", "integer", "每页条数").dft("10"),
|
||||
ToolParam.optional("keyword", "string", "作业标题关键词"))
|
||||
.readAt("/drafts"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 「新建作业」页的完整字段({@code HomeworkV2Request})。
|
||||
*
|
||||
* @param withSchedule 是否带「定时发布时间」—— 只有定时发布工具需要
|
||||
*/
|
||||
private static ToolParam[] homeworkPublishParams(boolean withSchedule) {
|
||||
List<ToolParam> ps = new ArrayList<>();
|
||||
ps.add(ToolParam.required("title", "string", "作业标题(50 字以内)")
|
||||
.label("作业标题").eg("第三章课后练习"));
|
||||
ps.add(ToolParam.required("subject", "string", "学科:来自本人任教学科(subId)")
|
||||
.label("学科")
|
||||
.optionsFrom("homework.teachingSubjects").optionsKeys("subId", "subName"));
|
||||
ps.add(ToolParam.required("classIds", "array", "接收班级:班级 id 列表(仅本人任教班级,可多选)")
|
||||
.label("接收班级")
|
||||
.optionsFrom("homework.classTree").optionsKeys("id", "name"));
|
||||
ps.add(ToolParam.optional("description", "string", "作业内容(纯文本,对应模块页的富文本编辑器)")
|
||||
.label("作业内容").as("textarea").eg("完成课本 P32 第 1~5 题,写出解题过程。"));
|
||||
ps.add(ToolParam.optional("deadlineAt", "string", "截止时间 yyyy-MM-dd HH:mm:ss(留空=不设截止)")
|
||||
.label("截止时间").eg("2026-10-20 22:00:00"));
|
||||
ps.add(ToolParam.optional("submitType", "array",
|
||||
"提交方式(可多选):image 拍照/图片 / attach 上传附件 / text 文字作答 / none 仅查阅(不必提交)。"
|
||||
+ "勾「none」表示学生只预览、教师无需批改,与其余三种互斥")
|
||||
.label("提交方式")
|
||||
.dft("image")
|
||||
.options(new String[]{"image", "attach", "text", "none"},
|
||||
new String[]{"拍照/图片", "上传附件", "文字作答", "仅查阅(不必提交)"}));
|
||||
ps.add(ToolParam.optional("allowLate", "integer", "是否允许逾期提交(截止后仍可补交)")
|
||||
.label("逾期提交").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"不允许", "允许"}));
|
||||
ps.add(ToolParam.optional("remind1d", "integer", "是否在截止前 1 天提醒未交学生")
|
||||
.label("截止前 1 天提醒").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
ps.add(ToolParam.optional("remind1h", "integer", "是否在截止前 1 小时提醒未交学生")
|
||||
.label("截止前 1 小时提醒").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}));
|
||||
ps.add(ToolParam.optional("attachments", "array", "作业附件(由表单的附件上传得到)")
|
||||
.label("添加附件").asAttachments().multiple(true).upload(UPLOAD_PATH));
|
||||
if (withSchedule) {
|
||||
ps.add(ToolParam.required("scheduledAt", "string", "定时发布时间 yyyy-MM-dd HH:mm:ss(须晚于当前时间)")
|
||||
.label("定时发布时间").eg("2026-10-20 08:00:00"));
|
||||
}
|
||||
return ps.toArray(new ToolParam[0]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,11 +130,15 @@ public final class LeaveTools {
|
||||
.optionsFrom("leave.myClasses").optionsKeys("classId", "className"),
|
||||
ToolParam.optional("continuousLeave", "integer", "是否连续请假(同一时段连续多天)")
|
||||
.label("连续请假").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"})
|
||||
// 选「是」时按下面的连续天数把起止时间复制成逐天时间段,
|
||||
// 生成模块要的 continuousLeaveTime(与模块页 LeaveTimeSection 一致)
|
||||
.expandDaily("continuousLeaveNum", "startDate", "endDate"),
|
||||
ToolParam.optional("continuousLeaveNum", "integer", "连续天数(continuousLeave=1 时有效,1~30)")
|
||||
.label("连续天数").dft("1"),
|
||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到)")
|
||||
.label("佐证材料").as("file").multiple(false).join(","))
|
||||
// ★ 模块传的是 number[](JSON 数组),不是逗号串,故不声明 join
|
||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到,可多份)")
|
||||
.label("佐证材料").as("file").multiple(true))
|
||||
.fix("type", 2)
|
||||
.fullForm()
|
||||
.write("/my")
|
||||
@@ -166,11 +170,13 @@ public final class LeaveTools {
|
||||
.label("请假事由").as("textarea").eg("家中有事,需请假一天"),
|
||||
ToolParam.optional("continuousLeave", "integer", "是否连续请假(同一时段连续多天)")
|
||||
.label("连续请假").dft("0")
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"}),
|
||||
.options(new String[]{"0", "1"}, new String[]{"否", "是"})
|
||||
.expandDaily("continuousLeaveNum", "startDate", "endDate"),
|
||||
ToolParam.optional("continuousLeaveNum", "integer", "连续天数(continuousLeave=1 时有效,1~30)")
|
||||
.label("连续天数").dft("1"),
|
||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到)")
|
||||
.label("佐证材料").as("file").multiple(false).join(","))
|
||||
// ★ 模块传的是 number[](JSON 数组),不是逗号串,故不声明 join
|
||||
ToolParam.optional("fileIds", "array", "佐证材料附件 id 列表(由表单的附件上传得到,可多份)")
|
||||
.label("佐证材料").as("file").multiple(true))
|
||||
.fix("type", 1)
|
||||
.fullForm()
|
||||
.write("/register")
|
||||
|
||||
Reference in New Issue
Block a user